【Linux實踐系列】:進程間通信:萬字詳解共享內存實現通信

🔥 本文專欄:Linux Linux實踐項目
🌸作者主頁:努力努力再努力wz

在這里插入圖片描述
在這里插入圖片描述

在這里插入圖片描述

💪 今日博客勵志語錄人生就像一場馬拉松,重要的不是起點,而是堅持到終點的勇氣

★★★ 本文前置知識:

匿名管道

命名管道


前置知識大致回顧(對此十分熟悉的讀者可以跳過)

那么我們知道進程之間具有通信的需求,因為某項任務需要幾個進程共同來完成,那么這時候就需要進程之間協同分工合作,那么進程之間就需要知道彼此之間的完成的進度以及完成的情況,那么此時進程之間就需要通信來告知彼此,而由于進程之間具有獨立性,那么進程無法直接訪問對方的task_struct結構體以及頁表來獲取其數據,那么操作系統為了滿足進程之間通信的需求又保證進程的獨立性,那么采取的核心思想就是創建一份公共的內存區域,然后讓通信的進程雙方能夠看到這份公共的內存區域,從而能夠實現通信

那么對于父子進程來說,由于子進程是通過拷貝父進程的task_struct結構體得到自己的一份task_struct結構體,那么意味著子進程會拷貝父進程的文件描述表,從而子進程會繼承父進程打開的文件,而操作系統讓進程通信的核心思想就是創建一塊公共的內存區域,那么這里對于父子進程來說,那么文件就可以作為這個公共的內存區域,來保存進程之間通信的信息,所以這里就要求父進程在創建子進程之前,先自己創建一份文件,這樣再調用fork創建出子進程,這樣子進程就能繼承到該文件,那么雙方就都持有該文件的文件描述符,然后通過文件描述符向該文件進行寫入以及讀取,而我們知道該文件只是用來保存進程之間通信的臨時數據,而不需要刷新到磁盤中長時間保存,那么必定該文件的性質是一個內存級別的文件,那么創建一個內存級別的文件就不能在調用open接口,因為open接口是用來創建一個磁盤級文件,其次就是雙方通過該文件來進行進程之間通信的時候,那么雙方不能同時對該文件進行讀寫,因為會造成偏移量錯位以及文件內容混亂的問題,所以該文件只能用來實現單向通信,也就是智能一個進程向該文件寫入,然后另一個進程從該文件中進行讀取,那么由于該文件單向通信的特點,并且進程雙方是通過文件描述符來訪問,所以該文件其沒有路徑名以及文件名,因此該文件被稱作匿名管道文件,那么我們要創建匿名管道文件,就需要調用pipe接口,那么pipe接口的返回值就是該匿名管道文件讀寫端對應的file結構體的文件描述符

而對于非父子進程來說,此時他們無法再看到彼此的文件描述表,那么意味著對于非父子進程來說,那么這里只能采取建立一個普通的文件,該普通的文件作為公共區域,那么一個進程向該文件中寫入,另一個進程從該文件讀取,根據父子進程通信的原理,我們知道該普通文件肯定不是一般的普通文件,它一定也得是內存級別文件,其次也只能實現單向通信,而對于匿名管道來說,通信進程雙方看到該匿名管道是通過文件描述符來訪問到這塊資源,而對于命名管道則是通過通過路徑加文件名的方式來訪問命名管道,那么訪問的方式就是通信進程的雙方各自通過open接口以只讀和只寫的權限分別打開該命名管道文件,獲取其文件描述符,然后通信進程雙方通過文件描述符然后調用write以及read接口來寫入以及讀取數據,而創建一個命名管道就需要我們調用mkfifo接口

那么這就是對前置知識的一個大致回顧,如果讀者對于上面講的內容感到陌生或者想要知道其中的更多細節,那么可以看我之前的博客


共享內存

那么此前我們已經學習了兩種通信方式,分別是匿名管道以及命名管道來實現進程的通信,那么這期博客,我便會介紹第三種通信方式,便是共享內存,那么我會從三個維度來解析共享內存,分別是什么是共享內存以及共享內存的底層相關的原理和結合前面兩個維度的理論知識,如何利用共享內存來實現進程的通信,也就是文章的末尾我們會寫一個用共享內存實現通信的小項目

什么是共享內存以及共享內存的底層原理

那么我們知道進程間通信的核心思想就是通過開辟一塊公共的區域,然后讓進程雙方能夠看到這份資源從而實現通信,所以這里的共享內存其實本質就是操作系統為其通信進程雙方分配的一個物理內存,那么這份物理內存就是共享內存,所以共享內存的概念其實很簡單與直接

根據進程間通信的核心思想,那么這里的公共的區域已經有了,那么下一步操作系統要解決的問題便是創建好了共享內存,如何讓進程雙方能夠看到這份共享內存資源

那么對于進程來說,按照進程的視角,那么它手頭上只持有虛擬地址,那么進程訪問各種數據都只能通過虛擬地址去訪問,然后系統再借助頁表將虛擬地址轉換為物理地址從而訪問到相關數據,所以要讓通信進程雙方看到共享內存,那么此時操作系統的任務就是提供給通信進程雙方各自一個指向共享內存的虛擬地址,然后通信進程雙方就可以通過該虛擬地址來向共享內存中寫入以及讀取數據了,那么這個時候操作系統要進行的工作,就是創建通信的進程的同時,設置好該進程對應的mm_struct結構體中的共享內存段,并且在其對應的頁表添加其共享內存的虛擬地址到物理地址的映射的條目

那么知道了共享內存來實現進程雙方通信的一個大致的原理,那么現在的問題就是如何請求讓操作系統來為該通信進程雙方創建共享內存

那么這里就要讓操作系統為該其創建一份共享內存,就需要我們在代碼層面上調用shmget接口,那么該接口的作用就是讓內核為我們創建一份共享內存,但是在介紹這個接口如何使用之前,我們還得補充一些相關的理論基礎,有了這些理論基礎,我們才能夠認識到shmget這些參數的意義是什么

  • shmget
  • 頭文件:<sys/shm.h> 和<sys/ipc.h>
  • 函數聲明:int shmget(ket_t key,size_t size,int shmflg);
  • 返回值:調用成功返回shmid,調用失敗則返回-1,并設置errno

key/shmid

那么這里的shmget的一個參數就是一個key,那么讀者對于key的疑問無非就是這兩個方面:這個key是什么?key的作用是什么?

那么接下來的講解會以這兩個問題為核心,來為你解析這個key究竟是何方神圣

首先我們一定要清楚的是系統中存在不只有一個共享內存,因為系統中需要通信的進程不只有一對,所以此時系統中的共享內存就不只有一個,那么系統中存在這么多的共享內存,那么每一個共享內存都會涉及到創建以及讀取和寫入以及最后的銷毀,那么操作系統肯定就要管理存在的所有的共享內存,那么管理的方式就是我們熟悉的先描述再組織的方式來管理這些共享內存,也就是為每一個共享內存創建一個struct shm_kernel結構體,那么該結構體就記錄了該共享內存的相關的屬性,比如共享內存的大小以及共享內存的權限以及掛載時間等等,那么每一個共享內存都有對應的結構體,那么內核會持有這些結構體,并且會采取特定的數據結構將這些結構體組織起來,比如鏈表或者哈希表,那么系統中每一個共享內存肯定是不相同的,那么為了區分這些不同的共享內存,那么系統就得給這些共享內存分配一個標識符,通過標識符來區分這些共享內存

