【linux內核系列】:萬字詳解進程間通信:消息隊列

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

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

在這里插入圖片描述

💪 今日博客勵志語錄你討厭的現在,是未來的你拼命想回去修正的戰場。

★★★ 本文前置知識:

匿名管道

命名管道

共享內存


前置知識回顧(如果對此內容熟悉的讀者,可以跳過)

那么我們知道進程之間具有通信的需求,因為有的任務需要進程之間合作協同來完成,那么這個時候就需要進程之間分工合作,那么進程之間就需要知道彼此的進度以及完成情況,所以進程之間具有通信的需求,但是由于進程之間的獨立性,那么進程無法訪問到彼此的數據比如地址空間以及頁表等等,那么操作系統為了滿足進程間通信的需求以及保證進程之間的獨立性,那么此時操作系統讓進程間通信的核心思想就是創建一份公共內存區域,然后讓通信的進程雙方看到這塊公共區域從而實現通信,而此前,我們學習了三種進程間通信的方式,分別是匿名管道以及命名管道和共享內存,那么接下來我們就來簡單回顧這三種方式

1.匿名管道

那么我們知道進程間通信的核心思想就是創建一份公共的內存區域,然后讓通信的進程雙方能夠看到,而對于父子進程來說,那么當我們的父進程在代碼層面調用fork接口,那么此時會創建一個子進程,那么該子進程會拷貝父進程的task_struct結構體并且修改其中的部分屬性并且與父進程共享物理內存頁,那么我們知道task_struct結構體中有一個關鍵的屬性,那么其指向了一個指針數組,那么該指針數組就是文件描述符表,其中每一個元素都是一個指向file結構體的指針,記錄了父進程打開的所有的文件,那么由于子進程是拷貝父進程的task_struct結構體,其中就包括拷貝父進程的指針數組,那么意味著子進程會繼承以及共享父進程打開的文件,那么根據剛才進程通信的核心思想,那么此時文件就可以作為這個公共資源來讓父子進程實現通信,那么這就要求父進程在調用fork創建子進程之前,先打開一個用于和之后創建的子進程通信的文件,那么打開之后再調用fork創建子進程,那么子進程就可以順利的繼承該文件,然后父子進程就可以向該文件讀寫從而發送消息來實現通信,這就是父子進程通信的原理

但是如果父進程打開的該文件是一個普通文件,然后父進程以讀寫方式打開,獲取其文件描述符,然后再創建完子進程,那么父子進程就可以持有這個文件描述符來向該文件讀寫從而發送消息,但是由于普通文件的讀寫偏移量是共用的,那么意味著只要一個進程讀或者寫,那么其都會修改偏移量,而讀寫共用一個偏移量意味著進程無法讀取到之前的內容,并且如果通信雙方進程都向文件中有寫入,進程也無法區分該內容究竟是哪個進程寫的導致內容混亂,所以這里我們知道父子進程要通過該文件進行通信,必然雙方不能既可以讀又可以寫,否則會造成內容混亂,只能一個進程只讀而另一個只寫,所以這里父進程可以分別以只讀和只寫打開同一個文件,那么此時會創建兩個file結構體,然后父子進程各自持有這兩者中的其中一個的文件描述符,那么兩個file結構體都有各自的偏移量,那么父子進程之間就可以實現通信

那么這個方式肯定是正確的,但是唯一的問題就是這里創建的是磁盤級別的文件,那么意味著該文件保存的進程間通信的消息會被寫入到磁盤中,而磁盤是用來保存需要長期存儲的數據的,而這些通信之間的消息顯然不會長期存儲,所以這里最完美的情況則是創建一個內存級別的文件,那么創建內存級別的文件就不再是調用open接口,而是調用pipe系統調用接口,那么它底層會為我們創建一個內存級別的文件,并且還會分別創建只讀以及只寫的file結構體,并且返回這兩個結構體對應的文件描述符,然后父進程調用完pipe接口,然后再調用fork接口來創建子進程,此時子進程就會和父進程共同持有只讀以及只寫的file結構體的文件描述符,接著雙發各自持有其中一個讀寫端,也就是一個只讀,一個只寫,然后雙方就可以利用文件描述符來訪問該文件從而進行讀或者寫,而該文件就是我們的匿名管道文件,這就是匿名管道通信的原理

2.命名管道

那么匿名管道則是針對父子進程之間通信,因為創建子進程,其中子進程的task_struct結構體是拷貝復制父進程的task_struct結構體并且其中修改部分屬性,所以子進程能夠繼承或者說共享與父進程打開的文件,那么所以父子之間可以通過文件描述符來訪問文件,而對于非父子進程來說,那么它們之間便無法通過文件描述符來訪問文件由于非父子關系,而根據上文講述的進程通信的原理,那么首先是要創建一份共享資源,那么這個共享資源此時還是由文件來承擔,那么就需要我們創建一個文件,那么有了匿名管道的通信經驗,這里我們就知道通信的進程雙方各自調用open接口應該以讀或者寫打開該文件而不能以讀寫模式打開該文件,那么此時雙方進程各自持有讀寫一端的文件描述符,然后就調用write或者read來對文件進行讀寫即可,而這里有上文匿名管道的經驗,那么我們也知道,這里的文件一定也是內存級別文件,而與匿名管道的區別則是,由于后序我們要通過open接口打來創建好的該管道文件,而open接口接收一個帶有路徑以及文件名的字符串,所以這里的管道一定是具有路徑和名稱的,這也是為什么稱之其為命名管道文件,那么這里就需要我們調用mkfifo接口來創建命名管道文件

3.共享內存

