一:什么是通信机制
是linux提供的用于内核和用户态进程之间的通信方式。
但是注意虽然主要用于用户空间和内核空间的通信,但是也能用于用户空间的两个进程通信。只是进程间通信有其他很多方式,一般不用。除非需要用到的广播特性时。
那么有什么优势呢?
一般来说用户空间和内核空间的通信方式有三种:/proc、ioctl、。而前两种都是单向的,但是可以实现双工通信。
协议基于BSD 和地址簇( ),使用32位的端口号寻址(以前称作PID),每个协议(或称作总线,man手册中则称之为 ),通常与一个或一组内核服务/组件相关联,如用于获取和设置路由与链路信息、NT用于内核向用户空间的udev进程发送通知等。
具有以下特点:
① 支持全双工、异步通信(当然同步也支持)
② 用户空间可使用标准的BSD 接口(但并没有屏蔽掉协议包的构造与解析过程,推荐使用libnl等第三方库)
③ 在内核空间使用专用的内核API接口
④ 支持多播(因此支持“总线”式通信,可实现消息订阅)
⑤ 在内核端可用于进程上下文与中断上下文
二:用户态数据结构
首先看一下几个重要的数据结构的关系:
1.
这个结构在变成中就会用到,并不算专有的,这里不在过多说明。只说明一下如何更好理解这个结构的功能。我们知道消息的发送和接收函数一般有这几对:recv/send、readv/、/。当然还有/,前面三对函数各有各的特点功能,而/就是要囊括前面三对的所有功能,当然还有自己特殊的用途。的前两个成员就是为了满足/的功能,中间两个成员和则是为了满足readv/的功能,而最后的则是为了满足recv/send中flag的功能,剩下的和则是满足/特有的功能。
2.
为的地址,和我们通常编程中的作用一样,他们的结构对比如下:
的详细定义和描述如下:
; /*该字段总是为 */
short ; /* 目前未用到,填充为0*/
__u32 ; /* pid */
__u32 ; /* mask */
};
(1) :在规范里,PID全称是Port-ID(),其主要作用是用于唯一的标识一个基于的通道。通常情况下都设置为当前进程的进程号。前面我们也说过,不仅可以实现用户-内核空间的通信还可使现实用户空间两个进程之间,或内核空间两个进程之间的通信。该属性为0时一般指内核。
(2) :如果用户空间的进程希望加入某个多播组,则必须执行bind()系统调用。该字段指明了调用者希望加入的多播组号的掩码(注意不是组号,后面我们会详细讲解这个字段)。如果该字段为0则表示调用者不希望加入任何多播组。对于每个隶属于协议域的协议,最多可支持32个多播组(因为的长度为32比特),每个多播组用一个比特来表示。
3.
的报文由消息头和消息体构成, 即为消息头。消息头定义在文件里,由结构体表示:
__u32 ; /* of */
__u16 ; /* */
__u16 ; /* flags */
__u32 ; /* */
__u32 ; /* PID */
};
消息头中各成员属性的解释及说明:
(1) :整个消息的长度,按字节计算。包括了消息头本身。
(2) :消息的类型,即是数据还是控制消息。目前(内核版本2.6.21)仅支持四种类型的控制消息,如下:
a) -空消息,什么也不做;
b) -指明该消息中包含一个错误;
c) -如果内核通过队列返回了多个消息,那么队列的最后一条消息的类型为,其余所有消息的属性都被设置位有效。
d) -暂时没用到。
(3) :附加在消息上的额外说明信息,如上面提到的。
三:用户空间 API
1.创建
int (int , int type, int )
指代地址族,即;
套接字类型为或,因为是一个面向数据报的服务;
选择该套接字使用哪种特征。
以下是几种预定义的协议类型:
,
,
,
。
可以非常容易的添加自己的协议。
为每一个协议类型最多可以定义32个多播组。
每一个多播组用一个来表示,1<
2.地址绑定bind()
bind(fd, ( *)&, , ());
3.发送消息
为了发送一条消息到内核或者其他的用户空间进程,另外一个 需要作为目的地址,这和使用()发送一个UDP包是一样的。
如果该消息是发送至内核的,那么和都置为0.
如果消息是发送给另一个进程的单播消息,是另外一个进程的pid值而为零。
如果消息是发送给一个或多个多播组的多播消息,所有的目的多播组必须必须or起来从而形成域。
(fd, &, msg, 0);
4.接收消息
一个接收程序必须分配一个足够大的内存用于保存消息头和消息负载。然后其填充 msg,再使用标准的()函数来接收消息。
当消息被正确的接收之后,nlh应该指向刚刚接收到的消息的头。应该包含接收消息的目的地址,其中包括了消息发送者的pid和多播组。同时,宏(nlh),定义在.h中,返回一个指向消息负载的指针。调用close(fd)关闭fd描述符所标识的
(fd, &, msg, 0);
四:内核空间 API
1.创建
sock *e( net *net,
int unit, int ,
void (*input)( *skb),
mutex *, *);
参数说明:
(1) net:是一个网络名字空间,在不同的名字空间里面可以有自己的转发信息库,有自己的一套等等。默认情况下都是使用 这个全局变量。
(2) unit:表示协议类型,如、。
(3) :多播地址。
(4) input:为内核模块定义的消息处理函数,当有消 息到达这个 时,该input函数指针就会被引用,且只有此函数返回时,调用者的才能返回。
(5) :为访问数据时的互斥信号量。
(6) :一般为。
2.发送单播消息
int ( sock *ssk, *skb, u32 pid, int )
参数说明:
(1) ssk:为函数 e()返回的。
(2) skb:存放消息,它的data字段指向要发送的消息结构,而 skb的控制块保存了消息的地址信息,宏(skb)就用于方便设置该控制块。
(3) pid:为接收此消息进程的pid,即目标地址,如果目标为组或内核,它设置为 0。
(4) :表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回;而如果为0,该函数在没有接收缓存可利用定时睡眠。
3.发送广播消息
int ( sock *ssk, *skb, u32 pid, u32 group, gfp_t )
前面的三个参数与 相同,参数group为接收消息的多播组,该参数的每一个位代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播组组ID的位或。参数为内核内存分配类型,一般地为或,用于原子的上下文(即不可以睡眠),而用于非原子上下文。
4.释放
int ( sock *ssk, *skb, u32 pid, u32 group, gfp_t )
五:用户态范例一
#
#
#
#
#
#
#
#
#
#
#
# 1024 // size
# 25 //自定义的协议
int main(int argc, char* argv[])
int state;
, ;
*nlh = NULL; //数据包头
iovec iov;
msg;
int , ;
int = 0;
// a
= (, , );
if( == -1){
(“error : %s”, (errno));
-1;
// To
(&, 0, ());
. = ;
. = 100; //A:设置源端端口号
. = 0;
//Bind
= bind(, ( *)&, ());
if( < 0){
(“bind : %s”, (errno));
close();
-1;
// To
nlh = ( *)(());
if(!nlh){
(” error!n”);
close();
-1;
(&,0,());
. = ;
. = 0; //B:设置目的端口号
. = 0;
nlh-> = ();
nlh-> = 100; //C:设置源端口
nlh-> = 0;
((nlh),”Hello you!”); //设置消息体
iov. = (void *)nlh;
iov. = ();
//
(&msg, 0, (msg));
msg. = (void *)&;
msg. = ();
msg. = &iov;
msg. = 1;
//send
(“”);
= (,&msg,0);
if( == -1)
(“get error = %sn”,(errno));
(nlh,0,());
//
(” !n”);
while(1){
(“In while “);
state = (, &msg, 0);
if(state{
(“state}
(” : %sn”,(char *) (nlh));
close();
0;
上面程序首先向内核发送一条消息;“Hello you”,然后进入循环一直等待读取内核的回复,并将收到的回复打印出来。如果看上面程序感觉很吃力,那么应该首先复习一下UDP中使用的用法,特别时 的结构要清楚,这里再赘述。下面主要分析与UDP发送数据包的不同点:
地址结构不同,UDP为,为 ;与UDP发送数据相比,多了一个消息头结构 需要我们构造。
注意代码注释中的A、B、C三处分别设置了pid。首先解释一下什么是pid,网上很多文章把这个字段说成是进程的pid,其实这完全是望文生义。这里的pid和进程pid没有什么关系,仅仅相当于UDP的port。对于UDP来说port和ip标示一个地址,那对我们的协议(注意本身不是一个协议)来说,pid就唯一标示了一个地址。所以你如果用进程pid做为标示当然也是可以的。当然同样的pid对于协议和内核定义的其他使用的协议是不冲突的(就像TCP的80端口和UDP的80端口)。
下面分析这三处设置pid分别有什么作用,首先A和B位置的比较好理解,这是在地址()上进行的设置,就是相当于设置源地址和目的地址(其实是端口),只是注意B处设置pid为0,0就代表是内核,可以理解为内核专用的pid,那么用户进程就不能用0做为自己的pid吗?这个只能说如果你非要用也是可以的,只是会产生一些问题,后面在分析。
接下来看为什么C处的消息头仍然需要设置pid呢?这里首先要知道一个前提:内核不会像UDP一样根据我们设置的原、目的地址为我们构造消息头,所以我们不在包头写入我们自己的地址(pid),那内核怎么知道是谁发来的报文呢?当然如果内核只是处理消息不需要回复进程的话舍不设置这个消息头pid都可以。
所以每个pid的设置功能不同:A处的设置是要设置发送者的源地址,有人会说既然源地址又不会自动填充到报文中,我们为什么还要设置这个,因为你还可能要接收回复啊。就像寄信,你连“门牌号”都没有,即使你在写信时候写上你的地址是100号,对方回信目的地址也是100号,但是邮局发现根本没有这个地址怎么可能把信送到你手里呢?所以A的主要作用是注册源地址,保证可以收到回复,如果不需要回复当然可以简单将pid设置为0;B处自然就是收信人的地址,pid为0代表内核的地址,假如有一个进程在101号上注册了地址,并调用了,如果你将B处的pid设置为101,那数据包就发给了另一个进程,这就实现了使用进行进程间通信;C相当于你在信封上写的源地址,通常情况下这个应该和你的真实地址(A)处注册的源地址相同,当然你要是不想收到回信,又想恶搞一下或者有特殊需求,你可以写成其他进程注册的pid(比如101)。这和我们现实中寄信是一样的,你给你朋友写封情书,把写信人写成你的另一个好基友,然后后果你懂得……
好了,有了这个例子我们就大概知道用户态怎么使用了。
六:内核态程序范例一
#
#
#
#
#
#
#
# 25
# 1024
int (char *s);
int err;
sock *nl_sk = NULL;
int flag = 0;
//向用户态进程回发消息
void (char *, int pid)
*skb_1;
*nlh;
int len = ();
int slen = 0;
if(! || !nl_sk)
;
( “pid:%dn”,pid);
skb_1 = (len,);
if(!skb_1)
( “: “);
slen = ();
nlh = (skb_1,0,0,0,,0);
(skb_1).pid = 0;
(skb_1). = 0;
[slen]= ‘�’;
((nlh),,slen+1);
(“:send ‘%s’.n”,(char *)(nlh));
(nl_sk,skb_1,pid,);
int (char *s)
int slen = 0;
for(; *s; s++)
slen++;
slen;
//接收用户态发来的消息
void ( *__skb)
*skb;
*nlh;
char str[100];
cmpl;
(“begin “);
int i=10;
int pid;
skb = (__skb);
if(skb->len >= (0))
nlh = (skb);
(str, (nlh), (str));
(” :%sn”,str) ;
pid = nlh->;
while(i–)
{//我们使用做延时,每3秒钟向用户态回发一个消息
(&cmpl);
(&cmpl,3 * HZ);
(“I am from !”,pid);
flag = 1;
(skb);
//
int (void)
nl_sk = e(&, , 1,
, NULL, );
if(!nl_sk){
( “: error.n”);
1;
(“: ok.n”);
0;
void (void)
if(nl_sk != NULL){
(nl_sk->);
(“: self “);
();
();
(“”);
(“GPL”);
附上内核代码的文件:
ifneq ($(),)
obj-m :=netl.o
else
?=/lib//$(shell uname -r)/build
PWD :=$(shell pwd)
:
$(MAKE) -C $() M=$(PWD)
endif
我们将内核模块后,运行用户态程序,结果如下:
这个结果复合我们的预期,但是运行过程中打印出“”卡了好久才输出了后面的结果。这时候查看客户进程是处于D状态的(不了解D状态的同学可以一下)。这是为什么呢?因为进程使用向内核发数据是同步,内核向进程发数据是异步。什么意思呢?也就是用户进程调用发送消息后,内核会调用相应的接收函数,但是一定到这个接收函数执行完用户态的才能够返回。我们在内核态的接收函数中调用了10次回发函数,每次都等待3秒钟,所以内核接收函数30秒后才返回,所以我们用户态程序的也要等30秒后才返回。相反,内核回发的数据不用等待用户程序接收,这是因为内核所发的数据会暂时存放在一个队列中。
再来回到之前的一个问题,用户态程序的源地址(pid)可以用0吗?我把上面的用户程序的A和C处pid都改为了0,结果一运行就死机了。为什么呢?我们看一下内核代码的逻辑,收到用户消息后,根据消息中的pid发送回去,而pid为0,内核并不认为这是用户程序,认为是自身,所有又将回发的10个消息发给了自己(内核),这样就陷入了一个死循环,而用户态这时候进程一直处于D。
另外一个问题,如果同时启动两个用户进程会是什么情况?答案是再调用bind时出错:“ in use”,这个同UDP一样,同一个地址同一个port如果没有设置两次bind就会出错,之后我用同样的方式再的上设置了,但是并没有什么效果。
七:用户态范例二
之前我们说过UDP可以使用/也可以使用/,那么同样也可以使用/。具体实现如下:
#
#
#
#
#
#
#
#
#
#
#
# 1024 // size
# 25
int main(int argc, char* argv[])
, ;
*nlh = NULL;
int , ;
int state, = 0;
// a
= (, , );
if( == -1){
(“error : %s”, (errno));
-1;
// To
(&, 0, ());
. = ;
. = 100;
. = 0;
//Bind
= bind(, ( *)&, ());
if( < 0){
(“bind : %s”, (errno));
close();
-1;
// To head
nlh = ( *)(());
if(!nlh){
(” error!n”);
close();
-1;
(&,0,());
. = ;
. = 0;
. = 0;
nlh-> = ();
nlh-> = 100;
nlh-> = 0;
((nlh),”Hello you!”);
//send
(“”);
(,nlh,(),0,( *)(&),());
if( == -1)
(“get error = %sn”,(errno));
(nlh,0,());
//
(” !n”);
while(1){
(“In while “);
state=(,nlh,(),0,NULL,NULL);
if(state{
(“state}
(” : %sn”,(char *) (nlh));
(nlh,0,());
close();
0;
熟悉UDP编程的同学看到这个程序一定很熟悉,除了多了一个消息头的设置。但是我们发现程序中调用了bind函数,这个函数再UDP编程中的客户端不是必须的,因为我们不需要把UDP 与某个地址关联,同时再发送UDP数据包时内核会为我们分配一个随即的端口。但是对于必须要有这一步bind,因为内核可不会为我们分配一个pid。再强调一遍消息头()中的pid是告诉内核接收端要回复的地址,但是这个地址存不存在内核并不关心,这个地址只有用户端调用了bind后才存在。
我们看到这两个例子都是用户态首先发起的,那是否支持内核态主动发起的情况呢?
当然是可以的,只是内核一般需要事件触发,这里,只要和用户态约定号一个地址(pid),内核直接调用就可以了。
———END———
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,永久会员只需109元,全站资源免费下载 点击查看详情
站 长 微 信: nanadh666