而進程要用共享內存實現通信,那么進程首先得請求操作系統為我們該進程創建一份共享內存,然后獲取到指向該共享內存的虛擬地址,而進程間的通信,涉及的進程的數量至少為兩個,那么以兩個進程為例子,假設進程A和進程B要進行通信,那么此時需要為這對進程提供一個共享內存,那么就需要A進程或者B進程告訴操作系統來為其創建一份共享內存

那么這里你可以看到我將或者這兩個字加粗,那么就是為了告訴讀者,那么這里我們只需要一個進程來告訴內核創建一份共享內存,不需要兩個進程都向操作系統發出創建共享內存的請求,所以只需要一個進程請求內核創建一份共享內存,然后另一個進程直接訪問創建好的共享內存即可

那么知道了這點之后,那么假設這里創建共享內存的任務交給了A進程,那么此時A進程請求內核創建好了一份共享內存,那么對于B進程來說,它如何知道獲取到A進程創建好的共享內存呢,由于系統內存在那么多的共享內存,那么B進程怎么知道哪一個共享內存是A進程創建的,那么這個時候就需要key值,那么這個key值就是共享內存的標識符

key就好比酒店房間的一個門牌號,那么A和B進程只需要各自持有該房間的門牌號,那么就能夠找到該房間,但是這里要注意的就是這里的key值不是由內核自己生成的,而是由用戶自己來生成一個key值

那么有些讀者可能就會感到疑問,那么標識符這個概念對于大部分的讀者來說都不會感到陌生,早在學習進程的時候,我們就已經接觸到標識符這個概念,那么對內核為了管理進程,那么會為每一個進程分配一個標識符,那么就是進程的PID,而在文件系統中,任何類型的文件都有自己對應的inode結構體,那么內核為了管理inode結構體,那么也為每一個文件對應的inode結構體分配了標識符,也就是inode編號,所以讀者可能會感到疑惑:那么在這里共享內存也會存在標識符,但是這里的標識符為什么是用戶來提供而不是內核來提供呢,是內核無法做到為每一個共享內存分配標識符還是說因為其他什么原因?

那么這個疑問是理解這個key的關鍵,首先我要明確的就是內核肯定能夠做到為每一個共享內存提供標識符,這個工作對于內核來說,并不難完成,并且事實上,內核也的確為每一個共享內存提供了標識符,那么這個標識符就是shmid

在引入了shmid之后,可能有的讀者又會產生新的疑問:按照你這么說的話,那么實際上內核為每一個創建好的共享內存分配好了標識符,但是這里還需要用戶自己在創建一個標識符,那么理論上來說,豈不是一個共享內存會存在兩個所謂的標識符,一個是key,另一個是shmid,而我們訪問共享內存只需要一個標識符就夠了,那么這里共享內存擁有兩個標識符,豈不是會存在冗余的問題?并且為什么不直接使用內核的標識符來訪問呢?


那么接下來我就來依次解答讀者的這些疑問,那么首先關于為什么我們進程雙方為什么不直接通過shmid來訪問內存

那么我們知道內核在創建共享內存的同時會為該共享內存創建對應的struct shm_kernel結構體,那么其中就會涉及到為其分配一個唯一的shmid,而假設請求內核創建共享內存的任務是交給A進程來完成,而B進程只需要訪問A進程請求操作系統創建好的共享內存,而對于B進程來說,它首先得知道哪個共享內存是提供給我們兩個A個B兩個進程使用的,意味著B進程就得通過共享內存的標識符得知,因為每一個共享內存對應著一個唯一且不重復的標識符,對于A進程來說,由于它來完成共享內存的創建,而shmget接口是用來創建共享內存并且返回值就是共享內存的shmid,那么此時A進程能夠知道并且獲取進程的shmid標識符,但是它能否將這個shmget的返回值也就是shmid告訴該B進程嗎,毫無疑問,肯定是不可能的,因為進程之間就有獨立性!那么如果直接使用shmid來訪問共享內存,那么必然只能對于創建共享內存的那一方進程可以看到而另一個進程無法看到,那么無法看到就會讓該進程不知道哪一個共享內存是用來給我們A和B進程通信的,所以這就是為什么要有key存在

那么A和B進程雙方事先會持有一個相同的key,那么A進程是創建共享內存的一方,那么它會將將key傳遞給shmget接口,那么shmget接口獲取到key,會將key作為共享內存中其中一個字段填入,最終給A進程返回一個shmid,而對于B進程來說,那么它拿著同一個key值然后也調用shmget接口,而此時對于B進程來說,它的shmget的行為則不是創建共享內存,而是內核會拿著它傳遞進來的key,到組織共享內存所有結構體的數據結構中依次遍歷,找到匹配該key的共享內存,然后返回其shmid

而至于為什么A和B進程都調用shmget函數,但是shmget函數有著不同的行為,對于A來說是創建,對于B來說則可以理解為是“查詢”,那么這就和shmget的第三個參數有關,那么第三個參數會接受一個宏,該宏決定了shmget行為,所以A和B進程調用shmget接口傳遞的宏肯定是不一樣的,那么我會在下文會講解shmget接口的第三個參數,這里就先埋一個伏筆

所以綜上所述,這里的key雖然也是和shmid一樣是作為標識符,但是是給用戶態提供使用的,是用戶態的兩個進程在被創建之前的事先約定,而內核操作則是通過shmid,那么key的值沒有任何的意義,所以理論上我們用戶可以自己來生成任意一個無符號的整形作為key,但是要注意的就是由于這里key是用戶自己生成自己決定的,那么有可能會出現這樣的場景,那么就是用戶自己生成的key和已經創建好的共享內存的key的值一樣或者說沖突,所以這里系統為我們提供了ftok函數,那么該函數的返回值就是key值,那么我們可以不調用該函數,自己隨便生成一個key值,但是造成沖突的后果就得自己承擔,所以這里更推薦調用ftok函數生成一個key值

這里推薦使用ftok函數來生成的key,不是因為ftok函數生成的key完全不會與存在的共享內存的key造成沖突,而是因為其沖突的概率相比于我們自己隨手生成一個的key是很低的

  • ftok
  • 頭文件:<sys/types.h> 和<sys/ipc.h>
  • 函數聲明:key_t ftok(const char* pathname,int proj_id);
  • 返回值:調用成功返回key值,調用失敗則返回-1

那么這里ftok會接收兩個參數,首先是一個文件的路徑名以及文件名,那么這里注意的就是這里的文件的路徑名以及文件名一定是系統中存在的文件,因為它會解析這個路徑以及文件名從而獲取該文件的inode編號,然后得到對應的inode結構體,從中再獲取其設備編號,那么這里的proj_id的作用就是用來降低沖突的概率,因為到時候ftok函數獲取到文件的inode編號以及設備號和proj_id,然后會進行位運算,得到一個32位的無符號整形,那么其位運算就是:
ftok 通過文件系統元數據生成 key 的算法如下:

key = (st_dev & 0xFF) << 24 | (st_ino & 0xFFFF) << 8 | (proj_id & 0xFF)

? st_dev:文件所在設備的設備號(取低8位)

? st_ino:文件的inode編號(取低16位)

? proj_id:用戶指定的項目ID(取低8位)

共享內存的大小

