linux 策略路由


linux 策略路由

1.策略路由介绍
策略性是指对于IP包的路由是以网络管理员根据需要定下的一些策略为主要依据进行路由的。例如我们可以有这样的策略:"所有来直自网A的包,选择X路径;其他选择Y路径",或者是"所有TOS为A的包选择路径F;其他选者路径K"。
  Cisco 的网络操作系统 (Cisco IOS) 从11.0开始就采用新的策略性路由机制。而Linux是在内核2.1开始采用策略性路由机制的。策略性路由机制与传统的路由算法相比主要是引入了多路由表以及规则的概念。
2.linux方式
2.1 多路由表(multiple Routing Tables)

传统的路由算法是仅使用一张路由表的。但是在有些情形底下,我们是需要使用多路由表的。例如一个子网通过一个路由器与外界相连,路由器与外界有两条线路相连,其中一条的速度比较快,一条的速度比较慢。对于子网内的大多数用户来说对速度并没有特殊的要求,所以可以让他们用比较慢的路由;但是子网内有一些特殊的用户却是对速度的要求比较苛刻,所以他们需要使用速度比较快的路由。如果使用一张路由表上述要求是无法实现的,而如果根据源地址或其它参数,对不同的用户使用不同的路由表,这样就可以大大提高路由器的性能。
2.2 规则(rule)


规则是策略性的关键性的新的概念。我们可以用自然语言这样描述规则,例如我门可以指定这样的规则:

  规则一:"所有来自192.16.152.24的IP包,使用路由表10,本规则的优先级别是1500"

  规则二:"所有的包,使用路由表253,本规则的优先级别是32767"

  我们可以看到,规则包含3个要素:

  什么样的包,将应用本规则(所谓的SELECTOR,可能是filter更能反映其作用);

  符合本规则的包将对其采取什么动作(ACTION),例如用那个表;

  本规则的优先级别。优先级别越高的规则越先匹配(数值越小优先级别越高)。
3. linux策略路由配置方式
传统的linux下配置路由的工具是route,而实现策略性路由配置的工具是iproute2工具包。
3.1 接口地址的配置 IP Addr

对于接口的配置可以用下面的命令进行:

Usage: ip addr [ add | del ] IFADDR dev STRING
例如:
router># ip addr add 192.168.0.1/24 broadcast 192.168.0.255 label eth0 dev eth0
上面表示,给接口eth0赋予地址192.168.0.1 掩码是255.255.255.0(24代表掩码中1的个数),广播地址是192.168.0.255
3.2 路由的配置 IP Route

Linux最多可以支持255张路由表,其中有3张表是内置的:
  表255 本地路由表(Local table)本地接口地址,广播地址,已及NAT地址都放在这个表。该路由表由系统自动维护,管理员不能直接修改。
  表254 主路由表(Main table)如果没有指明路由所属的表,所有的路由都默认都放在这个表里,一般来说,旧的路由工具(如route)所添加的路由都会加到这个表。一般是普通的路由。
  表253 默认路由表(Default table)一般来说默认的路由都放在这张表,但是如果特别指明放的也可以是所有的网关路由。
  表 0 保留
  路由配置命令的格式如下:
Usage: ip route list SELECTOR
ip route { change | del | add | append | replace | monitor } ROUTE
  如果想查看路由表的内容,可以通过命令:
  ip route list table table_number
  对于路由的操作包括change、del、add 、append 、replace 、 monitor这些。例如添加路由可以用:
router># ip route add 0/0 via 192.168.0.4 table main
router># ip route add 192.168.3.0/24 via 192.168.0.3 table 1
 第一条命令是向主路由表(main table)即表254添加一条路由,路由的内容是设置192.168.0.4成为网关。
  第二条命令代表向路由表1添加一条路由,子网192.168.3.0(子网掩码是255.255.255.0)的网关是192.168.0.3。
  在多路由表的路由体系里,所有的路由的操作,例如网路由表添加路由,或者在路由表里寻找特定的路由,需要指明要操作的路由表,所有没有指明路由表,默认是对主路由表(表254)进行操作。而在单表体系里,路由的操作是不用指明路由表的。
3.3 规则的配置 IP Rule


  在Linux里,总共可以定义232个优先级的规则,一个优先级别只能有一条规则,即理论上总共可以有条规则。其中有3个规则是默认的。命令用法如下:
Usage: ip rule [ list | add | del ] SELECTOR ACTION
SELECTOR := [ from PREFIX ] [ to PREFIX ] [ tos TOS ]
[ dev STRING ] [ pref NUMBER ]
ACTION := [ table TABLE_ID ] [ nat ADDRESS ]
[ prohibit | reject | unreachable ]
[ flowid CLASSID ]
TABLE_ID := [ local | main | default | new | NUMBER

  首先我们可以看看路由表默认的所有规则:
root@netmonster# ip rule list
0: from all lookup local
32766: from all lookup main
32767: from all lookup default

  规则0,它是优先级别最高的规则,规则规定,所有的包,都必须首先使用local表(254)进行路由。本规则不能被更改和删除。

  规则32766,规定所有的包,使用表main进行路由。本规则可以被更改和删除。
  规则32767,规定所有的包,使用表default进行路由。本规则可以被更改和删除。
  在默认情况下进行路由时,首先会根据规则0在本地路由表里寻找路由,如果目的地址是本网络,或是广播地址的话,在这里就可以找到合适的路由;如果路由失败,就会匹配下一个不空的规则,在这里只有32766规则,在这里将会在主路由表里寻找路由;如果失败,就会匹配32767规则,即寻找默认路由表。如果失败,路由将失败。重这里可以看出,策略性路由是往前兼容的。

  还可以添加规则:
router># ip rule add [from 0/0] table 1 pref 32800
router >#ip rule add from 192.168.3.112/32 [tos 0x10] table 2 pref 1500prohibit
  第一条命令将向规则链增加一条规则,规则匹配的对象是所有的数据包,动作是选用路由表1的路由,这条规则的优先级是32800。
  第二条命令将向规则链增加一条规则,规则匹配的对象是IP为192.168.3.112,tos等于0x10的包,使用路由表2,这条规则的优先级是1500,动作是。添加以后,我们可以看看系统规则的变化。
router># ip rule
0: from all lookup local
1500 from 192.168.3.112/32 [tos 0x10] lookup 2
32766: from all lookup main
32767: from all lookup default
32800: from all lookup 1
  上面的规则是以源地址为关键字,作为是否匹配的依据的。除了源地址外,还可以用以下的信息:
  From -- 源地址
  To -- 目的地址(这里是选择规则时使用,查找路由表时也使用)
  Tos -- IP包头的TOS(type of sevice)域
  Dev -- 物理接口
  Fwmark -- 防火墙参数

  采取的动作除了指定表,还可以指定下面的动作:
   Table 指明所使用的表
   Nat 透明网关
   Action prohibit 丢弃该包,并发送 COMM.ADM.PROHIITED的ICMP信息
   Reject 单纯丢弃该包
   Unreachable丢弃该包,并发送 NET UNREACHABLE的ICMP信息
4.策略路由的应用
4.1 基于源地址选路( Source-Sensitive Routing)

  如果一个网络通过两条线路接入互联网,一条是比较快的ADSL,另外一条是比较慢的普通的调制解调器。这样的话,网络管理员既可以提供无差别的路由服务,也可以根据源地址的不同,使一些特定的地址使用较快的线路,而普通用户则使用较慢的线路,即基于源址的选路。

4.2 根据服务级别选路( Quality of Service)


  网络管理员可以根据IP报头的服务级别域,对于不同的服务要求可以分别对待对于传送速率、吞吐量以及可靠性的有不同要求的数据报根据网络的状况进行不同的路由。

4.3 节省费用的应用


  网络管理员可以根据通信的状况,让一些比较大的阵发性通信使用一些带宽比较高但是比较贵的路径一段短的时间,然后让基本的通信继续使用原来比较便宜的基本线路。例如,管理员知道,某一台主机与一个特定的地址通信通常是伴随着大量的阵发性通信的,那么网络管理员可以安排一些策略,使得这些主机使用特别的路由,这些路由是按需拨号,带宽比较高的线路,通信完成以后就停止使用,而普通的通信则不受影响。这样既提高网络的性能,又能节省费用。

4.4 负载平衡(Load Sharing)


  根据网络交通的特征,网络管理员可以在不同的路径之间分配负荷实现负载平衡。
5 linux下策略路由的实现
  在Linux下,策略性路由是由RPDB实现的。对于RPDB的内部机制的理解,可以加深对于策略性路由使用的理解。文件主要包含:
fib_hash.c
fib_rules.c
fib_sematic
fib_frontend.c
route.c
  RDPB主要由多路由表和规则组成。路由表以及对其的操作和其对外的接口是整个RPDB的核心部分。路由表主要由table,zone,node这些主要的数据结构构成。对路由表的操作主要包含物理的操作以及语义的操作。路由表除了向IP层提供路由寻找的接口以外还必须与几个元素提供接口:与用户的接口(即更改路由)、proc的接口、IP层控制接口、以及和硬件的接口(网络接口的改变会导致路由表内容的改变)。处在RDPB的中心的规则,由规则选取表。IP层并不直接使用路由表,而是通过一个路由适配层,路由适配层提供为IP层提供高性能的路由服务。
5.1 路由表(Fib Table)


  数据结构:
  在整个策略性路由的框架里,路由表是最重要的的数据结构,我们在上面以及对路由表的概念和结构进行了清楚的说明。Linux里通过下面这些主要的数据结构进行实现的。
主要数据结构

作用

位置

struct fib_table

路由表

ip_fib.h 116

struct fn_hash

路由表的哈希数据

fib_hash.c 104

struct fn_zone

zone域

fib_hash.c 85

struct fib_node

路由节点

fib_hash.c 68

struct fib_info

路由信息

ip_fib.h 57

struct fib_result

路由结果

ip_fib.h 86


  数据结构之间的主要关系如下。路由表由路由表号以及路由表的操作函数指针还有表数据组成。这里需要注意的是,路由表结构里并不直接定义zone域,而是通过一个数据指针指向fn_hash。只有当zone里有数据才会连接到fn_zone_list里。
  系统的所有的路由表由数组变量*fib_tables[RT_TABLE_MAX+1]维护,其中系统定义RT_TABLE_MAX为254,也就是说系统最大的路由表为255张,所有的路由表的操作都是对这个数组进行的。。同时系统还定义了三长路由表*local_table; *main_table。

  路由表的操作:
  Linux策略路由代码的主要部分是对路由表的操作。对于路由表的操作,物理操作是直观的和易于理解的。对于表的操作不外乎就是添加、删除、更新等的操作。还有一种操作,是所谓的语义操作,语义操作主要是指诸如计算下一条的地址,把节点转换为路由项,寻找指定信息的路由等。

  1、物理操作(operation):
  路由表的物理操作主要包括如下这些函数:

路由标操作实现函数 位置
新建路由表
删除路由表
搜索路由 fn_hash_lookup fib_hash.c 269
插入路由到路由表 fn_hash_insert fib_hash.c 341
删除路由表的路由 fn_hash_delete
fn_hash_dump
fib_hash.c 433
fib_hash.c 614
更新路由表的路由 fn_hash_flush fib_hash.c 729
显示路由表的路由信息 fn_hash_get_info fib_hash.c 750
选择默认路由 fn_hash_select_default fib_hash.c 842
  2、语义操作(semantics operation):

  语义操作并不涉及路由表整体框架的理解,而且,函数名也是不言自明的,所以请大家参考fib_semantics.c。

  3、接口(front end)
  对于路由表接口的理解,关键在于理解那里有
   IP
   首先是路由表于IP层的接口。路由在目前linux的意义上来说,最主要的还是IP层的路由,所以和IP层的的接口是最主要的接口。和ip层的衔接主要是向IP层提供寻找路由、路由控制、寻找指定ip的接口。
Fil_lookup
ip_rt_ioctl fib_frontend.c 286;" f
ip_dev_find 145
  Inet
  路由表还必须提供配置接口,即用户直接操作路由的接口,例如增加和删除一条路由。当然在策略性路由里,还有规则的添加和删除。
inet_rtm_delroute 351
inet_rtm_newroute 366
inet_check_attr 335
   proc
   在/proc/net/route里显示路由信息。
   fib_get_procinfo
  4、网络设备(net dev event)
  路由是和硬件关联的,当网络设备启动或关闭的时候,必须通知路由表的管理程序,更新路由表的信息。


fib_disable_ip 567
fib_inetaddr_event 575
fib_netdev_event

  5、内部维护( magic)

  上面我们提到,本地路由表(local table)的维护是由系统自动进行的。也就是说当用户为硬件设置IP地址等的时候,系统自动在本地路由表里添加本地接口地址以及广播地址。
fib_magic 417
fib_add_ifaddr 459
fib_del_ifaddr 498
  Rule
  1、数据结构
  规则在fib_rules.c的52行里定义为 struct fib_rule。而RPDB里所有的路由是保存在101行的变量fib_rules里的,注意这个变量很关键,它掌管着所有的规则,规则的添加和删除都是对这个变量进行的。
  2、系统定义规则:
  fib_rules被定义以后被赋予了三条默认的规则:默认规则,本地规则以及主规则。

u 本地规则local_rule
94 static struct fib_rule local_rule = {
r_next: &main_rule, /*下一条规则是主规则*/
r_clntref: ATOMIC_INIT(2),
r_table: RT_TABLE_LOCAL, /*指向本地路由表*/
r_action: RTN_UNICAST, /*动作是返回路由*/
};
u 主规则main_rule
86 static struct fib_rule main_rule = {
r_next: &default_rule,/*下一条规则是默认规则*/
r_clntref: ATOMIC_INIT(2),
r_preference: 0x7FFE, /*默认规则的优先级32766*/
r_table: RT_TABLE_MAIN, /*指向主路由表*/
r_action: RTN_UNICAST, /*动作是返回路由*/
};
u 默认规则default rule
79 static struct fib_rule default_rule = {
r_clntref: ATOMIC_INIT(2),
r_preference: 0x7FFF,/*默认规则的优先级32767*/
r_table: RT_TABLE_DEFAULT,/*指默认路由表*/
r_action: RTN_UNICAST,/*动作是返回路由*/
};
  规则链的链头指向本地规则。
  RPDB的中心函数fib_lookup
  现在到了讨论RPDB的实现的的中心函数fib_lookup了。RPDB通过提供接口函数fib_lookup,作为寻找路由的入口点,在这里有必要详细讨论这个函数,下面是源代码:,
310 int fib_lookup(const struct rt_key *key, struct fib_result *res)
311 {
312 int err;
313 struct fib_rule *r, *policy;
314 struct fib_table *tb;
315
316 u32 daddr = key->dst;
317 u32 saddr = key->src;
318
321 read_lock(&fib_rules_lock);
322 for (r = fib_rules; r; r=r->r_next) {/*扫描规则链fib_rules里的每一条规则直到匹配为止*/
323 if (((saddr^r->r_src) & r->r_srcmask) ||
324 ((daddr^r->r_dst) & r->r_dstmask) ||
325 #ifdef CONFIG_IP_ROUTE_TOS
326 (r->r_tos && r->r_tos != key->tos) ||
327

链表删除过程

c里面的malloc和free的应该一一对应,开辟了一个就要释放一个,因此,对于动态创建的链接,进行删除操作的基本步骤如下:
第一步,获得第一个节点的地址。
第二步,根据第一个节点获得第二个节点地址。
第三步,调用free函数释放第一个节点。
第四步,根据二个节点获得第三个节点地址。
第五步,调用free函数释放第二个节点。
第六步,以此类推从头到尾删除所有的对象。
struct Node
{
……
struct Node * Next; /*下一对象的位置*/
};
void RemoveAll(struct Node *List)
{
struct Node *pHead=NULL;
struct Node *pOne=NULL;
pOne= List;
while(pOne->Next!=NULL)
{
pPrevOne=pOne;
pOne=pOne->Next;
free(pPrevOne);
}
}

linux 网络接口程序总体架构分析

Linux的源码里,网络接口的实现部份是非常值得一读的,通过读源码,不仅对网络协议会有更深的了解,也有助于在网络编程的时候,对应用函数有更精确的了解和把握。

 

  本文把重点放在网络接口程序的总体结构上,希望能作为读源码时一些指导性的文字。

 

  本文以Linux2.4.16内核作为讲解的对象,内核源码可以在http://www.kernel.org上下载。我读源码时参考的是http://lxr.linux.no/这个交差参考的网站,我个人认为是一个很好的工具,如果有条件最好上这个网站。

 

  二.网络接口程序的结构

 

  Linux的网络接口分为四部份:网络设备接口部份,网络接口核心部份,网络协议族部份,以及网络接口socket层。

 

  网络设备接口部份主要负责从物理介质接收和发送数据。实现的文件在linu/driver/net目录下面。

 

  网络接口核心部份是整个网络接口的关键部位,它为网络协议提供统一的发送接口,屏蔽各种各样的物理介质,同时有负责把来自下层的包向合适的协议配送。它是网络接口的中枢部份。它的主要实现文件在linux/net/core目录下,其中linux/net/core/dev.c为主要管理文件。

 

  网络协议族部份是各种具体协议实现的部份。Linux支持TCP/IPIPXX.25AppleTalk等的协议,各种具体协议实现的源码在linux/net/目录下相应的名称。在这里主要讨论TCP/IP(IPv4)协议,实现的源码在linux/net/ipv4,其中linux/net/ipv4/af_inet.c是主要的管理文件。

 

  网络接口Socket层为用户提供的网络服务的编程接口。主要的源码在linux/net/socket.c

 

  三.网络设备接口部份

 

  物理层上有许多不同类型的网络接口设备, 在文件include/linux/if_arp.h28行里定义了ARP能处理的各种的物理设备的标志符。网络设备接口要负责具体物理介质的控制,从物理介质接收以及发送数据,并对物理介质进行诸如最大数据包之类的各种设置。这里我们以比较简单的3Com3c501 太网卡的驱动程序为例,大概讲一下这层的工作原理。源码在Linux/drivers/net/3c501.c

 

  我们从直觉上来考虑,一个网卡当然最主要的是完成数据的接收和发送,在这里我们来看看接收和发送的过程是怎么样的。

 

  发送相对来说比较简单,在Linux/drivers/net/3c501.c的行475 开始的el_start_xmit()这个函数就是实际向3Com3c501以太网卡发送数据的函数,具体的发送工作不外乎是对一些寄存器的读写,源码的注释很清楚,大家可以看看。

 

  接收的工作相对来说比较复杂。通常来说,一个新的包到了,或者一个包发送完成了,都会产生一个中断。Linux/drivers/net/3c501.c572开始el_interrupt()的函数里面,前半部份处理的是包发送完以后的汇报,后半部份处理的是一个新的包来的,就是说接收到了新的数据。el_interrupt()函数并没有对新的包进行太多的处理,就交给了接收处理函数el_receive()el_receive()首先检查接收的包是否正确,如果是一个“好”包就会为包分配一个缓冲结构(dev_alloc_skb()),这样驱动程序对包的接收工作就完成了,通过调用上层的函数netif_rx()(net/core/dev.c1214) ,把包交给上层。

 

  现在驱动程序有了发送和接收数据的功能了,驱动程序怎么样和上层建立联系呢?就是说接收到包以后怎么送给上层,以及上层怎么能调用驱动程序的发送函数呢?

 

  由下往上的关系,是通过驱动程序调用上层的netif_rx()(net/core/dev.c 1214)函数实现的,驱动程序通过这个函数把接到的数据交给上层,请注意所有的网卡驱动程序都需要调用这个函数的,这是网络接口核心层和网络接口设备联系的桥梁。

 

  由上往下的关系就复杂点。网络接口核心层需要知道有多少网络设备可以用,每个设备的函数的入口地址等都要知道。网络接口核心层会大声喊,“嘿,有多少设备可以帮我发送数据包?能发送的请给我排成一队!”。这一队就由dev_base开始,指针structnet_device *dev_base (Linux/include/linux/netdevice.h 436)就是保存了网络接口核心层所知道的所有设备。对于网络接口核心层来说,所有的设备都是一个net_device结构,它在include/linux/netdevice.h,line 233里被定义,这是从网络接口核心层的角度看到的一个抽象的设备,我们来看看网络接口核心层的角度看到的网络设备具有的功能:

 

  struct net_device {

 

  ………

 

  open()

 

  stop()

 

  hard_start_xmit()

 

  hard_header()

 

  rebuild_header()

 

  set_mac_address()

 

  do_ioctl()

 

  set_config()

 

  hard_header_cache()

 

  header_cache_update()

 

  change_mtu()

 

  tx_timeout()

 

  hard_header_parse()

 

  neigh_setup()

 

  accept_fastpath()

 

  ………

 

  }

 

  如果网络接口核心层需要由下层发送数据的时候,在dev_base找到设备以后,就直接调dev->hard_start_xmit()的这个函数来让下层发数据包。

 

  驱动程序要让网络接口核心层知道自己的存在,当然要加入dev_base所指向的指针链,然后把自己的函数以及各种参数和net_device里的相应的域对应起来。加入dev_base所指向的指针链是通过函数register_netdev(&dev_3c50)(linux/drivers/net/net_init.c, line 532)

 

  建立的。而把自己的函数以和net_device里的相应的域及各种参数关系的建立是在el1_probe1()(Linux/drivers/net/3c501.c)里进行的:

 

  el1_probe1(){

 

  ………

 

  dev->open = &el_open;

 

  dev->hard_start_xmit = &el_start_xmit;

 

  dev->tx_timeout = &el_timeout;

 

  dev->watchdog_timeo = HZ;

 

  dev->stop = &el1_close;

 

  dev->get_stats = &el1_get_stats;

 

  dev->set_multicast_list = &set_multicast_list;

 

  ………

 

  ether_setup(dev);

 

  ………

 

  }

 

  进一步的对应工作在ether_setup(dev) (drivers/net/net_init.c, line 405 )里进行。我们注意到dev->hard_start_xmit =&el_start_xmit,这样发送函数的关系就建立了,上层只知道调用dev->hard_start_xmit这个来发送数据,上面的语句就把驱动程序实际的发送函数告诉了上层。

 

  四.网络接口核心部分

 

  刚才谈论了驱动程序怎么和网络接口核心层衔接的。网络接口核心层知道驱动程序以及驱动程序的函数的入口是通过*dev_base指向的设备链的,而下层是通过调用这一层的函数netif_rx()(net/core/dev.c 1214) 把数据传递个这一层的。

 

  网络接口核心层的上层是具体的网络协议,下层是驱动程序,我们以及解决了下层的关系,但和上层的关系没有解决。先来讨论一下网络接口核心层和网络协议族部份的关系,这种关系不外乎也是接收和发送的关系。

 

  网络协议,例如IPARP等的协议要发送数据包的时候会把数据包传递给这层,那么这种传递是通过什么函数来发生的呢?网络接口核心层通过dev_queue_xmit()(net/core/dev.c,line975)这个函数向上层提供统一的发送接口,也就是说无论是IP,还是ARP协议,通过这个函数把要发送的数据传递给这一层,想发送数据的时候就调用这个函数就可以了。dev_queue_xmit()做的工作最后会落实到dev->hard_start_xmit(),而dev->hard_start_xmit()会调用实际的驱动程序来完成发送的任务。例如上面的例子中,调用dev->hard_start_xmit()实际就是调用了el_start_xmit()

 

  现在讨论接收的情况。网络接口核心层通过的函数netif_rx()(net/core/dev.c 1214)接收了上层发送来的数据,这时候当然要把数据包往上层派送。所有的协议族的下层协议都需要接收数据,TCP/IPIP协议和ARP协议,SPX/IPXIPX协议,AppleTalkDDPAARP协议等都需要直接从网络接口核心层接收数据,网络接口核心层接收数据是如何把包发给这些协议的呢?这时的情形和于下层的关系很相似,网络接口核心层的下面可能有许多的网卡的驱动程序,为了知道怎么向这些驱动程序发数据,前面以及讲过时,是通过*dev_base这个指针指向的链解决的,现在解决和上层的关系是通过static struct packet_ptype_base[16]( net/core/dev.c line 164)这个数组解决的。这个数组包含了需要接收数据包的协议,以及它们的接收函数的入口。

 

  从上面可以看到,IP协议接收数据是通过ip_rcv()函数的,而ARP协议是通过arp_rcv()的,网络接口核心层只要通过这个数组就可以把数据交给上层函数了。

 

  如果有协议想把自己添加到这个数组,是通过dev_add_pack()(net/core/dev.c, line233)函数,从数组删除是通过dev_remove_pack()函数的。Ip层的注册是在初始化函数进行的void __init ip_init(void) (net/ipv4/ip_output.c, line 1003)

 

  {

 

  ………

 

  dev_add_pack(&ip_packet_type);

 

  ………

 

  }

 

  重新到回我们关于接收的讨论,网络接口核心层通过的函数netif_rx()(net/core/dev.c 1214)接收了上层发送来的数据,看看这个函数做了些什么。

 

  由于现在还是在中断的服务里面,所有并不能够处理太多的东西,剩下的东西就通过cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ)

 

  交给软中断处理, 从open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL)可以知道NET_RX_SOFTIRQ软中断的处理函数是net_rx_action()(net/core/dev.c, line 1419)net_rx_action()根据数据包的协议类型在数组ptype_base[16]里找到相应的协议,并从中知道了接收的处理函数,然后把数据包交给处理函数,这样就交给了上层处理,实际调用处理函数是通过net_rx_action()里的pt_prev->func()这一句。例如如果数据包是IP协议的话,ptype_base[ETH_P_IP]->func()(ip_rcv()),这样就把数据包交给了IP协议。

 

  五.网络协议部分

 

  协议层是真正实现是在这一层。在linux/include/linux/socket.h里面,LinuxBSD

 

Socket定义了多至32支持的协议族,其中PF_INET就是我们最熟悉的TCP/IP协议族(IPv4, 以下没有特别声明都指IPv4)。以这个协议族为例,看看这层是怎么工作的。实现TCP/IP协议族的主要文件在inux/net/ipv4/目录下面,Linux/net/ipv4/af_inet.c为主要的管理文件。

 

  在Linux2.4.16里面,实现了TCP/IP协议族里面的的IGMP,TCP,UDP,ICMP,ARP,IP。我们先讨论一下这些协议之间的关系。IPARP协议是需要直接和网络设备接口打交道的协议,也就是需要从网络核心模块(core)

 

接收数据和发送数据的。而其它协议TCP,UDP,IGMP,ICMP是需要直接利用IP协议的,需要从IP协议接收数据,以及利用IP协议发送数据,同时还要向上层Socket层提供直接的调用接口。可以看到IP层是一个核心的协议,向下需要和下层打交道,又要向上层提供所以的传输和接收的服务。

 

  先来看看IP协议层。网络核心模块(core) 如果接收到IP层的数据,通过ptype_base[ETH_P_IP] 数组的IP层的项指向的IP协议的ip_packet_type->ip_rcv()函数把数据包传递给IP,也就是说IP层通过这个函数ip_rcv()(linux/net/ipv4/ip_input.c)接收数据的。ip_rcv()这个函数只对IP数据保做了一些checksum的检查工作,如果包是正确的就把包交给了下一个处理函数ip_rcv_finish()(注意调用是通过NF_HOOK这个宏实现的)。现在,ip_rcv_finish()这个函数真正要完成一些IP层的工作了。IP层要做的主要工作就是路由,要决定把数据包往那里送。路由的工作是通过函数ip_route_input()(/linux/net/ipv4/route.c,line 1622)实现的。对于进来的包可能的路由有这些:

 

  属于本地的数据(即是需要传递给TCPUDPIGMP这些上层协议的)

 

  需要要转发的数据包(网关或者NAT服务器之类的)

 

  不可能路由的数据包(地址信息有误)

 

  我们现在关心的是如果数据是本地数据的时候怎么处理。ip_route_input()调用ip_route_input_slow()(net/ipv4/route.c, line 1312),在ip_route_input_slow()里面的1559rth->u.dst.input=ip_local_deliver,这就是判断到IP包是本地的数据包,并把本地数据包处理函数的地址返回。好了,路由工作完成了,返回到ip_rcv_finish()ip_rcv_finish()最后调用拉skb->dst->input(skb),从上面可以看到,这其实就是调用了ip_local_deliver()函数,而ip_local_deliver(),接着就调用了ip_local_deliver_finish()。现在真正到了往上层传递数据包的时候了。

 

现在的情形和网络核心模块层(core) 往上层传递数据包的情形非常相似,怎么从多个协议选择合适的协议,并且往这个协议传递数据呢?网络网络核心模块层(core) 通过一个数组ptype_base[16]保存了注册了的所有可以接收数据的协议,同样网络协议层也定义了这样一个数组

struct net_protocol*inet_protos[MAX_INET_PROTOS](/linux/net/ipv4/protocol.c#L102),它保存了所有需要从IP协议层接收数据的上层协议(IGMPTCPUDPICMP)的接收处理函数的地址。我们来看看TCP协议的数据结构是怎么样的:

 

  linux/net/ipv4/protocol.c line67

 

  static struct inet_protocol tcp_protocol = {

 

  handler: tcp_v4_rcv,// 接收数据的函数

 

  err_handler: tcp_v4_err,// 出错处理的函数

 

  next: IPPROTO_PREVIOUS,

 

  protocol: IPPROTO_TCP,

 

  name: "TCP"

 

  };

 

  第一项就是我们最关心的了,IP层可以通过这个函数把数据包往TCP层传的。在linux/net/ipv4/protocol.c的上部,我们可以看到其它协议层的处理函数是igmp_rcv(),

 

udp_rcv(), icmp_rcv()。同样在linux/net/ipv4/protocol.c,往数组inet_protos[MAX_INET_PROTOS] 里面添加协议是通过函数inet_add_protocol()实现的,删除协议是通过 inet_del_protocol()实现的。inet_protos[MAX_INET_PROTOS]初始化的过程在linux/net/ipv4/af_inet.c inet_init()初始化函数里面。

 

  inet_init(){

 

  ……

 

  printk(KERN_INFO "IP Protocols: ");

 

  for (p = inet_protocol_base; p != NULL;) {

 

  struct inet_protocol *tmp = (struct inet_protocol *) p->next;

 

  inet_add_protocol(p);// 添加协议

 

  printk("%s%s",p->name,tmp?", ":"\n");

 

  p = tmp;

 

  ………

 

  }

 

  如果你在Linux启动的时候有留意启动的信息, 或者在linux下打命令dmesg就可以看到这一段程序输出的信息:

 

  IP Protocols ICMPUDPTCPIGMP也就是说现在数组inet_protos[]里面有了ICMPUDPTCPIGMP四个协议的inet_protocol数据结构,数据结构包含了它们接收数据的处理函数。

 

  Linux 2.4.16linux/include/linux/socket.h里定义了32种支持的BSDsocket协议,常见的有TCP/IP,IPX/SPX,X.25等,而每种协议还提供不同的服务,例如TCP/IP协议通过TCP协议支持连接服务,而通过UDP协议支持无连接服务,面对这么多的协议,向用户提供统一的接口是必要的,这种统一是通过socket来进行的。

 

  在BSD socket网络编程的模式下,利用一系列的统一的函数来利用通信的服务。例如一个典型的利用TCP协议通信程序是这样:

 

  sock_descriptor = socket(AF_INET,SOCK_STREAM,0);

 

  connect(sock_descriptor, 地址,)

 

  send(sock_descriptor,hello world);

 

  recv(sock_descriptor,buffer,1024,0);

 

  第一个函数指定了协议Inet协议,即TCP/IP协议,同时是利用面向连接的服务,这样就对应到TCP协议,以后的操作就是利用socket的标准函数进行的。

 

  从上面我们可以看到两个问题,首先socket层需要根据用户指定的协议族(上面是AF_INET)

 

从下面32种协议中选择一种协议来完成用户的要求,当协议族确定以后,还要把特定的服务映射到协议族下的具体协议,例如当用户指定的是面向连接的服务时,Inet协议族会映射到TCP协议。

 

  从多个协议中选择用户指定的协议,并把具体的出理交给选中的协议,这和一起网络核心层向上和向下衔接的问题本质上是一样的,所以解决的方法也是一样的,同样还是通过数组。在Linux/net/socket.c定义了这个数组static struct net_proto_family *net_families NPROTO  。数组的元素已经确定了,net_families[2] TCP/IP协议,net_families[3]

 

  是X.25协议,具体那一项对应什么协议,在include/linux/socket.h有定义。但是每一项的数据结构net_proto_familyops是空的,也就是具体协议处理函数的地址是不知道的。协议的处理函数和ops建立联系是通过sock_register()(Linux/net/socket.c)这个函数建立的,例如TCP/IP协议的是这样建立关系的:

 

  int __init inet_init(void) (net/ipv4/af_inet.c)

 

  {

 

  (void) sock_register(&inet_family_ops);

 

  }

 

  只要给出AF_INET(在宏里定义是2),就可以找到net_failies[2] 里面的处理函数了。

 

  协议的映射完成了,现在要进行服务的映射了。上层当然不可能知道下层的什么协议能对应特定的服务,所以这种映射自然由协议族自己完成。在TCP/IP协议族里,这种映射是通过struct

 

list_head inetsw[SOCK_MAX]( net/ipv4/af_inet.c)

 

  这个数组进行映射的,在谈论这个数组之前我们来看另外一个数组inetsw_array[](net/ipv4/af_inet.c)

 

  static struct inet_protosw inetsw_array[] =

 

  {

 

  {

 

  type: SOCK_STREAM,

 

  protocol: IPPROTO_TCP,

 

  prot: &tcp_prot,

 

  ops: &inet_stream_ops,

 

  capability: -1,

 

  no_check: 0,

 

  flags: INET_PROTOSW_PERMANENT,

 

  },

 

  {

 

  type: SOCK_DGRAM,

 

  protocol: IPPROTO_UDP,

 

  prot: &udp_prot,

 

  ops: &inet_dgram_ops,

 

  capability: -1,

 

  no_check: UDP_CSUM_DEFAULT,

 

  flags: INET_PROTOSW_PERMANENT,

 

  },

 

  {

 

  type: SOCK_RAW,

 

  protocol: IPPROTO_IP, /* wild card */

 

  prot: &raw_prot,

 

  ops: &inet_dgram_ops,

 

  capability: CAP_NET_RAW,

 

  no_check: UDP_CSUM_DEFAULT,

 

  flags: INET_PROTOSW_REUSE,

 

  }

 

  };

 

  我们看到,SOCK_STREAM映射到了TCP协议,SOCK_DGRAM映射到了UDP协议,SOCK_RAW映射到了IP协议。现在只要把inetsw_array里的三项添加到数组inetsw[SOCK_MAX]就可以了,添加是通过函数inet_register_protosw()实现的。在inet_init()

 

  (net/ipv4/af_inet.c) 里完成了这些工作。

 

  还有一个需要映射的就是socket其它诸如accept,send(),

 

  connect(),release(),bind()等的操作函数是怎么映射的呢?我们来看一下上面的数组的TCP的项

 

  {

 

  type: SOCK_STREAM,

 

  protocol: IPPROTO_TCP,

 

  prot: &tcp_prot,

 

  ops: &inet_stream_ops,

 

  capability: -1,

 

  no_check: 0,

 

  flags: INET_PROTOSW_PERMANENT,

 

  },

 

  我们看到这种映射是通过ops,和prot来映射的,我们再来看看 tcp_prot这一项:

 

  struct proto tcp_prot = {

 

  name: "TCP",

 

  close: tcp_close,

 

  connect: tcp_v4_connect,

 

  disconnect: tcp_disconnect,

 

  accept: tcp_accept,

 

  ioctl: tcp_ioctl,

 

  init: tcp_v4_init_sock,

 

  destroy: tcp_v4_destroy_sock,

 

  shutdown: tcp_shutdown,

 

  setsockopt: tcp_setsockopt,

 

  getsockopt: tcp_getsockopt,

 

  sendmsg: tcp_sendmsg,

 

  recvmsg: tcp_recvmsg,

 

  backlog_rcv: tcp_v4_do_rcv,

 

  hash: tcp_v4_hash,

 

  unhash: tcp_unhash,

 

  get_port: tcp_v4_get_port,

 

  };

 

  所以的映射都已经完成了,用户调用connect()函数,其实就是调用了tcp_v4_connect()函数,按照这幅图,读起源码来就简单了很多了。

 

  六 Socket

 

  上一节把socket层大多数要讨论的东西都谈论了,现在只讲讲socket 层和用户的衔接。

 

  系统调用socket(),bind(),connect(),accept,send(),release()等是在Linux/net/socket.c里面的实现的,系统调用实现的函数是相应的函数名加上sys_的前缀。

 

  现在看看当用户调用socket()这个函数,到底下面发生了什么。

 

  Socket(AF_INET,SOCK_STREAM,0)调用了sys_socket(),sys_socket()接着调用socket_creat(),socket_creat()就要根据用户提供的协议族参数在net_families[]里寻找合适的协议族,如果协议族没有被安装就要请求安装该协议族的模块,然后就调用该协议族的create()函数的处理句柄。根据参数AF_INETinet_creat()就被调用了,在inet_creat()根据服务类型在inetsw[SOCK_MAX]选择合适的协议,并把协议的操作集赋给socket就是了,根据SOCK_STREAMTCP协议被选中,

 

  inet_creat(){

 

  answer=inetsw [用户要求服务服务]

 

  sock->ops = answer->ops;

 

  sk->prot = answer->prot

 

  }

Re: sed 替换文件内容

-i表示在原文件中替换


SN=`getcfg /data/confbac/factory.conf SerialNumber`
sed "s#<SerialNumber type=\"s\" size=\"64\" rw=\"0\">000000131811</SerialNumber>#<SerialNumber type=\"s\" size=\"64\" rw=\"0\">$SN</SerialNumber>#g" m2.txt >mm.txt

1:s后面跟的字符代表分隔符
2:用到了变量,所以必须用双引号

sed 替换文件内容

SN=`getcfg /data/confbac/factory.conf SerialNumber`
sed "s#<SerialNumber type=\"s\" size=\"64\" rw=\"0\">000000131811</SerialNumber>#<SerialNumber type=\"s\" size=\"64\" rw=\"0\">$SN</SerialNumber>#g" m2.txt >mm.txt

1:s后面跟的字符代表分隔符
2:用到了变量,所以必须用双引号

fopen中w w+ wb区别:

在C语言中,大家常用到fopen打开文件,准备进行写操作,再用fwrite把数据写入文件,最后用fclose关闭文件。
如以下C代码:
 
#include <stdio.h>

char buf[10] = {10,10,10,10,10,10,10,10,10,10};

FILE *fp = fopen("c:\\test.dat","w");
fwrite(buf,1,10,fp);
fclose(fp);

FILE *fp1 = fopen("c:\\test1.dat","wb");
fwrite(buf,1,10,fp);
fclose(fp1);

我们注意到,程序的第一段定义一个数组,每个分量的值为:10,等价的十六进制为0A。

第二段,先是打开一个文件test.dat进行写操作,再是把10个数据写入文件,最后关闭文件。

第三段,功能差不多,只是文件名换成了test1.dat,打开文件的模式换成了wb.

第二段和第三段有什么区别呢?我们看一下最后的文件内容:

test.dat:  0D 0A 0D 0A 0D 0A 0D 0A 0D 0A 0D 0A 0D 0A 0D 0A 0D 0A 0D 0A

test1.dat: 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A

原因是为什么呢?这就是w 和 wb的区别,w是以文本方式打开文件,wb是二进制方式打开文件,以文本方式打开文件时,fwrite函数每碰到一个0x0A时,就在它的前面加入0x0D.其它内容不做添加操作。

 

r 以只读方式打开文件,该文件必须存在。

  r+ 以可读写方式打开文件,该文件必须存在。

  rb+ 读写打开一个二进制文件,只允许读写数据。

  rt+ 读写打开一个文本文件,允许读和写。

  w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。

  w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。

  a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)

  a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留)

  wb 只写打开或新建一个二进制文件;只允许写数据。

  wb+ 读写打开或建立一个二进制文件,允许读和写。

  wt+ 读写打开或着建立一个文本文件;允许读写。

  at+ 读写打开一个文本文件,允许读或在文本末追加数据。

  ab+ 读写打开一个二进制文件,允许读或在文件末追加数据。

  上述的形态字符串都可以再加一个b字符,如rb、w+b或ab+等组合,加入b 字符用来告诉函数库打开的文件为二进制文件,而非纯文字文件。不过在POSIX系统,包含Linux都会忽略该字符。由fopen()所建立的新文件会具有S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH(0666)权限,此文件权限也会参考umask 值。



注:如果在linux下运行改程序,则两种打开方式是一样的

vim显示不同回车的方法?

我想请问下vim下是否有选项可以定制windows和linux下不同回车的显示方式。 
或者可以在windows格式下显示出^M(现在被vim自动转换掉了) 

我之前搜索到一些像是 
:set list 
:set invlist 之类选项 
或是 
:set ff=xxx 之类的调整格式 

都没发现相关的方法

打开文件后,输入 :e ++ff=unix % 

然后就可以显示出所有 ^M 符号。

在 Linux 下用户空间与内核空间数据交换的方式,第 1 部分: 内核启动参数、模块参数与sysfs、sysctl、系统调用和netlink

一、引言

一般地,在使用虚拟内存技术的多任务系统上,内核和应用有不同的地址空间,因此,在内核和应用之间以及在应用与应用之间进行数据交换需要专门的机制来实现,众所周知,进程间通信(IPC)机制就是为实现应用与应用之间的数据交换而专门实现的,大部分读者可能对进程间通信比较了解,但对应用与内核之间的数据交换机制可能了解甚少,本文将详细介绍 Linux 系统下内核与应用进行数据交换的各种方式,包括内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs 和 relayfs。

二、内核启动参数

Linux 提供了一种通过 bootloader 向其传输启动参数的功能,内核开发者可以通过这种方式来向内核传输数据,从而控制内核启动行为。

通常的使用方式是,定义一个分析参数的函数,而后使用内核提供的宏 __setup把它注册到内核中,该宏定义在 linux/init.h 中,因此要使用它必须包含该头文件:


    __setup("para_name=", parse_func)  

para_name 为参数名,parse_func 为分析参数值的函数,它负责把该参数的值转换成相应的内核变量的值并设置那个内核变量。内核为整数参数值的分析提供了函数 get_option 和 get_options,前者用于分析参数值为一个整数的情况,而后者用于分析参数值为逗号分割的一系列整数的情况,对于参数值为字符串的情况,需要开发者自定义相应的分析函数。在源代码包中的内核程序kern-boot-params.c 说明了三种情况的使用。该程序列举了参数为一个整数、逗号分割的整数串以及字符串三种情况,读者要想测试该程序,需要把该程序拷贝到要使用的内核的源码目录树的一个目录下,为了避免与内核其他部分混淆,作者建议在内核源码树的根目录下创建一个新目录,如 examples,然后把该程序拷贝到 examples 目录下并重新命名为 setup_example.c,并且为该目录创建一个 Makefile 文件:


    obj-y = setup_example.o  

Makefile 仅许这一行就足够了,然后需要修改源码树的根目录下的 Makefile文件的一行,把下面行


    core-y          := usr/  

修改为


    core-y          := usr/ examples/  

注意:如果读者创建的新目录和重新命名的文件名与上面不同,需要修改上面所说 Makefile 文件相应的位置。 做完以上工作就可以按照内核构建步骤去构建新的内核,在构建好内核并设置好lilo或grub为该内核的启动条目后,就可以启动该内核,然后使用lilo或grub的编辑功能为该内核的启动参数行增加如下参数串:

setup_example_int=1234 setup_example_int_array=100,200,300,400 setup_example_string=Thisisatest

当然,该参数串也可以直接写入到lilo或grub的配置文件中对应于该新内核的内核命令行参数串中。读者可以使用其它参数值来测试该功能。

下面是作者系统上使用上面参数行的输出:


    setup_example_int=1234  setup_example_int_array=100,200,300,400  setup_example_int_array includes 4 intergers  setup_example_string=Thisisatest  

读者可以使用


    dmesg | grep setup  

来查看该程序的输出。

三、模块参数与sysfs

内核子系统或设备驱动可以直接编译到内核,也可以编译成模块,如果编译到内核,可以使用前一节介绍的方法通过内核启动参数来向它们传递参数,如果编译成模块,则可以通过命令行在插入模块时传递参数,或者在运行时,通过sysfs来设置或读取模块数据。

Sysfs是一个基于内存的文件系统,实际上它基于ramfs,sysfs提供了一种把内核数据结构,它们的属性以及属性与数据结构的联系开放给用户态的方式,它与kobject子系统紧密地结合在一起,因此内核开发者不需要直接使用它,而是内核的各个子系统使用它。用户要想使用 sysfs 读取和设置内核参数,仅需装载 sysfs 就可以通过文件操作应用来读取和设置内核通过 sysfs 开放给用户的各个参数:


    $ mkdir -p /sysfs  $ mount -t sysfs sysfs /sysfs  

注意,不要把 sysfs 和 sysctl 混淆,sysctl 是内核的一些控制参数,其目的是方便用户对内核的行为进行控制,而 sysfs 仅仅是把内核的 kobject 对象的层次关系与属性开放给用户查看,因此 sysfs 的绝大部分是只读的,模块作为一个 kobject 也被出口到 sysfs,模块参数则是作为模块属性出口的,内核实现者为模块的使用提供了更灵活的方式,允许用户设置模块参数在 sysfs 的可见性并允许用户在编写模块时设置这些参数在 sysfs 下的访问权限,然后用户就可以通过sysfs 来查看和设置模块参数,从而使得用户能在模块运行时控制模块行为。

对于模块而言,声明为 static 的变量都可以通过命令行来设置,但要想在 sysfs下可见,必须通过宏 module_param 来显式声明,该宏有三个参数,第一个为参数名,即已经定义的变量名,第二个参数则为变量类型,可用的类型有 byte, short, ushort, int, uint, long, ulong, charp 和 bool 或 invbool,分别对应于 c 类型 char, short, unsigned short, int, unsigned int, long, unsigned long, char * 和 int,用户也可以自定义类型 XXX(如果用户自己定义了 param_get_XXX,param_set_XXX 和 param_check_XXX)。该宏的第三个参数用于指定访问权限,如果为 0,该参数将不出现在 sysfs 文件系统中,允许的访问权限为 S_IRUSR, S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH 和 S_IWOTH 的组合,它们分别对应于用户读,用户写,用户组读,用户组写,其他用户读和其他用户写,因此用文件的访问权限设置是一致的。

源代码包中的内核模块 module-param-exam.c 是一个利用模块参数和sysfs来进行用户态与内核态数据交互的例子。该模块有三个参数可以通过命令行设置,下面是作者系统上的运行结果示例:


    $ insmod ./module-param-exam.ko my_invisible_int=10    my_visible_int=20 mystring="Hello,World"  my_invisible_int = 10  my_visible_int = 20  mystring = 'Hello,World'  $ ls /sys/module/module_param_exam/parameters/  mystring  my_visible_int  $ cat /sys/module/module_param_exam/parameters/mystring  Hello,World  $ cat /sys/module/module_param_exam/parameters/my_visible_int  20  $ echo 2000 > /sys/module/module_param_exam/parameters/my_visible_int  $ cat /sys/module/module_param_exam/parameters/my_visible_int  2000  $ echo "abc" > /sys/module/module_param_exam/parameters/mystring  $ cat /sys/module/module_param_exam/parameters/mystring  abc  $ rmmod module_param_exam  my_invisible_int = 10  my_visible_int = 2000  mystring = 'abc'  

四、sysctl

Sysctl是一种用户应用来设置和获得运行时内核的配置参数的一种有效方式,通过这种方式,用户应用可以在内核运行的任何时刻来改变内核的配置参数,也可以在任何时候获得内核的配置参数,通常,内核的这些配置参数也出现在proc文件系统的/proc/sys目录下,用户应用可以直接通过这个目录下的文件来实现内核配置的读写操作,例如,用户可以通过


    Cat /proc/sys/net/ipv4/ip_forward  

来得知内核IP层是否允许转发IP包,用户可以通过


    echo 1 > /proc/sys/net/ipv4/ip_forward  

把内核 IP 层设置为允许转发 IP 包,即把该机器配置成一个路由器或网关。 一般地,所有的 Linux 发布也提供了一个系统工具 sysctl,它可以设置和读取内核的配置参数,但是该工具依赖于 proc 文件系统,为了使用该工具,内核必须支持 proc 文件系统。下面是使用 sysctl 工具来获取和设置内核配置参数的例子:


$ sysctl net.ipv4.ip_forward  net.ipv4.ip_forward = 0  $ sysctl -w net.ipv4.ip_forward=1  net.ipv4.ip_forward = 1  $ sysctl net.ipv4.ip_forward  net.ipv4.ip_forward = 1  

注意,参数 net.ipv4.ip_forward 实际被转换到对应的 proc 文件/proc/sys/net/ipv4/ip_forward,选项 -w 表示设置该内核配置参数,没有选项表示读内核配置参数,用户可以使用 sysctl -a 来读取所有的内核配置参数,对应更多的 sysctl 工具的信息,请参考手册页 sysctl(8)。

但是 proc 文件系统对 sysctl 不是必须的,在没有 proc 文件系统的情况下,仍然可以,这时需要使用内核提供的系统调用 sysctl 来实现对内核配置参数的设置和读取。

源代码包中给出了一个实际例子程序,它说明了如何在内核和用户态使用sysctl。头文件 sysctl-exam.h 定义了 sysctl 条目 ID,用户态应用和内核模块需要这些 ID 来操作和注册 sysctl 条目。内核模块在文件 sysctl-exam-kern.c 中实现,在该内核模块中,每一个 sysctl 条目对应一个 struct ctl_table 结构,该结构定义了要注册的 sysctl 条目的 ID(字段 ctl_name),在 proc 下的名称(字段procname),对应的内核变量(字段data,注意该该字段的赋值必须是指针),条目允许的最大长度(字段maxlen,它主要用于字符串内核变量,以便在对该条目设置时,对超过该最大长度的字符串截掉后面超长的部分),条目在proc文件系统下的访问权限(字段mode),在通过proc设置时的处理函数(字段proc_handler,对于整型内核变量,应当设置为&proc_dointvec,而对于字符串内核变量,则设置为 &proc_dostring),字符串处理策略(字段strategy,一般这是为&sysctl_string)。

Sysctl 条目可以是目录,此时 mode 字段应当设置为 0555,否则通过 sysctl 系统调用将无法访问它下面的 sysctl 条目,child 则指向该目录条目下面的所有条目,对于在同一目录下的多个条目,不必一一注册,用户可以把它们组织成一个 struct ctl_table 类型的数组,然后一次注册就可以,但此时必须把数组的最后一个结构设置为NULL,即


    {  	.ctl_name = 0  }  

注册sysctl条目使用函数register_sysctl_table(struct ctl_table *, int),第一个参数为定义的struct ctl_table结构的sysctl条目或条目数组指针,第二个参数为插入到sysctl条目表中的位置,如果插入到末尾,应当为0,如果插入到开头,则为非0。内核把所有的sysctl条目都组织成sysctl表。

当模块卸载时,需要使用函数unregister_sysctl_table(struct ctl_table_header *)解注册通过函数register_sysctl_table注册的sysctl条目,函数register_sysctl_table在调用成功时返回结构struct ctl_table_header,它就是sysctl表的表头,解注册函数使用它来卸载相应的sysctl条目。 用户态应用sysctl-exam-user.c通过sysctl系统调用来查看和设置前面内核模块注册的sysctl条目(当然如果用户的系统内核已经支持proc文件系统,可以直接使用文件操作应用如cat, echo等直接查看和设置这些sysctl条目)。

下面是作者运行该模块与应用的输出结果示例:


$ insmod ./sysctl-exam-kern.ko  $ cat /proc/sys/mysysctl/myint  0  $ cat /proc/sys/mysysctl/mystring  $ ./sysctl-exam-user  mysysctl.myint = 0  mysysctl.mystring = ""  $ ./sysctl-exam-user 100 "Hello, World"  old value: mysysctl.myint = 0  new value: mysysctl.myint = 100  old vale: mysysctl.mystring = ""  new value: mysysctl.mystring = "Hello, World"  $ cat /proc/sys/mysysctl/myint  100  $ cat /proc/sys/mysysctl/mystring  Hello, World  $  

五、系统调用

系统调用是内核提供给应用程序的接口,应用对底层硬件的操作大部分都是通过调用系统调用来完成的,例如得到和设置系统时间,就需要分别调用 gettimeofday 和 settimeofday 来实现。事实上,所有的系统调用都涉及到内核与应用之间的数据交换,如文件系统操作函数 read 和 write,设置和读取网络协议栈的 setsockopt 和 getsockopt。本节并不是讲解如何增加新的系统调用,而是讲解如何利用现有系统调用来实现用户的数据传输需求。

一般地,用户可以建立一个伪设备来作为应用与内核之间进行数据交换的渠道,最通常的做法是使用伪字符设备,具体实现方法是:

1.定义对字符设备进行操作的必要函数并设置结构 struct file_operations

结构 struct file_operations 非常大,对于一般的数据交换需求,只定义 open, read, write, ioctl, mmap 和 release 函数就足够了,它们实际上对应于用户态的文件系统操作函数 open, read, write, ioctl, mmap 和 close。这些函数的原型示例如下:


    ssize_t exam_read (struct file * file, char __user * buf, size_t count,   loff_t * ppos)  {  …  }  ssize_t exam_write(struct file * file, const char __user * buf, size_t count,    loff_t * ppos)  {  …  }  int exam_ioctl(struct inode * inode, struct file * file, unsigned int cmd,    unsigned long argv)  {  …  }  int exam_mmap(struct file *, struct vm_area_struct *)  {  …  }  int exam_open(struct inode * inode, struct file * file)  {  …  }  int exam_release(struct inode * inode, struct file * file)  {  …  }  

在定义了这些操作函数后需要定义并设置结构struct file_operations


    struct file_operations exam_file_ops = {  	.owner = THIS_MODULE,  	.read = exam_read,  	.write = exam_write,  	.ioctl = exam_ioctl,  	.mmap = exam_mmap,  	.open = exam_open,  	.release = exam_release,  };  

2. 注册定义的伪字符设备并把它和上面的 struct file_operations 关联起来:


    int exam_char_dev_major;  exam_char_dev_major = register_chrdev(0, "exam_char_dev", &exam_file_ops);  

注意,函数 register_chrdev 的第一个参数如果为 0,表示由内核来确定该注册伪字符设备的主设备号,这是该函数的返回为实际分配的主设备号,如果返回小于 0,表示注册失败。因此,用户在使用该函数时必须判断返回值以便处理失败情况。为了使用该函数必须包含头文件 linux/fs.h。

在源代码包中给出了一个使用这种方式实现用户态与内核态数据交换的典型例子,它包含了三个文件: 头文件 syscall-exam.h 定义了 ioctl 命令,.c 文件 syscall-exam-user.c为用户态应用,它通过文件系统操作函数 mmap 和 ioctl 来与内核态模块交换数据,.c 文件 syscall-exam-kern.c 为内核模块,它实现了一个伪字符设备,以便与用户态应用进行数据交换。为了正确运行应用程序 syscall-exam-user,需要在插入模块 syscall-exam-kern 后创建该实现的伪字符设备,用户可以使用下面命令来正确创建设备:


    $ mknod /dev/mychrdev c `dmesg | grep "char device mychrdev" | sed 's/.*major is //g'` 0  

然后用户可以通过 cat 来读写 /dev/mychrdev,应用程序 syscall-exam-user则使用 mmap 来读数据并使用 ioctl 来得到该字符设备的信息以及裁减数据内容,它只是示例如何使用现有的系统调用来实现用户需要的数据交互操作。

下面是作者运行该模块的结果示例:


    $ insmod ./syscall-exam-kern.ko  char device mychrdev is registered, major is 254  $ mknod /dev/mychrdev c `dmesg | grep "char device mychrdev" | sed 's/.*major is //g'` 0  $ cat /dev/mychrdev  $ echo "abcdefghijklmnopqrstuvwxyz" > /dev/mychrdev  $ cat /dev/mychrdev  abcdefghijklmnopqrstuvwxyz  $ ./syscall-exam-user  User process: syscall-exam-us(1433)  Available space: 65509 bytes  Data len: 27 bytes  Offset in physical: cc0 bytes  mychrdev content by mmap:  abcdefghijklmnopqrstuvwxyz  $ cat /dev/mychrdev  abcde  $  

六、netlink

Netlink 是一种特殊的 socket,它是 Linux 所特有的,类似于 BSD 中的AF_ROUTE 但又远比它的功能强大,目前在最新的 Linux 内核(2.6.14)中使用netlink 进行应用与内核通信的应用很多,包括:路由 daemon(NETLINK_ROUTE),1-wire 子系统(NETLINK_W1),用户态 socket 协议(NETLINK_USERSOCK),防火墙(NETLINK_FIREWALL),socket 监视(NETLINK_INET_DIAG),netfilter 日志(NETLINK_NFLOG),ipsec 安全策略(NETLINK_XFRM),SELinux 事件通知(NETLINK_SELINUX),iSCSI 子系统(NETLINK_ISCSI),进程审计(NETLINK_AUDIT),转发信息表查询(NETLINK_FIB_LOOKUP),netlink connector(NETLINK_CONNECTOR),netfilter 子系统(NETLINK_NETFILTER),IPv6 防火墙(NETLINK_IP6_FW),DECnet 路由信息(NETLINK_DNRTMSG),内核事件向用户态通知(NETLINK_KOBJECT_UEVENT),通用 netlink(NETLINK_GENERIC)。

Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。

Netlink 相对于系统调用,ioctl 以及 /proc 文件系统而言具有以下优点:

1,为了使用 netlink,用户仅需要在 include/linux/netlink.h 中增加一个新类型的 netlink 协议定义即可, 如 #define NETLINK_MYTEST 17 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换。但系统调用需要增加新的系统调用,ioctl 则需要增加设备或文件, 那需要不少代码,proc 文件系统则需要在 /proc 下添加新的文件或目录,那将使本来就混乱的 /proc 更加混乱。

2. netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接收队列,而不需要等待接收者收到消息,但系统调用与 ioctl 则是同步通信机制,如果传递的数据太长,将影响调度粒度。

3.使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖,但系统调用就有依赖,而且新的系统调用的实现必须静态地连接到内核中,它无法在模块中实现,使用新系统调用的应用在编译时需要依赖内核。

4.netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink 组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性,任何对内核事件感兴趣的应用都能收到该子系统发送的内核事件,在后面的文章中将介绍这一机制的使用。

5.内核可以使用 netlink 首先发起会话,但系统调用和 ioctl 只能由用户应用发起调用。

6.netlink 使用标准的 socket API,因此很容易使用,但系统调用和 ioctl则需要专门的培训才能使用。

用户态使用 netlink

用户态应用使用标准的socket APIs, socket(), bind(), sendmsg(), recvmsg() 和 close() 就能很容易地使用 netlink socket,查询手册页可以了解这些函数的使用细节,本文只是讲解使用 netlink 的用户应该如何使用这些函数。注意,使用 netlink 的应用必须包含头文件 linux/netlink.h。当然 socket 需要的头文件也必不可少,sys/socket.h。

为了创建一个 netlink socket,用户需要使用如下参数调用 socket():


socket(AF_NETLINK, SOCK_RAW, netlink_type)  

第一个参数必须是 AF_NETLINK 或 PF_NETLINK,在 Linux 中,它们俩实际为一个东西,它表示要使用netlink,第二个参数必须是SOCK_RAW或SOCK_DGRAM, 第三个参数指定netlink协议类型,如前面讲的用户自定义协议类型NETLINK_MYTEST, NETLINK_GENERIC是一个通用的协议类型,它是专门为用户使用的,因此,用户可以直接使用它,而不必再添加新的协议类型。内核预定义的协议类型有:


    #define NETLINK_ROUTE           0       /* Routing/device hook                          */  #define NETLINK_W1              1       /* 1-wire subsystem                             */  #define NETLINK_USERSOCK        2       /* Reserved for user mode socket protocols      */  #define NETLINK_FIREWALL        3       /* Firewalling hook                             */  #define NETLINK_INET_DIAG       4       /* INET socket monitoring                       */  #define NETLINK_NFLOG           5       /* netfilter/iptables ULOG */  #define NETLINK_XFRM            6       /* ipsec */  #define NETLINK_SELINUX         7       /* SELinux event notifications */  #define NETLINK_ISCSI           8       /* Open-iSCSI */  #define NETLINK_AUDIT           9       /* auditing */  #define NETLINK_FIB_LOOKUP      10  #define NETLINK_CONNECTOR       11  #define NETLINK_NETFILTER       12      /* netfilter subsystem */  #define NETLINK_IP6_FW          13  #define NETLINK_DNRTMSG         14      /* DECnet routing messages */  #define NETLINK_KOBJECT_UEVENT  15      /* Kernel messages to userspace */  #define NETLINK_GENERIC         16  

对于每一个netlink协议类型,可以有多达 32多播组,每一个多播组用一个位表示,netlink 的多播特性使得发送消息给同一个组仅需要一次系统调用,因而对于需要多拨消息的应用而言,大大地降低了系统调用的次数。

函数 bind() 用于把一个打开的 netlink socket 与 netlink 源 socket 地址绑定在一起。netlink socket 的地址结构如下:


     struct sockaddr_nl  {    sa_family_t    nl_family;    unsigned short nl_pad;    __u32          nl_pid;    __u32          nl_groups;  };  

字段 nl_family 必须设置为 AF_NETLINK 或着 PF_NETLINK,字段 nl_pad 当前没有使用,因此要总是设置为 0,字段 nl_pid 为接收或发送消息的进程的 ID,如果希望内核处理消息或多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。字段 nl_groups 用于指定多播组,bind 函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组。

传递给 bind 函数的地址的 nl_pid 字段应当设置为本进程的进程 ID,这相当于 netlink socket 的本地地址。但是,对于一个进程的多个线程使用 netlink socket 的情况,字段 nl_pid 则可以设置为其它的值,如:


pthread_self() << 16 | getpid();  

因此字段 nl_pid 实际上未必是进程 ID,它只是用于区分不同的接收者或发送者的一个标识,用户可以根据自己需要设置该字段。函数 bind 的调用方式如下:


    bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl));  

