精华内容
下载资源
问答
  • Linux用户空间与内核空间数据交换的方式一  本系列文章包括两篇,它们文详细地介绍了 Linux 系统下用户空间与内核空间数据交换的九种方式,包括内核启动参数、模块参数与sysfs、sysctl、系统调用、netlink、...
    在Linux下用户空间与内核空间数据交换的方式一
    
        本系列文章包括两篇,它们文详细地介绍了 Linux 系统下用户空间与内核空间数据交换的九种方式,包括内核启动参数、模块参数与sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs和relayfs,并给出具体的例子帮助读者掌握这些技术的使用。
         本文是该系列文章的第一篇,它介绍了内核启动参数、模块参数与sysfs、sysctl、系统调用和netlink,并结合给出的例子程序详细地说明了它们如何使用。
    引言
        一般地,在使用虚拟内存技术的多任务系统上,内核和应用有不同的地址空间,因此,在内核和应用之间以及在应用与应用之间进行数据交换需要专门的机制来实现,众所周知,进程间通信(IPC)机制就是为实现应用与应用之间的数据交换而专门实现的,大部分读者可能对进程间通信比较了解,但对应用与内核之间的数据交换机制可能了解甚少,本文将详细介绍 Linux 系统下内核与应用进行数据交换的各种方式,包括内核启动参数、模块参数与sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs 和 relayfs。

    1.内核启动参数
        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
    来查看该程序的输出。

    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="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’

    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 则指向该目录条目下面的所有条目,对于在同一目录下的多个条目,不必一一注册,用户可以把它们组织成一个 structctl_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
    $

    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, "exam_char_dev", &exam_file_ops);
        注意,函数 register_chrdev 的第一个参数如果为0,表示由内核来确定该注册伪字符设备的主设备号,这是该函数的返回为实际分配的主设备号,如果返回小于0,表示注册失败。因此,用户在使用该函数时必须判断返回值以便处理失败情况。为了使用该函数必须包含头文件 linux/fs.h。
        在源代码包中给出了一个使用这种方式实现用户态与内核态数据交换的典型例子,它包含了三个文件:
    .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: 6550Array 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           Array       /* 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[] = "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
    宏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消息处理函数,当有消息到达这个netlinksocket时,该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 socketnl_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,有兴趣的读者请参看该系列文章第二篇。

    声明:本文转载自杨燚的文章。

    展开全文
  • Linux内核态用户态

    千次阅读 2016-04-15 18:44:17
     内核态(Kernel Mode) 和 用户态(User Mode) 实际上是指 2 种不同的访问权限。  x86处理器包含 4 个不同的特权等级,分别是 Ring0~Ring3。Ring0下,可以执行特权级指令,对任何 I/O 设备都有访问权限,Ring3则...
    【简述】
    
        内核态(Kernel Mode) 和 用户态(User Mode) 实际上是指 2 种不同的访问权限
        x86处理器包含 4 个不同的特权等级,分别是 Ring0~Ring3。Ring0下,可以执行特权级指令,对任何 I/O 设备都有访问权限,Ring3则被限制很多操作。
        在使用 x86处理器的设备上,用户代码运行在 Ring3,系统内核代码运行在 Ring0。

    【区别】
        内核态:CPU可以访问内存所有数据,包括外围设备,例如硬盘,网卡。CPU也可以将自己从一个程序切换到另一个程序。
        用户态:只能受限的访问内存,且不允许访问外围设备。占用CPU的能力被剥夺,CPU资源可以被其他程序获取。

    【划分原因】
        防止进程获取别的程序的内存数据,或者获取外围设备的数据,并发送到网络。

    【状态切换】
        有 3种方式可以实现状态的切换
        1、系统调用
        所有用户程序都运行在用户态,当要进行从键盘输入数据或从硬盘读取数据这些内核态操作时,需要向操作系统请求以程序的身份执行这些操作。
        这时用户态程序切换到内核态,但是不能控制在内核态中执行的指令。
        这个过程称作系统调用,在CPU中的实现称之为陷阱指令(Trap Instruction)。
        2、外围设备中断
        CPU接收到中断信号时会暂停执行当前命令,转而执行中断信号对应的处理程序。
        因为中断的处理属于内核态,如果CPU在处理中断前处于用户态,那么将发生状态切换。
        典型的例子是硬盘读写。
        3、异常
        CPU在用户态下运行程序时发生了异常,会转到一个处理异常的内核相关程序中,也会导致用户态到内核态的转换。
        比如,缺页异常。

    【执行步骤】
        1、用户态程序将一些数据值放在寄存器中,或者使用参数创建一个栈帧(Stack Frame)。这些数据说明了需要操作系统提供的服务。
        2、用户态程序执行陷阱指令。
        3、CPU切换到内核态开始执行位于内存指定位置的指令。这些指令属于操作系统的一部分,具有内存保护,不可被用户程序访问。
        4、这些指定位置上的指令被称为陷阱(Trap)或者系统调用句柄(System Call Handler)。她们会读取程序放入内存的数据参数并执行程序请求任务。
        5、系统调用完成后,操作系统会重置CPU为用户态并将系统调用的结果返回。
    展开全文
  • 由于linux内核本身非常强大,编写自定义的内核模块往往是为了实现我们独特的功能或需求,这经常涉及到内核用户态之间通信。Linux本身提供丰富的系统调用(如ioctl、open)来实现用户态与内核交互,此文基于...

    1.   简介

    前篇博文(linux内核编程一:模块的装载和卸载)中我们已经知道了自定义内核模块的装载和卸载。由于linux内核本身非常强大,编写自定义的内核模块往往是为了实现我们独特的功能或需求,这经常涉及到内核态与用户态之间通信。Linux本身提供丰富的系统调用(如ioctl、open)来实现用户态与内核态交互,此文基于getsockopt系统调用,介绍一种简单的用户态与内核态通信方法:增加sockopt命令字。

    2.   Socket选项

    我们知道,系统调用getsockopt/ setsockopt可以操作socket文件描述符(FD)的相关选项,常用的有:

    SO_BROADCAST----Set or get 描述符广播标志。Set socket文件描述符SO_BROADCAST可以使FD能发送广播报文;

    SO_OOBINLINE ---- 使FD发送的带外数据直接放置在报文中(默认情况下是通过MSG_OOB标识接收到另外一个缓冲区中);

    SO_RCVBUF ----- set or get FD的接收缓存区长度,如在卫星通信中,可能需要将缓存区长度扩大已接收超大报文;

             其实这些选项就相对于命令字,内核根据命令字对FD的内核结构进行相关操作。内核通过以下数据结构区分不同命令字的不同处理方法,并用全局链表串联起来:

    static LIST_HEAD(nf_sockopts);

    struct nf_sockopt_ops {
             structlist_head list;   -------------链表,用于串联不同的命令字
             u_int8_tpf;                  ------------ 协议类型,如PF_INET、PF_INET6等
             /*Non-inclusive ranges: use 0/0/NULL to never get called. */
             intset_optmin;           ------------set操作命令字最小值
             intset_optmax;          ----------- set操作命令字最大值
             int(*set)(struct sock *sk, int optval, void __user *user, unsigned int len);             ------set操作回调指针
    #ifdef CONFIG_COMPAT
             int(*compat_set)(struct sock *sk, int optval,
                                void__user *user, unsigned int len);
    #endif
             intget_optmin;                 ------------get操作命令字最小值
             intget_optmax;                ------------get操作命令字最大值
             int(*get)(struct sock *sk, int optval, void __user *user, int *len);                            ------get操作回调指针
    #ifdef CONFIG_COMPAT
             int(*compat_get)(struct sock *sk, int optval,
                                void__user *user, int *len);
    #endif
             /*Use the module struct to lock set/get code in place */
             structmodule *owner;                                                                              ------------此结构所属模块
    };

             不同的命令字初始化不同的上述结构,然后调用函数nf_register_sockopt()注册此结构,此函数先检测注册节点是否与链表中已有节点重叠:若重叠,则返回错误,注册失败;若不重叠,则挂接到链表中;

    nf_register_sockopt() 内核源码:

    /* Functions to register sockopt ranges(exclusive). */
    int nf_register_sockopt(structnf_sockopt_ops *reg)
    {
             structnf_sockopt_ops *ops;
             int ret = 0;
     
             mutex_lock(&nf_sockopt_mutex);
             list_for_each_entry(ops,&nf_sockopts, list) {   #检查是否重叠
                       if(ops->pf == reg->pf
                           && (overlap(ops->set_optmin,ops->set_optmax,
                                         reg->set_optmin,reg->set_optmax)
                                ||overlap(ops->get_optmin, ops->get_optmax,
                                            reg->get_optmin, reg->get_optmax))) {
                                NFDEBUG("nf_sockoverlap: %u-%u/%u-%u v %u-%u/%u-%u\n",
                                         ops->set_optmin,ops->set_optmax,
                                         ops->get_optmin,ops->get_optmax,
                                         reg->set_optmin,reg->set_optmax,
                                         reg->get_optmin,reg->get_optmax);
                                ret= -EBUSY;
                                gotoout;
                       }
             }
     
             list_add(&reg->list, &nf_sockopts);   #-----挂接链表
    out:
             mutex_unlock(&nf_sockopt_mutex);
             return ret;
    }

    3.   自定义sock选项

    有了上述理论,再结合前面博文(内核模块装载和卸载),我们就可以注册自己的sock选项用于内核态与用户态通信,模块代码如下:

    内核态module_sock.c

    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/init.h>
    #include <linux/types.h>
    #include <linux/netfilter_ipv4.h> /* for struct nf_sockopt_ops */
    #include <asm/uaccess.h>/* for copy_from_user */
    
    #define SOCKET_OPS_BASE          128  
    #define SOCKET_OPS_SET       (SOCKET_OPS_BASE)  
    #define SOCKET_OPS_GET      (SOCKET_OPS_BASE)  
    #define SOCKET_OPS_MAX       (SOCKET_OPS_BASE + 1) 
    
    static struct nf_sockopt_ops g_stSockOpt_Self;
    
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("zhaogang");  
    MODULE_DESCRIPTION("This is a simple example!/n");  
    MODULE_ALIAS("A simplest example");  
    
    static int sock_self_Recv(struct sock *sk, int optval, void __user *user, unsigned int len)
    {
        int iRet = 0;  
        char szMsg[1000] = {0, };
        printk(KERN_INFO "sockopt: sock_self_Recv()\n"); 
        
        if (optval == SOCKET_OPS_SET)  
        {     
            /* 从用户空间拷贝 */
            iRet = copy_from_user(szMsg, user, sizeof(szMsg));  
            printk("sock_self_Recv: szMsg = %s. ret = %d. success\n", szMsg, iRet);  
        }     
        return iRet;  
    }
    static int sock_self_Send(struct sock *sk, int optval, void __user *user, unsigned int len)
    {
        int iRet = 0;  
        printk(KERN_INFO "sockopt: send_msg()\n");
    
        char szSendBuf[1000] = "This is kernel msg";
        if (optval == SOCKET_OPS_GET)  
        {     
            iRet = copy_to_user(user, szSendBuf, strlen(szSendBuf));  
            printk("sock_self_Send: szSendBuf = %s. iRet = %d. success\n", szSendBuf, iRet);  
        }     
        return 0;   
    }
    
    static void sock_Self_Init(struct nf_sockopt_ops *pstSock)
    {
        memset(pstSock, 0, sizeof(struct nf_sockopt_ops));
        pstSock->pf = PF_INET;
        pstSock->set_optmax = SOCKET_OPS_MAX;
        pstSock->set_optmin = SOCKET_OPS_SET;
        pstSock->set = sock_self_Recv;
        pstSock->get_optmax = SOCKET_OPS_MAX;
        pstSock->get_optmin = SOCKET_OPS_GET;
        pstSock->get = sock_self_Send;
        pstSock->owner = THIS_MODULE;
    
        return;
    }
    
    static int __init test_init(void)
    {
        int iRet = 0;
        printk(KERN_ALERT "Linux cai ji.------start----\n");
        sock_Self_Init(&g_stSockOpt_Self);
        
        iRet = nf_register_sockopt(&g_stSockOpt_Self);
        return iRet;
    }
    static void __exit test_fini(void)
    {
        printk(KERN_ALERT "Linux cai ji.------end----\n");
    
        nf_unregister_sockopt(&g_stSockOpt_Self);  
        return;
    }
    module_init(test_init);
    module_exit(test_fini);
    
     

    用户态:在一个比较大的工程测试中,此处只贴出了一个使用命令字的函数:

     

    ULONG CV_Comm_Init()
    {
    	/* 开启TCP服务 */
    	struct sockaddr_in stAddr;
    	INT iFd = ZHAOGANG_INVALID_FD;
    	INT iRet;
    	ULONG ulRet = ERROR_SUCCESS;
    
    	/* socket */
    	iFd = socket(AF_INET, SOCK_STREAM, 0);
    	if (iFd < 0)
    	{
    		return ERROR_FAILED;
    	}
    	stAddr.sin_addr.s_addr = INADDR_ANY;
    	stAddr.sin_port = htons(SERVER_OPENCVD_TCP_PORT); /* 这里端口地址要统一定义 */
    	stAddr.sin_family = AF_INET;
    
    	/* bind & listen */
    	iRet = bind(iFd, (SockAddr_S *)&stAddr, sizeof(stAddr));
        iRet |= listen(iFd, 10);
    	if (iRet < 0)
    	{
    		return ERROR_FAILED;
    	}
    
        /* 内核命令字测试 */
        CHAR szKernelBuf[1000] = {0, }; 
        socklen_t stLen;
        (VOID)getsockopt(iFd, IPPROTO_IP, SOCKET_OPS_GET, szKernelBuf, &stLen); 
        CVD_TRACE("Get kernel cmdmsg: (%s) .", szKernelBuf);
        LIST_HEAD
        /* 设置非阻塞 */
        ulRet = UTIL_SetFdFlag(iFd, O_NONBLOCK | O_CLOEXEC);
        if (ERROR_SUCCESS != ulRet)
        {
            return ERROR_FAILED;
        }
        
        g_iCvTcpListenFd = iFd;
        ulRet = CV_Func_FdCtl(iFd, EPOLL_CTL_ADD, cv_Comm_ConnProc);
        if (ERROR_SUCCESS != ulRet)
        {
            close(g_iCvTcpListenFd);
            g_iCvTcpListenFd = ZHAOGANG_INVALID_FD;
        }
    
    	return ERROR_SUCCESS;
    }
    

    代码分析:

    1.      注册了SOCKET_OPS_SET、SOCKET_OPS_GET命令字,都是128此宏定义一般放在include目录的某个.h中,内核态与用户态用相同的宏定义;

    2.      Set操作:sock_self_Recv回调只是简单的将命令字所带的内容拷贝进内核,然后通过printk打印出来(用dmesg可以查看),其中函数:copy_from_user()用于从用户态拷贝内存;

    3.      Get操作:sock_self_Send回调也是简单的将字符串"Thisis kernel msg"拷贝到用户态,其中函数copy_to_user()用户将内核buffer拷贝到用户态空间;

    4.      默认装载时注册命令字:nf_register_sockopt,模块卸载时去注册命令字:nf_unregister_sockopt

    4.   Makefile文件

    如何编译、装载、卸载内核模块请参阅博文

    obj-m += module_sock.o  
    
    #generate the path  
    CURRENT_PATH:=$(shell pwd)
    
    #the current kernel version number  
    LINUX_KERNEL:=$(shell uname -r) 
    
    #the absolute path--根据获取的内核版本拼装绝对路径
    LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)  
    
    #complie object  
    all:  
    	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    #clean  
    clean:  
    	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean  
    
    

    5.   测试

    内核态:

     


    用户态:


    展开全文
  • 用户态与内核态用户态与内核态1、背景2、定义3、用户态访问内核态资源的方式3.1、系统调用3.2、库函数3.3、Shell脚本4、用户态到内核态怎样切换?参考 用户态与内核态 1、背景 当我们在写程序是,凡是涉及到IO读写、...

    用户态与内核态

    1、背景

    当我们在写程序是,凡是涉及到IO读写、内存分配等硬件资源的操作时,往往不能直接操作,而是通过一种叫系统调用的过程,让程序陷入到内核态运行,然后内核态的CPU执行有关硬件资源操作指令,得到相关的硬件资源后在返回到用户态继续执行,之间还要进行一系列的数据传输。

    假设没有这种内核态和用户态之分,程序随随便便就能访问硬件资源,比如说分配内存,程序能随意的读写所有的内存空间,如果程序员一不小心将不适当的内容写到了不该写的地方,就很可能导致系统崩溃。

    为什么要有用户态和内核态?

    由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 – 用户态和内核态。

    2、定义

    在这里插入图片描述
    系统调用将Linux整个体系分为用户态和内核态(或者说内核空间和用户空间),

    1、内核:本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。

    • cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。

    2、用户态:上层应用程序的活动空间,只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。

    • 应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用

    具体说明
    在这里插入图片描述
    从这个图上可以更进一步对内核所做的事有一个“全景式”的印象。主要表现为:向下控制硬件资源,向内管理操作系统资源:包括进程的调度和管理、内存的管理、文件系统的管理、设备驱动程序的管理以及网络资源的管理,向上则向应用程序提供系统调用的接口。从整体上来看,整个操作系统分为两层:用户态和内核态,这种分层的架构极大地提高了资源管理的可扩展性和灵活性,而且方便用户对资源的调用和集中式的管理,带来一定的安全性。

    3、用户态访问内核态资源的方式

    用户态的应用程序可以通过三种方式来访问内核态的资源:

    • 1)系统调用
    • 2)库函数
    • 3)Shell脚本

    3.1、系统调用

    • 系统调用是操作系统的最小功能单位,根据不同的应用场景,不同的Linux发行版本提供的系统调用数量也不尽相同,大致在240-350之间。系统调用组成了用户态跟内核态交互的基本接口。
      • 例如:用户态想要申请一块20K大小的动态内存,就需要brk系统调用,将数据段指针向下偏移,如果用户态多处申请20K动态内存,同时又释放呢?这个内存的管理就变得非常的复杂。

    我们可以把系统调用看成是一种不能再化简的操作(类似于原子操作,但是不同概念),有人把它比作一个汉字的一个“笔画”,而一个“汉字”就代表一个上层应用,因此,有时候如果要实现一个完整的汉字(给某个变量分配内存空间),就必须调用很多的系统调用

    3.2、库函数

    库函数就是屏蔽这些复杂的底层实现细节,减轻程序员的负担,从而更加关注上层的逻辑实现。它对系统调用进行封装,提供简单的基本接口给用户,这样增强了程序的灵活性,当然对于简单的接口,也可以直接使用系统调用访问资源,例如:open(),write(),read()等等。库函数根据不同的标准也有不同的版本,例如:glibc库,posix库等

    接着上面的系统调用继续说:

    • 系统调用过多,这势必会加重程序员的负担,良好的程序设计方法是:重视上层的业务逻辑操作,而尽可能避免底层复杂的实现细节。
    • 库函数正是为了将程序员从复杂的细节中解脱出来而提出的一种有效方法。它实现对系统调用的封装,将简单的业务逻辑接口呈现给用户,方便用户调用,从这个角度上看,库函数就像是组成汉字的“偏旁”。
      • “人”,对于复杂操作,我们借助于库函数来实现,如“仁”。显然,这样的库函数依据不同的标准也可以有不同的实现版本,如ISO C标准库,POSIX标准库等。

    3.3、Shell脚本

    Shell是一个特殊的应用程序,俗称命令行,本质上是一个命令解释器,它下通系统调用,上通各种应用,通常充当着一种“胶水”的角色,来连接各个小功能程序,让不同程序能够以一个清晰的接口协同工作,从而增强各个程序的功能。
    在这里插入图片描述

    • Shell 就是一个“中间商”,它在用户和内核之间“倒卖”数据,只是用户不知道罢了。
    • Shell 本身并不是内核的一部分,它只是站在内核的基础上编写的一个应用程序,它和 QQ、迅雷、Firefox等其它软件没有什么区别。然而Shell 也有着它的特殊性,就是开机立马启动,并呈现在用户面前;用户通过 Shell 来使用Linux,不启动 Shell 的话,用户就没办法使用 Linux。

    Shell 是如何连接用户和内核的?

    shell 能够接收用户输入的命令,并对命令进行处理,处理完毕后再将结果反馈给用户,比如输出到显示器、写入到文件等,这就是大部分读者对 Shell 的认知。

    你看,我一直都在使用 Shell,哪有使用内核哦?我也没有看到 Shell 将我和内核连接起来呀?

    其实,Shell 程序本身的功能是很弱的,比如文件操作、输入输出、进程管理等都得依赖内核。我们运行一个命令,大部分情况下 Shell 都会去调用内核暴露出来的接口,这就是在使用内核,只是这个过程被 Shell 隐藏了起来,它自己在背后默默进行,我们看不到而已。

    接口其实就是一个一个的函数,使用内核就是调用这些函数。这就是使用内核的全部内容了吗?嗯,是的!除了函数,你没有别的途径使用内核。

    4、用户态到内核态怎样切换?

    • Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态用户态
    • 运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。
    • 很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程
      • 比如C函数库中的内存分配函数malloc(),它具体是使用sbrk()系统调用来分配内存,当malloc调用sbrk()的时候就涉及一次从用户态到内核态的切换,
      • 类似的函数还有printf(),调用的是wirte()系统调用来输出字符串,等等。
        在这里插入图片描述
        U ——user(用户), K——kernal(内核)

    从用户态到内核态切换可以通过三种方式:

    • 系统调用:其实系统调用本身就是中断,但是软件中断,跟硬中断不同。
    • 异常: 当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常
    • 外设中断(硬中断):当外围设备完成用户的请求操作后,会像CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。

    注意:

    • 系统调用的本质其实也是中断,相对于外围设备的硬中断,这种中断称为软中断,这是操作系统为用户特别开放的一种中断,如Linux int 80h中断。
    • 从触发方式和效果上来看,这三种切换方式是完全一样的,都相当于是执行了一个中断响应的过程。但是从触发的对象来看,系统调用是进程主动请求切换的,而异常和硬中断则是被动的

    参考

    1、https://www.cnblogs.com/bakari/p/5520860.html
    2、https://zhuanlan.zhihu.com/p/69554144
    3、https://www.cnblogs.com/maxigang/p/9041080.html

    展开全文
  • linux内核态用户态通信方式

    万次阅读 2014-01-06 16:50:42
    下面对linux内核态用户态通信方式中的procfs进行讲解。 /proc主要存放内核的一些控制信息,所以这些信息大部分的逻辑位置位于内核控制的内存,在/proc下使用ls -l你会发现大部分的文件或者文件夹的大小都是0,...
  •  一个进程的内存映象由下面几部分组成:代码段、数据段、BSS段和堆栈段,以及内存映射的区域等部分,内存映射函数mmap(), 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的...
  • linux-3.10.36版本的内核相比linux-2.6.36版本中netlink的代码有所变化,以前的代码已经不能...netlink是linux内核的一套基于socket的通信机制,那么,只需要知道怎么创建套接字,发送数据,接收数据就行了。
  • Linux用户态内核态交互的几种方式

    千次阅读 2019-01-22 18:45:49
    创建于 2013-04-13 迁移自本人的百度空间 -------------------------------- 1/内核态-&... 在linux中,用户对设备的操作往往被抽象为对文件的操作。利用这一特性,可以通过注册和实现伪字符设备...
  • 作者:Godbach ...  本文的大纲如下: 一、基础知识 1. Netfilter ...三、一个实现接收内核态发送的IP Queue数据包的用户态例程 1. libipq.h 2. libipq.c 3. ipq_user.c 四、应用程序的测试 1.
  • • 在 Linux 2.4 版以后版本的内核中,几乎全部的...netlink 套接字的最大特点是对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函数
  • 本文介绍了如何通过两种方式实现一个系统调用的。一种是通过系统API函数的方式实现,另外一种通过软中断int0x80来实现,通过软...禹晓博+ 原创作品转载请注明出处 + 欢迎加入《Linux内核分析》MOOC网易云课堂学习yiw哦
  • Linux用户态内核态之间的交互

    千次阅读 2012-05-18 16:43:14
    Linux用户态内核态之间的交互: 读书笔记:  原文:《在 Linux用户空间与内核空间数据交换的方式》  链接:http://www.ibm.com/developerworks/cn/linux/l-kerns-usrs/  Netlink 是一种在内核用户应用...
  • Linux用户态内核态通信方法--netlink

    千次阅读 2019-06-18 10:38:00
    netlink 是 Linux 用户态内核态通信最常用的一种方式。 netlink:netlink socekt是一种用于在内核用户态进程之间进行数据传输的特殊的IPC。它通过为内核模块提供一组特殊的API,并为用户程序提供了一组标准的...
  • 现在的情况是,我建立这个Netfilter的机器,只用接收流量用作监控,不用对流量包做后续处理。...而且还要考虑到内核态用户态的并行处理:内核态接收数据包、传输给用户态用户态接受数据包,进行处理。
  • OpenWRT数据接收过程 这里使用的是ath9k网卡驱动,硬件平台是TP-link TL-WR841N V7.1 路由器
  • linux 内核网络,数据接收流程图

    千次阅读 2011-11-20 20:09:32
    4.3 数据接收流程图 各层主要函数以及位置功能说明:  1)sock_read:初始化msghdr{}的结构类型变量msg,并且将需要接收数据存放的地址传给msg.msg_iov->iov_base. net/socket.c  2)sock_recvmsg: 调用...
  •  功能Ø 实现一个并发的echo服务器程序,它(接收端)将接收到字符串进行转换处理后,返回给发送端;允许有多个发送端同时存在;Ø 字符串的处理包括:直接返回、全部转换为大写、全部转换为小写;处理方式应可以...
  • 一、Netlink概述 二、用户态如何使用netlink 三、内核态如何使用netlink 四、内核态用户态之间使用netlink...Netlink 是一种在内核用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket A
  • Linux 内核态 Socket 编程

    千次阅读 2015-04-01 16:18:49
    Linux 内核态 Socket 编程 分类: Socket2013-11-27 15:48 909人阅读 评论(0) 收藏 举报 1.内核态 socket API 内核态socket编程的过程和用户态下的socket编程流程一样,但是接口不同。Kernel...
  •  Netlink 是一种特殊的 socket,它是一种在内核用户间进行双向数据传输的一种方式,用户态应用使用标准的 socket API 就可以使用 Netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 Netlink。...
  • linux用户态内核态通信netlink

    千次阅读 2013-12-11 21:55:54
    1 引言 Linux中的进程间通信机制源自于Unix平台上的进程通信机制。Unix的两大分支AT&T Unix和BSD ...同时Linux也遵循IEEE制定的Posix IPC标准,在三者的基础之上实现了以下几种主要的IPC机制:管道(Pipe)及命名管
  • Linux内核二层数据包接收流程

    千次阅读 2013-08-24 23:15:55
    本文主要讲解了Linux内核二层数据包接收流程,使用的内核的版本是2.6.32.27 为了方便理解,本文采用整体流程图加伪代码的方式从内核高层面上梳理了二层数据包接收的流程,希望可以对大家有所帮助。阅读本文章假设...
  • 这是一篇学习笔记,主要是对《Linux 系统内核空间与用户空间通信的实现与分析》中的源码imp2的分析。其中的源码,可以到以下URL下载:  ...
  • Linux内核用户空间通信的方法

    千次阅读 2013-04-13 08:42:32
    Linux内核用户空间通信的方法(二)— 使用netlink   作者:Kendo 2006-9-3 这是一篇学习笔记,主要是对《Linux 系统内核空间与用户空间通信的实现与分析》中的源码imp2的...
  • 本文分析用户态接收到IP Queue的数据包后,根据数据包的相关信息决定数据包的下一步处理,并将处理后的数据包和处理的结果传递到内核态。文中如有任何疏漏和差错,欢迎各位朋友指正。    本文欢迎自由转载,但请...
  • 这是一篇学习笔记,主要是对《Linux 系统内核空间与用户空间通信的实现与分析》中的源码imp2的分析。其中的源码,可以到以下URL下载:http://www-128.ibm.com/developerworks/cn/linux/l-netlink/imp2.tar.gz参考...
  • Linux内核数据结构——链表

    千次阅读 2015-03-17 22:02:18
    Linux内核中的链表实现 offsetof container_of container_of 第一部分 container_of 第二部分 链表初始化 向链表中增加一个节点 删除节点 移动节点 判断链表是否为空 遍历链表 Demo测试 tlisth mlistc 执行结果简介...
  • Linux 内核态 Socket TCP 编程

    千次阅读 2016-04-22 10:57:12
    标签: linux c++ socket编程 c++ sock linux下socket c++  转自: ... ...1.内核态 socket API ...内核态socket编程的过程和用户态下的socket编程流程一样,但是接口不同。Kernel提供了一组

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 129,065
精华内容 51,626
关键字:

linux内核接收用户态数据

linux 订阅