而這里的第三種通信方式也就是共享內存,本質上就是一份或者多份物理內存頁,然后被系統用來分配通信的進程所使用,而根據通信的核心思想,那么假設系統為我們分配好了物理內存頁,那么此時我們首先解決了第一步創也就是建一份共享資源解決了,那么下一步就是讓通信的進程雙方看到該共享資源,而我們知道進程訪問數據,那么它手頭上持有只有虛擬地址,也就是說進程只能通過虛擬地址來訪問內存中的數據,然后再經過頁表的映射再轉化為實際的物理內存地址,所以這里要讓進程雙方看見該共享內存,那么系統就得想辦法把在通信的進程雙方的頁表建立該共享內存對應的物理內存頁的虛擬地址到物理地址的映射條目,以及進程的進程地址空間設置好相應的共享內存段,那么這一步我們稱之為掛接,最后再讓進程持有該物理內存頁的虛擬地址從而讓進程雙方能夠訪問到共享內存,那么這就是共享內存通信的一個大思想,那么接下來我們再來回顧一下該過程的一個具體細節

那么假設現在有一對采取共享內存通信的進程A和B,那么他們做的第一步就是請求操作系統為該通信的進程雙方分配物理內存用來通信,而在內核中通信的進程不只有一對,而是多對,那么意味著系統中存在多份共享內存,那么系統肯定就要管理這么多的共享內存,那么管理的方式就是先描述再組織,所以系統會為每一個共享內存創建一個結構體,其中記錄了其相關屬性,并且為了區分不同的共享內存,那么每一個共享內存肯定被分配了一個唯一的標識符shmid,而請求系統創建共享內存這個動作就是我們調用系統提供的shmget接口來完成,那么該接口會返回就給通信的進程雙方返回該共享內存的標識符shmid,而shmid也是該結構體中的其中一個重要屬性,所以到時候,通信的進程雙方再通過該標識符,讓系統將對應的共享內存掛接到通信的進程雙方即可

而現在關鍵問題就是這里請求操作系統為該通信的進程雙方創建一份共享內存的動作假設交給進程A來完成,而此時不需要進程B再重復請求系統創建共享內存了,而進程B只需要獲取A進程請求系統為其創建好的共享內存的shmid,那么問題就是其中此時A進程請求該系統創建共享內存,那么A進程作為請求方,那么其調用完shmget,那么系統成功創建完共享內存肯定會向其向其返回shmid,所以A進程能夠成功獲取shmid,而對于B進程來說,它則需要讓操作系統返回為A進程請求系統為其創建好的共享內存的shmid,但是共享內存那么多,系統怎么知道哪一個共享內存是用來該對進程通信用的呢,并且進程之間具有獨立性,那么它也不可能訪問到另一個A進程的數據從而獲取shmid

所以這個時候就需要key值登場了,那么它和shmid一樣叫共享內存的標識符,它也是共享內存結構體中的其中一個屬性,但是與shmid不同的是,那么它是用戶態的標識符,在雙方進程通信之前,都會先事先持有該key,那么接下來我可以來舉一個例子來理解這里所謂的key:假設A進程和B進程分別對應今晚住酒店的一對情侶,那么此時A到酒店前臺來預定了今晚的房間,那么酒店前臺的人會告訴他房間號是多少,但是由于A和B彼此之間不能見面交流(進程的獨立性),那么A為了讓B找到房間號,所以他們在預定酒店之前彼此都持有一個相同的標記,那么A會讓酒店前臺的服務員到該預定的房間門口畫一個標記,然后B來獲取A今晚預定的酒店的房間號的時候,那么就只需要告訴前臺的服務員標記長什么樣子,然后前臺的服務員就會依次查看各個房間的門口,然后找到帶有特定對應的標記房門,然后將房間號告訴給B

那么通過這個例子,你應該能夠理解key雖然也叫共享內存的標識符,但是它的作用是不一樣的,而再剛才的例子中,這個標記就是key值,而酒店前臺的服務人員就是操作系統,那么這里我們就能理解shmget的第一個參數key的作用了:那么一個進程用來創建共享內存,并且告訴該系統key值也就是向shmget接口傳遞第一個參數,然后系統分配好了物理內存頁以及創建了對應的結構體的同時還會設置好該結構體對應的key值字段,就像上文例子中服務員在房間門口畫標記一樣,那么另一個進程只需告訴系統key值,然后系統會遍歷共享內存的結構體,然后找到匹配的key值,最后返回shmid,而上文我加粗了“特定對應的標記”這幾個字,那么是因為每一個通信的進程雙方獲取對應共享內存的shmid,都會通過借助key值來尋找,所以這里的key值會有沖突,意味著會有相同的key值的共享內存存在因為key是由用戶設定不是由內核設定,就會導致返回不屬于該通信進程雙方的共享內存的shmid,所以為了盡量避免相同的key值,可以調用ftok函數來生成一個key值,那么它會接收一個已存在的帶有路徑和文件名的字符串和一個整數,然后根據路徑和文件名得到其inode編號然后與整數進行相應的運算得到一個沖突概率較小的key值,那么shmget接口的參數就是一個key值和一個宏定義,那么這個宏定義就決定了該接口的行為,那么IPC_CREAT是創建一個共享內存并返回其shmid,而IPC_CREAT|IPC_EXEL則是返回一個已創建的共享內存shmid

那么獲取完共享內存之后,那么下一步便是掛載,此時通信進程雙方持有共享內存的shmid,那么接下來就各自調用shmat接口,然后讓操作系統將對應的共享內存掛載進程雙方,也就是添加頁表的映射條目以及設置好地址空間的共享內存段,那么最后返回該共享內存對應的物理內存頁的起始的虛擬地址,那么有了虛擬地址之后,我們就可以將這個虛擬看做一個類似于在堆上申請的字符數組的首元素的地址,那么進程雙方就以字節為單位向共享內存中寫入以及讀取了,那么這就是共享內存通信的一個大致原理