那么shmget函數的第二個參數便是指定的就是共享內存的大小,那么這里至于內核在申請分配物理內存的單位是以頁為單位,也就是以4KB為單位來分配物理內存,而這里shmget的第二個參數是以字節為單位,那么這里我建議我們開辟的共享內存是以4096的整數倍來開辟,因為假設你申請一個4097個字節,那么此時內核實際上為你分配的物理內存是2*4096,也就是8kb的空間,雖然人家內核給你分配了8kb的空間,但是它只允許你使用其中的4097個字節,也就是剩下的空間就全部浪費了,所以這就是為什么建議申請的空間大小是4096的整數倍,那么這就是shmget的第二個參數

shmget的宏

那么shmget的第三個參數便是宏來指定其行為,那么上文我們就埋了一個伏筆,就是兩個進程都調用了shmget但是卻有著不同的行為,那么就和這里的宏有關:

  • IPC_CREAT (01000):如果共享內存不存在則創建
  • IPC_EXCL (02000):不能單獨使用,與IPC_CREAT一起使用,若共享內存已存在則失敗
  • SHM_HUGETLB (04000):使用大頁內存(Linux特有)

那么這里我們著重要掌握的便是IPC_CREAT以及IPC_EXEC這兩個宏
IPC_CREAT:
那么傳遞IPC_CREAT這個宏給shmget接口,其底層涉及到工作,就是內核首先會持有我們傳遞的key值,然后去遍歷組織所有共享內存的結構體的數據結構,如果遍歷完了所有共享內存對應的結構體并且發現沒有匹配的key值,那么這里就會創建一個新的共享內存并且同時創建對應的結構體,然后對其結構體的屬性進行初始化其中就包括填入key值并將其放入組織共享內存結構體的數據結構中,那么最后創建完成后,會返回該共享內存的shmid,而如果說發現有匹配的key值的共享內存,那么就直接返回該共享內存的shmid
IPC_EXCL:
而IPC_EXCL則是和IPC_CREAT一起使用,那么傳遞IPC_CREAT| IPC_EXCL這個宏給shmget接口,其底層涉及到工作,j就是如果內核發現了有匹配的key值的共享內存,那么這里就不會返回該共享內存的shmid而是返回-1并設置errno,沒有的話就創建新的共享內存并返回器shmid,所以這個選項就是保證了創建的共享內存是最新的共享內存

而這里的宏本質上就是一個特定值的32位的二進制序列,那么他們的每一個比特位代表著特定含義的標記位,而該標記位則是分布在二進制序列的高24位,而低8位則是表示該共享內存的權限,所以在上文所舉的例子中,對于A進程來說,它是創建共享內存的一方,那么它傳遞的宏就應該是IPC_CREAT|IPC_EXCL|0666,而對于B進程來說他是訪問的一方,那么它傳遞的就是IPC_CREAT|0666

那么shmget會根據其宏進行相應的行為,并且還會核對其權限是否一致,不一致則返回-1,調用失敗


shmat

那么此時上面所講的所有內容都是關于創建共享內存的一些理論知識,那么我們現在已經知道如何創建共享內存,那么下一步就是如何讓通信的進程雙方看到該共享內存,那么從上文的共享內存實現通信的大致原理,我們知道創建完共享內存的下一個環節就是讓進程雙方持有指向該共享內存的虛擬地址,那么這個時候就需要請求操作系統來設置通信進程的雙方的虛擬地址空間的共享內存段以及在頁表中添加共享內存的虛擬地址到物理地址的映射條目,所以此時就需要我們在代碼層面上調用shmat系統調用接口,那么該系統調用接口的背后所做的工作就是剛才所說的內容,而shmat所做的工作也叫做掛載

  • shmat
  • 頭文件:<sys/shm.h> 和<sys/types.h>
  • 函數聲明:void* shmat(int shmid,void *shmadder,int shmflg);
  • 返回值:調用成功返回指向共享內存起始位置的虛擬地址,調用失敗則返回(void*)-1,并設置errno

那么這里shmat接口會接收之前我們調用shmget接口獲取的共享內存的shmid,然后內核會根據該shmid遍歷共享內存對應的結構體,然后找到匹配的共享內存,接著在將共享內存掛載到通信的進程雙方,那么這里第二個參數就是我們可以指定內核掛載到共享內存段的哪個具體區域,但是這第二個參數最好設置為NULL,那么設置為NULL意味著會讓內核來在共享內存段中選擇并且分配一個合適的虛擬地址,那么該虛擬地址就會作為返回值

而shmat的第三個參數則是會接收一個宏,那么這個宏就是用來指定該進程對于該共享內存的一個讀寫權限:

  • SHM_RDONLY (只讀模式):以只讀方式附加共享內存
  • SHM_RND (地址對齊):當指定了非NULL的shmaddr時,自動將地址向下對齊到SHMLBA邊界
  • SHM_REMAP (Linux特有,重新映射): 替換指定地址的現有映射,需要與具體的shmaddr配合使用

那么這里對于我們來說,那么我們不用傳遞任何宏就進去,就傳遞一個NULL或者0,那么我們該進程就能夠正常的寫入以及讀取該共享內存的內容,那么這三個宏的使用場景,在目前現階段的學習來說,我們暫時還使用不到。

那么這就是shmat接口,那么認識了shmat接口之后,那么我們就可以來利用共享內存來實現正常的進程之間的通信了,那么首先第一個環節就是先讓各自通信的進程雙方持有key,然后一個進程通過key來調用shmget接口來創建共享內存并且獲得其shmid,而另一個進程也是同樣通過key值來調用shmge接口來獲取已經創建好的共享內存的shmid,那么下一個環節就是掛載,那么此時就需要請求系統設置通信的進程雙方的虛擬地址空間的共享內存段,并且添加相應的關于該共享內存的虛擬地址到物理地址的映射的條目,并且返回給進程雙方該共享內存的起始位置的虛擬地址,那么此時進程雙方就可以持有該虛擬地址去訪問共享內存了

shmdet

那么進程通信完之后,那此時就要清理相關的資源,其中就包括打開的共享內存,那么我們要注意的就是共享內存對應的shm_kernel結構體中會有一個屬性,那么該屬性便是引用計數,記錄了有多少個進程指向它或者說有多少個進程的頁表中有關于該共享內存的虛擬地址到物理地址的映射條目,那么此時shmdet接口的作用就是刪除該進程對應的頁表中共享內存的映射條目或者將該頁表的條目設置為無效,從而解除該進程與共享內存之間的綁定,讓該進程無法再訪問到共享內存的資源,并且還會減少該共享內存的引用計數

  • shmdet
  • 頭文件:<sys/shm.h> 和<sys/ipc.h>
  • 函數聲明:int shmdet(const void *shmadder);
  • 返回值:成功返回0,失敗返回-1,并設置errno

那么可能會有的讀者會感到疑惑的就是,這里shmdet接口只接收一個虛擬地址,而該虛擬地址是共享內存的起始位置的虛擬地址,那么內核可以通過該虛擬地址借助頁表來訪問到共享內存,而引用計數這個屬性是存儲在共享內存對應的結構體中,那么意味著這里shmdet能夠通過虛擬地址來訪問到共享內存對應的物理結構體,而共享內存中存儲的內容去啊不是通信的消息,那么這里內核是如何通過該虛擬地址訪問到共享內存對應的結構體的呢?

