找回密码
 注册
搜索
热搜: 回贴
微赢网络技术论坛 门户 服务器 Linux/BSD 查看内容

消息队列(报文队列)实践到内核--消息的发送

2009-12-20 13:25| 发布者: admin| 查看: 37| 评论: 0|原作者: 心然

我是无名小卒,一直想写一些关于内核方面的资料,学习内核很久了,市面上的内核书我都读过了,无法对任何一本书加以总结,因为他就象linux的内核一样在不断更新和升级,针对2.6内核现在市面上非常缺少相关内核的分析资料情况,当然,也有不少网友写了一些关于2.6内核的博客文章,我也看过,但是写的不够深刻具体,总是在内核的过程上粗略的一笔带过,因此我下决心只要有空闲时间就写一些日志来与大家分享,很多书籍和博客都是直接剖析内核的,我读过这些书后发现,这种学习方法虽然快,但是效果不太好,特别是我读了几遍后还是对很多知识点和结构记忆不深,对初学者来说更是枯燥无味的过程,使很多朋友放弃了研读内核代码的兴趣,确实如此,就象缺一张好的导游地图一样,如果我们目的明确,但是缺乏了指路的地图那无非是在大森林里迷了路一样,再高的旅游兴趣也荡然无存,因此,我想要是有一本书或者资料能够在实践中逐步深入到内核该有多好,那样能够使我们在即看到效果的时候导游到如何产生这样效果的内核中将会是一件非常有趣的事情,肯定能够轻松地掌握全部想要的知识,目前市面上有这样的书,但是评价不怎么好,所以我想根据多年读内核的书来整理和书写这类的文章,希望对有兴趣的朋友起到导游地图的效果。这些文章有可能是来自大家所熟悉的资料中也有可能来自互联网,总之,多多益善,我不会在文中注明具体出处,也希望原作者勿怪,我们的目标是大家共同进步。请注意本文使用的是基于2.6.26的内核,这是个人认为应该稳定使用的版本。

上一节我们追踪了消息队列是如何创建的,这节我们追踪一下消息是如何发送的,我们还是根据前边章节中的发送界面函数看一下:
msgsnd(msgid,(void*)&some_data,BUFSIZ,0);
参数msgid是从创建消息队列函数后返回的id号,some_data是我们自己声明的一个用于存放发送数据的结构变量,而BUFSIZ是存放数字的个数即数组的大小。好了我们已经回忆了应用程序的调用界面了,现在追踪到内核,同创建消息队列函数一样,通过sys_ipc ()函数后会执行到:

case MSGSND:
return sys_msgsnd (first, (struct msgbuf __user *) ptr,
second, third);
我们来看一下sys_msgsnd函数
asmlinkage long
sys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz, int msgflg)
{
long mtype;
if (get_user(mtype, &msgp->mtype))
return -EFAULT;
return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg);
}
参数指针msgp是一个msgbuf数据结构是从我们的程序中传递过来的,__user 是个宏简单告诉编译器(通过 noderef)不应该解除这个指针的引用(因为在当前地址空间中它是没有意义的)。我们看一下msgbuf数据结构
/* message buffer for msgsnd and msgrcv calls */
struct msgbuf {
long mtype; /* type of message */
char mtext[1]; /* message text */
};
注释中说明他是一个为消息发送和接收用的缓冲数据结构,mtype是消息的类型,而mtext[1]则是消息的内容。虽然我们在这里看到这个数组的个数是1,这种用法实际上在为了节省空间而使用的,必要的时候可以通过扩充数组来满足要求。因为msgbuf的内容还在应用程序中,即在用户空间。所以需要通过getuser先把消息的类型复制到内核空间中来,我们在这里提到用户空间和内核空间不熟悉的朋友可以看看有关资料,为什么是在内核空间?因为现在系统调用内部,而系统调用本来就是在内核空间中了,此后进入函数do_msgsnd()了,跟进去看看,这个函数有点大,分段来分析吧
long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
struct msg_queue *msq;
struct msg_msg *msg;
int err;
struct ipc_namespace *ns;
ns = current->nsproxy->ipc_ns;
if (msgsz > ns->msg_ctlmax || (long) msgsz 0 || msqid 0)
return -EINVAL;
if (mtype 1)
return -EINVAL;
msg = load_msg(mtext, msgsz);
if (IS_ERR(msg))
return PTR_ERR(msg);
msg->m_type = mtype;
msg->m_ts = msgsz;
msq = msg_lock_check(ns, msqid);
if (IS_ERR(msq)) {
err = PTR_ERR(msq);
goto out_free;
}
宏_user是为了编译器的,我们上边介绍了,首先是对消息的大小进行检验,以及消息的类型,下边是一个关键的函数load_msg()我们看到有一个参数一个是我们程序中的消息内容另一个是消息的大小