消息隊列

那么繼匿名管道以及命名管道和共享內存,那么這次我們就要介紹我們的通信的第四種方式,便是我們的消息隊列,那么消息隊列從名字我們便能看出一些內容來,那么它的名字帶有隊列兩個字,那么沒錯,消息隊列的底層結構其實就是隊列這個數據結構,那么這個數據結構不是由我們通信的進程來創建,而是由我們系統來創建,那么隊列的特性我們也知道,那么便是先進先出,從隊尾插入從隊頭刪除,那么隊列的實現一般采取的是雙向鏈表來實現

1.原理

那么消息隊列是如何實現我們的通信的了,那么我們知道消息隊列本質上就是一個鏈表結構的一個隊列,那么該隊列中或者說鏈表中的每一個節點就是一個消息塊,那么這個消息塊可能是A進程或者B進程寫的,那么之前學過匿名以及命名管道以及共享內存的讀者,那么知道采取這三種方式的通信的進程雙方不能同時對共享資源進行讀寫,也就是只能一個進程擔任信息的發送方,也就是作為寫端,而另一個進程作為接收方,也就是作為讀端,那么之所以進程只能擔任一個角色就是因為通信的進程雙方在進行讀取或者寫入的時候都是訪問的同一份共享資源,那么以匿名管道為例,那么如果通信的進程雙方都進行讀寫,就會導致內容混亂,那么一個進程寫入后的內容可能被另一個進程所覆蓋,而在消息隊列這個場景下,那么如果通信的進程此時有發送消息的需求,那么它就只需要創建一個節點,然后將消息寫入,再將節點放入到隊列當中,那么在這個場景下,那么每一個進程只會操作自己的消息塊或者說節點,而不會存在兩個或者多個進程共同使用一個消息塊的情況,所以消息隊列的優勢相較于前面的共享內存以及匿名和命名管道相比,就是進程既可以作為發送方也可以作為接收方,那么如果想要讀取消息,那么就直接從隊列中取出節點即可

msgget

那么了解了消息隊列通信的一個大致的原理之后,那么我們再來梳理一下消息隊列通信的一個過程,那么假設現在我們有一個進程對,A和B進程,那么這兩個進程之間要采取消息隊列的方式進行通信,那么首先第一步就是需要A或者B其中一個進程來請求操作系統為該通信的進程雙方創建一個消息隊列,那么假設這個動作交給A完成,那么對于B來說,那么它要做的就是獲取A請求操作系統為其創建好的消息隊列,那么在系統中通信的進程不只有一個,那么意味著內核中的消息隊列也不只有一個,那么系統要管理這么多的消息隊列,那么采取的方式就是先描述,再組織,那么內核會為消息隊列定義一個struct msg_queue結構體,其中記錄了該消息隊列的相關屬性,包括最后一次發送消息的進程的PID以及節點數等等,同時為了區分內核中不同的消息隊列,那么每一個消息隊列也具有唯一的一個標識符msqid,所以在請求系統創建好消息隊列之后,那么A和B進程就需要獲取內核為它們創建好的消息隊列的標識符,從而再之后的消息的發送以及接收便于告訴內核我是發送消息到哪個消息隊列以及接收哪個消息隊列的消息

那么這里A進程請求完系統,那么系統創建好消息隊列后,肯定會返回消息隊列的msgid給A進程,那么關鍵是B進程怎么得知系統為其創建好的消息隊列是哪一個呢,那么它不可能訪問A進程的數據來獲取msqid,所以這里就會面臨和共享內存同樣的問題,那么解決方法也是一樣,那么就是A和B進程事先會約定一個key值,那么A進程在請求系統創建消息隊列的時候,會把key值交給內核,那么內核創建好消息隊列以及對應的結構體之后,同時會設置好該結構體的key值字段,那么B進程只需要告訴系統key值,那么系統只需遍歷結構體然后找到匹配的key值,最后返回對應的msqid即可

那么這個請求隊列創建消息隊列以及獲取已經創建好的消息隊列的內容便是msgget接口的一個內容

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

那么第一個參數就是接收key值,那么至于為什么要接收key值以及key值的作用,那么上文已經說過

而第二個參數就是接收一個宏,那么這個宏就控制了msgget的行為,那么是創建消息隊列還是獲取已經存在的消息隊列:

  • IPC_CREAT (01000):如果消息隊列不存在則創建
  • IPC_EXCL (02000):不能單獨使用,與IPC_CREAT一起使用,若消息隊列已存在則失敗

msgsnd

那么第一個過程也就是創建消息隊列就已經結束,那么此時A和B進程已經獲取了消息隊列的標識符也就是msqid,那么下一步就是發送消息和接收消息,那么這里我們假設A進程它作為發送發,而B進程作為接收方,我們知道消息隊列本質就是一個鏈表,那么這里A進程就需要定義自己的消息塊:

struct msgbuf {long mtype;       // 消息類型/優先級char mtext[]; // 自定義數據區
};
//包含在頭文件中,不需要用戶自己定義

那么這里的消息塊是由兩部分所組成,分別是消息類型以及數據區,那么數據區就是寫入的消息,而這里的mytype字段則有兩個作用,那么它即可以作為消息類型也可以作為優先級,那么這里我們先講解一下mytype作為消息類型的一個含義,那么之前的匿名管道以及命名管道和共享內存,那么我們通信的場景都是只建立在一對進程上,也就是只有兩個進程通信,而在消息隊列這里,通信的進程就不僅僅只局限與一對進程而是一個進程組,那么這里A進程發出的消息,可以給進程B發,也可以給進程c發或者進程D,那么到時候這個消息肯定是針對發給某個特定的進程,那么我們就可以利用mytype給當前信息做一個標記,比如0代表是給進程B發送的,1代表是給進程C發送的,那么到時候b進程就只接受0號類型的消息而不接受1號類型的消息