那么我們知道進程對應的task_struct結構體中會有一個字段mm_struct結構體其中會維護一個vma(虛擬內存區域)的數據結構,那么該數據結構一般是采取鏈表來實現,其中該鏈表的每一個節點是一個結構體,用來描述以及記錄該虛擬內存區域的相關屬性,其中就包括該虛擬內存區域的虛擬地址的起始位置以及虛擬地址的結束位置,以及相關的讀寫權限以及其文件的大小和文件的類型

struct mm_struct {struct vm_area_struct *mmap;       // VMA 鏈表的頭節點(單鏈表)struct rb_root mm_rb;               // VMA 紅黑樹的根節點(用于快速查找)// ...其他字段(如頁表、內存計數器等)
};struct vm_area_struct {// 內存范圍unsigned long vm_start;unsigned long vm_end;// 權限與標志unsigned long vm_flags;// 文件與偏移struct file *vm_file;unsigned long vm_pgoff;// 操作函數const struct vm_operations_struct *vm_ops;// 鏈表與樹結構struct vm_area_struct *vm_next;struct rb_node vm_rb;// 其他元數據struct mm_struct *vm_mm;// ...
};

其中在vma的視角下,那么它將每一個虛擬內存區域比如棧或者堆,以文件的形式來看待,那么其中這里的vm_file字段會指向該虛擬內存區域創建的一個file結構體,其中就會包含該共享內存對應的struct shm_kernal結構體,所以這里shmdet接口會獲取到虛擬地址,然后會查詢mm_struct結構體中記錄的vma鏈表根據該虛擬地址確定落在哪一個vma結構體中,那么該vma結構體就是共享內存段區域所對應的vma結構體,然后通過vm_file來間接獲取到共享內存的shmid,最后再拿著shmid從保存共享內存對應的數據結構中找到對應匹配的共享內存對應的結構體,然后讓其引用計數減一

shmctl

而shmdet只是解除了進程與共享內存之間的掛載,那么shmdet的作用就好比指向一個動態數組的首元素的指針,那么我們只是將該指針置空,讓我們無法在之后的代碼中通過該指針來訪問該動態數組,但是該動態數組所對應的空間并沒有釋放,而對于共享內存來說,那么內核要真正釋放共享內存的資源得滿足兩個前提條件,那么就是該共享內存對應的引用計數為0并且該共享內存還得被標記為已刪除,因為內核沒有規定該共享內存只能給創建它的進程雙方通信用,那么一旦該進程雙方結束通信了,那么可以讓該進程雙方解除與該共享內存的掛載,然后讓其他進程與該共享內存掛載,從而通過再次利用該共享內存來進行通信,所以這里就是為什么要設計一個刪除標志

所以說這里的shmctl接口的作用就是用來控制共享內存,那么我們可以通過調用該接口將共享內存標記為可刪除,那么一旦該共享內存對應的引用計數為0,那么此時內核就會釋放該共享內存的資源

  • shmctl
  • 頭文件:<sys/shm.h> 和<sys/ipc.h>
  • 函數聲明:int shmctl(int shmid,int cmd,struct shmid_ds* buffer);
  • 返回值:調用成功返回0,調用失敗則返回-1,并設置errno

那么這里對于shmctl來說,其第一個參數是shmid,那么到時內核會持有該參數去尋找對應的共享內存的結構體,而shmctl的第二個參數則是控制shmctl接口的行為,那么這里還是通過宏以及位運算的方式來指定shmctl接口的行為:

  • IPC_STAT:獲取共享內存段的狀態
  • IPC_RMID:刪除共享內存段
  • IPC_SET:設置共享內存段的狀態

那么這里IPC_SET這個宏,我們目前還應用不到,那么這里我們如果要將共享內存標記為刪除,那么就傳入IPC_RMID即可

而如果我們要獲取共享內存的狀態,那么我們可以傳入IPC_STAT這個宏,此時shmctl的第三個參數就有意義,那么它會接收一個指向struct shm_ds的結構體,那么該結構體的定義是存放在sys/shm.h頭文件中,那么這里內核會通過shmid然后訪問到該共享內存對應的結構體,根據其結構體來初始化struct shm_ds

struct shmid_ds {struct ipc_perm shm_perm;   // 共享內存段的權限信息size_t          shm_segsz;  // 共享內存段的大小(字節)time_t          shm_atime;  // 最后一次附加的時間time_t          shm_dtime;  // 最后一次斷開的時間time_t          shm_ctime;  // 最后一次修改的時間pid_t           shm_cpid;   // 創建共享內存段的進程 IDpid_t           shm_lpid;   // 最后一次操作的進程 IDshmatt_t        shm_nattch; // 當前附加到共享內存段的進程數(引用計數)// ... 其他字段(可能因系統而異)
};
/* 定義在 sys/ipc.h 中 */
struct ipc_perm {key_t          __key;     /* 用于標識 IPC 對象的鍵值 */uid_t          uid;       /* 共享內存段的所有者用戶 ID */gid_t          gid;       /* 共享內存段的所有者組 ID */uid_t          cuid;      /* 創建該 IPC 對象的用戶 ID */gid_t          cgid;      /* 創建該 IPC 對象的組 ID */unsigned short mode;      /* 權限位(類似于文件權限) */unsigned short __seq;     /* 序列號,用于防止鍵值沖突 */
};

那么我們就可以通過訪問該結構體中的相關成員變量來獲取共享內存的相關屬性信息


利用共享內存來實現進程間的通信

那么在上文我介紹了共享內存相關的理論基礎以及關于共享內存相關的系統調用接口,那么這里我們就會結合前面所學的知識以及系統調用接口來實現兩個進程間通信的一個小項目,那么這里介紹這個項目之前,我們還是來梳理一下大致的框架,然后再具體落實具體各個模塊的代碼怎么去寫

大體框架

那么這里既然要實現進程間的通信,那么我首先就得準備兩個陌生進程,分別是processA以及processB,那么processA進程的任務就是就是負責創建共享內存,然后將創建好的共享內存掛載,然后processA作為共享內存的寫入方,向共享內存寫入數據,最后解除掛載,然后清理共享內存資源,而processB的任務則是訪問processA創建好的共享內存,然將該共享內存掛載到其虛擬地址空間,然后processB作為共享內存的讀取法,讀取數據,最后解除掛載

comm.hpp

那么這里我們知道到時候A和B進程會接收到一個共同的key,然后一個進程用這個key來創建共享內存,而另一個進程則是用該key來獲取該共享內存的shmid,所以到時候這兩個進程對應的源文件會各自引用該comm.hpp頭文件,那么comm.hpp中就會定義一個全局變量的key,然后其中會定義一個Creatkey函數,那么該函數內部就會調用ftok接口來生成一個key值并且返回,而comm.hpp中還會定義CreaShm函數和Getshm函數,那么從這兩個函數名我們就知道它們各自的作用,那么CreatShm函數就是提供給A進程使用的,它的作用是創建共享內存,并且返回其shmid,而GetShm則是提供給process B使用,那么它的作用就是獲取process A打開的共享內存并且返回其shmid,而這里只是梳理大致框架,那么具體的實現會在后文給出

processA.cpp

1.創建共享內存

那么這里對于process A來說,那么它的第一個環節就是創建共享內存,也就是調用CreatShm函數來獲取shmid

