Linux 進程間通信和同步—消息隊列

消息隊列

  消息隊列是內核地址空間中的內部鏈表,通過 Linux 內核在各個進程之間傳遞內容。消息順序地發送到消息隊列中,並以幾種不同的方式從隊列中獲取,每個消息隊列可以用 IPC 標識符唯一地進行標識。內核中的消息隊列是通過 IPC 的標識符來區別的,不同的消息隊列之間是相對獨立的。每個消息隊列中的消息,又構成一個獨立的鏈表

1. 消息緩衝區結構

  常用的結構是 msgbuf 結構。程序員可以以這個結構爲模板定義自己的消息結構。在頭文件 <linux/msg.h> 中,它的定義如下:

struct msgbuf 
{ 
	long mtype; 
	char mtext[1];
}

  在結構 msgbuf 中有以下兩個成員。
  █ mtype:消息類型,以正數來表示。用戶可以給某個消息設定一個類型,可以在消息隊列中正確地發送和接收自己的消息。例如,在 socket 編程過程中,一個服務器可以接受多個客戶端的連接,可以爲每個客戶端設定一個消息類型,服務器和客戶端之間的通信可以通過此消息類型來發送和接收消息,並且多個客戶端之間通過消息類型來區分。

  █ mtext:消息數據。
  消息數據的類型爲 char,長度爲 1。在構建自己的消息結構時,這個域並不一定要設爲 char 或者長度爲 1。可以根據實際的情況進行設定,這個域能存放任意形式的任意數據,應用程序編程人員可以重新定義 msgbuf 結構。例如:

struct msgmbuf
{
	long mtype; 
	char mtext[10]; 
	long length;
};

  上面定義的消息結構與系統模板定義的不一致,但是 mtype 是一致的。消息在通過內核在進程之間收發時,內核不對 mtext 域進行轉換,任意的消息都可以發送。具體的轉換工作是在應用程序之間進行的。但是,消息的大小,存在一個內部的限制。在 Linux 中, 它在 Linux/msg.h 中的定義如下:

#define MSGMAX 8192

  消息總的大小不能超過 8192 個字節,這其中包括 mtype 成員,它的長度是 4 個字節(long 類型)。

2. 結構 msgid_ds

  內核 msgid_ds 結構—— IPC 對象分爲 3 類,每一類都有一個內部數據結構,該數據結構是由內核維護的。對於消息隊列而言,它的內部數據結構是 msgid_ds 結構。對於系統上創建的每個消息隊列,內核均爲其創建、存儲和維護該結構的一個實例。該結構在 Linux/msg.h 中定義,如下所示。

struct msgid_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 */
};

  爲了敘述的完整性,下面對每個成員都給出一個簡短的介紹。

  █ msg_perm:它是 ipc_perm 結構的一個實例,ipc_perm 結構是在 Linux/ipc.h 中定義 的。用於存放消息隊列的許可權限信息,其中包括訪問許可信息,以及隊列創建者的有關信息(如 uid 等)。
  █ msg_stime:發送到隊列的最後一個消息的時間戳(time_t)。
  █ msg_rime:從隊列中獲取最後一個消息的時間戳。
  █ msg_ctime:對隊列進行最後一次變動的時間戳。
  █ msg_cbytes:在隊列上所駐留的字節總數(即所有消息的大小的總和)。
  █ msg_qnum:當前處於隊列中的消息數目。
  █ msg_qbytes:隊列中能容納的字節的最大數目。
  █ msg_lspid:發送最後一個消息進程的 PID
  █ msg_lrpid:接收最後一個消息進程的 PID

3. 結構 ipc_perm

  內核把 IPC 對象的許可權限信息存放在 ipc_perm 類型的結構中。例如在前面描述的某個消息隊列的內部結構中,msg_perm 成員就是 ipc_perm 類型的,它的定義是在文件 <linux/ipc.h>,如下所示。

struct ipc_perm 
{
	key_t key;		/* 函數 msgget() 使用的鍵值 */
	uid_t uid;		/* 用戶的 UID */
	gid_t gid;		/* 用戶的 GID */
	uid_t cuid;		/* 建立者的 UID */
	gid_t cgid;		/* 建立者的 GID */
	unsigned short mode;	/* 權限 */
	unsigned short seq;		/* 序列號 */
}

  這個結構描述的主要是一些底層的東西,簡單介紹如下。
  █ keykey 參數用於區分消息隊列。
  █ uid:消息隊列用戶的 ID 號,
  █ gid:消息隊列用戶組的 ID 號。
  █ cuid:消息隊列創建者的 ID 號。
  █ cgid:消息隊列創建者的組 ID 號。
  █ mode:權限,用戶控制讀寫,例如 0666,可以對消息進行讀寫操作。
  █ seq:序列號。