struct msg_msg *load_msg(const void __user *src, int len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
int err;
int alen;
alen = len;
if (alen > DATALEN_MSG)
alen = DATALEN_MSG;
msg = kmalloc(sizeof(*msg) alen, GFP_KERNEL);
if (msg == NULL)
return ERR_PTR(-ENOMEM);
msg->next = NULL;
msg->security = NULL;
if (copy_from_user(msg 1, src, alen)) {
err = -EFAULT;
goto out_err;
}
len -= alen;
src = ((char __user *)src) alen;
pseg = &msg->next;
while (len > 0) {
struct msg_msgseg *seg;
alen = len;
if (alen > DATALEN_SEG)
alen = DATALEN_SEG;
seg = kmalloc(sizeof(*seg) alen,
GFP_KERNEL);
if (seg == NULL) {
err = -ENOMEM;
goto out_err;
}
*pseg = seg;
seg->next = NULL;
if (copy_from_user(seg 1, src, alen)) {
err = -EFAULT;
goto out_err;
}
pseg = &seg->next;
len -= alen;
src = ((char __user *)src) alen;
}
err = security_msg_msg_alloc(msg);
if (err)
goto out_err;
return msg;
out_err:
free_msg(msg);
return ERR_PTR(err);
}
这个函数中出现了二个数据结构
struct msg_msg *msg;
struct msg_msgseg **pseg;
每一个消息都要有msg_msg数据结构
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list;
long m_type;
int m_ts; /* message text size */
struct msg_msgseg* next;
void *security;
/* the actual message follows immediately */
};
消息的内容如果小于一个页面时可以放在msg_msg结构的后面,如果消息的内容大于一个页面除了第一个页面仍旧放在msg_msg结构后面外,其他的则分开放在另外的页面中,那些页面的开头将用msg_msgseg结构来表示,其他页面中的消息内容的大小需要用一个页面的大小减去msg_msgseg结构大小
struct msg_msgseg {
struct msg_msgseg* next;
/* the next part of the message follows immediately */
};
明白上边的叙述,代码就容易看了,在load_msg()函数中首先是分配用于消息内容用的数据结构msg_msg,kmalloc()函数将在高速缓存章节学习,它是通用的高速缓存slab中分配msg_msg数据结构,分配数据后就要从应用程序中复制消息内容放在这个数据结构的后面,这是copy_from_user(msg 1, src, alen)中有msg 1的含义,这个函数我们先放一放,只知道他是从应用程序(用户空间)复制数据到内核中即这里的消息结构msg_msg的后边,src是我们上边看到的应用程序中准备的消息内容,alen是调整后的消息的大小即长度。如果一个页面没有装下所有消息内容,此时就需要分配msg_msgseg结构来放在每个页的开头其后存放剩余的消息内容。最后load_msg()函数返回这个消息的msg_msg数据结构指针给do_msgsnd()函数,接着要通过msg_lock_check()对消息队列进行一下检验
static inline struct msg_queue *msg_lock_check(struct ipc_namespace *ns,
int id)
{
struct kern_ipc_perm *ipcp = ipc_lock_check(&msg_ids(ns), id);
if (IS_ERR(ipcp))
return (struct msg_queue *)ipcp;
return container_of(ipcp, struct msg_queue, q_perm);
}
这里简单的提一下参数ns,它是一个ipc_namespace数据结构,这个参数是从当前进程中的task_struct结构的struct nsproxy *nsproxy存放着当前进程使用的哪一个ipc_namespace,这个结构指针赋值我们会在以后的章节中碰到。暂且放一放,接着我们通过ipc_lock_check()函数取得了消息队列头kern_ipc_perm结构指针
struct kern_ipc_perm *ipc_lock_check(struct ipc_ids *ids, int id)
{
struct kern_ipc_perm *out;
out = ipc_lock(ids, id);
if (IS_ERR(out))
return out;
if (ipc_checkid(out, id)) {
ipc_unlock(out);
return ERR_PTR(-EIDRM);
}
return out;
}
最关键的函数是ipc_lock(),我们跟进
struct kern_ipc_perm *ipc_lock(struct ipc_ids *ids, int id)
{
struct kern_ipc_perm *out;
int lid = ipcid_to_idx(id);
down_read(&ids->rw_mutex);
rcu_read_lock();
out = idr_find(&ids->ipcs_idr, lid);
if (out == NULL) {
rcu_read_unlock();
up_read(&ids->rw_mutex);
return ERR_PTR(-EINVAL);
}
up_read(&ids->rw_mutex);
spin_lock(&out->lock);

/* ipc_rmid() may have already freed the ID while ipc_lock
* was spinning: here verify that the structure is still valid
*/
if (out->deleted) {
spin_unlock(&out->lock);
rcu_read_unlock();
return ERR_PTR(-EINVAL);
}
return out;
}
不要担心我们马上就要看到目标了
void *idr_find(struct idr *idp, int id)
{
int n;
struct idr_layer *p;
n = idp->layers * IDR_BITS;
p = idp->top;
/* Mask off upper bits we don't use for the search. */
id &= MAX_ID_MASK;
if (id >= (1 n))
return NULL;
while (n > 0 && p) {
n -= IDR_BITS;
p = p->ary[(id >> n) & IDR_MASK];
}
return((void *)p);
}
最终ipc_find()找到了我们上节提到的消息队列头所在的idr_layer并层层返回他的地址(指针就是地址),接着进行了一些检测后,我们看到他最终通过container_of()函数取得了我们已经建立的消息队列的结构msg_queue指针。这个追踪过程中我们忽略了其中关于锁的细节,锁的问题我们以后会详解,这里以主线消息发送为主,我们所以只看消息队列相关部分了,回到do_msgsnd()中我们继续往下看
for (;;) {
struct msg_sender s;
err = -EACCES;
if (ipcperms(&msq->q_perm, S_IWUGO))
goto out_unlock_free;
err = security_msg_queue_msgsnd(msq, msg, msgflg);
if (err)
goto out_unlock_free;
if (msgsz msq->q_cbytes = msq->q_qbytes &&
1 msq->q_qnum = msq->q_qbytes) {
break;
}
/* queue full, wait: */
if (msgflg & IPC_NOWAIT) {
err = -EAGAIN;
goto out_unlock_free;
}
ss_add(msq, &s);
ipc_rcu_getref(msq);
msg_unlock(msq);
schedule();
ipc_lock_by_ptr(&msq->q_perm);
ipc_rcu_putref(msq);
if (msq->q_perm.deleted) {
err = -EIDRM;
goto out_unlock_free;
}
ss_del(&s);
if (signal_pending(current)) {
err = -ERESTARTNOHAND;
goto out_unlock_free;
}
}
msq->q_lspid = task_tgid_vnr(current);
msq->q_stime = get_seconds();
if (!pipelined_send(msq, msg)) {
/* noone is waiting for this message, enqueue it */
list_add_tail(&msg->m_list, &msq->q_messages);
msq->q_cbytes = msgsz;
msq->q_qnum ;
atomic_add(msgsz, &ns->msg_bytes);
atomic_inc(&ns->msg_hdrs);
}
err = 0;
msg = NULL;
out_unlock_free:
msg_unlock(msq);
out_free:
if (msg != NULL)
free_msg(msg);
return err;
}
这是一个非常大的for循环,循环中做了些什么呢?我们看到出现了一个新的结构变量,还是不用担心这么多数据结构是否记得过来,学习一下linux的效率高的秘诀吧“按需分配”,所以我们应该这样“按需学习”,这个数据结构并不可怕,可怕的是我们自己没有信心
/* one msg_sender for each sleeping sender */
struct msg_sender {
struct list_head list;
struct task_struct *tsk;
};
其实并不难,里面用二个结构其一肯定是关于消息队列头用的,其二是当前进程用的。注释中也说明了是为了一个发送进程用的数据结构。接着往下,通过ipcperms()函数对消息队列进行了权限的检验接着是检验消息字节数与队列的字节容量是否在规定范围内,下面就通过ss_add()将发送进程挂入到消息队列中的q_senders等待进程队列头中
static inline void ss_add(struct msg_queue *msq, struct msg_sender *mss)
{
mss->tsk = current;
current->state = TASK_INTERRUPTIBLE;
list_add_tail(&mss->list, &msq->q_senders);
}
这里注意一下当前进程的状态已经设置为了TASK_INTERRUPTIBLE,可中断状态,在进程调试时当前进程就会让出CPU,让接收消息的进程来读取消息队列中的消息。当接收的进程读走了消息后就会腾出足够的空间,发送进程就会被唤醒,即在上面的shedule()处返回,如果有信号等待处理就会先处理信号,接着就会继续进行下一个消息的发送过程,我们可以看到整个for循环只有一个break,也就是说只有在消息队列满了以后不能再发送了。就会跳出循环,从理论上看,似乎发送消息已经完成了,不过整个发送的过程以效率看可以优化也就是如果有接收进程正在等待就不必将消息链入队列再在接收过程中摘链,可以直接把消息直接交给接收进程,提高了效率,如果没有接收进程等待就可以挂入消息队列中了。这是在pipelined_send()函数中实现的
static inline int pipelined_send(struct msg_queue *msq, struct msg_msg *msg)
{
struct list_head *tmp;
tmp = msq->q_receivers.next;
while (tmp != &msq->q_receivers) {
struct msg_receiver *msr;
msr = list_entry(tmp, struct msg_receiver, r_list);
tmp = tmp->next;
if (testmsg(msg, msr->r_msgtype, msr->r_mode) &&
!security_msg_queue_msgrcv(msq, msg, msr->r_tsk,
msr->r_msgtype, msr->r_mode)) {
list_del(&msr->r_list);
if (msr->r_maxsize msg->m_ts) {
msr->r_msg = NULL;
wake_up_process(msr->r_tsk);
smp_mb();
msr->r_msg = ERR_PTR(-E2BIG);
} else {
msr->r_msg = NULL;
msq->q_lrpid = task_pid_vnr(msr->r_tsk);
msq->q_rtime = get_seconds();
wake_up_process(msr->r_tsk);
smp_mb();
msr->r_msg = msg;
return 1;
}
}
}
return 0;
}
这里通过msr->r_msg = msg把消息直接传递给了接收进程,上面的函数中有一个新的表示接收消息队列的进程结构struct 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 *volatile r_msg;
};
可以看出r_msg就是装载消息的指针。函数pipelined_send()会从消息队列中的q_receivers队列头开始逐个比对消息的类型和模式,如果相同就会进一步检查是否缓存区足够大,条件都满足了就会把消息直接交给代表接收进程的msg_receiver,并通过wake_up_process()将接收进程唤醒,让他来处理接收到的消息,如果没有找到合适的接收进程从pipelined_send()返回后就会把消息挂入到队列中的q_messages队列头中
list_add_tail(&msg->m_list, &msq->q_messages);
以后等到有接收进程时会从这个队列头中摘取消息,那是在msgrcv()应用中实现的,下一节讲述。




最新评论

QQ|小黑屋|最新主题|手机版|微赢网络技术论坛 ( 苏ICP备08020429号 )

GMT+8, 2024-9-30 05:28 , Processed in 0.259730 second(s), 12 queries , Gzip On, MemCache On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

返回顶部