同理這里的mytype還可以用作優先級,那么如果當前消息是特別重要的消息,那么我們可以將mytype設置為2,如果是正常消息則設置為1,那么mytype值越大意味著優先級越高,那么優先級越高那么意味著它越容易被彈出被進程接收到,所以這里mytype也有優先級的一個場景


而這里的mtext數組就是存放我們的消息內容,并且該數組是柔性數組,那么所謂的柔性數組,就是我們結構體里面定義一個長度為0或者沒有長度的數組:

struct node
{datatype member1;datatype array[];//或者datatype[0];
}

那么此時這種寫法,那么編譯器默認不會為該結構體的所有的成員變量開辟空間,那么這里編譯器就將最后一個成員變量也就是該數組識別為一個標識符,那么如果我們要開辟空間,那么則是我們需要在malloc或者new的時候額外指定一個長度或者說大小,那么該大小就是給柔性數組分配的大小,并且注意柔性數組一定得得是最后一個成員變量,如果是中間的某個成員變量,因為柔性數組的長度是由用戶決定而編譯器未知,而結構體的各個成員變量的偏移量要遵從內存對齊,而由于中間的柔性數組的大小未知,那么編譯器無法確定柔性數組之后的成員變量,所以柔性數組只能在末尾,其次它不能是棧對象,因為棧對象要求編譯期間就能確定結構體的大小,而這里的柔性數組的長度是可以變化,比如:

struct node
{datatype member1;datatype array[];
};
int main()
{int N;scanf("%d",&N);struct node* ptr=(struct node*)malloc(sizeof(node)+N);
}

在這個場景下,那么編譯期間是編譯器是無法確定該node結構體的大小,只能在運行期間獲取了用戶的鍵盤輸入之后才能確定大小,所以意味著柔性數組所在的結構體的,只能是堆對象,不能是棧對象

其次柔性數組的意義也就是為了節省空間,避免空間的利用率,那么這里如果你開辟了假設100個字節的數組,但是其中只用了4字節,那么明顯大部分空間浪費,而有的小伙伴可能會采取的是指針,定義一個指針成語變量,到時候創建一個結構體對象,再按需malloc一定大小的數組,然后讓指針指向該動態數組的首元素從而減少空間的浪費

struct node
{datatype member1;datatype* ptr;
};
int main(){struct node l1;
l1.ptr=(datatype*)malloc(size);
}

那么這里理論上肯定是可行的,但并不是最完美的,因為指針的出現,必然會讓CPU進行二次尋址,那么訪問兩次內存,雖然節省了空間,但是卻付出了性能的代價,而我們一般在意時間以及性能上的損失,所以這里最優秀的還是柔性數組的設計

那么有了消息塊之后,那么接下來發送方進程只需要調用msgsed接口:

  • msgsnd
  • 頭文件:<sys/types.h> <sys/ipc.h> <sys/msg.h>
  • 函數聲明:int msgsnd(int msqid,const void* msgp,size_t msgsz,int msgflg);
  • 返回值:調用成功返回0,調用失敗則返回-1,并設置errno

那么這里的第一個參數就是發送的消息隊列的標識符,第二個參數就是消息塊也就是結構體,而第三個參數就是消息的長度,那么這里的第三個參數就要注意,很多讀者傳的第三個參數是結構體的大小,也就是sizeof(msgp),但其實第三個參數傳遞的是結構體的數組也就是消息的長度,那么之所以傳的是消息的長度,那么是因為到時候內核獲取到消息塊之后,那么這個結構體的兩個成員變量分別是mytype以及存儲消息的數組會被嵌入或者說拷貝到消息隊列的鏈表的節點的結構體中,那么這里的mytype是第一個成員變量,那么其數據類型固定是long類型,那么系統就可以獲取第二個成員變量也就是柔性數組的起始位置,然后系統在根據我們傳遞的第三個參數也就是msgsz來確定拷貝的數組長度避免越界,所以這里我們就只需要傳遞數組中的消息的長度即可:

// 發送示例
// 發送一條消息
struct msgbuf* msg = (struct msgbuf*)malloc(sizeof(long) + text_len);msg->mtype = 1;                    // 消息類型 (8字節)
strcpy(msg->mtext, "Hello");       // 消息內容// text_len = strlen("Hello") + 1 = 6
msgsnd(msqid, msg, 6, 0);         // 第三個參數只指定柔性數組大小

而這里msgsnd的第4個參數則是一個宏定義,因為我們一個消息隊列中存儲的節點的上限是由要求的,那么意味著會存在消息隊列已滿的情況,那么消息隊列已滿,此時消息的發送方就要等待一個空閑的節點,那么此時就會被陷入阻塞,那么這里的第4個參數就是可以采取等待的方式:

  • msgflg : 控制標志,可以是以下值的組合:

  • IPC_NOWAIT: 如果消息隊列已滿,立即返回而不等待

  • 0:阻塞等待直到有空間可用

// 位置:include/linux/msg.h
//消息隊列節點對應的結構體
struct msg_msg {struct list_head m_list;   // 鏈表指針(連接隊列節點)long mtype;                // ? 消息類型/優先級size_t m_ts;               // ? 消息正文長度(字節)// 大消息分塊管理struct msg_msgseg *next;   // 指向下一個分塊(>4KB消息)// 安全模塊相關void *security;            // SELinux/AppArmor安全上下文// ? 核心:消息內容存儲區unsigned char payload[];   // 柔性數組(存儲實際消息)
};
struct list_head {struct list_head *next, *prev;
};

msgcrv