fd为前面的 socket 调用返回的文件描述符,参数 nladdr 为 struct sockaddr_nl 类型的地址。 为了发送一个 netlink 消息给内核或其他用户态应用,需要填充目标 netlink socket 地址 ,此时,字段 nl_pid 和 nl_groups 分别表示接收消息者的进程 ID 与多播组。如果字段 nl_pid 设置为 0,表示消息接收者为内核或多播组,如果 nl_groups为 0,表示该消息为单播消息,否则表示多播消息。 使用函数 sendmsg 发送 netlink 消息时还需要引用结构 struct msghdr、struct nlmsghdr 和 struct iovec,结构 struct msghdr 需如下设置:


    struct msghdr msg;  memset(&msg, 0, sizeof(msg));  msg.msg_name = (void *)&(nladdr);  msg.msg_namelen = sizeof(nladdr);  

其中 nladdr 为消息接收者的 netlink 地址。

struct nlmsghdr 为 netlink socket 自己的消息头,这用于多路复用和多路分解 netlink 定义的所有协议类型以及其它一些控制,netlink 的内核实现将利用这个消息头来多路复用和多路分解已经其它的一些控制,因此它也被称为netlink 控制块。因此,应用在发送 netlink 消息时必须提供该消息头。


    struct nlmsghdr  {    __u32 nlmsg_len;   /* Length of message */    __u16 nlmsg_type;  /* Message type*/    __u16 nlmsg_flags; /* Additional flags */    __u32 nlmsg_seq;   /* Sequence number */    __u32 nlmsg_pid;   /* Sending process PID */  };  

