ipc通信-消息队列

简介

目的

本文对最新的Linux-4.19.4内核源码进行分析,详细分析了内核IPC机制中的消息队列的原理

进程间通信

IPC(进程间通信,InterProcess Communication)是内核提供的系统中进程间进行通信的一种机制。系统中每个进程的用户地址空间互不干扰,所以需要内核来提供进程之间进行通信机制。 进程间通信的七种方式:

  • 管道/匿名管道(pipe)
  • 有名管道(FIFO)
  • 信号(Signal)
  • 消息(Message)队列
  • 共享内存(share memory)
  • 信号量(semaphore)
  • 套接字(socket)

消息队列简介

消息队列是消息的链接表,包括Posix消息队列和system V消息队列。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点,克服了早期unix通信机制的一些缺点。消息队列将消息看作一个记录,具有特定的格式以及特定的优先级,对消息队列有写权限的进程可以向中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息,消息队列是随内核持续的。

源码分析

重要文件

ipc
 |
 |-- ./ipc/msg.c
 |-- ./ipc/msgutil.c
 |-- ./ipc/mqueue.c

数据结构

总体结构见图:

消息队列数据结构架构图

上图描述一个消息队列的数据结构。

q_messages

消息队列的q_messages字段指向一个消息的链表,这里面存放着等待读取的消息。每个msg_msg结构占有一个页(若消息小于一个则只占用消息大小 + msg_msg 数据结构大小),页头部为msg_msg数据结构剩余为数据区。若消息超过一个页,剩余消息会存放在页头为msg_msgseg数据结构的页中。因此对于n个页。设页大小的M,struct msg_msg结构大小为a,msg_msgseg数据结构的大小为b,n个页所最大能用于存储数据的空间为:n*M - a – b *(n-1)。每页最大值为2^13 (8192)

q_receiver

指向一个sleeping的接收者链表,这个链表上每个结构都指向一个等待接受的进程(阻塞),等待消息队列被写入它需要的信息。

q_senders

指向一个sleeping发送者链表,这个链表上每个结构都指向一个等待发送消息的进程(阻塞),等待消息队列可以让它写入的信息。

msg_msg数据结构

每个msg_msg代码进程放入到队列中一个数据。

/* one msg_msg structure for each message */
struct msg_msg {
    //双向列表,通过这个变量将队列中的消息连接
	struct list_head m_list;
	//消息类型
	long m_type;
	size_t m_ts;		/* message text size */
	// 消息内容超过一个页时,需要额外的存储空间存储剩余的内容,该指针指向剩余内容空间
	struct msg_msgseg *next;
	void *security;
	/* the actual message follows immediately */
};

msg_msgseg数据结构

用于存储msg_msg中溢出的数据

struct msg_msgseg {
	struct msg_msgseg *next;
	/* the next part of the message follows immediately */
};

msg_receiver

消息队列中睡眠的接受者进程数据结构

/* one msg_receiver structure for each sleeping receiver */
struct msg_receiver {
	struct list_head	r_list;
	//接收者进程描述符
	struct task_struct	*r_tsk;
	int			r_mode;
	long			r_msgtype;
	long			r_maxsize;
	struct msg_msg		*r_msg;
};

msg_sender

消息队列中睡眠的发送者进程数据结构

/* one msg_sender for each sleeping sender */
struct msg_sender {
	struct list_head	list;
	struct task_struct	*tsk;
	size_t                  msgsz;
};

函数调用

系统调用通用定义

早前64位的Linux有一个名为CVE-2009-2009的漏洞,所以新版本的Linux在调用这些函数的时候外加了一层封装SYSCALL_DEFINEx来调用函数。例 如虽然在程序上层可以直接调用msgsnd(msqid,&msgs,sizeof(struct msgstru),IPC_NOWAIT)这样的形式来发送消息,但是在底层是这样的形 式来调用SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz,int, msgflg)。对于SYSCALL_DEFINE4,首个变量用于函数名,剩下的偶数对 参数,依次代表参数类型与参数变量。SYSCALL_DEFINEx,随后的x就是对于不同的参数的个数。

#ifndef SYSCALL_DEFINE0
#define SYSCALL_DEFINE0(sname)					\
	SYSCALL_METADATA(_##sname, 0);				\
	asmlinkage long sys_##sname(void);			\
	ALLOW_ERROR_INJECTION(sys_##sname, ERRNO);		\
	asmlinkage long sys_##sname(void)
#endif /* SYSCALL_DEFINE0 */

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE_MAXARGS	6

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)

/*
 * The asmlinkage stub is aliased to a function named __se_sys_*() which
 * sign-extends 32-bit ints to longs whenever needed. The actual work is
 * done within __do_sys_*().
 */
#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...)					\
	__diag_push();							\
	__diag_ignore(GCC, 8, "-Wattribute-alias",			\
		      "Type aliasing is used to sanitize syscall arguments");\
	asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))	\
		__attribute__((alias(__stringify(__se_sys##name))));	\
	ALLOW_ERROR_INJECTION(sys##name, ERRNO);			\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	__diag_pop();							\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */

SYSCALL_DEFINEx 的定义和它具体调用的方法##是连接符,直接将参数的原来的字符替换为##后的占位符。__VA_ARGS__代表前面…里面的可变参数其实最后就是调用了__do_sys##name的方法,正好在后面加上大括号就是一个函数的具体定义了

msgget函数

该函数用于获得一个消息队列, 内核从指定ipc_namespace对应的ipc_ids获取相应键值的消息队列,如果没有的话就会新建一个队列。

msgsnd函数

该函数用于向指定消息队列中发送信息,该函数会将消息插入到链表尾部。函数还会将信息发送到队列关联的sleep接收者

msgrcv函数

该函数用于从指定消息队列中接收信息,如果队列中没有消息,就会将进程加入到休眠进程列表中,等待队列中有新的消息加入。

尾记

转载请注明原地址,冯杰的博客:http://Lfengjie.github.io 谢谢!

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