Linux 内核 IPC 通信源码分析-消息队列

简介
目的
本文对最新的 Linux-4.19.4 内核源码进行分析,并详细指出内核 IPC 机制中的消息队列的原理。

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

进程间通信的七种方式:

管道/匿名管道 (PIPE)
有名管道 (FIFO)
信号 (Signal)
消息 (Message) 队列
共享内存 (Share Memory)
信号量 (Semaphore)
套接字 (Socket)
消息队列简介
消息队列是消息的链接表,包括 Posix 消息队列和 System V 消息队列。消息队列克服了信号承载信息量少、管道只能承载无格式字节流以及缓冲区大小受限等缺点,克服了早期 Lunix 通信机制的一些缺点。消息队列将消息看作一个记录,具有特定的格式以及特定的优先级,对消息队列有写权限的进程可以向中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息,消息队列是随内核持续的。

源码分析
重要文件
./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 */
};
1
2
3
4
5
6
7
8
9
10
11
12
msg_msgseg 数据结构
用于存储 msg_msg 中溢出的数据。

struct msg_msgseg {
    struct msg_msgseg *next;
    /* the next part of the message follows immediately */
};
1
2
3
4
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;
};
1
2
3
4
5
6
7
8
9
10
msg_sender
消息队列中睡眠的发送者进程数据结构。

/* one msg_sender for each sleeping sender */
struct msg_sender {
    struct list_head    list;
    struct task_struct    *tsk;
    size_t                  msgsz;
};
1
2
3
4
5
6
函数调用
系统调用通用定义
早前 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)
1
对于 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 */

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
SYSCALL_DEFINEx 的定义和它具体调用的方法中 ## 是连接符,直接将参数的原来的字符替换为 ## 后的占位符。VA_ARGS 代表前面 … 里面的可变参数最后调用了__do_sys##name 的方法,在后面加上大括号就是一个函数的具体定义了。

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

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

msgrcv 函数
该函数用于从指定消息队列中接收信息,如果队列中没有消息,就会将进程加入到休眠进程列表中,等待队列中有新的消息加入。
————————————————
版权声明:本文为CSDN博主「付江」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/java060515/article/details/84564469

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值