建设网站的主要流程有哪些,九一人才网找工作,做一个家乡网站有什么可以做,网站制作公司排名级别#xff1a; 初级燚 杨 (), 计算机科学硕士2006 年 2 月 16 日本系列文章包括两篇#xff0c;它们文详细地介绍了 Linux 系统下用户空间与内核空间数据交换的九种方式#xff0c;包括内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、deb…级别 初级燚 杨 (), 计算机科学硕士2006 年 2 月 16 日本系列文章包括两篇它们文详细地介绍了 Linux 系统下用户空间与内核空间数据交换的九种方式包括内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs和relayfs并给出具体的例子帮助读者掌握这些技术的使用。本文是该系列文章的第一篇它介绍了内核启动参数、模块参数与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.oMakefile 仅许这一行就足够了然后需要修改源码树的根目录下的 Makefile文件的一行把下面行core-y : usr/修改为core-y : usr/ examples/注意如果读者创建的新目录和重新命名的文件名与上面不同需要修改上面所说 Makefile 文件相应的位置。做完以上工作就可以按照内核构建步骤去构建新的内核在构建好内核并设置好lilo或grub为该内核的启动条目后就可以启动该内核然后使用lilo或grub的编辑功能为该内核的启动参数行增加如下参数串setup_example_int1234 setup_example_int_array100,200,300,400 setup_example_stringThisisatest当然该参数串也可以直接写入到lilo或grub的配置文件中对应于该新内核的内核命令行参数串中。读者可以使用其它参数值来测试该功能。下面是作者系统上使用上面参数行的输出setup_example_int1234setup_example_int_array100,200,300,400setup_example_int_array includes 4 intergerssetup_example_stringThisisatest读者可以使用dmesg | grep setup来查看该程序的输出。内核子系统或设备驱动可以直接编译到内核也可以编译成模块如果编译到内核可以使用前一节介绍的方法通过内核启动参数来向它们传递参数如果编译成模块则可以通过命令行在插入模块时传递参数或者在运行时通过sysfs来设置或读取模块数据。Sysfs是一个基于内存的文件系统实际上它基于ramfssysfs提供了一种把内核数据结构它们的属性以及属性与数据结构的联系开放给用户态的方式它与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_XXXparam_set_XXX 和 param_check_XXX)。该宏的第三个参数用于指定访问权限如果为 0该参数将不出现在 sysfs 文件系统中允许的访问权限为 S_IRUSR S_IWUSRS_IRGRPS_IWGRPS_IROTH 和 S_IWOTH 的组合它们分别对应于用户读用户写用户组读用户组写其他用户读和其他用户写因此用文件的访问权限设置是一致的。在源代码包中的内核模块 module-param-exam.c 是一个利用模块参数和sysfs来进行用户态与内核态数据交互的例子。该模块有三个参数可以通过命令行设置下面是作者系统上的运行结果示例$ insmod ./module-param-exam.ko my_invisible_int10 my_visible_int20 mystringHello,Worldmy_invisible_int 10my_visible_int 20mystring Hello,World$ ls /sys/module/module_param_exam/parameters/mystring my_visible_int$ cat /sys/module/module_param_exam/parameters/mystringHello,World$ cat /sys/module/module_param_exam/parameters/my_visible_int20$ echo 2000 /sys/module/module_param_exam/parameters/my_visible_int$ cat /sys/module/module_param_exam/parameters/my_visible_int2000$ echo abc /sys/module/module_param_exam/parameters/mystring$ cat /sys/module/module_param_exam/parameters/mystringabc$ rmmod module_param_exammy_invisible_int 10my_visible_int 2000mystring abcSysctl是一种用户应用来设置和获得运行时内核的配置参数的一种有效方式通过这种方式用户应用可以在内核运行的任何时刻来改变内核的配置参数也可以在任何时候获得内核的配置参数通常内核的这些配置参数也出现在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_forwardnet.ipv4.ip_forward 0$ sysctl -w net.ipv4.ip_forward1net.ipv4.ip_forward 1$ sysctl net.ipv4.ip_forwardnet.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/myint0$ cat /proc/sys/mysysctl/mystring$ ./sysctl-exam-usermysysctl.myint 0mysysctl.mystring $ ./sysctl-exam-user 100 Hello, Worldold value: mysysctl.myint 0new value: mysysctl.myint 100old vale: mysysctl.mystring new value: mysysctl.mystring Hello, World$ cat /proc/sys/mysysctl/myint100$ cat /proc/sys/mysysctl/mystringHello, 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_operationsstruct 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.kochar 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/mychrdevabcdefghijklmnopqrstuvwxyz$ ./syscall-exam-userUser process: syscall-exam-us(1433)Available space: 65509 bytesData len: 27 bytesOffset in physical: cc0 bytesmychrdev content by mmap:abcdefghijklmnopqrstuvwxyz$ cat /dev/mychrdevabcde$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 的应用部分和内核部分没有编译时依赖但系统调用就有依赖而且新的系统调用的实现必须静态地连接到内核中它无法在模块中实现使用新系统调用的应用在编译时需要依赖内核。4netlink 支持多播内核模块或应用可以把消息多播给一个netlink组属于该neilink 组的任何内核模块或应用都能接收到该消息内核事件向用户态的通知机制就使用了这一特性任何对内核事件感兴趣的应用都能收到该子系统发送的内核事件在后面的文章中将介绍这一机制的使用。5内核可以使用 netlink 首先发起会话但系统调用和 ioctl 只能由用户应用发起调用。6netlink 使用标准的 socket API因此很容易使用但系统调用和 ioctl则需要专门的培训才能使用。用户态应用使用标准的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 1024char 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 1024struct 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的内核实现在.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_KERNELGFP_ATOMIC用于原子的上下文(即不可以睡眠)而GFP_KERNEL用于非原子上下文。在内核中使用函数sock_release来释放函数netlink_kernel_create()创建的netlink socketvoid 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有兴趣的读者请参看该系列文章第二篇。Linux 2.6.13的内核源码。内核文档Documentation/filesystems/sysfs.txt。Linux Device Drivers, Third Edition, 。内核文档Documentation/sysctl/*。Linux Kernel Module Programming Guide, 。[6] Linux 2.6.14的内核源码。。Linux Device Drivers, Third Edition, Kernel Korner - Why and How to Use Netlink Socket, 。netlink手册, netlink(7)。Extending netlink, 。杨燚计算机科学硕士毕业于中科院计算技术研究所有4年的Linux内核编程经验目前从事嵌入式实时Linux的开发与性能测试。您可以通过 与作者联系。