字段 nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,字段 nlmsg_type 用于应用内部定义消息的类型,它对 netlink 内核实现是透明的,因此大部分情况下设置为 0,字段 nlmsg_flags 用于设置消息标志,可用的标志包括:


/* Flags values */  #define NLM_F_REQUEST           1       /* It is request message.       */  #define NLM_F_MULTI             2       /* Multipart message, terminated by NLMSG_DONE */  #define NLM_F_ACK               4       /* Reply with ack, with zero or error code */  #define NLM_F_ECHO              8       /* Echo this request            */  /* Modifiers to GET request */  #define NLM_F_ROOT      0x100   /* specify tree root    */  #define NLM_F_MATCH     0x200   /* return all matching  */  #define NLM_F_ATOMIC    0x400   /* atomic GET           */  #define NLM_F_DUMP      (NLM_F_ROOT|NLM_F_MATCH)  /* Modifiers to NEW request */  #define NLM_F_REPLACE   0x100   /* Override existing            */  #define NLM_F_EXCL      0x200   /* Do not touch, if it exists   */  #define NLM_F_CREATE    0x400   /* Create, if it does not exist */  #define NLM_F_APPEND    0x800   /* Add to end of list           */  

标志NLM_F_REQUEST用于表示消息是一个请求,所有应用首先发起的消息都应设置该标志。