2.將共享內存掛載到虛擬地址空間

那么接下來獲取到共享內存的shmid之后,那么下一步便是調用shmat接口來將該共享內存給掛載到processA進程的虛擬地址空間,然后獲取其共享內存的起始位置的虛擬地址

3.向共享內存中寫入數據

那么這個環節就是根據之前獲取到的共享內存的虛擬地址,然后通過該虛擬地址向共享內存中寫入數據,其中寫入數據會回封裝到一個死循環的邏輯當中

4.解除掛載

那么這個環節就是解除共享內存與process A的關聯,其中涉及調用shmdet

5.清理共享內存資源

那么這里清理共享內存資源會調用shmctl接口,因為shmdet只是減少引用計數以及刪除該進程關于該共享內存的映射條目

processB.cpp

1.獲取process A進程創建的共享內存

那么這里就會通過調用GetShm來獲取process A進程創建的共享內存的shmid

2.將共享內存掛載

那么這個環節和上文的process A所做的內容是一樣的,就是調用shmat接口,然后獲取該共享內存的起始位置的虛擬地址

3.讀取process A向共享內存寫入的數據

那么這里我們會同樣會根據上一個環節獲取到的虛擬地址,而通過該虛擬地址讀取共享內存的內容

4.解除掛載

那么這里對于process A進程來說,那么由于process A進程來完成的共享內存的刪除,所以這里對于B進程來說,那么這里它只需解除與共享內存的掛載即可

各個模塊的具體實現

comm.hpp

那么這里的comm.hpp的內容就包括process A進程以及process B進程會持有的key,以及Creatkey函數,該函數內部會調用ftok函數來獲取到要創建的共享內存的key值,而ftok函數會接收一個已存在的路徑以及文件名,和project_id,那么這里就得保證傳遞給ftok函數的路徑名以及文件名一致,那么這里我們將文件的路徑以及文件名定義為全局的string類型的變量同時將project_id也定義為了全局變量,而CreatShm函數則是創建共享內存,那么這里內部實現就會涉及到調用shmget接口,那么GetShm函數則是獲取到process A創建的共享內存的shmid,那么這里內部也要調用shmget,只不過傳遞給shmget接口的宏不一樣

而這里我進行一個巧妙的處理,那么這里我直接函數的復用,那么直接在GetShm函數內部直接復用定義好的CreatShm函數,那么這里就得利用缺省參數,那么這里的默認缺省參數就是IPC_CREAT|IPC_EXCL|066,那么這里調用GetShm函數中就會顯示傳遞一個IPC_CREAT的宏,那么此時GetShm函數就會返回一個與相同key值的共享內存的shmid,也就是process A創建的共享內存的shmid

const std::string pathname="/home/WangZhe";
int key;
log a;
void CreatKey()
{key=ftok(pathname.c_str(),ProjectId);if(key<0){a.logmessage(Fatal,"ftoke調用失敗");exit(EXIT_FAILURE);}
}size_t CreatShm(int flag=IPC_CREAT|IPC_EXCL|0666)
{CreatKey();int shmid=shmget(key,SHM_SIZE,flag);if(shmid<0){a.logmessage(Fatal,"shemget fail:%s",strerror(errno));exit(EXIT_FAILURE);}return shmid;
}
size_t GetShm()
{int shmid=CreatShm(IPC_CREAT|0666);return shmid;
}

那么這里在函數內部還進行了相應的日志打印邏輯,那么如果對日志不熟悉的讀者,那么建議看我之前的一期博客

processA.cpp

1.創建共享內存

那么這里對于processA.cpp來說,第一個環節就是調用CreatShm函數來創建大小為4096字節的共享內存并且獲取返回值,那么這里我們還要對返回值進行檢查,如果shmget接口調用失敗,那么返回值是-1,那么這個錯誤是致命的,那么程序就無法再繼續往下正常運行,然后進行相應的日志打印,并且退出

int shmid=CreatShm();a.logmessage(debug,"processA creat Shm successfully:%d",shmid);int n=mkfifo(FIFO_FILE,0666);if(n<0){a.logmessage(Fatal,"creat fifo fail:%s",strerror(errno));exit(EXIT_FAILURE);}
2.將共享內存掛載到虛擬地址空間

那么該環節會利用上一個步驟創建的共享內存的shmid,那么這里會調用shmat接口將共享內存掛載到process A的地址空間,并且此時會返回一個void* 的指針,那么該指針就是指向共享內存起始位置的虛擬地址,那么這里接下來process A進程向共享內存中寫入數據就會利用該虛擬地址,那么這里我們使用該虛擬地址可以聯系我們通過調用malloc函數在堆上申請了一個連續的空間,然后得到該空間的首元素的地址,然后我通過該首元素地址來訪問該空間并且寫入的過程

那么這里由于之后我們要寫入的消息是字符串,那么這里我們就可以將共享內存視作一個字符數組,那么這里我們就要將void*的指針強制轉化為char *類型

而如果shmat調用失敗,那么此時會返回(void*)-1的指針,那么這里注意的就是這里的-1是指針類型,也就是說這里的-1不能按照所謂的數值為-1來理解,而是得按照一個值為-1的二進制序列,那么這里我們比較返回值與(void *)-1,判斷shmat是否調用成功