那么消息發出去之后,那么就得有接收方進程來接收消息隊列中的消息,那么接收方進程也得準備一個緩沖區用來拷貝接收的消息,那么接收方接收消息就是調用msgrcv接口:

  • msgrcv
  • 頭文件:<sys/types.h> <sys/ipc.h> <sys/msg.h>
  • 函數聲明:int msgrcv(int msqid,const void* msgp,size_t msgsz,long msgtyp,int msgflg);
  • 返回值:調用成功返回0,調用失敗則返回-1,并設置errno

那么這里的第一個參數我們已經很熟悉了,而這里的第二個參數就是我們要定義一個結構體來作為緩沖區,其中的消息會被寫入到該結構體的柔性數組中去,而第三個參數就是讀取的消息的長度,按照該長度拷貝到柔性數組中去,而第四個參數就是消息的類型

而最后一個參數則是接收一個宏定義來控制該接口的行為:

IPC_NOWAIT :如果沒有符合條件的消息,立即返回而不等待

MSG_NOERROR :如果消息長度超過 msgsz,截斷消息而不返回錯誤

MSG_EXCEPT (Linux特有) :接收類型不等于 msgtyp 的第一條消息

struct msgbuf {long mtype;     // 消息類型,必須 > 0char mtext[100];  // 消息數據(實際可以是任意長度)
};
int main()
{struct msgbuf msg;msgrcv(msqid,&msg,sizeof(msg.mtext),0,IPC_NOWAIT);
}

msgctl

那么最后我們使用完該消息隊列資源,結束通信之后,那么接下來要做的就是清理消息隊列資源,那么就需要一個進程來請求操作系統來刪除該消息隊列,那么就需要調用msgctl接口:

  • msgctl
  • 頭文件:<sys/types.h> <sys/ipc.h> <sys/msg.h>
  • 函數聲明:int msgctl(int msqid,int cmd,struct msqid_ds *buf);
  • 返回值:調用成功返回0,調用失敗則返回-1,并設置errno

那這里的msgctl的第二個參數就是接收一個宏定義來控制其行為:

IPC_STAT - 獲取消息隊列的狀態信息,存儲在 buf 指向的結構中

IPC_SET - 設置消息隊列的參數,從 buf 指向的結構中獲取

IPC_RMID - 立即刪除消息隊列

IPC_INFO - 獲取系統范圍內的消息隊列限制信息 (Linux 特有)

MSG_INFO - 獲取消息隊列資源消耗信息 (Linux 特有)

MSG_STAT - 類似 IPC_STAT ,但通過索引查找隊列 (Linux 特有)

其中要刪除,我們就傳IPC_RMID即可,而第三個參數就傳NULL,而如果你想要查看當前消息隊列的屬性,那么你可以傳IPC_STAT,然后再自己定義一個struct msqid_ds結構體作為輸出型參數傳遞第三個參數,到時候就可以訪問成員變量來查看其屬性

struct msqid_ds {struct ipc_perm msg_perm;  /* 所有權和權限 */time_t          msg_stime; /* 最后發送消息的時間 */time_t          msg_rtime; /* 最后接收消息的時間 */time_t          msg_ctime; /* 最后修改時間 */unsigned long   msg_cbytes; /* 當前隊列中的字節數 */msgqnum_t       msg_qnum;  /* 當前隊列中的消息數 */msglen_t        msg_qbytes; /* 隊列最大字節數 */pid_t           msg_lspid; /* 最后發送消息的進程PID */pid_t           msg_lrpid; /* 最后接收消息的進程PID */
};

2.補充知識

1.消息隊列的模型優化

那么大部分讀者可能會認為消息隊列的一個模型就是內核會創建以及維護一個鏈表,然后該鏈表中的所有的節點便是包含通信的進程發送的消息,但是在我們上文的介紹中,我們知道一個進程發送的消息塊,除了有自己進程自己要發送的消息的內容,那么還有一個mytype字段,而mytype字段其中一個非常關鍵的含義,便是優先級,那么我們知道雖然隊列這個數據結構是從隊尾插入然后從隊頭刪除,但是由于消息有優先級的存在,那么雖然該消息是當前隊列中新發送的消息,但是它的位置卻不一定在隊尾,因為它還有優先級存在,所以系統還得根據其優先級調整它的位置,而隊列是由鏈表實現的,那么如果所有節點都在一個鏈表中,那么意味著系統要從隊頭開始往后遍歷然后找到合適位置插入該節點,那么遍歷的代價是O(N),其次由于每一個節點物理內存不連續,每一個節點都通過指針來連接,那么意味著CPU還需要多次尋址訪問內存,那么還會付出性能上的代價,所以這里內核的實現上不是將所有節點都放在一個鏈表中,而是按照優先級分成了多個鏈表,那么我們知道內核中存在多個消息隊列,那么系統為了管理消息隊列,那么采取的方式是先描述再組織,會為消息隊列定義對應的結構體struct msg_queue,那么這里我們可以在結構體中查看到一個字段:

// 位置:include/linux/msg.h
struct msg_queue {struct kern_ipc_perm q_perm;  // ? IPC權限控制塊// 時間戳time64_t q_stime;             // 最后發送時間time64_t q_rtime;             // 最后接收時間time64_t q_ctime;             // 最后修改時間// 資源統計unsigned long q_cbytes;       // ? 當前隊列總字節數unsigned long q_qnum;         // ? 當前消息數量unsigned long q_qbytes;       // ? 隊列最大字節限制// 進程追蹤pid_t q_lspid;                // ? 最后發送進程PIDpid_t q_lrpid;                // ? 最后接收進程PID// ??? 核心:優先級分桶鏈表struct list_head q_messages[PRIO_BUCKETS]; // 高級功能struct mq_attr attr;          // POSIX擴展屬性struct user_struct *user;     // 用戶資源追蹤struct ns_common *ns;         // 命名空間
};