标志NLM_F_MULTI 用于指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得。

宏NLM_F_ACK表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来。

标志NLM_F_ECHO表示该消息是相关的一个包的回传。

标志NLM_F_ROOT 被许多 netlink 协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条目地返回。有该标志的请求通常导致响应消息设置NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段 nlmsg_type 中指定协议类型。

标志 NLM_F_MATCH 表示该协议特定的请求只需要一个数据子集,数据子集由指定的协议特定的过滤器来匹配。

标志 NLM_F_ATOMIC 指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改。

标志 NLM_F_DUMP 未实现。

标志 NLM_F_REPLACE 用于取代在数据表中的现有条目。

标志 NLM_F_EXCL_ 用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败。

标志 NLM_F_CREATE 指示应当在指定的表中创建一个条目。

标志 NLM_F_APPEND 指示在表末尾添加新的条目。

内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作),字段 nlmsg_seq 和 nlmsg_pid 用于应用追踪消息,前者表示顺序号,后者为消息来源进程 ID。下面是一个示例:


#define MAX_MSGSIZE 1024  char buffer[] = "An example message";  struct nlmsghdr nlhdr;  nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));  strcpy(NLMSG_DATA(nlhdr),buffer);  nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));  nlhdr->nlmsg_pid = getpid();  /* self pid */  nlhdr->nlmsg_flags = 0;  