char* Shmadder=(char*)shmat(shmid,NULL,NULL);if(Shmadder==(void*)-1){a.logmessage(Fatal,"processA attach Fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA attch successfully:0x%x",Shmadder);
3.向共享內存中寫入數據

那么這里就是根據之前獲取到的共享內存的虛擬地址,然后通過該虛擬地址向共享內存中寫入數據,而我們將寫入的操作封裝到一個死循環中,那么這里我們就得注意一個同步機制的問題

同步:

那么這里由于A進程和B進程都是一個死循環的讀取以及打印的邏輯,那么這里就會導致一個問題,那么我們知道A進程是寫入方進程,那么在A進程在寫入的過程中,那么在同一個時刻下的B進程會一直從共享內存中讀取數據,那么就會出現這樣的場景,那么假設此時A進程向共享內存寫入了一條消息,那么同一個時刻下的B進程讀取到了這條消息,那么接著A進程便會等待獲取用戶的鍵盤輸入的下一條消息,而我們知道此時對于共享內存來說,它里面存儲的數據還是之前上一個時刻的A進程寫入的,那么數據沒有被覆蓋,而與此同時對于B進程來說,它根本不管A進程此時是否正在寫入下一條消息,那么它只是無腦的從共享內存中不停的讀取,那么此時它在當前時刻會獲取到的消息則是A進程在上一個時刻寫的消息,而此時A進程還在等待用戶的鍵盤輸入,沒有向共享內存中寫入,那么此時共享內存中的數據還未被覆蓋,那么此時B進程的視角下,那么B進程在當前時刻讀取的消息就會被視作是A進程在當前時刻寫入的消息,但是事實上,A還沒有往共享內存中寫入所以這個場景就是典型的一個讀寫不同步帶來的問題

其次如果說此時B進程正在讀取拷貝共享內存中的數據,但是此時在同一時刻的A進程正在向共享內存中寫入數據,那么會導致數據被覆蓋,那么B進程最終讀取的消息就是混亂的,這也是讀寫不同步帶來的問題

所以我希望的就是,當A還沒寫或者說正在往共享內存中寫入一條消息的時候,那么此時B進程就站住不要動,也就是不要向共享內存中讀取數據,那么一旦A進程消息寫完了,然后你B進程在動,開始從共享內存或者讀取數據,那么這樣就是讀寫同步,那么這里實現讀寫同步,可以通過鎖來實現,但是對于初學真來說,可能當前沒有學過或者接觸過鎖,那么這里我們就采取的就是命名管道來實現同步機制


那么這里可能有的讀者會有疑惑,這里我知道此時A和B進程采取共享內存通信,會有讀寫不同步的問題,但是這里你采取的措施是通過命名管道來實現讀寫同步,而我們知道命名管道的作用就是可以實現非父子進程的通信,那么你干脆就直接用命名管道通信就結束了,那么還搞一個共享內存,豈不是多次一舉?

那么對于這個疑問,那么首先我想說的就是,命名管道確實可以傳遞消息,但是對于共享內存來說,我們是直接向物理內存中寫入以及讀取數據,雖然A和B進程雙方持有的是虛擬地址,但是我們只需要經歷一次虛擬地址到物理地址的映射轉換便能直接訪問到物理內存,而這里通過命名管道寫入消息,那么就會涉及到調用系統調用接口,比如write以及read接口,而系統接口的調用是有成本有代價的,那么這里你比如調用write接口向共享內存中寫入數據,那么其中涉及到的工作,就是會首先找到該文件描述符對應的file結構體,然后還要定位其物理內存,最后再拷貝寫入,那么這個時間代價明顯比共享內存更大,所以說這里采取共享內存是更優秀的


所以這里首先A進程需要先調用mkfifo接口來創建一個命名管道,然后再調用open接口打開該命名管道,獲取到該命名管道文件的文件描述符,那么命名管道的內容就是一個字符,那么這個字符的內容代表的就是當前是否繼續讀取以及是否退出,那么這里字符x表示退出,如果進程B從管道讀取到了字符x,那么代表著此時進程A結束通信,那么就直接退出,而如果讀取到的是字符a,那么代表這此時進程A向共享內存中寫入了一條有效消息,那么需要B進程去讀取

那么接下來就有一個問題,那么這里我們是A進程是先向管道文件中寫入信息,還是先向共享內存中寫入信息,那么可能有的讀者會有這樣的感覺,那么就是這里A進程先向管道文件中寫入信息,先告訴b進程我現在是要給你發送一條消息還是說我要結束通信了,那么發送完信息之后,此時我A進程在向共享內存中寫入消息,然后讓B進程去讀

那么這里就得注意一個問題,那么如果采取的是這種方式,對于B進程來說,那么它毫無疑問肯定是得先讀取管道文件的信息,確定A進程的意圖是要我讀取還是說結束通信,然后下一步再來讀取共享內存,那么如果此時A沒有向管道文件中寫入信息,那么此時B進程作為讀取端,由于調用了read接口讀取管道文件,此時B進程會陷入阻塞,如果此時A進程先向管道文件中直接寫入了信息,那么在同一時刻下,B進程讀取到管道文件的信息,那么它從立即阻塞狀態切換為運行,那么它就會立即執行后面的讀取共享內存的代碼,而在同一個時刻下,A進程此時還在等待用戶的鍵盤輸入的消息,還沒有往共享內存中寫入,而此時你B進程就已經開始讀了,那么讀的消息就是之前A進程寫入的消息,那么還是會導致讀寫不同步的問題

所以這里就得梳理清楚這個關鍵的細節,那么就是這里A進程得先向共享內存中寫入消息,然后再寫向管道寫入信息,這里對于B進程來說,它會一直陷入阻塞直到A進程向管道寫入了消息,然后開始讀取,這樣就可以做到讀寫同步

 while(true){std::cout<<"Please Enter the messasge:"<<std::endl;fgets(Shmadder,1024,stdin);size_t len=strlen(Shmadder);if(len>0&&Shmadder[len-1]=='\n'){Shmadder[len-1]='\0';}char c;std::cout<<"Please Enter the code(Press a:continue to send message/Press x:stop sending):"<<std::endl;std::cin>>c;getchar();int n=write(fd,&c,1);if(c=='x'){break;}if(n<0){a.logmessage(Fatal,"write fail:%s",strerror(errno));exit(EXIT_FAILURE);}sleep(1);}

那么這里我采取的就是fets函數往共享內存中寫入數據,因為它會首先會讀取空格,直到讀取換行符結束,那么這里注意的就是fets會讀取換行符,并且還會再數據的最后添加一個’\0’標記,,這樣就能夠方便B進程來確定消息的結尾,但是由于fets會讀取換行符,而到時我們B進程通過日志打印終端消息的時候,也會輸入一個換行符,所以這里就要處理末尾的換行符,用’\0’來覆蓋

而這里要注意的就是我們這里向管道文件寫入字符c的時候,那么這里我們是從標準輸入中讀取將其賦值給字符c,而這里我們最后會敲一個回車鍵也就是換行符,而這里cin讀取標準輸入和fets不同的是,它這里不會讀取換行符,讀到換行符就結束,那么就會導致緩沖區會遺留一個換行符,那么這里我們就通過getchar來將這個換行符給讀取出來

4.解除掛載

那么最后剩下的兩個環節就很簡單了,那么這里就是調用shmdet接口解除掛載,然后判斷一下返回值,然后進行相應的日志打印

n=shmdt(Shmadder);if(n<0){a.logmessage(Fatal,"processA detach FAILER:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA detach successfully");
5.清理資源

那么最后一步就是清理資源,包括之前創建的管道文件以及共享內存

close(fd);
unlink(FIFO_FILE);
n=shmctl(shmid,IPC_RMID,NULL);if(n<0){a.logmessage(Fatal,"processA shmctl fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(info,"processA quit successfully");

processB.cpp

1.獲取process A進程創建的共享內存

那么這里這個環節調用GetShm來獲取進程A創建的共享內存,獲取其shmid

 int shmid=GetShm();a.logmessage(debug,"processB get Shm successfully:%d",shmid);
2.將共享內存掛載

那么這個環節和上文的process A所做的內容是一樣的,就是調用shmat接口,然后獲取該共享內存的起始位置的虛擬地址

 a.logmessage(debug,"processB open fifo successfully");char* Shmadder=(char*)shmat(shmid,NULL,NULL);if(Shmadder==(void*)-1){a.logmessage(Fatal,"attch fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processB attch successfully:0x%x",Shmadder);
讀取process A向共享內存中寫入的數據

那么這里由于在上文,我介紹了進程雙方的讀寫同步的機制,那么這里對于B進程來說,那么它首先就要讀取管道中的信息,確定A進程的意圖,如果讀取到的字符是a,說明A進程此時向共享內存寫入了一條消息,然后我定義了一個臨時的字符數組,從共享內存中讀取1024個字節數據拷貝到該字符數組中,而如果此時讀到的字符是x,說明A進程此時結束通信,那么就退出循環

 while(true){char c;int n=read(fd,&c,1);if(c=='x'){break;}else if(n==0){break;}else if(n<0){a.logmessage(Fatal," processB read fail:%s",strerror(errno));exit(EXIT_FAILURE);}else {char buffer[1024]={0};memcpy(buffer,Shmadder,1024);a.logmessage(info,"processB get a message:%s",buffer);}}
5.清理資源

那么這里對于B進程來說,那么它只需要關閉管道文件的讀端以及解除掛載即可,因為管道文件以及共享內存的刪除都交給了A進程

  close(fd);int n=shmdt(Shmadder);if(n<0){a.logmessage(Fatal,"processB detach fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processB detach successfully");a.logmessage(info,"processB quit successfully");

源碼

comm.hpp

#pragma once
#include<sys/ipc.h>
#include<iostream>
#include<sys/shm.h>
#include<sys/types.h>
#include<sys/stat.h>
#include"log.hpp"
#include<cerrno>
#include<cstring>
#include<cstdio>
#define SHM_SIZE 4096
#define FIFO_FILE "./myfifo"
#define EXIT_FAILURE  1
#define ProjectId 110
const std::string pathname="/home/WangZhe";
int key;
log a;
void CreatKey()
{key=ftok(pathname.c_str(),ProjectId);if(key<0){a.logmessage(Fatal,"ftoke調用失敗");exit(EXIT_FAILURE);}
}size_t CreatShm(int flag=IPC_CREAT|IPC_EXCL|0666)
{CreatKey();int shmid=shmget(key,SHM_SIZE,flag);if(shmid<0){a.logmessage(Fatal,"shemget fail:%s",strerror(errno));exit(EXIT_FAILURE);}return shmid;
}
size_t GetShm()
{int shmid=CreatShm(IPC_CREAT|0666);return shmid;
}

log.hpp

#pragma once
#include<iostream>
#include<string>
#include<time.h>
#include<stdarg.h>
#include<fcntl.h>
#include<unistd.h>
#define SIZE 1024
#define screen 0
#define File 1
#define ClassFile 2
enum
{info,debug,warning,Fatal,
};
class log
{private:std::string memssage;int method;public:log(int _method=screen):method(_method){}void logmessage(int leval,char* format,...){char* _leval;switch(leval){case info:_leval="info";break;case debug:_leval= "debug";break;case warning:_leval="warning";break;case Fatal:_leval="Fatal";break;}char timebuffer[SIZE];time_t t=time(NULL);struct tm* localTime=localtime(&t);snprintf(timebuffer,SIZE,"[%d-%d-%d-%d:%d]",localTime->tm_year+1900,localTime->tm_mon+1,localTime->tm_mday,localTime->tm_hour,localTime->tm_min);char rightbuffer[SIZE];va_list arg;va_start(arg,format);vsnprintf(rightbuffer,SIZE,format,arg);char finalbuffer[2*SIZE];snprintf(finalbuffer,sizeof(finalbuffer),"[%s]%s:%s",_leval,timebuffer,rightbuffer);int fd;switch(method){case screen:std::cout<<finalbuffer<<std::endl;break;case File:fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd>=0){write(fd,finalbuffer,sizeof(finalbuffer));close(fd);           }break;case ClassFile:switch(leval){case info:fd=open("log/info.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case debug:fd=open("log/debug.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case warning:fd=open("log/Warning.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);write(fd,finalbuffer,sizeof(finalbuffer));break;case Fatal:fd=open("log/Fatal.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);break;}if(fd>0){write(fd,finalbuffer,sizeof(finalbuffer));close(fd);}}}
};

processA.cpp

#include"comm.hpp"
int main()
{int shmid=CreatShm();a.logmessage(debug,"processA creat Shm successfully:%d",shmid);int n=mkfifo(FIFO_FILE,0666);if(n<0){a.logmessage(Fatal,"creat fifo fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA creat fifo successfully");a.logmessage(info,"processA is waiting for processB open");int fd=open(FIFO_FILE,O_WRONLY);if(fd<0){a.logmessage(Fatal,"processA open fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA open fifo successfully");char* Shmadder=(char*)shmat(shmid,NULL,NULL);if(Shmadder==(void*)-1){a.logmessage(Fatal,"processA attach Fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA attch successfully:0x%x",Shmadder);while(true){std::cout<<"Please Enter the messasge:"<<std::endl;fgets(Shmadder,1024,stdin);size_t len=strlen(Shmadder);if(len>0&&Shmadder[len-1]=='\n'){Shmadder[len-1]='\0';}char c;std::cout<<"Please Enter the code(Press a:continue to send message/Press x:stop sending):"<<std::endl;std::cin>>c;getchar();int n=write(fd,&c,1);if(c=='x'){break;}if(n<0){a.logmessage(Fatal,"write fail:%s",strerror(errno));exit(EXIT_FAILURE);}sleep(1);}close(fd);unlink(FIFO_FILE);n=shmdt(Shmadder);if(n<0){a.logmessage(Fatal,"processA detach FAILER:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processA detach successfully");n=shmctl(shmid,IPC_RMID,NULL);if(n<0){a.logmessage(Fatal,"processA shmctl fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(info,"processA quit successfully");exit(0);
}

processB.cpp

#include"comm.hpp"
int main()
{int shmid=GetShm();a.logmessage(debug,"processB get Shm successfully:%d",shmid);int fd=open(FIFO_FILE,O_RDONLY);if(fd<0){a.logmessage(Fatal,"processB open fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processB open fifo successfully");char* Shmadder=(char*)shmat(shmid,NULL,NULL);if(Shmadder==(void*)-1){a.logmessage(Fatal,"attch fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processB attch successfully:0x%x",Shmadder);while(true){char c;int n=read(fd,&c,1);if(c=='x'){break;}else if(n==0){break;}else if(n<0){a.logmessage(Fatal," processB read fail:%s",strerror(errno));exit(EXIT_FAILURE);}else {char buffer[1024]={0};memcpy(buffer,Shmadder,1024);a.logmessage(info,"processB get a message:%s",buffer);}}close(fd);int n=shmdt(Shmadder);if(n<0){a.logmessage(Fatal,"processB detach fail:%s",strerror(errno));exit(EXIT_FAILURE);}a.logmessage(debug,"processB detach successfully");a.logmessage(info,"processB quit successfully");exit(0);
}

運行截圖:
在這里插入圖片描述

結語

那么這就是本篇關于共享內存的全面介紹了,帶你從多個維度來全面剖析共享內存,那么下一期博客將會進入Linux的倒數第二座大山,那么便是信號,那么我會持續更新,希望你能夠多多關注,如果本文有幫組到你,還請三連加關注哦,你的支持就是我創作的最大的動力!
在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/904884.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/904884.shtml
英文地址,請注明出處:http://en.pswp.cn/news/904884.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

CogView4 文本生成圖像

CogView4 文本生成圖像 flyfish 基于 CogView4Pipeline 的圖像生成程序&#xff0c;其主要目的是依據 JSON 文件里的文本提示信息來生成圖像&#xff0c;并且把生成的圖像保存到指定文件夾。 JSON 文件格式 [{"prompt": "your first prompt"},{"pr…

路由重發布

路由重發布 實驗目標&#xff1a; 掌握路由重發布的配置方法和技巧&#xff1b; 掌握通過路由重發布方式實現網絡的連通性&#xff1b; 熟悉route-pt路由器的使用方法&#xff1b; 實驗背景&#xff1a;假設學校的某個分區需要配置簡單的rip協議路由信息&#xff0c;而主校…

機器人領域和心理學領域 恐怖谷 是什么

機器人領域和心理學領域 恐怖谷 是什么 恐怖谷是一個在機器人領域和心理學領域備受關注的概念,由日本機器人專家森政弘于1970年提出。 含義 當機器人與人類的相似度達到一定程度時,人類對它們的情感反應會突然從積極變為消極,產生一種毛骨悚然、厭惡恐懼的感覺。這種情感…

Go-GJSON 組件,解鎖 JSON 讀取新姿勢

現在的通義靈碼不但全面支持 Qwen3&#xff0c;還支持配置自己的 MCP 工具&#xff0c;還沒體驗過的小伙伴&#xff0c;馬上配置起來啦~ https://click.aliyun.com/m/1000403618/ 在 Go 語言開發領域&#xff0c;json 數據處理是極為常見的任務。Go 標準庫提供了 encoding/jso…

數據分析_數據預處理

1 數據預處理流程 ①數據清洗:處理數據缺失、數據重復、數據異常等問題,提升數據質量. ②數據轉換:涵蓋基本數據轉換、語義數據轉換、衍生數據轉換和隱私數據轉換,適配分析需求. ③數據集成:整合多源數據. 2 數據清洗 2.1 數據缺失 2.1.1 數值型數據缺失 數值型列的部分數值不…

vue +xlsx+exceljs 導出excel文檔

實現功能&#xff1a;分標題行導出數據過多&#xff0c;一個sheet表里表格條數有限制&#xff0c;需要分sheet顯示。 步驟1:安裝插件包 npm install exceljs npm install xlsx 步驟2&#xff1a;引用包 import XLSX from xlsx; import ExcelJS from exceljs; 步驟3&am…

ThinkPad T440P如何從U盤安裝Ubuntu24.04系統

首先制作一個安裝 U 盤。我使用的工具是 Rufus &#xff0c;它的官網是 rufus.ie &#xff0c;去下載最新版就可以了。直接打開這個工具&#xff0c;選擇自己從ubuntu官網下載Get Ubuntu | Download | Ubuntu的iso鏡像制作U盤安裝包即可。 其次安裝之前&#xff0c;還要對 Thi…

第十七次博客打卡

今天學習的內容是動態規劃算法。 動態規劃算法&#xff08;Dynamic Programming&#xff0c;簡稱 DP&#xff09;是一種通過將復雜問題分解為更小的子問題來求解的算法思想。它主要用于解決具有重疊子問題和最優子結構特性的問題。 一、動態規劃的基本概念 1. 最優子結構 一個復…

視覺革命來襲!ComfyUI-LTXVideo 讓視頻創作更高效

探索LTX-Video 支持的ComfyUI 在數字化視頻創作領域&#xff0c;視頻制作效果的提升對創作者來說無疑是一項重要的突破。LTX-Video支持的ComfyUI便是這樣一款提供自定義節點的工具集&#xff0c;它專為改善視頻質量、提升生成速度而開發。接下來&#xff0c;我們將詳細介紹其功…

Java版ERP管理系統源碼(springboot+VUE+Uniapp)

ERP系統是企業資源計劃&#xff08;Enterprise Resource Planning&#xff09;系統的縮寫&#xff0c;它是一種集成的軟件解決方案&#xff0c;用于協調和管理企業內各種關鍵業務流程和功能&#xff0c;如財務、供應鏈、生產、人力資源等。它的目標是幫助企業實現資源的高效利用…

CenOS7切換使用界面

永久切換 在開始修改之前&#xff0c;我們首先需要查看當前的啟動模式。可以通過以下命令來實現&#xff1a; systemctl get-default執行此命令后&#xff0c;系統會返回當前的默認啟動模式&#xff0c;例如graphical.target表示當前默認啟動為圖形界面模式。 獲取root權限&…

Dify使用總結

最近完成了一個Dify的項目簡單進行總結下搭建服務按照官方文檔操作就行就不寫了。 進入首頁之后由以下組成&#xff1a; 探索、工作室、知識庫、工具 探索&#xff1a; 可以展示自己創建的所有應用&#xff0c;一個應用就是一個APP&#xff0c;可以進行測試使用 工作室包含…

計網學習筆記———網絡

&#x1f33f;網絡是泛化的概念 網絡是泛化的概念 &#x1f342;泛化理解 網絡的概念在生活中無處不在舉例&#xff1a;社交網絡、電話網路、電網、計算機網絡 &#x1f33f;網絡的定義 定義&#xff1a; 離散的個體通過通訊手段連成群體&#xff0c;實現資源的共享與交流、個…

《Python星球日記》 第53天:卷積神經網絡(CNN)入門

名人說&#xff1a;路漫漫其修遠兮&#xff0c;吾將上下而求索。—— 屈原《離騷》 創作者&#xff1a;Code_流蘇(CSDN)&#xff08;一個喜歡古詩詞和編程的Coder&#x1f60a;&#xff09; 目錄 一、圖像表示與通道概念1. 數字圖像的本質2. RGB顏色模型3. 圖像預處理 二、卷積…

SpringBoot2集成xxl-job詳解

官方教程 搭建調度中心 Github Gitee 注&#xff1a;版本3.x開始要求Jdk17&#xff1b;版本2.x及以下支持Jdk1.8。如對Jdk版本有訴求&#xff0c;可選擇接入不同版本 clone源代碼執行xxl-job\doc\db\tables_xxl_job.sql # # XXL-JOB v2.4.1 # Copyright (c) 2015-present, x…

HashMap中put()方法的執行流程

HashMap 是 Java 中最常用的數據結構之一&#xff0c;用于存儲鍵值對。其 put() 方法是向哈希表中插入或更新鍵值對的核心操作。本文將詳細解析 put() 方法的執行過程&#xff0c;涵蓋哈希值計算、桶定位、沖突處理和擴容等步驟。 一、put() 方法的執行過程 put() 方法通過一系…

【Oracle認證】MySQL 8.0 OCP 認證考試英文版(MySQL30 周年版)

文章目錄 1、MySQL OCP考試介紹2、考試注冊流程3、考試復習題庫 Oracle 為慶祝 MySQL 30 周年&#xff0c;截止到2025.07.31 之前。所有人均可以免費考取原價245美元 &#xff08;約1500&#xff09;的MySQL OCP 認證。 1、MySQL OCP考試介紹 OCP考試 OCP認證是Oracle公司推…

SpringBoot框架開發網絡安全科普系統開發實現

概述 基于SpringBoot框架的網絡安全科普系統開發指南&#xff0c;該系統集知識科普、案例學習、在線測試等功能于一體&#xff0c;本文將詳細介紹系統架構設計、功能實現及技術要點&#xff0c;幫助開發者快速構建專業的網絡安全教育平臺。 主要內容 系統功能架構 本系統采…

瀏覽器HTTP錯誤、前端常見報錯 和 Java后端報錯

以下是 瀏覽器HTTP錯誤、前端常見報錯 和 Java后端報錯 的綜合整理&#xff0c;包括原因和解決方法&#xff0c;幫助你快速排查問題。 一、HTTP 錯誤&#xff08;瀏覽器報錯&#xff09; 錯誤碼原因解決方法400 Bad Request請求語法錯誤&#xff08;如參數格式錯誤、請求體過…

TypeScript簡介

&#x1f31f; TypeScript入門 TypeScript 是 JavaScript 的超集&#xff0c;由微軟開發并維護&#xff0c;通過靜態類型檢查和現代語言特性&#xff0c;讓大型應用開發變得更加可靠和高效。 // 一個簡單的 TypeScript 示例 interface User {name: string;age: number;greet():…