那么就是struct list_head q_message[],那么該字段的本質是一個指針數組,一般長度為32,那么這里的指針數組的每一個元素是一個指針,指向的就是一個鏈表,那么指向的每一個鏈表就代表著優先級,那么該鏈表中的每一個節點的優先級都是相同的,而數組的索引也就對應這優先級的大小,數組索引越大,其對應的鏈表的優先級越高,那么最后一個位置的指針指向的鏈表的優先級是最高的,所以假設現在有一個進程發送了一個消息塊,那么其mytype也就是優先級的大小是31,那么這里節點的插入就是只需要確定其在哪一個鏈表即可,確定完之后直接尾插,那么這里就采取取模運算31%32=31來定位指針數組的索引,那么確定是在下標為31的鏈表中,而這里數組中指向的每一個鏈表都是帶有哨兵節點的帶頭雙向循環鏈表,那么意味著我們不用遍歷一遍鏈表找到尾結點,那么直接通過哨兵節點就可以找到尾結點,然后直接尾插即可,而該數組中的每一位位置的指針指向的就是鏈表的哨兵節點,那么這個模型相比于之前的所有節點存儲在一個鏈表,那么它的時間復雜度是O(1),并且采取指針數組,那么也對CPU的緩存友好,可以將整個指針數組加載到緩存,然后再只需要訪問一次內存,將鏈表該加載進來,那么消息的彈出就是會從后往前遍歷數組,如果當前位置的鏈表為空就遍歷之后的位置指向的鏈表,然后刪除隊頭元素

其次注意的就是這里的優先級的問題,根據上文我們知道優先級是會進行取模運算確定數組的下標,那么這里可能存在這種情況,那么假設我們現在有一個mytype為1的消息和一個mytype為65的消息,那么從數值上來看,那65的優先級比61的優先級大,但是實際模上32之后,他們的優先級都是1,會被視作優先級相同的消息,所以這里我們就得注意在設置消息的優先級的時候,盡量不要設置太高,否則這里如果你定義65是緊急消息,1是正常消息,緊急消息需要先被接收彈出,但是根據上文的原理,實際上65的消息可能在正常消息也就是優先級為1的消息之后

而如果這里你的mytype的含義不是優先級,比如0的含義是給A進程接收,1是給B進程接收,這里你的mytype表示是類型,所以這里0和1按照你的理解優先級是一樣的,不需要誰先誰后接收,但是對于內核來說,它不知道mytype的具體含義,內核還是統一將0和1視作優先級來處理

2.IDR樹

我們之前已經介紹了共享內存,而現在有引入了消息隊列,那么我們目前知道,系統中存在多種不同類型的共享資源,而每一種類型的共享資源又有多個,那么系統管理特定類型的共享資源比如共享內存或者消息隊列,那么采取的方式就是先描述再組織的方式為每一個特定類型的共享資源定義結構體,但是操作系統又是如何管理整個不同類型的共享資源呢?

而這里我們可以發現不同ipc資源對應的結構體都有一個共性,不管是共享內存對應的結構體還是消息隊列乃至后面的信號量對應的結構體,那么它們的結構體的第一個字段都是一個ipc_perm結構體,其中記錄了相關的權限以及key值和所屬組編號等,那么此時我們可以建立這樣一個模型,那么就是線性的數組模型,那么操作系統可以創建一個數組,那么該數組是一個指針數組,其中里面存儲了所以不同類型的ipc資源,那么數組中每一個元素都是一個指向ipc_perm結構體的指針,該ipc_perm結構體可以是屬于不同ipc資源的結構體,那么每一個ipc資源的結構體中還有一個ipc類型的字段,不管是什么類型的ipc資源,那么該字段相對于起始位置的偏移量是固定的,而由于ipc_prem是結構體的首個成員變量,那么其指針指向ipc_perm的地址,就是該結構體的首地址,那么我們可以移動固定的偏移量,獲取到ipc類型,那么接下來就可以將指針強制類型轉化,就可以訪問該ipc資源對應結構體的各個屬性

那么在這個模型下,我們創建一個ipc資源,比如共享內存或者消息隊列,我們知道內核會持有兩個東西,分別是key值以及ipc資源類型,然后會遍歷該數組找到空閑位置,如果找到就創建對應ipc類型的結構體,然后讓數組的該位置的元素也就是指針指向ipc_perm,并且返回該數組的下標,所以我們之前的共享內存的shmid和消息隊列的msgid就是這個數組下標

而查找ipc資源的時候,那么數組也會持有key值和ipc資源類型,遍歷該數組,如果發現ipc_perm的key值匹配,再移動一定的偏移量看ipc類型是否匹配,如果兩者都匹配,那么就返回該數組的下標即可,而之后比如像共享內存的shmat以及消息隊列的msgsnd接口,那么都會用到下標,那么系統獲取到下標就可以直接定位數組對應位置訪問到對應的ipc資源。

那么該模型理論上肯定是沒有問題的,但是唯一的缺點就是效率問題,因為當我們創建一個ipc資源的時候,我們有可能需要掃描整個數組,那么時間復雜度就是O(N),所以內核在此模型下進行了一個優化,那么就是采取的樹的結構來實現,采取的是一個三層的64叉樹,那么每一層的每個節點對應64個分支,那么最后一層的葉子節點就是存儲實際有效內容也就是指向ipc_perm結構體的指針,而首層的根節點和中間層的節點,那么他們的節點的構造就是包含一個位圖和一個長度為64的指針數組,那么位圖則是64個比特位,每一個比特位對應一個分支,該比特位為1代表該分支有空閑位置,比特位為0代表該分支沒有空閑位置,那么64個比特位對應8個字節,那么就可以用long類型的數來表示,那么長度為64的指針數組則是指向下一層的分支,并且這里我們不是把不同的ipc資源都存放到該樹中,建立多個獨立的IDR樹用來存放對應類型的ipc資源,比如共享內存對應一個IDR樹,然后消息隊列也對應一個IDR樹,這樣就更提高了查找的效率相比于之前的混合存儲,那么接下來我們再來看一下IDR節點對應的結構體的樣子:

// 包含IDR節點的內核源碼 (linux/lib/idr.c)
struct idr_layer {int          prefix;        // 本節點管理的ID前綴int          layer;         // 當前層深度(0=葉子)unsigned long bitmap; // ?核心位圖:64位比特位struct idr_layer *slots[64];// ?指針數組:64個指針槽位
};

那么我們知道我們在訪問某個共享資源的時候,比如對于共享內存來說,那么要進行掛接我們需要調用shmat接口,那么需要給內核傳遞一個shmid,而對于消息隊列,我們要發一個消息,需要告訴內核msqid,那么這個標識符就對應了IDR樹中葉子節點的一個編號,那么內核到時候會根據這個編號到對應的IDR樹中去定位葉子節點,那么這里我們是從根節點逐層往下遍歷知道最后一層的葉子節點,那么每一層的節點都有64個分支,那么每一個節點內部都會對應一個前綴,那么我們就可以根據這個前綴去匹配,那么一個IPC資源的標識符是36位,那么其中的前18位就是用來定位的,那么其中高6位就是對應根節點的指針數組的索引,那么利用位運算獲取高6位,然后直接跳轉到下一層的對應的節點,然后再解析緊挨著的低6位,獲取中層節點的指針數組的索引,然后最后在緊挨著低6為獲取最后葉子節點的指針數組的索引,其中對于葉子節點來說,那么它也有一個64個比特位的位圖和長度為64的指針數組,那么這里的指針數組的每一個指針指向不是下一層的節點,而是ipc_perm結構體

ID二進制:0110 1000 1010 0011 1111 1001 1100 0010
分解:根索引:[31:26] 0110100x1A (26)中索引:[25:20] 0010100x0A (10)葉索引:[19:14] 0011110x0F (15)

在這里插入圖片描述