结构 struct iovec 用于把多个消息通过一次系统调用来发送,下面是该结构使用示例:


    struct iovec iov;  iov.iov_base = (void *)nlhdr;  iov.iov_len = nlh->nlmsg_len;  msg.msg_iov = &iov;  msg.msg_iovlen = 1;  

在完成以上步骤后,消息就可以通过下面语句直接发送:


    sendmsg(fd, &msg, 0);  

应用接收消息时需要首先分配一个足够大的缓存来保存消息头以及消息的数据部分,然后填充消息头,添完后就可以直接调用函数 recvmsg() 来接收。


    #define MAX_NL_MSG_LEN 1024  struct sockaddr_nl nladdr;  struct msghdr msg;  struct iovec iov;  struct nlmsghdr * nlhdr;  nlhdr = (struct nlmsghdr *)malloc(MAX_NL_MSG_LEN);  iov.iov_base = (void *)nlhdr;  iov.iov_len = MAX_NL_MSG_LEN;  msg.msg_name = (void *)&(nladdr);  msg.msg_namelen = sizeof(nladdr);  msg.msg_iov = &iov;  msg.msg_iovlen = 1;  recvmsg(fd, &msg, 0);   

注意:fd为socket调用打开的netlink socket描述符。

在消息接收后,nlhdr指向接收到的消息的消息头,nladdr保存了接收到的消息的目标地址,宏NLMSG_DATA(nlhdr)返回指向消息的数据部分的指针。

