《Linux6.5源码分析:进程管理与调度系列文章》
本系列文章将对进程管理与调度进行知识梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中进程调度机制进行数据实时拿取与分析。
在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:
- 《深入理解Linux内核》
- 《Linux操作系统原理与应用》
- 《奔跑吧Linux内核》
- 《深入理解Linux进程与内存》
- 《基于龙芯的Linux内核探索解析》
- 进程调度 - 标签 - LoyenWang - 博客园 (cnblogs.com)
- 专栏文章目录 - 知乎 (zhihu.com)
Linux进程调度与管理:(番外)Linux进程间通信——消息队列(Message_queue_delay消息队列延迟监测)
1、消息队列的学习:
消息队列是进程间通信的一种方法,用于两个进程或多个进程之间通信。
两个或多个处于用户态的进程要进行通信时,会通过相应的系统调用将消息块发送至内核中的相应的消息队列;而接收方也会通过相应的系统调用从内核态的消息队列中按顺序或按某一类型接收消息块;当发送方或接收方进程结束时,内核中对应的消息队列并不会被删除。
Linux提供了两种消息队列的方式,二者的区别可见下图(来自网络)
- System V消息队列;
- POSIX 消息队列;
本文将尽量详细全面的分析两种消息队列的工作流程,并从源码层面给出相应的解读;
1.1、System V 消息队列
待整理中….
1.2、POSIX 消息队列
关于POSIX消息队列,阅读了《深入理解Linux内核》第十九章进程通信模块后,有了一定的了解;
以下为相关参考资料:
-
《深入理解Linux内核》19章节POSIX消息队列部分;
-
由浅入深探讨Linux进程间通信
- https://mp.weixin.qq.com/s/DvhNX7zWWviYCwKW1lHfJQ
- https://mp.weixin.qq.com/s/DpwWJfFGh1UUkEgQNVLXqA
- https://mp.weixin.qq.com/s/WlkBfMMkQd1g40P5voJaog
-
进程间通信(IPC) 系列 | Posix 消息队列:https://mp.weixin.qq.com/s/axOGVotn937kBVKey-z4tg
1.2.1、POSIX消息队列实现原理:mqueue文件系统
POSIX消息队列是一种进程间通信的方式,允许同一主机上进程间传递消息;如之前介绍的一样,消息队列是在内核中维护的,不同于System V消息队列,POSIX消息队列是基于特定的文件系统实现的:mqueue文件系统
【mqueue文件系统在哪】
我们可以在/proc/filesystem文件中查看到该文件系统、并且可以查到该文件系统的挂载路径:
也即进程间通过POSIX消息队列通信时,会将消息队列中的相关信息存储在dev/mqueue目录下;
【创建消息队列时与mqueue文件系统的联系】
借鉴下面这张图(来自网络)来清晰的梳理创建消息队列时,是如何与文件系统联系起来的:
- 通过C库的mq_open函数创建或打开POSIX消息队列;
- POSIX消息队列是一个mqueue文件系统中的mqueue inode节点;
- mqueue inode节点有一个msg_tree成员(红黑树)用于用户存储消息;
- 在创建一个消息队列时:
- 会创建mqueue inode节点;
- 会创建file结构;
- 获得当前进程task_struct,并将其中files字段中的fd填入;
1.2.1、mq_open()——创建或打开一个POSIX消息队列:
在C库调用mq_open()去创建或打开一个消息队列时,会通过mq_open系统调用去在内核中实现
【一句话】:mq_open函数其实就是打开或创建了消息队列所需要的inode节点;
在/ipc/mqueue.c文件中可以``找到对于mq_open()系统调用的源码:
【mq_open系统调用】
SYSCALL_DEFINE4(mq_open, const char __user *, u_name, int, oflag, umode_t, mode,struct mq_attr __user *, u_attr)
{
// 定义一个消息队列属性结构体
struct mq_attr attr;
// 如果用户传入的消息队列属性指针不为空,并且从用户空间复制属性到内核空间失败,则返回错误代码 -EFAULT
if (u_attr && copy_from_user(&attr, u_attr, sizeof(struct mq_attr)))
return -EFAULT;
// 调用 do_mq_open 函数打开消息队列,传入参数为消息队列名、标志、权限以及消息队列属性指针(如果用户传入了属性指针,则传入复制后的内核空间地址,否则传入 NULL)
return do_mq_open(u_name, oflag, mode, u_attr ? &attr : NULL);
}
此处通过系统调用宏 SYSCALL_DEFINE4(mq_open, const char __user *, u_name, int, oflag, umode_t, mode,struct mq_attr __user *, u_attr)
进行打开或创建消息列表;
-
copy_from_user:尝试从用户空间复制属性到内核空间中的
attr
结构体中 -
调用
do_mq_open
函数来实际打开消息队列;
【do_mq_open】
static int do_mq_open(const char __user *u_name, int oflag, umode_t mode, struct mq_attr *attr)
{
// 获取当前进程的 IPC 命名空间中的消息队列挂载点
struct vfsmount *mnt = current->nsproxy->ipc_ns->mq_mnt;
// 获取挂载点的根目录
struct dentry *root = mnt->mnt_root;
// 定义一个文件名结构体指针,用于存储消息队列的名字
struct filename *name;
// 定义一个路径结构体,用于表示消息队列的路径
struct path path;
// 定义文件描述符和错误码变量
int fd, error;
// 用于标记挂载点是否为只读
int ro;
// 对消息队列的打开进行审计
audit_mq_open(oflag, mode, attr);
// 从用户空间获取消息队列的名字,并存储到 filename 结构体中
if (IS_ERR(name = getname(u_name)))
return PTR_ERR(name);
// 获取一个未使用的文件描述符,并设置 CLOEXEC 标志
fd = get_unused_fd_flags(O_CLOEXEC);
if (fd < 0)
goto out_putname;
// 获取挂载点的写锁,将其标记为只读
ro = mnt_want_write(mnt); /* we'll drop it in any case */
// 锁定挂载点的 inode
inode_lock(d_inode(root));
// 在挂载点的根目录中查找消息队列的路径
path.dentry = lookup_one_len(name->name, root, strlen(name->name));
if (IS_ERR(path.dentry)) {
// 如果查找失败,则返回错误码
error = PTR_ERR(path.dentry);
goto out_putfd;
}
// 获取挂载点的引用计数
path.mnt = mntget(mnt);
// 准备打开消息队列,包括权限检查和其他验证操作
error = prepare_open(path.dentry, oflag, ro, mode, name, attr);
if (!error) {
// 打开消息队列,获取文件指针
struct file *file = dentry_open(&path, oflag, current_cred());
if (!IS_ERR(file))
// 将文件指针安装到指定的文件描述符上
fd_install(fd, file);
else
// 如果打开文件失败,则返回错误码
error = PTR_ERR(file);
}
// 释放路径资源
path_put(&path);
out_putfd:
// 处理错误情况下的文件描述符和错误码
if (error) {
put_unused_fd(fd);
fd = error;
}
// 解锁 inode
inode_unlock(d_inode(root));
// 如果挂载点不是只读,则释放挂载点的写锁
if (!ro)
mnt_drop_write(mnt);
out_putname:
// 释放消息队列名字的内存空间
putname(name);
// 返回文件描述符
return fd;
}
该函数由底而上的创建了/打开了消息队列对应的inode数据结构,并将struct file对应的f_inode字段指向该inode;
- 通过do_mq_open函数获取消息队列的路径以及inode地址;
- 通过
prepare_open
函数准备打开或创建消息队列; - 打开消息队列,获取文件的指针;
- 释放相应的资源;
【prepare_open】
static int prepare_open(struct dentry *dentry, int oflag, int ro,
umode_t mode, struct filename *name,
struct mq_attr *attr)
{
// 定义一个数组,用于将打开标志转换为权限位掩码
static const int oflag2acc[O_ACCMODE] = { MAY_READ, MAY_WRITE, MAY_READ | MAY_WRITE };
int acc;
// 如果 dentry 指向的是一个空文件,则根据打开标志和只读标志进行相应处理
if (d_really_is_negative(dentry)) {
// 如果打开标志没有包含 O_CREAT,则返回 ENOENT 错误
if (!(oflag & O_CREAT))
return -ENOENT;
// 如果只读标志为真,则返回 ro(挂载点是否为只读)作为错误码
if (ro)
return ro;
// 对创建消息队列进行审计,并调用 vfs_mkobj 创建消息队列
audit_inode_parent_hidden(name, dentry->d_parent);
return vfs_mkobj(dentry, mode & ~current_umask(), mqueue_create_attr, attr);
}
/* it already existed */
// 对已存在的消息队列进行审计
audit_inode(name, dentry, 0);
// 如果同时设置了 O_CREAT 和 O_EXCL 标志,则返回 EEXIST 错误
if ((oflag & (O_CREAT|O_EXCL)) == (O_CREAT|O_EXCL))
return -EEXIST;
// 如果同时打开了 O_RDWR 和 O_WRONLY,则返回 EINVAL 错误
if ((oflag & O_ACCMODE) == (O_RDWR | O_WRONLY))
return -EINVAL;
// 根据打开标志获取权限位掩码
acc = oflag2acc[oflag & O_ACCMODE];
// 检查当前用户对消息队列的权限
return inode_permission(&init_user_ns, d_inode(dentry), acc);
}
- dentry路径指向空文件,则查看是否需要创建消息队列
- 通过文件系统中的创建inode对应的函数:vfs_mkobj()创建消息队列对应的inode;
1.2.2、mq_send()/mq_timedsend()——发送消息块到消息队列:
Linux mq在内核的发送、唤醒流程简介_mq_timedsend-优快云博客
C库提供的mq_send()、mq_timedsend()函数都是调用的mq_timedsend()系统调用进入内核;
在对发消息块和收消息块进行源码分析之前,先对于两个结构体进行学习:
【struct ext_wait_queue】:
struct ext_wait_queue {//休眠队列
struct task_struct *task; // 指向等待的任务的指针
struct list_head list; // 用于将等待项链接到等待队列中的链表节点
struct msg_msg *msg; // 指向加载的消息的指针
int state; // 等待项的状态,可以是 STATE_* 中定义的一个值
};
该结构体表示扩展的等待队列中的一个等待项,其中包含一个指向消息块(要传送的)的指针;
【struct mqueue_inode_info】:
struct mqueue_inode_info {
spinlock_t lock; // 自旋锁,用于保护结构体的访问
struct inode vfs_inode; // VFS inode 结构体,用于表示文件系统中的一个文件
wait_queue_head_t wait_q; // 等待队列头,用于等待消息队列的操作
struct rb_root msg_tree; // 红黑树根节点,用于按消息标识符存储消息
struct rb_node *msg_tree_rightmost; // 指向红黑树中最右边的节点,用于快速查找最大的消息标识符
struct posix_msg_tree_node *node_cache; // 指向用于缓存节点的指针,用于提高性能
struct mq_attr attr; // 消息队列的属性,包括最大消息数、最大消息大小等信息
struct sigevent notify; // 用于通知的事件
struct pid *notify_owner; // 通知的所有者的进程 ID
u32 notify_self_exec_id; // 通知自身的执行 ID
struct user_namespace *notify_user_ns; // 通知的用户命名空间
struct ucounts *ucounts; // 创建该消息队列的用户的计数器信息
struct sock *notify_sock; // 用于通知的套接字
struct sk_buff *notify_cookie; // 通知的数据包
/* 用于等待空闲空间和消息的任务 */
struct ext_wait_queue e_wait_q[2]; // 两个扩展等待队列,用于等待空闲空间和消息
unsigned long qsize; // 队列在内存中的大小,即所有消息的总大小
};
用于表示消息队列的 inode 相关信息,其中消息队列中维护着一套等待队列(struct ext_wait_queue);
在/ipc/mqueue.c文件中可以找到对于mq_timedsend()系统调用的源码:
【mq_timedsend 系统调用】
SYSCALL_DEFINE5(mq_timedsend, mqd_t, mqdes, const char __user *, u_msg_ptr,
size_t, msg_len, unsigned int, msg_prio,
const struct __kernel_timespec __user *, u_abs_timeout)
{
// 定义一个 timespec64 结构体,用于存储超时时间
struct timespec64 ts, *p = NULL;
// 如果用户传入的绝对超时时间指针不为空,则准备超时时间
if (u_abs_timeout) {
// 调用 prepare_timeout 函数准备超时时间,并将结果存储到 ts 结构体中
int res = prepare_timeout(u_abs_timeout, &ts);
if (res)
return res; // 如果准备超时时间失败,则直接返回错误码
p = &ts; // 否则,将超时时间结构体的地址赋值给 p 指针
}
// 调用 do_mq_timedsend 函数执行消息队列的定时发送操作
return do_mq_timedsend(mqdes, u_msg_ptr, msg_len, msg_prio, p);
}
- mq_send 传入的参数
u_abs_timeout
为NULL;mq_timedsend传入的参数u_abs_timeout
为时间限制; - 执行mq_timedsend()函数,则需要
prepare_timeout
函数准备超时时间,其中prepare_timeout
函数通过调用get_timespec64()
函数来从用户空间获取超时时间; - 调用真正的处理函数:do_mq_timedsend;
【do_mq_timedsend】:
static int do_mq_timedsend(mqd_t mqdes, const char __user *u_msg_ptr,
size_t msg_len, unsigned int msg_prio,
struct timespec64 *ts)
{
struct fd f;
struct inode *inode;
struct ext_wait_queue wait;
struct ext_wait_queue *receiver;
struct msg_msg *msg_ptr;
struct mqueue_inode_info *info;
ktime_t expires, *timeout = NULL;
struct posix_msg_tree_node *new_leaf = NULL;
int ret = 0;
DEFINE_WAKE_Q(wake_q);
// 检查消息优先级是否有效
if (unlikely(msg_prio >= (unsigned long) MQ_PRIO_MAX))
return -EINVAL;
// 如果指定了超时时间,则将其转换为 ktime_t 结构体
if (ts) {
expires = timespec64_to_ktime(*ts);
timeout = &expires;
}
// 对消息发送进行审计记录
audit_mq_sendrecv(mqdes, msg_len, msg_prio, ts);
// 通过文件描述符获取文件对象
f = fdget(mqdes);
if (unlikely(!f.file)) {
ret = -EBADF;
goto out;
}
// 获取文件对象的 inode
inode = file_inode(f.file);
// 检查文件是否是消息队列文件
if (unlikely(f.file->f_op != &mqueue_file_operations)) {
ret = -EBADF;
goto out_fput;
}
// 获取消息队列的信息结构体
info = MQUEUE_I(inode);
// 对文件进行审计记录
audit_file(f.file);
// 检查文件是否具有写权限
if (unlikely(!(f.file->f_mode & FMODE_WRITE))) {
ret = -EBADF;
goto out_fput;
}
// 检查消息长度是否超过消息队列的最大长度
if (unlikely(msg_len > info->attr.mq_msgsize)) {
ret = -EMSGSIZE;
goto out_fput;
}
// 从用户空间加载消息
msg_ptr = load_msg(u_msg_ptr, msg_len);
if (IS_ERR(msg_ptr)) {
ret = PTR_ERR(msg_ptr);
goto out_fput;
}
msg_ptr->m_ts = msg_len;
msg_ptr->m_type = msg_prio;
// 锁定消息队列信息结构体,开始消息发送操作
spin_lock(&info->lock);
// 尝试预先分配内存,以避免在操作现有队列之前进行分配
if (!info->node_cache)
new_leaf = kmalloc(sizeof(*new_leaf), GFP_KERNEL);
// 如果消息队列已满,等待直到有可用空间或者超时
if (info->attr.mq_curmsgs == info->attr.mq_maxmsg) {
if (f.file->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
} else {
// 进入等待队列
wait.task = current;
wait.msg = (void *)msg_ptr;
WRITE_ONCE(wait.state, STATE_NONE);
ret = wq_sleep(info, SEND, timeout, &wait);
goto out_free;
}
} else {
// 如果有接收者在等待消息,则直接将消息发送给接收者
receiver = wq_get_first_waiter(info, RECV);
if (receiver) {
pipelined_send(&wake_q, info, msg_ptr, receiver);
} else {
// 否则,将消息插入队列,并通知等待的接收者
ret = msg_insert(msg_ptr, info);
if (ret)
goto out_unlock;
__do_notify(info);
}
// 更新文件的访问时间、修改时间和更改时间
inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);
}
out_unlock:
// 解锁消息队列信息结构体
spin_unlock(&info->lock);
// 唤醒等待队列中的等待者
wake_up_q(&wake_q);
out_free:
// 如果出现错误,则释放消息
if (ret)
free_msg(msg_ptr);
out_fput:
// 释放文件对象
fdput(f);
out:
return ret;
}
-
通过mqdes标识的消息队列,获取到文件对象file的文件描述符;再通过文件描述符fd获得具体的消息队列:
// 通过文件描述符获取文件对象 f = fdget(mqdes); if (unlikely(!f.file)) { ret = -EBADF; goto out; } // 获取文件对象的 inode inode = file_inode(f.file); // 检查文件是否是消息队列文件 if (unlikely(f.file->f_op != &mqueue_file_operations)) { ret = -EBADF; goto out_fput; } // 获取消息队列的信息结构体 info = MQUEUE_I(inode);
-
通过load_msg()函数从用户空间加载消息;
// 从用户空间加载消息 msg_ptr = load_msg(u_msg_ptr, msg_len); if (IS_ERR(msg_ptr)) { ret = PTR_ERR(msg_ptr); goto out_fput; } msg_ptr->m_ts = msg_len; msg_ptr->m_type = msg_prio;
-
若消息队列满,则等待直到有可用空间/超时;其中通过
wq_sleep()
函数将等待项放入等待队列,并持续等待被唤醒;if (info->attr.mq_curmsgs == info->attr.mq_maxmsg) {//消息队列已满 if (f.file->f_flags & O_NONBLOCK) { ret = -EAGAIN; } else { // 进入等待队列 wait.task = current; wait.msg = (void *)msg_ptr; WRITE_ONCE(wait.state, STATE_NONE); ret = wq_sleep(info, SEND, timeout, &wait);//将处于等待状态的进程放人 goto out_free; }
-
如果消息队列未满,并且有接受者在等待消息,则直接调用
pipelined_send()
函数将消息发送给接受者;//wq_get_first_waiter函数获得等待队列第一个等待项 receiver = wq_get_first_waiter(info, RECV); if (receiver) {//有等待者 pipelined_send(&wake_q, info, msg_ptr, receiver);//调用pipelined_send }
-
如果消息队列未满,且没有等待消息的进程,则调用
msg_insert
向消息队列插入发送的消息块;else { // 否则,将消息插入队列,并通知等待的接收者 ret = msg_insert(msg_ptr, info); if (ret)//插入成功,则通过wake_up_q(&wake_q)唤醒等待队列的等待者 goto out_unlock; __do_notify(info); }
【pipelined_send】:
该函数用来向接收进程直接发送消息块;
static inline void pipelined_send(struct wake_q_head *wake_q,
struct mqueue_inode_info *info,
struct msg_msg *message,
struct ext_wait_queue *receiver)
{
// 将要发送的消息设置到接收者的消息指针中
receiver->msg = message;
// 调用管道式操作函数进行发送操作
__pipelined_op(wake_q, info, receiver);
}
- 参数:struct wake_q_head *wake_q:等待队列的首部;
- 参数:struct mqueue_inode_info *info:消息队列;
- 参数:struct msg_msg *message:要传送的消息块;
- 参数:struct ext_wait_queue *receiver:接收进程的等待结构体;
- receiver->msg = message;将要发送的消息设置到接收者的消息指针中
- 通过调用__pipelined_op()函数,发送
【__pipelined_op】:
static inline void __pipelined_op(struct wake_q_head *wake_q,
struct mqueue_inode_info *info,
struct ext_wait_queue *this)
{
struct task_struct *task;
// 从等待队列中移除当前等待项
list_del(&this->list);
// 获取当前等待任务的任务结构体指针
task = get_task_struct(this->task);
/* 使用 smp_store_release 释放内存屏障保证状态修改的可见性 */
smp_store_release(&this->state, STATE_READY);
// 将任务添加到唤醒队列中
wake_q_add_safe(wake_q, task);
}
- 删除位于消息队列的对应等待型;
- 准备唤醒处于等待中的接收者进程;
【msg_insert】:
该函数实现了向消息队列中插入消息块的操作;
static int msg_insert(struct msg_msg *msg, struct mqueue_inode_info *info)
{
struct rb_node **p, *parent = NULL;
struct posix_msg_tree_node *leaf;
bool rightmost = true;
// 从根节点开始搜索插入位置
p = &info->msg_tree.rb_node;
while (*p) {
parent = *p;
leaf = rb_entry(parent, struct posix_msg_tree_node, rb_node);
// 如果消息的优先级与当前节点的优先级相同,则直接插入到当前节点的消息列表中
if (likely(leaf->priority == msg->m_type))
goto insert_msg;
// 如果消息的优先级小于当前节点的优先级,则向左子树搜索
else if (msg->m_type < leaf->priority) {
p = &(*p)->rb_left;
rightmost = false;
} else
// 否则向右子树搜索
p = &(*p)->rb_right;
}
// 如果存在缓存的节点,则使用缓存的节点
if (info->node_cache) {
leaf = info->node_cache;
info->node_cache = NULL;
} else {
// 否则分配一个新的节点
leaf = kmalloc(sizeof(*leaf), GFP_ATOMIC);
if (!leaf)
return -ENOMEM;
INIT_LIST_HEAD(&leaf->msg_list);
}
// 设置节点的优先级为消息的优先级
leaf->priority = msg->m_type;
// 如果是最右边的节点,则更新消息队列中最右边的节点指针
if (rightmost)
info->msg_tree_rightmost = &leaf->rb_node;
// 将节点链接到树中
rb_link_node(&leaf->rb_node, parent, p);
// 将节点插入到红黑树中
rb_insert_color(&leaf->rb_node, &info->msg_tree);
insert_msg:
// 更新消息队列的当前消息数量和总大小
info->attr.mq_curmsgs++;
info->qsize += msg->m_ts;
// 将消息添加到节点的消息列表中
list_add_tail(&msg->m_list, &leaf->msg_list);
return 0;
}
1.2.4、mq_receive()/mq_timedreceive()——接收消息块从消息队列:
Linux mq在内核的发送、唤醒流程简介_mq_timedsend-优快云博客
C库提供的mq_receive()、mq_timedreceive()函数都是调用的mq_timedreceive()系统调用进入内核;
以上是接收进程通过调用mq_tiemedreceive()系统调用来从消息队列接收消息的大体流程,下面将从源码层面进行分析:
【mq_timedreceive】:
用户程序通过C库中的mq_receive()、mq_timedreceive()函数进行进程间接收消息,主要是通过mq_timedreceive系统调用实现的。
在该系统调用中,如果设置了超时时间,先获取超时时间,再通过调用内核函数do_mq_timedreceive()进行具体的获取消息的实现。
SYSCALL_DEFINE5(mq_timedreceive, mqd_t, mqdes, char __user *, u_msg_ptr,
size_t, msg_len, unsigned int __user *, u_msg_prio,
const struct __kernel_timespec __user *, u_abs_timeout)
{
struct timespec64 ts, *p = NULL; // 定义 timespec64 结构体和指针变量 p
// 如果指定了超时时间
if (u_abs_timeout) {
int res = prepare_timeout(u_abs_timeout, &ts); // 准备超时时间
if (res) // 如果准备失败
return res; // 返回准备失败的错误码
p = &ts; // 否则将超时时间赋值给 p
}
// 调用底层函数 do_mq_timedreceive 进行消息接收操作
return do_mq_timedreceive(mqdes, u_msg_ptr, msg_len, u_msg_prio, p);
}
【do_mq_timedreceive】:
该内核函数是mq_timedreceive系统调用的主要函数,该函数主要实现了如下功能:
- 获取消息队列所在的文件信息和inode结构;
- 给消息缓冲区分配空间;给消息队列上锁;为接收消息做准备;
- 开始准备接收消息:
- if(消息队列没有消息) 即需要等待消息的到来。
- a. 并且处于非阻塞模式 ==即消息队列没有消息,也不等消息的到来;==1). 解锁消息队列 2). 跳出系统调用;
- b. ==消息队列无消息,且愿意等消息的到来;==调用wq_sleep()等待消息得到来。关于该函数是如何实现的,会在下文给出详细的源码介绍。
- else(消息队列中有消息等待接收)即不需要等待消息的到来。
- a. 唤醒消息队列
DEFINE_WAKE_Q(wake_q);
- b. 通过
msg_get(info);
从消息队列中获取具体的消息; - c. 通过
pipelined_receive()
从管道中接收消息;该函数具体怎么实现,会在下文详细介绍; - d. 解锁消息队列,唤醒等待队列;
- a. 唤醒消息队列
- if(消息队列没有消息) 即需要等待消息的到来。
- 处理接收到的消息;
static int do_mq_timedreceive(mqd_t mqdes, char __user *u_msg_ptr,
size_t msg_len, unsigned int __user *u_msg_prio,
struct timespec64 *ts)
{
ssize_t ret; // 定义返回值
struct msg_msg *msg_ptr; // 消息指针
struct fd f; // 文件描述符
struct inode *inode; // inode 结构
struct mqueue_inode_info *info; // 消息队列 inode 信息
struct ext_wait_queue wait; // 等待队列
ktime_t expires, *timeout = NULL; // 超时时间
struct posix_msg_tree_node *new_leaf = NULL; // 新节点
// 如果设置了超时时间,则将其转换为内核时间格式
if (ts) {
expires = timespec64_to_ktime(*ts);
timeout = &expires;
}
// 记录系统调用的审计信息
audit_mq_sendrecv(mqdes, msg_len, 0, ts);
// 获取文件描述符对应的文件
f = fdget(mqdes);
if (unlikely(!f.file)) {
ret = -EBADF; // 如果文件描述符无效,返回 EBADF 错误码
goto out; // 跳转到 out 标签
}
// 获取文件的 inode 结构
inode = file_inode(f.file);
if (unlikely(f.file->f_op != &mqueue_file_operations)) {
ret = -EBADF; // 如果文件操作不是消息队列操作,返回 EBADF 错误码
goto out_fput; // 跳转到 out_fput 标签
}
info = MQUEUE_I(inode); // 获取消息队列 inode 信息
audit_file(f.file); // 记录文件的审计信息
// 检查文件是否可读
if (unlikely(!(f.file->f_mode & FMODE_READ))) {
ret = -EBADF; // 如果文件不可读,返回 EBADF 错误码
goto out_fput; // 跳转到 out_fput 标签
}
// 检查消息缓冲区是否足够大
if (unlikely(msg_len < info->attr.mq_msgsize)) {
ret = -EMSGSIZE; // 如果消息缓冲区大小不够,返回 EMSGSIZE 错误码
goto out_fput; // 跳转到 out_fput 标签
}
// 如果没有缓存节点,则分配一个新的节点
if (!info->node_cache)
new_leaf = kmalloc(sizeof(*new_leaf), GFP_KERNEL);
spin_lock(&info->lock);
if (!info->node_cache && new_leaf) {
/* Save our speculative allocation into the cache */
INIT_LIST_HEAD(&new_leaf->msg_list); // 初始化消息链表
info->node_cache = new_leaf; // 将新节点存储到缓存中
} else {
kfree(new_leaf); // 释放新节点的内存
}
// 如果消息队列中没有消息
if (info->attr.mq_curmsgs == 0) {
if (f.file->f_flags & O_NONBLOCK) { // 如果设置了非阻塞标志
spin_unlock(&info->lock); // 解锁消息队列
ret = -EAGAIN; // 返回 EAGAIN 错误码(表示资源暂时不可用)
} else { // 否则
wait.task = current; // 设置等待队列的任务为当前任务
/* memory barrier not required, we hold info->lock */
WRITE_ONCE(wait.state, STATE_NONE); // 设置等待状态为 STATE_NONE
ret = wq_sleep(info, RECV, timeout, &wait); // 等待消息的到来
msg_ptr = wait.msg; // 获取等待队列中的消息指针
}
} else { // 如果消息队列中有消息
DEFINE_WAKE_Q(wake_q); // 定义唤醒队列
// 从消息队列中获取消息
msg_ptr = msg_get(info);
// 更新 inode 的访问时间
inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);
/* There is now free space in queue. */
pipelined_receive(&wake_q, info); // 从管道中接收消息
spin_unlock(&info->lock); // 解锁消息队列
wake_up_q(&wake_q); // 唤醒等待队列
ret = 0; // 设置返回值为 0(表示成功)
}
// 处理接收到的消息
if (ret == 0) {
ret = msg_ptr->m_ts; // 获取消息的时间戳
// 将消息的优先级存储到用户空间
if ((u_msg_prio && put_user(msg_ptr->m_type, u_msg_prio)) ||
store_msg(u_msg_ptr, msg_ptr, msg_ptr->m_ts)) {
ret = -EFAULT; // 如果存储消息失败,返回 EFAULT 错误码
}
free_msg(msg_ptr); // 释放消息占用的内存
}
out_fput:
fdput(f); // 释放文件描述符
out:
return ret; // 返回结果
}
【wq_sleep】:
该函数主要通过wq_add()将等待项添加到消息队列中,并进入一个无限循环中进行睡眠等待。
在该循环中,会将当前进程状态设为可中断睡眠状态,并解锁消息消息队列,一直等待被唤醒(即ewp->state == STATE_READY
时被唤醒),或呗信号/超时被唤醒。
在被唤醒时,会将该等待项从等待队列中移除,并解锁消息队列。
static int wq_sleep(struct mqueue_inode_info *info, int sr,
ktime_t *timeout, struct ext_wait_queue *ewp)
__releases(&info->lock) // 函数释放 info->lock 锁
{
int retval; // 返回值
signed long time; // 超时时间
wq_add(info, sr, ewp); // 将等待队列添加到消息队列中
for (;;) { // 无限循环,直到条件满足或者超时或者被信号中断
/* memory barrier not required, we hold info->lock */
__set_current_state(TASK_INTERRUPTIBLE); // 设置当前进程状态为可中断睡眠状态
spin_unlock(&info->lock); // 解锁消息队列
time = schedule_hrtimeout_range_clock(timeout, 0,
HRTIMER_MODE_ABS, CLOCK_REALTIME); // 设置超时时间
if (READ_ONCE(ewp->state) == STATE_READY) { // 如果等待状态为 STATE_READY
/* see MQ_BARRIER for purpose/pairing */
smp_acquire__after_ctrl_dep(); // 内存屏障
retval = 0; // 返回成功
goto out; // 跳转到 out 标签
}
spin_lock(&info->lock); // 加锁消息队列
/* we hold info->lock, so no memory barrier required */
if (READ_ONCE(ewp->state) == STATE_READY) { // 如果等待状态为 STATE_READY
retval = 0; // 返回成功
goto out_unlock; // 跳转到 out_unlock 标签
}
if (signal_pending(current)) { // 如果当前进程有信号待处理
retval = -ERESTARTSYS; // 返回重新启动系统调用的错误码
break; // 跳出循环
}
if (time == 0) { // 如果超时时间为 0
retval = -ETIMEDOUT; // 返回超时错误码
break; // 跳出循环
}
}
list_del(&ewp->list); // 从列表中删除等待队列
out_unlock:
spin_unlock(&info->lock); // 解锁消息队列
out:
return retval; // 返回结果
}
疑问:为何在该函数中,会进行两次相同的状态判断if (READ_ONCE(ewp->state) == STATE_READY)
:
答:这两个判断的作用是在多线程或者多进程环境中确保对等待状态的准确性。在解锁和重新锁定之间可能会有其他线程或者进程修改了 ewp->state
的值,为了确保获取到最新的状态,需要使用 READ_ONCE
来获取 ewp->state
的值,并且确保在相应的位置使用内存屏障来保证内存一致性。
使用两次判断的目的是为了确保获取到最新的状态,并且避免在解锁和重新锁定之间产生竞态条件。
【msg_get】:
该函数主要通过按优先级一次遍历消息队列红黑树,获取到消息后更新消息数和消息总大小;其作用是从消息队列中获取一个消息,并返回该消息的指针。
- 函数首先尝试从优先级最高的节点开始查找消息,然后从该节点的消息列表中获取第一个消息。
- 如果该节点的消息列表为空,则移除该节点,并重新尝试获取消息。
- 最后,函数更新消息队列中的消息数和总消息大小,并返回获取到的消息指针。
【pipelined_receive】:
在通过msg_get函数从消息队列中获取到消息指针后,通过pipelined_receive()函数从等待发送队列中拿出首个等待项,将其插入消息队列中;即msg_get函数从消息队列中拿走一个消息块,通过pipelined_receive()函数再向消息队列放一个消息块;
- 获取“等待发送队列”中的第一个等待发送项;
- 等待发送队列中没有要发送的等待项,则唤醒等待队列,通知其他进程;
- 通过调用msg_insert()将发送者等待项中要发送的信息插入消息队列中;
- 通过调用
__pipelined_op(wake_q, info, sender);
来从等待发送队列中移除 当前发送的等待项,并唤醒发送进程。
/* pipelined_receive() - if there is task waiting in sys_mq_timedsend()
* gets its message and put to the queue (we have one free place for sure). */
static inline void pipelined_receive(struct wake_q_head *wake_q,
struct mqueue_inode_info *info)
{
struct ext_wait_queue *sender = wq_get_first_waiter(info, SEND); // 获取第一个等待发送的任务
if (!sender) { // 如果没有等待发送的任务
/* for poll */
wake_up_interruptible(&info->wait_q); // 唤醒等待队列
return; // 返回
}
if (msg_insert(sender->msg, info)) // 将发送任务的消息插入到队列中
return; // 如果插入失败,直接返回
__pipelined_op(wake_q, info, sender); // 执行管道操作
}
持续更新中…