`

結語

那么這就是消息對了通信的全部過程,那么至此我們就學習了4種通信方式,那么下一期我會更新linux的信號,那么我會持續更新,希望你能夠多多關照,如果本文幫組到你的話,還請三連加關注,你的支持就是我創作最大的動力!
在這里插入圖片描述

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

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

相關文章

React 19 革命性升級:編譯器自動優化,告別手動性能調優時代

概述 React 19 是 React 框架的一個重要里程碑版本&#xff0c;帶來了眾多突破性的改進和新特性。本文檔將詳細介紹 React 19 的主要變化&#xff0c;幫助開發者了解并遷移到新版本。 &#x1f680; 主要新特性 React Compiler (編譯器) React 19 引入了全新的 React Compi…

UE5的渲染Debug技巧

ShaderPrint UE5相對UE4使用的ComputeShader(GPU Driven)的地方多很多。因為UE5為了方便查看ComputeShader的某些值&#xff0c;開發了“ShaderPrint”&#xff0c;方便直接在Shader 打印信息到屏幕&#xff0c;而不用采用CPUReadback在print的方式。 比如r.nanite.ShowStats…

【2025/08/03】GitHub 今日熱門項目

GitHub 今日熱門項目 &#x1f680; 每日精選優質開源項目 | 發現優質開源項目&#xff0c;跟上技術發展趨勢 &#x1f4cb; 報告概覽 &#x1f4ca; 統計項&#x1f4c8; 數值&#x1f4dd; 說明&#x1f4c5; 報告日期2025-08-03 (周日)GitHub Trending 每日快照&#x1f55…

Android系統模塊編譯調試與Ninja使用指南

模塊編譯調試方法 (此處舉例framework、installd、SystemUI等模塊的編譯調試&#xff0c;其他類似) 1. Framework模塊編譯 Android系統代碼的framework目錄內&#xff0c;一共有3個模塊單獨編譯&#xff1a;framework、services、framework-res.apk。 注意&#xff1a;偶爾會有…

【硬件-筆試面試題】硬件/電子工程師,筆試面試題-51,(知識點:stm32,GPIO基礎知識)

目錄 1、題目 2、解答 3、相關知識點 一、GPIO 基本結構與特性 1. GPIO 硬件結構 2. 主要特性 二、GPIO 工作模式 1. 輸入模式 2. 輸出模式 3. 復用功能模式 4. 特殊模式 三、GPIO 配置步驟&#xff08;以 STM32Cube HAL 庫為例&#xff09; 1. 初始化 GPIO 時鐘 …

小智服務器Java安裝編譯(xinnan-tech)版

github&#xff1a;https://github.com/xinnan-tech/xiaozhi-esp32-server 一、JDK 1、JDK21下載&#xff1a; https://www.oracle.com/cn/java/technologies/downloads/#jdk21-windows RPM安裝&#xff1a; rpm -ivh jdk-21_linux-x64_bin.rpm 2、IDEA設置JDK File → P…

智能平臺的感知進化:AI × 視頻通感在群體終端協同中的應用探索

?? 引言&#xff1a;從單兵到集群&#xff0c;未來智能平臺的協同演進 從傳統的單兵執行任務到如今的“群體智能平臺編組”&#xff0c;現代感知系統正經歷一場由 AI、機器人與智能計算平臺驅動的深度變革。過去&#xff0c;履帶式無人平臺在平坦地形中承擔支援任務&#xf…

基于定制開發開源AI智能名片S2B2C商城小程序的B站私域流量引流策略研究

摘要&#xff1a;隨著移動互聯網進入存量競爭階段&#xff0c;私域流量運營成為企業數字化轉型的核心戰略。B站作為中國最大的Z世代文化社區&#xff0c;其3.41億月活躍用戶中Z世代占比達58%&#xff0c;且25歲以上用戶增速顯著&#xff0c;用戶日均使用時長超108分鐘&#xff…

Spring+K8s+AI實戰:3全棧開發指南

Spring、K8s、人工智能、Docker及Windows實例 以下是與Spring、K8s、人工智能、Docker及Windows實例相關的實用示例,涵蓋開發、部署和集成場景: Spring Boot微服務開發 示例1:REST API構建 使用Spring Boot創建帶Swagger文檔的RESTful服務,集成JPA和Hibernate進行數據庫…

C++ 生成動態庫.dll 及 C++調用DLL,C++ 生成靜態庫.lib及 C++調用lib

文章目錄1 C 動態庫.dll生成 及 調用1.1 生成C 動態庫dll1.1.1 創建項目MyDLL1.1.2 編寫.h 和 .cpp文件1.1.3 設置 及 生成 DLL1.2 調用 C 動態庫dll1.2.1 創建C 空項目DLLtest1.2.2 動態庫配置 及代碼調用測試2 C 靜態庫.lib 生成 及 調用3 C 生成靜態庫.lib及調用 &#xff0…

信創應用服務器TongWeb安裝教程、前后端分離應用部署全流程

TongWeb 簡介TongWeb 是東方通&#xff08;TongTech&#xff09;開發的國產Java應用服務器&#xff08;中間件&#xff09;&#xff0c;類似于國外的 WebLogic、WebSphere 和開源的 Tomcat、Jetty&#xff0c;主要用于企業級Java應用&#xff08;如J2EE&#xff09;的部署和運行…

Rust 同步方式訪問 REST API 的完整指南

Rust 同步方式訪問 REST API 的完整指南 在 Rust 中不使用異步機制訪問 REST API 是完全可行的&#xff0c;特別適合簡單應用、腳本或不需要高并發的場景。以下是完整的同步實現方案&#xff1a; &#x1f4e6; 依賴選擇 推薦庫&#xff1a; [dependencies] reqwest { version…

32.【.NET8 實戰--孢子記賬--從單體到微服務--轉向微服務】--單體轉微服務--財務服務--賬本與預算

在我們的孢子記賬應用中&#xff0c;賬本是用于記錄每一筆收支流水的核心模塊。通過賬本&#xff0c;我們可以清晰地追蹤資金的流入與流出&#xff0c;進行數據統計和分析&#xff0c;為后續的報表生成和決策支持提供基礎數據。預算模塊則是用于設置和管理預算的功能&#xff0…

模型預估打分對運籌跟蹤的影響

在uplift建模中&#xff0c;模型離線指標(QINI、AUUC)提升并不意味著在線A/B實驗的收益&#xff0c;因為在線運籌還需要λ\lambdaλ約束。如果模型打分不滿足單調增且roi邊際遞減&#xff0c;那么λ\lambdaλ運籌求解會非常不穩定&#xff0c;導致線上發券偏高&#xff0c;毛利…

音視頻學習(四十六):聲音的三要素

聲音是人類感知世界的重要途徑之一。在自然界中&#xff0c;聲波本質上是介質中傳播的機械振動&#xff0c;而人類對聲音的主觀感受主要通過三種屬性來認知和描述&#xff0c;即音調&#xff08;音高&#xff09;、響度&#xff08;強弱&#xff09;、音色&#xff08;音質&…

spring batch處理數據模板(Reader-Processor-Writer模式)

步驟監聽器 Component public class StepListener implements StepExecutionListener {private StepExecution stepExecution;public StepExecution getStepExecution() {return this.stepExecution;}Overridepublic void beforeStep(StepExecution stepExecution) {this.stepE…

【華為OD機試】從小桶里取球

題目描述 某部門開展Family Day開放日活動,其中有個從桶里取球的游戲,游戲規則如下: 有N個容量一樣的小桶等距排開,且每個小桶都默認裝了數不等的小球, 每個小桶裝的小球數量記錄在數組bucketBallNums中, 游戲開始時,要求所有桶的小球總數不能超過SUM, 如果小球總…

std::unordered_map 和 std::map的區別【C++】

std::unordered_map 和 std::map 是 C 標準庫中兩種不同的關聯容器&#xff0c;它們都用于存儲鍵值對&#xff0c;但在實現方式、性能特點和使用場景上存在顯著區別。以下是它們的主要區別&#xff1a; 1. 數據結構 std::map&#xff1a; 基于 紅黑樹&#xff08;一種自平衡二叉…

云原生環境里的顯示變革:Docker虛擬瀏覽器與cpolar穿透技術實戰

文章目錄前言【視頻教程】1. 關于neko2. 本地部署neko3. neko簡單使用4. 安裝內網穿透5. 配置neko公網地址6. 配置固定公網地址前言 現代遠程協作本該是無縫銜接的過程&#xff0c;卻被這些障礙不斷打斷&#xff1a;多設備屏幕同步存在延遲、跨平臺訪問需要復雜配置、公網IP申…

LVGL + ESP-Brookesia 在Windows下的編譯和運行

LVGL ESP-Brookesia 在Windows下的編譯和運行 1. 項目介紹 本項目是基于 LVGL&#xff08;輕量級多功能圖形庫&#xff09;和 ESP-Brookesia 的嵌入式模擬桌面應用開發框架&#xff0c;專為嵌入式設備構建豐富的圖形界面而設計。通過在Windows環境下模擬嵌入式設備的圖形界面…