在linux/netlink.h中定义了一些方便对消息进行处理的宏,这些宏包括:


#define NLMSG_ALIGNTO   4  #define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )  

宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值。


    #define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))  

宏NLMSG_LENGTH(len)用于计算数据部分长度为len时实际的消息长度。它一般用于分配消息缓存。


    #define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))  

宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,它也用于分配消息缓存。


    #define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))  

宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏。


    #define NLMSG_NEXT(nlh,len)      ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \                        (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))  

宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址,同时len也减少为剩余消息的总长度,该宏一般在一个消息被分成几个部分发送或接收时使用。


    #define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \                             (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \                             (nlh)->nlmsg_len <= (len))  

宏NLMSG_OK(nlh,len)用于判断消息是否有len这么长。


    #define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))  

宏NLMSG_PAYLOAD(nlh,len)用于返回payload的长度。

函数close用于关闭打开的netlink socket。

netlink内核API

netlink的内核实现在.c文件net/core/af_netlink.c中,内核模块要想使用netlink,也必须包含头文件linux/netlink.h。内核使用netlink需要专门的API,这完全不同于用户态应用对netlink的使用。如果用户需要增加新的netlink协议类型,必须通过修改linux/netlink.h来实现,当然,目前的netlink实现已经包含了一个通用的协议类型NETLINK_GENERIC以方便用户使用,用户可以直接使用它而不必增加新的协议类型。前面讲到,为了增加新的netlink协议类型,用户仅需增加如下定义到linux/netlink.h就可以:


    #define NETLINK_MYTEST  17  