4. 內核中的消息隊列關係

  作爲 IPC 的消息隊列,其消息的傳遞是通過 Linux 內核來進行的。如下圖(消息機制在內核中的實現)所示的結構成員與用戶空間的表述基本一致。在消息的發送和接收的時候,內核通過一個比較巧妙的設置來實現消息插入隊列的動作和從消息中査找消息的算法。

消息機制在內核中的實現

  結構 list_head 形成一個鏈表,而結構 msg_msg 之中的 m_list 成員是一個 struct list_head 類型的變景,通過此變量,消息形成了一個鏈表,在查找和插入時,對 m_ist 域進行偏移操作就可以找到對應的消息體位置。內核中的代碼在頭文件 <linux/msg.h><linux/msg.c> 中,主要的實現是插入消息和取出消息的操作。

5. 鍵值構建 ftok() 函數

  ftok() 函數將路徑名和項目的表示符轉變爲一個系統的 IPC 鍵值。其原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

  其中 pathname 必須是已經存在的目錄,而 proj_id 則是一個 8 位(bit)的值,通常用 ab 等表示。例如建立如下目錄:

$ mkdir -p /ipc/msg/

  然後用如下代碼生成一個鍵值:

......
key_t key;
char *msgpath = "/ipc/msg/";	/* 生成魔數的文件路徑 */
key = ftok(msgpath,'a');		/* 生成魔數 */
if(key != -1)					/* 成功 */
{
	printf("成功建立 KEY\n");
}
else	/* 失敗 */
{
	printf("建立 KEY 失敗\n");
}
......

6. 獲得消息 msgget() 函數

  創建一個新的消息隊列,或者訪問一個現有的隊列,可以使用函數 msgget(),其原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

  msgget() 函數的第一個參數是鍵值,可以用 ftok() 函數生成,這個關鍵字的值將被拿來與內核中其他消息隊列的現有關鍵字值相比較。比較之後,打開或者訪問操作依賴於 msgflg 參數的內容。

  █ IPC_CREAT:如果在內核中不存在該隊列,則創建它。
  █ IPC_EXCL:當與 IPC_CREAT 一起使用時,如果隊列早已存在則將出錯。

  如果只使用了 lPC_CREATmsgget() 函數或者返回新創建消息隊列的消息隊列標識符,或者會返回現有的具有同一個關鍵字值的隊列的標識符。如果同時使用了 IPC_EXCLIPC_CREAT,那麼將可能會有兩個結果:創建一個新的隊列,如果該隊列存在,則調用將出錯,並返回 -1IPC_EXCL 本身是沒有什麼用處的,但在與 IPC_CREAT 組合使用時,它可以用於保證沒有一個現存的隊列爲了訪問而被打開。例如,下面的代碼創建一個消息隊列:

......
key_t key;
int msg_flags, msg_id; 
msg_flags = IPC_CREAT|IPC_EXCL; 	/* 消息的標誌爲建立、可執行 */
mag_id = msgget(key, msg_flags|0x0666);	/* 建立消息 */
if( -1 == msg_id)	/* 建立消總失敗 */
{
	printf("消息建立失敗\n"); /* 打印信息 */
	return 0;	/* 退出 */
}
......

7. 發送消息 msgsnd() 函數

  一旦獲得了隊列標識符,用戶就可以開始在該消息隊列上執行相關操作了。爲了向隊列傳遞消息,用戶可以使用 msgsnd() 函數:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

  msgsnd() 函數的第 1 個參數是隊列標識符,它是前面調用 msgget() 獲得的返回值。第二個參數是 msgp,它是一個 void 類型的指針,指向一個消息緩衝區。msgsz 參數則包含着消息的大小,它是以字節爲單位的,其中不包括消息類型的長度(4 個字節長)。

  msgflg 參數可以設置爲 0(表示忽略),也可以設置爲 IPC_NOWAIT。如果消息隊列己滿,則消息將不會被寫入到隊列中。如果沒有指定 IPC_NOWAIT,則調用進程將被中斷(阻塞),直到可以寫消息爲止。例如,如下代碼向已經打開的消息隊列發送消息:

......
struct msgmbuf		/* 消息的結構 */
{ 
	int mtype; 		/* 消息中的字節數 */
	char mtext[10];	/* 消息數據 */
};
int msg_sflags; /* 消息的標記 */
int msg_id;		/* 消息 ID 識別號 */
struct msgmbuf msg_mbuf; /* 建立消息結構變量 */
msg_sflags = IPC_NOWAIT; /* 直接讀取消息,不等待 */ 
msg_mbuf.mtype = 10;	/* 消息的大小爲 10 字節 */
memcpy(msg_mbuf.mtext,"測試消息",sizeof("測試消息")); /* 將數據複製如消息數據緩衝區 */
ret = msgsnd(msg_id, &msg_mbuf, sizeof("測試消息"),msg_sflags); /* 向消息 ID 發送消息 */
if(-1 == ret)	/* 發送消息失敗 */
{
	printf("發送消息失敗\n");	/* 打印消息 */
}
......

  首先將要發送的消息打包到 msg_mbuf.mtext 域中,然後調用 msgsnd 發送消息給內核。這裏的 mtype 設置了類型爲 10,當接受時必須設置此域爲 10,才能接收到這時發送的消息。msgsnd() 函數的 msg_id 是之前 msgget 創建的。

