内核启动参数
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
来查看该程序的输出。
//filename: kern-boot-params.c
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/string.h>
#define MAX_SIZE 5
static int setup_example_int;
static int setup_example_int_array[MAX_SIZE];
static char setup_example_string[16];
static int __init parse_int(char * s)
{
int ret;
ret = get_option(&s, &setup_example_int);
if (ret == 1) {
printk("setup_example_int=%d\n", setup_example_int);
}
return 1;
}
static int __init parse_int_string(char *s)
{
char * ret_str;
int i;
ret_str = get_options(s, MAX_SIZE, setup_example_int_array);
if (*ret_str != '\0') {
printk("incorrect setup_example_int_array paramters: %s\n", ret_str);
}
else {
printk("setup_example_int_array=");
for (i=1; i<MAX_SIZE; i++) {
printk("%d", setup_example_int_array[i]);
if (i < (MAX_SIZE -1)) {
printk(",");
}
}
printk("\n");
printk("setup_example_int_array includes %d intergers\n", setup_example_int_array[0]);
}
return 1;
}
static int __init parse_string(char *s)
{
if (strlen(s) > 15) {
printk("Too long setup_example_string parameter, \n");
printk("maximum length is less than or equal to 15\n");
}
else {
memcpy(setup_example_string, s, strlen(s) + 1);
printk("setup_example_string=%s\n", setup_example_string);
}
return 1;
}
/*宏__setup()将分析参数的函数注册到内核中*/
__setup("setup_example_int=", parse_int);
__setup("setup_example_int_array=", parse_int_string);
__setup("setup_example_string=", parse_string);
模块参数与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'
以下为示例源码:
//filename: module-para-exam.c
#include <linux/config.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/stat.h>
static int my_invisible_int = 0;
static int my_visible_int = 0;
static char * mystring = "Hello, World";
module_param(my_invisible_int, int, 0);
MODULE_PARM_DESC(my_invisible_int, "An invisible int under sysfs");
module_param(my_visible_int, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(my_visible_int, "An visible int under sysfs");
module_param(mystring, charp, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(mystring, "An visible string under sysfs");
static int __init exam_module_init(void)
{
printk("my_invisible_int = %d\n", my_invisible_int);
printk("my_visible_int = %d\n", my_visible_int);
printk("mystring = '%s'\n", mystring);
return 0;
}
static void __exit exam_module_exit(void)
{
printk("my_invisible_int = %d\n", my_invisible_int);
printk("my_visible_int = %d\n", my_visible_int);
printk("mystring = '%s'\n", mystring);
}
module_init(exam_module_init);
module_exit(exam_module_exit);
MODULE_AUTHOR("Yang Yi");
MODULE_DESCRIPTION("A module_param example module");
MODULE_LICENSE("GPL");
sysctrl
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_tabl
e结构的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
#
示例:
头文件:sysctl-exam.h
:
//header: sysctl-exam.h
#ifndef _SYSCTL_EXAM_H
#define _SYSCTL_EXAM_H
#include <linux/sysctl.h>
#define MY_ROOT (CTL_CPU + 10)
#define MY_MAX_SIZE 256
enum {
MY_INT_EXAM = 1,
MY_STRING_EXAM = 2,
};
#endif
内核模块代码sysctl-exam-kern.c
:
//kernel module: sysctl-exam-kern.c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/sysctl.h>
#include "sysctl-exam.h"
static char mystring[256];
static int myint;
static struct ctl_table my_sysctl_exam[] = {
{
.ctl_name = MY_INT_EXAM,
.procname = "myint",
.data = &myint,
.maxlen = sizeof(int),
.mode = 0666,
.proc_handler = &proc_dointvec,
},
{
.ctl_name = MY_STRING_EXAM,
.procname = "mystring",
.data = mystring,
.maxlen = MY_MAX_SIZE,
.mode = 0666,
.proc_handler = &proc_dostring,
.strategy = &sysctl_string,
},
{
.ctl_name = 0
}
};
static struct ctl_table my_root = {
.ctl_name = MY_ROOT,
.procname = "mysysctl",
.mode = 0555,
.child = my_sysctl_exam,
};
static struct ctl_table_header * my_ctl_header;
static int __init sysctl_exam_init(void)
{
my_ctl_header = register_sysctl_table(&my_root, 0);
return 0;
}
static void __exit sysctl_exam_exit(void)
{
unregister_sysctl_table(my_ctl_header);
}
module_init(sysctl_exam_init);
module_exit(sysctl_exam_exit);
MODULE_LICENSE("GPL");
用户程序 sysctl-exam-user.c
:
//application: sysctl-exam-user.c
#include <linux/unistd.h>
#include <linux/types.h>
#include <linux/sysctl.h>
#include "sysctl-exam.h"
#include <stdio.h>
#include <errno.h>
_syscall1(int, _sysctl, struct __sysctl_args *, args);
int sysctl(int *name, int nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen)
{
struct __sysctl_args args={name,nlen,oldval,oldlenp,newval,newlen};
return _sysctl(&args);
}
#define SIZE(x) sizeof(x)/sizeof(x[0])
#define OSNAMESZ 100
int oldmyint;
int oldmyintlen;
int newmyint;
int newmyintlen;
char oldmystring[MY_MAX_SIZE];
int oldmystringlen;
char newmystring[MY_MAX_SIZE];
int newmystringlen;
int myintctl[] = {MY_ROOT, MY_INT_EXAM};
int mystringctl[] = {MY_ROOT, MY_STRING_EXAM};
int main(int argc, char ** argv)
{
if (argc < 2)
{
oldmyintlen = sizeof(int);
if (sysctl(myintctl, SIZE(myintctl), &oldmyint, &oldmyintlen, 0, 0)) {
perror("sysctl");
exit(-1);
}
else {
printf("mysysctl.myint = %d\n", oldmyint);
}
oldmystringlen = MY_MAX_SIZE;
if (sysctl(mystringctl, SIZE(mystringctl), oldmystring, &oldmystringlen, 0, 0)) {
perror("sysctl");
exit(-1);
}
else {
printf("mysysctl.mystring = \"%s\"\n", oldmystring);
}
}
else if (argc != 3)
{
printf("Usage:\n");
printf("\tsysctl-exam-user\n");
printf("Or\n");
printf("\tsysctl-exam-user aint astring\n");
}
else
{
newmyint = atoi(argv[1]);
newmyintlen = sizeof(int);
oldmyintlen = sizeof(int);
strcpy(newmystring, argv[2]);
newmystringlen = strlen(newmystring);
oldmystringlen = MY_MAX_SIZE;
if (sysctl(myintctl, SIZE(myintctl), &oldmyint, &oldmyintlen, &newmyint, newmyintlen)) {
perror("sysctl");
exit(-1);
}
else {
printf("old value: mysysctl.myint = %d\n", oldmyint);
printf("new value: mysysctl.myint = %d\n", newmyint);
}
if (sysctl(mystringctl, SIZE(mystringctl), oldmystring, &oldmystringlen, newmystring, newmystringlen))
{
perror("sysctl");
exit(-1);
}
else {
printf("old vale: mysysctl.mystring = \"%s\"\n", oldmystring);
printf("new value: mysysctl.mystring = \"%s\"\n", newmystring);
}
}
exit(0);
}
syscall
系统调用是内核提供给应用程序的接口,应用对底层硬件的操作大部分都是通过调用系统调用来完成的,例如得到和设置系统时间,就需要分别调用gettimeofday
和 settimeofday
来实现。事实上,所有的系统调用都涉及到内核与应用之间的数据交换,如文件系统操作函数 read 和 write,设置和读取网络协议栈的 setsockopt
和getsockopt
。本节并不是讲解如何增加新的系统调用,而是讲解如何利用现有系统调用来实现用户的数据传输需求。
一般地,用户可以建立一个伪设备来作为应用与内核之间进行数据交换的渠道,最通常的做法是使用伪字符设备,具体实现方法是:
1).定义对字符设备进行操作的必要函数并设置结构struct file_operations
结构 struct file_operations
非常大,对于一般的数据交换需求,只定义open
, read
, write
, ioctl
,mmap
和release
函数就足够了,它们实际上对应于用户态的文件系统操作函数open
, read
, write
, ioct
, 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
$
示例:
头文件 syscall-exam.h
:
//header: syscall-exam.h
#ifndef _SYSCALL_EXAM_H
#define _SYSCALL_EXAM_H
#include <linux/ioctl.h>
#undef TASK_COMM_LEN
#define TASK_COMM_LEN 16
typedef struct mychrdev_info {
pid_t user_pid;
char user_name[TASK_COMM_LEN];
unsigned int available_len;
unsigned int len;
unsigned long offset_in_ppage;
} mydev_info_t;
struct mychrdev_window {
unsigned int head;
unsigned int tail;
};
#define MYCHRDEV_IOCTL_BASE 'm'
#define MYCHRDEV_IOR(nr, size) _IOR(MYCHRDEV_IOCTL_BASE, nr, size)
#define MYCHRDEV_IOW(nr, size) _IOW(MYCHRDEV_IOCTL_BASE, nr, size)
#define MYCHRDEV_IOCTL_GET_INFO MYCHRDEV_IOR(0x01,mydev_info_t)
#define MYCHRDEV_IOCTL_SET_TRUNCATE MYCHRDEV_IOW(0x02,int)
#endif
内核模块源码syscall-exam-kern.c
:
//kernel module: syscall-exam-kern.c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <asm/uaccess.h>
#include <linux/mm.h>
#include "syscall-exam.h"
#define MYCHRDEV_MAX_MINOR 4
#define MYCHRDEV_CAPACITY 65536
struct mychrdev_data {
char buf[MYCHRDEV_CAPACITY];
unsigned int headptr;
unsigned int tailptr;
};
struct mychrdev_data * mydata[MYCHRDEV_MAX_MINOR];
static atomic_t mychrdev_use_stats[MYCHRDEV_MAX_MINOR];
static int mychrdev_major;
struct mychrdev_private {
pid_t user_pid;
char user_name[TASK_COMM_LEN];
int minor;
struct mychrdev_data * data;
#define headptr data->headptr
#define tailptr data->tailptr
#define buffer data->buf
};
ssize_t mychrdev_read(struct file * file, char __user * buf, size_t count, loff_t * ppos)
{
int len;
struct mychrdev_private * myprivate = (struct mychrdev_private *)file->private_data;
len = (int)(myprivate->tailptr - myprivate->headptr);
if (*ppos >= len) {
return 0;
}
if (*ppos + count > len) {
count = len - *ppos;
}
if (copy_to_user(buf, myprivate->buffer + myprivate->headptr + *ppos, count)) {
return -EFAULT;
}
*ppos += count;
return count;
}
ssize_t mychrdev_write(struct file * file, const char __user * buf, size_t count, loff_t * ppos)
{
int leftlen;
struct mychrdev_private * myprivate = (struct mychrdev_private *)file->private_data;
leftlen = (MYCHRDEV_CAPACITY - myprivate->tailptr);
if (* ppos >= MYCHRDEV_CAPACITY) {
return -ENOBUFS;
}
if (*ppos + count > leftlen) {
count = leftlen - *ppos;
}
if (copy_from_user(myprivate->buffer + myprivate->headptr + *ppos, buf, count)) {
return -EFAULT;
}
*ppos += count;
myprivate->tailptr += count;
return count;;
}
int mychrdev_ioctl(struct inode * inode, struct file * file, unsigned int cmd, unsigned long argp)
{
struct mychrdev_private * myprivate = (struct mychrdev_private *)file->private_data;
mydev_info_t a;
struct mychrdev_window window;
switch(cmd) {
case MYCHRDEV_IOCTL_GET_INFO:
a.user_pid = myprivate->user_pid;
memcpy(a.user_name, myprivate->user_name, strlen(myprivate->user_name));
a.available_len = MYCHRDEV_CAPACITY - myprivate->tailptr;
a.len = myprivate->tailptr - myprivate->headptr;
a.offset_in_ppage = __pa(myprivate) & 0x00000fff;
if (copy_to_user((void *)argp, (void *)&a, sizeof(a))) {
return -EFAULT;
}
break;
case MYCHRDEV_IOCTL_SET_TRUNCATE:
if (copy_from_user(&window, (void *)argp, sizeof(window))) {
return -EFAULT;
}
if (window.head < myprivate->headptr) {
return -EINVAL;
}
if (window.tail > myprivate->tailptr) {
return -EINVAL;
}
myprivate->headptr = window.head;
myprivate->tailptr = window.tail;
break;
default:
return -EINVAL;
}
return 0;
}
int mychrdev_open(struct inode * inode, struct file * file)
{
struct mychrdev_private * myprivate = NULL;
int minor;
if (current->euid != 0) {
return -EPERM;
}
minor = MINOR(inode->i_rdev);
if (atomic_read(&mychrdev_use_stats[minor])) {
return -EBUSY;
}
else {
atomic_inc(&mychrdev_use_stats[minor]);
}
myprivate = (struct mychrdev_private *)kmalloc(sizeof(struct mychrdev_private), GFP_KERNEL);
if (myprivate == NULL) {
return -ENOMEM;
}
myprivate->user_pid = current->pid;
sprintf(myprivate->user_name, "%s", current->comm);
myprivate->minor = minor;
myprivate->data = mydata[minor];
file->private_data = (void *)myprivate;
return 0;
}
int mychrdev_mmap(struct file * file, struct vm_area_struct * vma)
{
unsigned long pfn;
struct mychrdev_private * myprivate = (struct mychrdev_private *)file->private_data;
/* Turn a kernel-virtual address into a physical page frame */
pfn = __pa(&(mydata[myprivate->minor]->buf)) >> PAGE_SHIFT;
if (!pfn_valid(pfn))
return -EIO;
vma->vm_flags |= VM_RESERVED;
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
/* Remap-pfn-range will mark the range VM_IO and VM_RESERVED */
if (remap_pfn_range(vma,
vma->vm_start,
pfn,
vma->vm_end - vma->vm_start,
vma->vm_page_prot))
return -EAGAIN;
return 0;
}
int mychrdev_release(struct inode * inode, struct file * file)
{
atomic_dec(&mychrdev_use_stats[MINOR(inode->i_rdev)]);
kfree(((struct mychrdev_private *)(file->private_data))->data);
kfree(file->private_data);
return 0;
}
loff_t mychrdev_llseek(struct file * file, loff_t offset, int seek_flags)
{
struct mychrdev_private * myprivate = (struct mychrdev_private *)file->private_data;
int len = myprivate->tailptr - myprivate->headptr;
switch (seek_flags) {
case 0:
if ((offset > len)
|| (offset < 0)) {
return -EINVAL;
}
case 1:
if ((offset + file->f_pos < 0)
|| (offset + file->f_pos > len)) {
return -EINVAL;
}
offset += file->f_pos;
case 2:
if ((offset > 0)
|| (-offset > len)) {
return -EINVAL;
}
offset += len;
break;
default:
return -EINVAL;
}
if ((offset >= 0) && (offset <= len)) {
file->f_pos = offset;
file->f_version = 0;
return offset;
}
else {
return -EINVAL;
}
}
struct file_operations mychrdev_fops = {
.owner = THIS_MODULE,
.read = mychrdev_read,
.write = mychrdev_write,
.ioctl = mychrdev_ioctl,
.open = mychrdev_open,
.llseek = mychrdev_llseek,
.release = mychrdev_release,
.mmap = mychrdev_mmap,
};
static int __init mychardev_init(void)
{
int i;
for (i=0;i<MYCHRDEV_MAX_MINOR;i++) {
atomic_set(&mychrdev_use_stats[i], 0);
mydata[i] = NULL;
mydata[i] =
(struct mychrdev_data *)kmalloc(sizeof(struct mychrdev_data), GFP_KERNEL);
if (mydata[i] == NULL) {
return -ENOMEM;
}
memset(mydata[i], 0, sizeof(struct mychrdev_data));
}
mychrdev_major = register_chrdev(0, "mychrdev", &mychrdev_fops);
if (mychrdev_major <= 0) {
printk("Fail to register char device mychrdev.\n");
return -1;
}
printk("char device mychrdev is registered, major is %d\n", mychrdev_major);
return 0;
}
static void __exit mychardev_remove(void)
{
unregister_chrdev(mychrdev_major, NULL);
}
module_init(mychardev_init);
module_exit(mychardev_remove);
MODULE_LICENSE("GPL");
用户程序 syscall-exam-user.c
:
//application: syscall-exam-user.c
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/mman.h>
#include "syscall-exam.h"
int main(void)
{
int fd;
mydev_info_t mydev_info;
struct mychrdev_window truncate_window;
char * mmap_ptr = NULL;
int i;
fd = open("/dev/mychrdev", O_RDWR);
if (fd < 0) {
perror("open:");
exit(-1);
}
ioctl(fd, MYCHRDEV_IOCTL_GET_INFO, &mydev_info);
printf("User process: %s(%d)\n", mydev_info.user_name, mydev_info.user_pid);
printf("Available space: %d bytes\n", mydev_info.available_len);
printf("Data len: %d bytes\n", mydev_info.len);
printf("Offset in physical: %lx bytes\n", mydev_info.offset_in_ppage);
mmap_ptr = mmap(NULL, 65536, PROT_READ, MAP_PRIVATE, fd, 0);
if ((int) mmap_ptr == -1) {
perror("mmap:");
close(fd);
exit(-1);
}
printf("mychrdev content by mmap:\n");
printf("%s\n", mmap_ptr);
munmap(mmap_ptr, 65536);
truncate_window.head = 0;
truncate_window.tail = 5;
ioctl(fd, MYCHRDEV_IOCTL_SET_TRUNCATE, &truncate_window);
close(fd);
}
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
/*宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值*/
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/*宏NLMSG_LENGTH(len)用于计算数据部分长度为len时实际的消息长度。它一般用于分配消息缓存*/
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/*宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,它也用于分配消息缓存*/
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/*宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏*/
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/*宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址,同时len也减少为剩余消息的总长度,该宏一般
在一个消息被分成几个部分发送或接收时使用*/
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
/*宏NLMSG_OK(nlh,len)用于判断消息是否有len这么长*/
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len <= (len))
/*宏NLMSG_PAYLOAD(nlh,len)用于返回payload的长度*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
函数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-exam-kern.c
:
//kernel module: netlink-exam-kern.c
#include <linux/config.h>
#include <linux/module.h>
#include <linux/netlink.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <linux/proc_fs.h>
#define BUF_SIZE 16384
static struct sock *netlink_exam_sock;
static unsigned char buffer[BUF_SIZE];
static unsigned int buffer_tail = 0;
static int exit_flag = 0;
static DECLARE_COMPLETION(exit_completion);
static void recv_handler(struct sock * sk, int length)
{
wake_up(sk->sk_sleep);
}
static int process_message_thread(void * data)
{
struct sk_buff * skb = NULL;
struct nlmsghdr * nlhdr = NULL;
int len;
DEFINE_WAIT(wait);
daemonize("mynetlink");
while (exit_flag == 0) {
prepare_to_wait(netlink_exam_sock->sk_sleep, &wait, TASK_INTERRUPTIBLE);
schedule();
finish_wait(netlink_exam_sock->sk_sleep, &wait);
while ((skb = skb_dequeue(&netlink_exam_sock->sk_receive_queue))
!= NULL) {
nlhdr = (struct nlmsghdr *)skb->data;
if (nlhdr->nlmsg_len < sizeof(struct nlmsghdr)) {
printk("Corrupt netlink message.\n");
continue;
}
len = nlhdr->nlmsg_len - NLMSG_LENGTH(0);
if (len + buffer_tail > BUF_SIZE) {
printk("netlink buffer is full.\n");
}
else {
memcpy(buffer + buffer_tail, NLMSG_DATA(nlhdr), len);
buffer_tail += len;
}
nlhdr->nlmsg_pid = 0;
nlhdr->nlmsg_flags = 0;
NETLINK_CB(skb).pid = 0;
NETLINK_CB(skb).dst_pid = 0;
NETLINK_CB(skb).dst_group = 1;
netlink_broadcast(netlink_exam_sock, skb, 0, 1, GFP_KERNEL);
}
}
complete(&exit_completion);
return 0;
}
static int netlink_exam_readproc(char *page, char **start, off_t off,
int count, int *eof, void *data)
{
int len;
if (off >= buffer_tail) {
* eof = 1;
return 0;
}
else {
len = count;
if (count > PAGE_SIZE) {
len = PAGE_SIZE;
}
if (len > buffer_tail - off) {
len = buffer_tail - off;
}
memcpy(page, buffer + off, len);
*start = page;
return len;
}
}
static int __init netlink_exam_init(void)
{
netlink_exam_sock = netlink_kernel_create(NETLINK_GENERIC, 0, recv_handler, THIS_MODULE);
if (!netlink_exam_sock) {
printk("Fail to create netlink socket.\n");
return 1;
}
kernel_thread(process_message_thread, NULL, CLONE_KERNEL);
create_proc_read_entry("netlink_exam_buffer", 0444, NULL, netlink_exam_readproc, 0);
return 0;
}
static void __exit netlink_exam_exit(void)
{
exit_flag = 1;
wake_up(netlink_exam_sock->sk_sleep);
wait_for_completion(&exit_completion);
sock_release(netlink_exam_sock->sk_socket);
}
module_init(netlink_exam_init);
module_exit(netlink_exam_exit);
MODULE_LICENSE("GPL");
netlink-exam-user-send.c
:
//application sender: netlink-exam-user-send.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#define MAX_MSGSIZE 1024
int main(int argc, char * argv[])
{
FILE * fp;
struct sockaddr_nl saddr, daddr;
struct nlmsghdr *nlhdr = NULL;
struct msghdr msg;
struct iovec iov;
int sd;
char text_line[MAX_MSGSIZE];
int ret = -1;
if (argc < 2) {
printf("Usage: %s atextfilename\n", argv[0]);
exit(1);
}
if ((fp = fopen(argv[1], "r")) == NULL) {
printf("File %s dosen't exist.\n");
exit(1);
}
sd = socket(AF_NETLINK, SOCK_RAW,NETLINK_GENERIC);
memset(&saddr, 0, sizeof(saddr));
memset(&daddr, 0, sizeof(daddr));
saddr.nl_family = AF_NETLINK;
saddr.nl_pid = getpid();
saddr.nl_groups = 0;
bind(sd, (struct sockaddr*)&saddr, sizeof(saddr));
daddr.nl_family = AF_NETLINK;
daddr.nl_pid = 0;
daddr.nl_groups = 0;
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
while (fgets(text_line, MAX_MSGSIZE, fp)) {
memcpy(NLMSG_DATA(nlhdr), text_line, strlen(text_line));
memset(&msg, 0 ,sizeof(struct msghdr));
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(text_line));
nlhdr->nlmsg_pid = getpid(); /* self pid */
nlhdr->nlmsg_flags = 0;
iov.iov_base = (void *)nlhdr;
iov.iov_len = nlhdr->nlmsg_len;
msg.msg_name = (void *)&daddr;
msg.msg_namelen = sizeof(daddr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ret = sendmsg(sd, &msg, 0);
if (ret == -1) {
perror("sendmsg error:");
}
}
close(sd);
}
netlink-exam-user-recv.c
:
//application receiver: netlink-exam-user-recv.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#define MAX_MSGSIZE 1024
int main(void)
{
struct sockaddr_nl saddr, daddr;
struct nlmsghdr *nlhdr = NULL;
struct msghdr msg;
struct iovec iov;
int sd;
int ret = 1;
sd = socket(AF_NETLINK, SOCK_RAW,NETLINK_GENERIC);
memset(&saddr, 0, sizeof(saddr));
memset(&daddr, 0, sizeof(daddr));
saddr.nl_family = AF_NETLINK;
saddr.nl_pid = getpid();
saddr.nl_groups = 1;
bind(sd, (struct sockaddr*)&saddr, sizeof(saddr));
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
while (1) {
memset(nlhdr, 0, NLMSG_SPACE(MAX_MSGSIZE));
iov.iov_base = (void *)nlhdr;
iov.iov_len = NLMSG_SPACE(MAX_MSGSIZE);
msg.msg_name = (void *)&daddr;
msg.msg_namelen = sizeof(daddr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ret = recvmsg(sd, &msg, 0);
if (ret == 0) {
printf("Exit.\n");
exit(0);
}
else if (ret == -1) {
perror("recvmsg:");
exit(1);
}
printf("%s", NLMSG_DATA(nlhdr));
}
close(sd);
}