只要增加这个定义之后,用户就可以在内核的任何地方引用该协议。

在内核中,为了创建一个netlink socket用户需要调用如下函数:


    struct sock *  netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));  

参数unit表示netlink协议类型,如NETLINK_MYTEST,参数input则为内核模块定义的netlink消息处理函数,当有消息到达这个netlink socket时,该input函数指针就会被引用。函数指针input的参数sk实际上就是函数netlink_kernel_create返回的struct sock指针,sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个struct sock结构来表示。下面是一个input函数的示例:


    void input (struct sock *sk, int len)  {   struct sk_buff *skb;   struct nlmsghdr *nlh = NULL;   u8 *data = NULL;   while ((skb = skb_dequeue(&sk->receive_queue))          != NULL) {   /* process netlink message pointed by skb->data */   nlh = (struct nlmsghdr *)skb->data;   data = NLMSG_DATA(nlh);   /* process netlink message with header pointed by     * nlh and data pointed by data    */   }     }  

函数input()会在发送进程执行sendmsg()时被调用,这样处理消息比较及时,但是,如果消息特别长时,这样处理将增加系统调用sendmsg()的执行时间,对于这种情况,可以定义一个内核线程专门负责消息接收,而函数input的工作只是唤醒该内核线程,这样sendmsg将很快返回。

函数skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收队列上的消息,返回为一个struct sk_buff的结构,skb->data指向实际的netlink消息。

函数skb_recv_datagram(nl_sk)也用于在netlink socket nl_sk上接收消息,与skb_dequeue的不同指出是,如果socket的接收队列上没有消息,它将导致调用进程睡眠在等待队列nl_sk->sk_sleep,因此它必须在进程上下文使用,刚才讲的内核线程就可以采用这种方式来接收消息。

下面的函数input就是这种使用的示例:


    void input (struct sock *sk, int len)  {    wake_up_interruptible(sk->sk_sleep);  }  

当内核中发送netlink消息时,也需要设置目标地址与源地址,而且内核中消息是通过struct sk_buff来管理的, linux/netlink.h中定义了一个宏:


    #define NETLINK_CB(skb)         (*(struct netlink_skb_parms*)&((skb)->cb))  

来方便消息的地址设置。下面是一个消息地址设置的例子:


    NETLINK_CB(skb).pid = 0;  NETLINK_CB(skb).dst_pid = 0;  NETLINK_CB(skb).dst_group = 1;  

字段pid表示消息发送者进程ID,也即源地址,对于内核,它为 0, dst_pid 表示消息接收者进程 ID,也即目标地址,如果目标为组或内核,它设置为 0,否则 dst_group 表示目标组地址,如果它目标为某一进程或内核,dst_group 应当设置为 0。

在内核中,模块调用函数 netlink_unicast 来发送单播消息:


    int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);  

参数sk为函数netlink_kernel_create()返回的socket,参数skb存放消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便设置该控制块, 参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠。

内核模块或子系统也可以使用函数netlink_broadcast来发送广播消息:


    void netlink_broadcast(struct sock *sk, struct sk_buff *skb,         u32 pid, u32 group, int allocation);  

前面的三个参数与netlink_unicast相同,参数group为接收消息的多播组,该参数的每一个代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。

在内核中使用函数sock_release来释放函数netlink_kernel_create()创建的netlink socket:


    void sock_release(struct socket * sock);  

注意函数netlink_kernel_create()返回的类型为struct sock,因此函数sock_release应该这种调用:


    sock_release(sk->sk_socket);  

sk为函数netlink_kernel_create()的返回值。

源代码包中给出了一个使用 netlink 的示例,它包括一个内核模块 netlink-exam-kern.c 和两个应用程序 netlink-exam-user-recv.c, netlink-exam-user-send.c。内核模块必须先插入到内核,然后在一个终端上运行用户态接收程序,在另一个终端上运行用户态发送程序,发送程序读取参数指定的文本文件并把它作为 netlink 消息的内容发送给内核模块,内核模块接受该消息保存到内核缓存中,它也通过proc接口出口到 procfs,因此用户也能够通过 /proc/netlink_exam_buffer 看到全部的内容,同时内核也把该消息发送给用户态接收程序,用户态接收程序将把接收到的内容输出到屏幕上。

小结

本文是系列文章的第一篇,它详细介绍了五种用户空间与内核空间的数据交换方式,并通过实际例子程序向读者讲解了如何在内核开发中使用这些技术,其中内核启动参数方式是单向的,即只能向内核传递,而不能从内核获取,其余的均可以进行双向数据交换,即既可以从用户应用传递给内核,有可以从内核传递给应用态应用。netlink 是一种双向的数据交换方式,它使用起来非常简单高效,特别是它的广播特性在一些应用中非常方便。作者认为,它是所有这些用户态与内核态数据交换方式中最有效的最强大的方式。

该系列文章的第二篇将详细地讲解另外三种用户态与内核态的数据交换方式,包括 procfs、seq_file、debugfs 和 relayfs,有兴趣的读者请参看该系列文章第二篇。