8. 接收消息 msgrcv() 函數

  當獲得隊列標識符後,用戶就可以開始在該消息隊列上執行消息隊列的接收操作。msgrcv() 函數用於接收隊列標識符中的消息,函數原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

  █ msgrcv() 函數的第 1 個參數 msqid 是用來指定,在消息獲取過程中所使用的隊列(該值是由前面調用 msgget() 得到的返回值)。
  █ 第 2 個參數 msgp 代表消息緩衝區變量的地址,獲取的消息將存放在這裏。
  █ 第 3 個參數 msgsz 代表消息緩衝區結構的大小,不包括 mtype 成員的長度。
  █ 第 4 個參數 msgtyp 指定要從隊列中獲取的消息類型。內核將查找隊列中具有匹配類型的第一個到達的消息,並把它複製返回到由 msgp 參數所指定的地址中。如果 msgtyp 參數傳送一個爲 0 的值,則將返回隊列中最老的消息,不管該消息的類型是什麼。msgtyp=0:收到的第一條消息,任意類型。msgtyp>0:收到的第一條 msgtyp 類型的消息。msgtyp<0:收到的第一條最低類型(小於或等於 msgtyp 的絕對值)的消息。

  如果把 IPC_NOWAIT 作爲一個標誌傳送給該函數,而隊列中沒有任何消息,則該次調用將會向調用進程返回 ENOMSG。否則,調用進程將阻塞,直到滿足 msgrcv() 參數的消息到達隊列爲止。如果在客戶等待消息的時候隊列被刪除了,則返回 EIDRM。如果在進程阻塞並等待消息的到來時捕獲到一個信號,則返回 EINTR。函數 msgrcv 的使用代碼如下:

msg_rflags = IPC_NOWAIT|MSG_NOERROR;	/* 消息接收標記 */
ret = msgrcv(msg _id, &msg_mbuf, 10,10,msg_rflags); /* 接收消息 */
if( -1 == ret)	/* 接收消息失敗 */
{
	printf("接收消息失敗\n");	/* 打印信息 */
}
else /* 接收消息成功 */
{
	printf("接收消息成功,長度:%d\n",ret); /* 打印信息 */
} 

  上面的代碼中將 mtype 設置爲 10,可以獲得之前發送的內核的消息獲得(因爲之前發送的 mtype 值也設背爲 10),msgrcv 返回值爲接收到的消息長度。

9. 消息控制 msgctl() 函數

  通過前面的介紹已經知道如何在應用程序中簡單地創建和利用消息隊列。下面介紹一下如何直接地對那些與特定的消息隊列相聯繫的內部結構進行操作。爲了在一個消息隊列上執行控制操作,用戶可以使用 msgctl() 函數。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

  msgctl() 向內核發送一個 cmd 命令,內核根據此來判斷進行何種操作,buf 爲應用層和內核空間進行數據交換的指針。其中的 cmd 可以爲如下值。

  █ IPC_STAT:獲取隊列的 msqid_ds 結構,並把它存放在 buf 變量所指定的地址中,通過這種方式,應用層可以獲得當前消息隊列的設置情況,例如是否有消息到來、消息隊列的緩衝區設置等。
  █ IPC_SET:設置隊列的 msqid_ds 結構的 ipc_perm 成員值,它是從 buf 中取得該值的。通過 IPC_SET 命令,應用層可以設置消息隊列的狀態,例如修改消息隊列的權限,使其他用戶可以訪問或者不能訪問當前的隊列;甚至可以設置消息隊列的某些當前值來僞裝。
  █ IPC_RMID:內核刪除隊列。使用此命令執行後,內核會把此消息隊列從系統中刪除。

4. 消息隊列的一個例子

  本例在建立消息隊列後,打印其屬性,並在每次發送和接收後均查看其屬性,最後對消息隊列進行修改。

1. 顯示消息屬性的函數 msg_show_attr()

  msg_show_attr() 函數根據用戶輸入的消息 ID,將消息隊列中的字節數、消息數、最大字節數、最後發送消息的進程、最後接收消息的進程、最後發送消息的時間、最後接收消息的時間、最後消息變化的時間,以及消息的 UIDGID 等信息進行打印。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <unistd.h>
