在 Linux 下用户空间和内核空间数据交换的方式

更新时间:2024-03-22 16:17:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

本系列文章包括两篇,他们文周详地地介绍了Linux系统下用户空间和内核空间数据交换的九种方式,包括内核启动参数、模块参数和sysfs、

sysctl、系统调用、netlink、procfs、seq_file、debugfs和relayfs,并给出具体的例子帮助读者掌控这些技术的使 用。

本文是该系列文章的第二篇,他介绍了procfs、seq_file、debugfs和relayfs,并结合给出的例子程式周详地说明了他们怎么使用。

1、内核启动参数

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

通常的使用方式是,定义一个分析参数的函数,而后使用内核提供的宏 __setup把他注册到内核中,该宏定义在 linux/init.h 中,因此要使用他必须包含该头文件: __setup(\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 来查看该程式的输出。

2、模块参数和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=\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 \$ cat /sys/module/module_param_exam/parameters/mystring abc

$ rmmod module_param_exam my_invisible_int = 10 my_visible_int = 2000

mystring = ’abc’

3、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 \old value: mysysctl.myint = 0 new value: mysysctl.myint = 100 old vale: mysysctl.mystring = \

new value: mysysctl.mystring = \$ cat /proc/sys/mysysctl/myint 100

$ cat /proc/sys/mysysctl/mystring Hello, World $

4、系统调用

系统调用是内核提供给应用程式的接口,应用对底层硬件的操作大部分都是通过调用系统调用来完成的,例如得到和设置系统时间,就需要分别调用

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, \注意,函数 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 \ 然后用户能通过 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 \\ $ cat /dev/mychrdev

$ echo \$ 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 $

5、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()

因此字段 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[] = \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

宏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

o relay_open(base_filename, parent, subbuf_size, n_subbufs, overwrite,

callbacks)

o o o o o o o

relay_close(chan) relay_flush(chan) relay_reset(chan)

relayfs_create_dir(name, parent) relayfs_remove_dir(dentry) relay_commit(buf, reserved, count)

relay_subbufs_consumed(chan, cpu, subbufs_consumed)

写函数包括:

o o o

relay_write(chan, data, length) __relay_write(chan, data, length) relay_reserve(chan, length)

回调函数包括:

o o o

subbuf_start(buf, subbuf, prev_subbuf_idx, prev_subbuf) buf_mapped(buf, filp) buf_unmapped(buf, filp)

辅助函数包括:

o o

relay_buf_full(buf)

subbuf_start_reserve(buf, length)

前面已讲过,每一个channel由一组channel缓存组成,每个CPU对应一个该channel的缓存,每一个缓存又由一个或多个子缓存组成,每一个缓存是子缓存组成的一个环型缓存。 函数relay_open用于创建一个channel并分配对应于每一个CPU的缓存,用户空间应用通过在relayfs文件系统中对应的文件能

访问channel缓存,参数base_filename用于指定channel的文件名,relay_open函数将在relayfs文件系统中创建

base_filename0..base_filenameN-1,即每一个CPU对应一个channel文件,其中N为CPU数,缺省情况下,这些文

件将建立在relayfs文件系统的根目录下,但如果参数parent非空,该函数将把channel文件创建于parent目录下,parent目录使

用函数relay_create_dir创建,函数relay_remove_dir用于删除由函数relay_create_dir创建的目录,谁创建

的目录,谁就负责在不用时负责删除。参数subbuf_size用于指定channel缓存中每一个子缓存的大小,参数n_subbufs用于指定

channel缓存包含的子缓存数,因此实际的channel缓存大小为(subbuf_size x

n_subbufs),参数overwrite用于指定该channel的操作模式,relayfs提供了两种写模式,一种是覆盖式写,另一种是非覆盖式 写。使用哪一种模式完全取决于函数subbuf_start的实现,覆盖写将在缓存已满的情况下无条件地继续从缓存的开始写数据,而不管这些数据是否已

被用户应用读取,因此写操作决不失败。在非覆盖写模式下,如果缓存满了,写将失败,但内核将在用户空间应用读取缓存数据时通过函数

relay_subbufs_consumed()通知relayfs。如果用户空间应用没来得及消耗缓存中的数据或缓存已满,两种模式都将导致数据丢

失,唯一的差别是,前者丢失数据在缓存开头,而后者丢失数据在缓存末尾。一旦内核再次调用函数relay_subbufs_consumed(),已满的

缓存将不再满,因而能继续写该缓存。当缓存满了以后,relayfs将调用回调函数buf_full()来通知内核模块或子系统。当新的数据太大无法写

入当前子缓存剩余的空间时,relayfs将调用回调函数subbuf_start()来通知内核模块或子系统将需要使用新的子缓存。内核模块需要在该回 调函数中实现下述功能: 初始化新的子缓存;

如果1正确,完成当前子缓存;

如果2正确,返回是否正确完成子缓存转换;

在非覆盖写模式下,回调函数subbuf_start()应该如下实现: static int subbuf_start(struct rchan_buf *buf, void *subbuf,

void *prev_subbuf,

unsigned int prev_padding) {

if (prev_subbuf)

*((unsigned *)prev_subbuf) = prev_padding; if (relay_buf_full(buf)) return 0;

subbuf_start_reserve(buf, sizeof(unsigned int)); return 1; }

如果当前缓存满,即所有的子缓存都没读取,该函数返回0,指示子缓存转换没有成功。当子缓存通过函数relay_subbufs_consumed

()被读取后,读取者将负责通知relayfs,函数relay_buf_full()在已有读者读取子缓存数据后返回0,在这种情况下,子缓存转换成 功进行。

在覆盖写模式下, subbuf_start()的实现和非覆盖模式类似:

static int subbuf_start(struct rchan_buf *buf, void *subbuf,

void *prev_subbuf,

unsigned int prev_padding) {

if (prev_subbuf)

*((unsigned *)prev_subbuf) = prev_padding; subbuf_start_reserve(buf, sizeof(unsigned int)); return 1; }

只是不做relay_buf_full()检查,因为此模式下,缓存是环行的,能无条件地写。因此在此模式下,子缓存转换必定成功,函数

relay_subbufs_consumed() 也无须调用。如果channel写者没有定义subbuf_start(),缺省的实现将被使用。

能通过在回调函数subbuf_start()中调用辅助函数subbuf_start_reserve()在子缓存中预留头空间,预留空间能保存任

何需要的信息,如上面例子中,预留空间用于保存子缓存填充字节数,在subbuf_start()实现中,前一个子缓存的填充值被设置。前一个子缓存的填

充值和指向前一个子缓存的指针一道作为subbuf_start()的参数传递给subbuf_start(),只有在子缓存完成后,才能知道填充值。

subbuf_start()也被在channel创建时分配每一个channel缓存的第一个子缓存时调用,以便预留头空间,但在这种情况下,前一个子 缓存指针为NULL。

内核模块使用函数relay_write()或__relay_write()往channel缓存中写需要转发的数据,他们的差别是前者失效了本

地中断,而后者只抢占失效,因此前者能在所有内核上下文安全使用,而后者应当在没有所有中断上下文将写channel缓存的情况下使用。这两个函数没有 返回值,因此用户不能直接确定写操作是否失败,在缓存满且写模式为非覆盖模式时,relayfs将通过回调函数buf_full来通知内核模块。

函数relay_reserve()用于在channel缓存中预留一段空间以便以后写入,在那些没有临时缓存而直接写入channel缓存的内核

模块可能需要该函数,使用该函数的内核模块在实际写这段预留的空间时能通过调用relay_commit()来通知relayfs。当所有预留的空间全 部写完并通过relay_commit通知relayfs后,relayfs将调用回调函数deliver()通知内核模块一个完整的子缓存已填满。由

于预留空间的操作并不在写channel的内核模块完全控制之下,因此relay_reserve()不能非常好地保护缓存,因此当内核模块调用

relay_reserve()时必须采取恰当的同步机制。

当内核模块结束对channel的使用后需要调用relay_close() 来关闭channel,如果没有所有用户在引用该channel,他将和对应的缓存全部被释放。

函数relay_flush()强制在所有的channel缓存上做一个子缓存转换,他在channel被关闭前使用来终止和处理最后的子缓存。

函数relay_reset()用于将一个channel恢复到初始状态,因而不必释放现存的内存映射并重新分配新的channel缓存就能使用channel,不过该调用只有在该channel没有所有用户在

写的情况下才能安全使用。

回调函数buf_mapped() 在channel缓存被映射到用户空间时被调用。 回调函数buf_unmapped()在释放该映射时被调用。内核模块能通过他们触发一些内核操作,如开始或结束channel写操作。

在原始码包中给出了一个使用relayfs的示例程式relayfs_exam.c,他只包含一个内核模块,对于复杂的使用,需要应用程式配合。该模块实现了类似于文章中seq_file示例实现的功能。 当然为了使用relayfs,用户必须让内核支持relayfs,并且要mount他,下面是作者系统上的使用该模块的输出信息: $ mkdir -p /relayfs

$ insmod ./relayfs-exam.ko

$ mount -t relayfs relayfs /relayfs $ cat /relayfs/example0 … $

relayfs是一种比较复杂的内核态和用户态的数据交换方式,本例子程式只提供了一个较简单的使用方式,对于复杂的使用,请参考relayfs用例页面 http://relayfs.sourceforge.net/examples.html 。

小结

本文是该系列文章最后一篇,他周详地讲解了其余四种用户空间和内核空间的数据交换方式,并通过实际例子程式向读者讲解了怎么在内核研发中使用这些技

术,其中seq_file是单向的,即只能向内核传递,而不能从内核获取,而另外三种方式均能进行双向数据交换,即既能从用户应用传递给内核,又能 从内核传递给应用态应用。procfs一般用于向用户出口少量的数据信息,或用户通过他设置内核变量从而控制内核行为。seq_file实际上依赖于

procfs,因此为了使用seq_file,必须使内核支持procfs。debugfs用于内核研发者调试使用,他比其他集中方式都方便,不过仅用于

简单类型的变量处理。relayfs是一种非常复杂的数据交换方式,要想准确使用并不容易,不过如果使用得当,他远比procfs和seq_file功能 强大。

本文来源:https://www.bwwdw.com/article/kt88.html

Top