#include <time.h>
#include <sys/ipc.h>
/* 打印消息屬性的函數 */
void msg_show_attr(int msg_id, struct msqid_ds msg_info)
{
	int ret = -1; 
	sleep(1);
	ret = msgctl(msg_id, IPC_STAT, &msg_info); /* 獲取消息 */
	if( -1 == ret)
	{
		printf("獲取消息信息失敗!\n"); /* 獲取消息失敗,返回 */
		return ;
	}

	printf("\n"); /* 以下打印消息的信息 */
	printf("現在隊列中的字節數:%ld\n",msg_info.msg_cbytes); /* 消息隊列中的字節數 */
	printf("隊列中消息數:%d\n",(int)msg_info.msg_qnum); /* 消息隊列中的消息數 */
	printf("隊列中最大字節數:%d\n", (int)msg_info.msg_qbytes); /* 消息隊列中的最大字節數 */
	printf("最後發送消息的進程 pid: %d\n",msg_info.msg_lspid); /* 最後發送消息的進程 */
	printf("最後接收消息的進程 pid: %d\n",msg_info.msg_lrpid); /* 最後接收消息的進程 */
	printf("最後發送消息的時間:%s",ctime(&(msg_info.msg_stime))); /* 最後發送消息的時間 */
	printf("最後接收消息的時間:%s", ctime(&(msg_info.msg_rtime))); /* 最後接受消息的時間 */
	printf("最後變化時間:%s", ctime(&(msg_info.msg_ctime))); /* 消息的最後變化時間 */
	printf("消息 UID 是:%d\n",msg_info.msg_perm.uid); /* 消息的 UID */
	printf("消息 GID 是:%d\n",msg_info.msg_perm.gid); /* 消息的 GID */
}

2. 主函數 main()

  主函數先用函數 ftok() 使用路徑 “/tmp/msg/b” 獲得一個鍵值,之後進行相關的操作並打印消息的屬性。

  █ 調用函數 msgget() 獲得一個消息後,打印消息的屬性;
  █ 調用函數 msgsnd() 發送一個消息後,打印消息的屬性;
  █ 調用函數 msgrcv() 接收一個消息後,打印消息的屬性;
  █ 最後,調用函數 msgctl() 併發送命令 IPC_RMID 銷燬消息隊列。

int main(void)
{
	int ret = -1; 
	int msg_flags, msg_id; 
	key_t key; 
	struct msgmbuf		/* 消息的緩衝區結構 */
	{
		int mtype; 
		char mtext[10];
	};
	struct msqid_ds msg_info; 
	struct msgmbuf msg_mbuf;

	int msg_sflags,msg_rflags; 
	char *msgpath = "/ipc/msg/"; 	/* 消息 key 產生所用的路徑 */
	key = ftok(msgpath,'b');		/* 產生 key */
	if(key != -1)	/* 產生 key 成功 */
	{
		printf("成功建立 KEY\n");
	}
	else	/* 產生 key 失敗 */
	{
		printf("建立 KEY 失敗\n");
	}
	msg_flags = IPC_CREAT|IPC_EXCL;  /* 消息的類型 */
	msg_id = msgget(key, msg_flags|0x0666); /* 建立消息 */
	if( -1 == msg_id)
	{
		printf("消息建立失敗\n");
		return 0;
	}
	msg_show_attr(msg_id, msg_info); /* 顯示消息的屬性 */
	
	msg_sflags = IPC_NOWAIT; 
	msg_mbuf.mtype = 10;
	memcpy(msg_mbuf.mtext,"測試消息",sizeof("測試消息")); /* 複製字符串 */
	ret = msgsnd(msg_id, &msg_mbuf, sizeof("測試消息"),msg_sflags); /* 發送消息 */
	if( -1 == ret)
	{
		printf("發送消息失敗\n");
	}
	msg_show_attr(msg_id,msg_info);	/* 顯示消息屬性 */
	
	msg_rflags = IPC_NOWAIT|MSG_NOERROR;
	ret = msgrcv(msg_id, &msg_mbuf, 10,10,msg_rflags);	/* 接收消息 */
	if( -1 == ret)
	{
		printf("接收消息失敗\n");
	}
	else
	{
		printf("接收消息成功,長度:%d\n",ret);
	}
	msg_show_attr(msg_id, msg_info); /* 顯示消息屬性 */

	msg_info.msg_perm.uid =8;
	msg_info.msg_perm.gid = 8;
	msg_info.msg_qbytes = 12345;
	ret = msgctl(msg_id, IPC_SET, &msg_info); /* 設置消息屬性 */
	if( -1 == ret)
	{
		printf("設置消息屬性失敗\n");
		return 0;
	}
	msg_show__attr(msg_id, msg_info); /* 顯示消息屬性 */
	
	ret = msgctl(msg_id, IPC_RMID,NULL); /* 刪除消息隊列 */
	if(-1 == ret) 
	{
		printf("刪除消息失敗\n");
		return 0;
	}
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章