Linux内核:用户空间与内核空间的数据传递的研究与实现

Gameisover

发布日期: 2019-02-13 09:16:49 浏览量: 331
评分:
star star star star star star star star star_border star_border
*转载请注明来自write-bug.com

摘 要

Linux是一个自由、开放源代码的类UNIX操作系统,目前为止Linux已经被移植到各种硬件平台,其支持的种类远远超出其他操作系统。Linux内核是以C语言写成,并符合POSIX标准的操作系统,其将内存分为“内核空间”和“用户空间”两部分,驱动程序和操作系统在内核空间运行,应用程序在用户空间运行,linux内核空间程序需要与用户空间程序进行数据交换。本文首先调查linux内核空间的分布情况,并将内核间与传统进程间的通信方式做一个对比,找到传统进程间的通信方式不能用于内核与用户空间通信的原因,接着分别介绍Linux内核空间与用户空间通信的八种通信方式:内核启动参数、模块参数、Sysctl、系统调用、Netlink、Procfs、Seq_file、Debugfs;然后,本文还将研究Linux内核的编译方法和内核程序的编写,以及Linux内核模块的运行环境,并将这八种通信方式的实现在Linux3.2.1内核版本中测试成功;最后,本文对比了这8种通信方式的优劣,并给出了相应的应用场景。

关键词: Linux;内核空间;用户空间;数据通信

Abstract

Linux is a free UNIX-like operating systems and open source, and the current Linux has been ported to a variety of computer hardware platform that supports the kind of far beyond any other operating system. Linux kernel is written in C and POSIX-compliant operating system, and its memory is divided into “kernel space” and “user space” , operating systems and drivers run in kernel space, the application runs in user space, linux kernel module and user module application program needs to exchange data. First, this paper will investigate the distribution of linux kernel space, and compare the communication between the kernel and the traditional way, find the reason of that traditional inter-process communication can not be used to communicate with the kernel space and the user, then we will introduce eight ways to communicate between Linux kernel space and user space: kernel boot parameter module parameters, Sysctl, system calls, Netlink, Procfs, Seq_file, Debugfs; next, this paper will also study the methods of build the Linux kernel, as well as Linux operating environment kernel modules, and communication methods to achieve the eight tested successfully in Linux3.2.1 kernel version; finally, we compare the advantages and disadvantages for the eight communication ways between linux kernel space and user space, also this paper presents the application scences for the eight ways.

Keywords: Linux; kernel space; user space; data communication

第一章 引言

1.1 Linux内核通信简介

Linux是一种自由、开放源代码的类UNIX操作系统,Linux内核最早是在芬兰赫尔辛基大学计算机系的Linus Torvalds于1991年10月5日发布[1],目前为止Linux内核已经被移植到各种硬件平台,其支持的种类远远超出其他操作系统。

Linux内核是用C语言写成的并符合POSIX标准的操作系统[2],从技术上说Linux内核并不是一个完整的操作系统,所以一般情况下Linux内核被打包成Linux发行版,比如Debian(及其派生版本Ubuntu),Fedora(及其相关版本Red Hat,CentOS)和openSUSE等,Linux发行版包含Linux内核和一些库,通常带有大量可以满足各种需求的应用程序。

由于内核本身有很大的局限性,比如在终端上不能打印,不能做大延时的处理等[3],所以当我们需要做这些工作的时候,就需要将在内核态收集到的数据传送到用户态进程中进行处理,来满足用户的需求,这样,内核空间与用户空间进程通信的方式就显得尤为重要。所以本文将调查研究linux内核空间与用户空间通信的八种通信方式:内核启动参数、模块参数、Sysctl、系统调用、Netlink、Procfs、Seq_file、Debugfs,并在Linux3.2.1内核版本中实现和测试这八种通信方式,并对这八种通信方式的优劣及应用场景作了对比总结。

1.2 本文的主要工作

为了调查研究Linux内核空间与用户空间的通信方式,本文首先将了解Linux内核对内存空间的划分情况;接着介绍Linux内核间通信与传统进程间的通信有何异同,包括传统的进程间通信为什么不能用于内核间通信;为了进行通信测试,本文还将研究Linux内核的编译方法和内核程序的编写,以及Linux内核模块的运行环境;本文最主要的内容是介绍八种通信方式原理及其实现,并全部在Linux3.2.1内核版本中测试成功,并且对这八种通信方式进行了研究对比,对比的内容主要包括每种通信相对其它通信方式的不足和优势,而正是这些不足和优势使得每一种通信方式都有其合适的应用场景,这些场景覆盖了Linux内核工作的方方面面,得以让我们能够使用如此优秀的Linux内核。

第二章 Linux内核概述

为了调查研究Linux内核空间与用户空间的通信方式,本章将介绍Linux内核环境,第一节将讲解Linux内核是如何把内存空间划分成内核空间和用户空间的,了解内核程序与用户程序的通信是跨越内存的,将比传统进程间通信要复杂的多;第二节将熟悉内核态与用户态的概念,以此来了解即便是运行在用户空间的用户态程序也可能在一定条件下转为内核态程序;第三节将介绍Linux内核模块的运行环境,并将解释传统进程间通信方式不能用于内核空间和用户空间通信的原因。

2.1 内核空间和用户空间

Linux采用了段页式存储管理方式,Linux的虚拟地址空间为0~4G,如图1,Linux内核将这4G空间分为两部分,0~3G(0xC0000000~0xFFFFFFFF)的部分为用户空间,供用户进程使用,3~4G(0x00000000~0xBFFFFFFF)的部分为内核空间,专门供内核使用[4]。

Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据,因为Linux使用的虚拟内存机制[5],用户空间的数据可能被换出,当内核空间使用用户空间指针时,对应的数据可能不在内存中。

2.2 内核态与用户态

目前广泛使用的x86架构CPU一共有0~3四个特权级,其中0级最高,3级最低。Linux内核使用了0级和3级两个特权级,运行在3级特权级上的程序被称为用户态程序,运行在0级特权级上的程序被称为内核态程序。内核态程序可以执行任何指令,用户态程序只可以执行非特权执行,用户态程序在系统调用、异常或外围设备的中断三种情况下可以切换到内核态下运行[6]。

2.3 Linux 内核模块的运行环境与传统进程间通信

在Linux操作系统的计算机中,CPU在任何时候都只会有如下四种状态:

  • 在处理一个硬中断

  • 在处理一个软中断

  • 处于内核态,即运行Linux内核

  • 出于用户态,即运行一个用户进程

其中,前三个在内核空间中运行,第四个在用户空间中运行[7]。

内核模块是一段具有独立功能的程序,其全称是动态可加载内核模块,它可以在系统运行时动态的在内核空间加载和卸载,加载进内核的程序便立刻在内核空间中工作起来。Linux内核模块有三种运行环境:用户上下文、硬中断和软中断。这三种运行环境都有其局限性,大致分以下两种,如表-1:

内核态环境 介绍 局限性
用户上下文 内核空间程序的运行和一个用户进程相关,因此内核中会有一个用户上下文环境。 不可以直接将本地变量传递给用户空间的内存区,因为内核空间和用户空间的内存映射机制不同。
硬中断和软中断环境 硬中断或软中断过程中代码的运行环境,比如 IP 数据报接收的代码运行环境等。 不可直接向用户空间内存区传递数据; 代码在运行过程中不可以阻塞。

Linux 传统的进程间通信有很多种,这里将介绍其中的一些传统进程间通信方式无法用于内核空间与用户空间通信的原因,如表-2:

通信方法 无法用于内核空间与用户空间通信的原因
管道 管道通信局限于父进程和子进程间的通信。
消息队列 消息队列在硬中断和软中断中不可以无阻塞地接收数据。
信号量 信号量无法在内核空间和用户空间之间使用。
内存共享 内存共享依赖信号量通信机制。
套接字 套接字在硬、软中断中不可以无阻塞地接收数据。

第三章 八种通信方式

本章将分成八节内容,每一节将介绍一种内核空间与用户空间的通信方式,每种通信方式的介绍包括技术理解、程序实现和测试结果截图三部分。

3.1 内核启动参数

内核开发者在用户空间可以通过 bootloader 向Linux内核传输启动参数数据,从而达到控制内核启动行为的目的[8]。

要在内核启动时分析启动参数,首先需要在内核中定义一个分析参数的函数,并使用内核提供的宏__setup把分析函数注册到内核中,该宏定义在 linux/init.h 头文件中,因此要使用它必须包含该头文件:

  1. __setup("para_name=", parse_func)

para_name 为内核启动参数的名字,parse_func函数为分析参数的函数,它负责把传递到内核的参数的值转换成内核变量的值并设置内核变量。

为了测试这种通信方式,首先需要在Linux内核源代码中加入一个专门用于分析内核启动参数的程序,附录一的程序代码便是本节测试使用的程序。这个例子测试了参数分别是一个整数、逗号分割的整数数组以及字符串三种情况的使用。为了避免与内核其他部分混淆,在Linux3.2.1内核源码的根目录下创建了一个新目录 examples,然后把附录一的程序拷贝到 examples 目录下并命名为 setup_example.c,另外为该目录创建一个 Makefile 文件:

  1. obj-y = setup_example.o

如果要把examples程序编译到内核中还需要修改源码根目录下的 Makefile文件的一行:

  1. core-y := usr/ examples/

按照内核构建的步骤构建新的内核并安装,设置Linux内核启动参数,只需要修改 /etc/default/grub 文件中的GRUB_CMDLINE_LINUX_DEFAULT配置项:

  1. GRUB_CMDLINE_LINUX_DEFAULT="quiet splash setup_example_int=1234 setup_example_int_array=100,200,300,400 setup_example_string=Thisisatest"

执行

  1. update-grub

命令更新系统启动参数并重新启动该内核,就可以使用下面的命令查看结果了:

  1. $ dmesg | grep setup_example
  2. setup_example_int=1234
  3. setup_example_int_array=100,200,300,400
  4. setup_example_int_array includes 4 intergers
  5. setup_example_string=Thisisates

内核启动参数测试结果如图:

3.2 模块参数和sysfs

上一节中介绍的用户空间与内核空间通信方式是直接编译内核来把程序添加到内核中的,另外也可以使用内核模块[9]的方式来向内核添加程序,这样就可以可以在内核运行时通过命令行在加载模块时传递参数,然后通过sysfs[10](一个基于内存的文件系统)来设置或读取模块数据,这是第二种介于内核空间与用户空间通信的方式。

​ 在模块中,我们声明了三个变量:

  1. static int my_invisible_int = 0;
  2. static int my_visible_int = 0;
  3. static char * mystring = "Hello, World";

通过宏 module_param 来声明这些变量,以告诉内核该如何处理这些变量:

  1. module_param(my_invisible_int, int, 0);
  2. module_param(my_visible_int, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
  3. module_param(mystring, charp, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);

module_param有三个参数,第一个是参数名,也即已经定义好的变量名,第二个参数则为参数类型,第三个参数用于指定该参数的访问权限,如果为 0,则该参数在 sysfs 文件系统中不被可见,访问权限可以是 S_IRUSR, S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH 和 S_IWOTH 的组合,它们分别是用户读,用户写,用户组读,用户组写,其他用户读和其他用户写,这和文件的访问权限设置是一致的。

下面是本节测试代码的运行结果:

  1. # insmod module_param.ko my_invisible_int=2 my_visible_int=3 mystring="welcome"
  2. # dmesg | grep my
  3. my_invisible_int = 2
  4. my_visible_int = 3
  5. mystring = 'welcome'
  6. # ls /sys/module/module_param/parameters/
  7. mystring my_visible_int
  8. # echo "change" > /sys/module/module_param/parameters/mystring
  9. # cat /sys/module/module_param/parameters/mystring
  10. Change

模块参数运行结果如图:

3.3 Sysctl

用户程序可以使用sysctl方式在内核运行的任何时候来获得或者设置内核配置参数,通常sysctl可操作的配置参数保存在/proc/sys目录下,用户程序也可以直接对这个目录中的文件进行读写来实现获取和设置内核配置的目的,例如,可以通过

  1. cat /proc/sys/net/ipv4/ip_forward

来获得内核IP层是否允许转发IP包的当前配置,可以通过

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

把内核 IP 层立即设置为允许转发 IP 包。一般地, Linux发行版会提供了一个系统工具 sysctl,它可以直接在命令行中设置和读取内核的配置,下面是使用 sysctl 工具来获取和设置内核配置的例子:

  1. # sysctl net.ipv4.ip_forward
  2. net.ipv4.ip_forward = 0
  3. # sysctl -w net.ipv4.ip_forward=1
  4. net.ipv4.ip_forward = 1
  5. # sysctl net.ipv4.ip_forward
  6. net.ipv4.ip_forward = 1

sysctl命令的参数 net.ipv4.ip_forward 实际上被转换为对应的 proc目录下的/proc/sys/net/ipv4/ip_forward文件,选项 -w 表示设置该内核配置的值,没有选项则表示读取该内核配置的值。

由于sysctl工具依赖proc文件系统,因此在没有 proc 文件系统的情况下,就不可以使用sysctl工具来进行工作了,这时可以使用内核提供的系统调用函数 sysctl 来读取或设置内核配置:

  1. int _sysctl(struct __sysctl_args *args );

该调用函数的参数类型为:

  1. struct __sysctl_args {
  2. int *name;
  3. int nlen;
  4. void *oldval;
  5. size_t *oldlenp;
  6. void *newval;
  7. size_t newlen;
  8. unsigned long __unused[4];
  9. };

name是一个数组,nlen表示该数组的长度,name数组的每一项表示目录的一段信息,比如

  1. int name[] = { CTL_NET, NET_IPV4, NET_IPV4_FORWARD };

该数组中,CTL_NET表示的是net目录,NET_IPV4表示ipv4,NET_IPV4_FORWARD表示ip_forward,更多定义可以在sysctl.h文件中查看;oldval、oldlenp表示旧值和旧值的大小,newval、newlen表示更新后的值及其所占内存的大小。

下面是使用sysctl系统调用设置ip_forward的结果:

  1. # cat /proc/sys/net/ipv4/ip_forward
  2. 1
  3. # ./ip_forward
  4. Input the ip_forword value(0 or 1):0
  5. The ip_forword old value is : 1
  6. The ip_forword new value is : 0
  7. # cat /proc/sys/net/ipv4/ip_forward
  8. 0

sysctl测试结果如图:

3.4 系统调用

系统调用在内核中实现,通过一定的方式提供用户使用,是内核提供给应用程序的接口,用户程序一般通过调用系统调用来对底层硬件进行操作。

一般用户程序工作在用户态,当用户程序需要使用系统资源的时候,便可以通过系统调用让自己进入内核态。系统调用其本质是内核为用户特别开放的一个中断,例如Linux内核就是使用的int 80h中断。

中断一般会有中断号和中断处理程序,不同的中断具有不同的中断号,不同的中断号又对应着不同的中断处理程序。内核维护着一个中断向量表,这个向量表的第n项包含了指向中断号为n的中断处理程序的指针。当中断到来的,cpu会暂停当前执行的代码,根据中断号在中断向量表中找到对应的中断处理程序并调用。中断处理程序执行完成之后,cpu会继续执行之前暂停的代码。

通常意义上,中断有两种类型,一种为硬件中断,另一种为软件中断。由于中断号是有限的,操作系统不会舍得用一个中断号来对应一个系统调用,而更倾向于用一个或少数几个中断号来对应所有的系统调用。比如Linux使用int0x80,Windows使用int0x2e。这样中断号只有一个,因此需要一个系统调用号来区分不同的系统调用,这个系统调用号通常就是系统调用在系统调用表中的位置,一般在执行int命令前会被放置在某个固定的寄存器里,对应的中断代码会取得这个系统调用号,并调用正确的函数,目前在Linux3.2.1内核中系统调用号已经用到了348,如果在增加一个系统调用函数,则可以使用349。

下面的例子便是通过在linux内核中自定义增加一个系统调用函数[11],然后通过用户程序调用,实现用户空间和内核空间的数据通信的。

首先在内核源码kernel目录下新加了一个内核程序mysyscall.c,内容如下:

  1. #include <linux/kernel.h>
  2. asmlinkage long sys_mysyscall(int number,char *str){
  3. printk(KERN_ALERT "Mysyscall the args is : %d,%s\n",number,str);
  4. return number;
  5. }

所做的事情就是在内核中输出用户程序传过来的参数,并返回用户的一个参数。然后就是在kernel/Makefile中添加一行:

  1. obj-y += mysyscall.o

在头文件include/linux/syscalls.h中加入:

  1. asmlinkage long sys_mysyscall(int number,char *str);

在arch/x86/kernel/syscall_table_32.S中加入:

  1. .long sys_mysyscall

在arch/x86/ia32/ia32entry.S中加入:

  1. .quad sys_mysyscall

为系统调用程序分配一个系统调用号,在arch/x86/include/asm/unistd_32.h中设置:

  1. #define __NR_mysyscall 349
  2. #define NR_syscalls 350

__NR_mysyscall就是这个系统调用程序的系统调用号,NR_syscalls是系统调用函数的总数。

编译内核并安装重启之后就可以测试了,下面是测试函数:

  1. int main(){
  2. char *str = "message from user-space!";
  3. printf("%ld\n", syscall(349,20,str));
  4. return 0;
  5. }

测试函数syscall的第一个参数是系统调用号,后面的参数便是系统调用函数的参数,测试结果如下:

  1. $ ./syscall
  2. 20
  3. $ dmesg | grep Mysyscall
  4. Mysyscall the args is : 20,message from user-space!

系统调用测试结果如图:

netlink是一种介于在内核空间与用户空间之间进行数据传输的特殊的通信方式。它通过为为用户程序提供了一组标准的socket 接口,并为内核模块提供一组特殊的API的方式,实现了全双工的通讯连接[12][13]。类似于TCP/IP使用AF_INET地址族,netlink socket使用另一种地址族AF_NETLINK。netlink socket在内核头文件

  1. include/linux/netlink.h

中定义自己的协议类型。

Netlink Socket 用户程序使用标准的socket API函数:

  1. socket(), sendmsg(), recvmsg()和close()

使用socket()函数创建一个socket:

  1. int socket(int domain, int type, int protocol)

domain参数是socket域(地址族),在netlink应用中设置为AF_NETLINK,

type参数为数据包类型,这里设置为SOCK_RAW或者SOCK_DGRAM。

protocol参数是一个表示协议类型的整数,这里使用了自定义的协议类型:

#define NETLINK_TEST 25

在用户空间,可以通过调用socket()来创建一个socket,但是在内核空间,则必须使用内核提供的netlink_kernel_create函数来创建,需要注意的是这个函数在Linux2.6版本的内核中变化是非常大的,这里介绍从Linux2.6.37内核开始到Linux3.2.1都在使用的函数原型:

  1. struct sock *netlink_kernel_create(struct net *net, int unit,
  2. unsigned int groups, void (*input)(struct sk_buff *skb),
  3. struct mutex *cb_mutex, struct module *module);

其中struct net参数是一个网络命名空间namespace,在这里使用init_net这个全局变量;unit参数表示一个整数表示的netlink协议类型,这里使用前文自定义的 NETLINK_MYTEST;input参数为内核模块中定义的netlink消息处理函数,函数指针input的skb参数实际上就是netlink_kernel_create函数返回的 struct sock指针。

从内核中发送netlink消息使用:

  1. int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock);

ssk参数是由 netlink_kernel_create()函数返回的netlink socket, skb是需要发送的消息内容,noblock参数设置当接收缓冲区不可用时是阻塞还是立即返回一个失败信息。

从内核空间关闭netlink socket:

netlink_kernel_create()函数返回的netlink socket为struct sock *nl_sk,我们可以通过访问下面的API来从内核空间关闭这个netlink socket:

  1. sock_release(nl_sk->socket);

附录二中的程序代码是关于NetLink的一个测试的例子,下面是这个例子的测试结果:

  1. # insmod ./netlink_kern.ko
  2. # ./client
  3. state_smg
  4. waiting received!
  5. Received message: hello i am kernel
  6. # rmmod netlink_kern
  7. # dmesg | tail -n 4
  8. my_net_link_3: create netlink socket ok.
  9. Message received:Hello you!
  10. my_net_link:send message 'hello i am kernel'.
  11. my_net_link: self module exited

NetLink测试结果如图:

3.6 Procfs

procfs是一种比较老的数据交换方式[14],内核中的很多数据都是通过procfs出口给用户的,另外内核中的很多配置参数也是通过procfs来让用户设置的[15]。除了sysctl出口到/proc下的配置参数,procfs提供的大部分内核配置参数是只读的。

Procfs提供了如下API:

  1. struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent);

该函数返回一个普通的proc条目,name参数是该proc条目的名称,mode参数设置了该proc条目的访问权限, parent参数则是该proc条目所在的目录,当然如果要在/proc目录下建立proc条目,parent应当设为NULL。否则parent应当是proc_mkdir函数返回的struct proc_dir_entry结构的指针。

  1. extern void remove_proc_entry(const char *name, struct proc_dir_entry *parent)

该函数用于删除proc条目,name参数是要删除的proc条目的名称,parent参数是proc条目所在的目录。

  1. struct proc_dir_entry *proc_mkdir(const char * name, struct proc_dir_entry *parent)

该函数用于创建proc目录,name参数是proc目录的名称,parent参数是proc目录所在的目录。

为了使用这些函数接口和结构struct proc_dir_entry,需要在模块中包含头文件linux/proc_fs.h。

struct proc_dir_entry结构体中需要设置两个函数,分别用于读取用户数据和写用户数据,两个函数原型为:

  1. int procfs_read( char *page, char **start, off_t off,
  2. int count, int *eof, void *data );
  3. ssize_t procfs_write( struct file *filp, const char __user *buff,
  4. unsigned long len, void *data );

下面是测试procfs的一个例子,用户程序client接收用户输入的字符串,通过procfs传递给内核模块transform,而transform模块做的事情是把传进来的字符串进行大小写转换,并再由procfs传回client用户程序,下面是测试结果输出:

  1. $ sudo insmod ./transform.ko
  2. $ ./client
  3. user input :abc
  4. kern output:ABC
  5. user input :Hello
  6. kern output:hELLO
  7. user input :quit

procfs测试结果如图:

3.7 seq_file

内核在通过procfs来向用户空间输出信息的时候,由于 procfs 的一个缺陷,对于内容大于1个内存页(通常为4KB)的数据需要多次读,因此处理起来比较麻烦。因此 Alexander Viro基于procfs方式又实现了一套新的功能,这便是seq_file,这种通信方式使得内核输出大文件信息非常容易。

seq_file的所有功能均定义在头文件linux/seq_file.h中,使用seq_file需要定义并设置一个seq_operations结构(类似于file_operations结构):

  1. struct seq_operations {
  2. void* (*start) (struct seq_file *m, loff_t *pos);
  3. void (*stop) (struct seq_file *m, void *v);
  4. void* (*next) (struct seq_file *m, void *v, loff_t *pos);
  5. int (*show) (struct seq_file *m, void *v);
  6. };

start函数指定seq_file文件的读开始位置,并返回实际的读开始位置;next函数将当前读位置移动到下一个可读位置,如果到达文件末尾,则返回NULL,否则返回实际的下一个读位置;stop函数会在读完seq_file文件后被调用,它与文件操作的close函数有些类似,可以做些最后的清理工作,比如释放先前动态分配的内存等;show函数用于输出格式化文本,成功返回0,否则返回错误码。

在设置好结构struct seq_operations之后,还需要使用seq_open函数将该结构与对应的struct file结构关联起来,例如,附录三中的struct seq_operations定义为:

  1. struct seq_operations exam_seq_ops = {
  2. .start = exam_seq_start,
  3. .stop = exam_seq_stop,
  4. .next = exam_seq_next,
  5. .show = exam_seq_show
  6. };

对应的open函数的定义:

  1. static int exam_seq_open(struct inode *inode, struct file *file){
  2. return seq_open(file, &exam_seq_ops);
  3. };

设置struct file_operations结构:

  1. struct file_operations exam_seq_file_ops = {
  2. .owner = THIS_MODULE,
  3. .open = exam_seq_open,
  4. .read = seq_read,
  5. .llseek = seq_lseek,
  6. .release = seq_release
  7. };

除了open函数,其它的都是seq_file提供的函数,在这里直接使用就可以了。

然后,创建一个/proc条目并把它的文件操作函数绑定到exam_seq_file_ops:

  1. struct proc_dir_entry *entry;
  2. entry = create_proc_entry("exam_seq_file", 0, NULL);
  3. if (entry)
  4. entry->proc_fops = &exam_seq_file_ops;

接下来就可以使用了,附录三中的内核模块将初始化一个链表,这个链表所保存的数据量大于一个内存页的大小,最后通过seq_file一次性将这些数据全部传递到用户空间中:

  1. for (i=0;i<500;i++){
  2. mydrv_new = kmalloc(sizeof(struct _mydrv_struct), GFP_ATOMIC);
  3. sprintf(mydrv_new->info, "Node No: %d\n", i);
  4. list_add_tail(&mydrv_new->list, &mydrv_list);
  5. }

链表总共有500个节点,每个节点的数据大小为11到13个字节不等,这样一次性输出到用户的数据大约有6KB,大于一个内存页的大小了,用户空间直接使用cat命令查看输出结果:

  1. # insmod seqfile_exam.ko
  2. # cat /proc/seqfile_exam
  3. Node No: 0
  4. Node No: 1
  5. ...
  6. Node No: 498
  7. Node No: 499

3.8 debugfs

内核开发者在开发过程中经常需要向用户空间输出调试信息,printk函数虽然可以在内核中输出信息,但它并不是最好的,因为调试信息只在开发中需要,而printk将会一直输出,因此在开发完毕后将需要清除不必要的printk语句,无疑这将给开发人员带来无穷的工作量,而且如果开发者希望在用户空间就能够改变内核行为时,printk便一点用处都没有了。

Greg Kroah-Hartman在Linux2.6.11引入了debugfs,它是一个非常小的虚拟文件系统,专门供开发人员输出内核调试信息使用,并可以在编译内核时选择是否构建到内核中,如果该文件系统没有被构建内核时,使用它提供的API的内核代码不需要做任何改动。

下面函数分别用于在debugfs文件系统下创建目录和文件:

  1. struct dentry *debugfs_create_dir(const char *name, struct dentry *parent);
  2. struct dentry *debugfs_create_file(const char *name, mode_t mode, struct dentry *parent, void *data, struct file_operations *fops);

fops参数为进行文件操作的file_operations结构指针,这和seq_file提供的文件操作是完全一样的,当然,在一些情况下,仅需要使用一些简单并可以控制的变量来调试就够了,debugfs提供了4个这样的API来操作简单的数据类型:

  1. struct dentry *debugfs_create_u8(const char *name, mode_t mode, struct dentry *parent, u8 *value);
  2. struct dentry *debugfs_create_u16(const char *name, mode_t mode, struct dentry *parent, u16 *value);
  3. struct dentry *debugfs_create_u32(const char *name, mode_t mode, struct dentry *parent, u32 *value);
  4. struct dentry *debugfs_create_bool(const char *name, mode_t mode, struct dentry *parent, u32 *value);

name和mode参数分别指定文件名和访问权限,value参数是可以由用户程序控制的内核变量指针。

下面给出了一个使用debufs的示例:

  1. # mkdir -p /debugfs
  2. # mount -t debugfs debugfs /debugfs
  3. # insmod ./debugfs_exam.ko
  4. # ls /debugfs
  5. debugfs-exam
  6. # ls /debugfs/debugfs-exam
  7. bool-var u16-var u32-var u8-var
  8. # cd /debugfs/debugfs-exam
  9. # cat u8_var
  10. 0
  11. # echo 200 > u8_var
  12. # cat u8_var
  13. 200
  14. # cat bool_var
  15. N
  16. # echo 1 > bool_var
  17. # cat bool_var
  18. Y

第四章 八种通信方式的优劣对比

本文详细地讲解了八种用户空间与内核空间的数据交换方式,这八种方式各有优劣,下面就每种方式的优劣做一下对比:

方式 不足 优势
内核启动参数 传递数据是单向的,只能向内核传递。且可以传递的数据量非常小。 可以在启动内核的时候就进行传递数据,可以改变内核的启动方式。
模块参数和sysfs 可以传递的数据量非常小,动态传参需要依赖sysfs文件系统。 可以在内核运行时传递数据,并可以动态的进行更改。
Sysctl 传递数据量非常小,需要了解内核每个配置文件所在procfs文件系统中的路径信息。 可以在内核运行的任何时刻来改变内核的配置参数,也可以在任何时候获得内核的配置参数。
系统调用 添加额外的系统调用函数需要重新编译内核。 只需要调用系统调用函数即可进行数据传递。
Netlink 随着Linux内核版本的升级,内核态的netlink接口变化速度太快,版本相关性很高。 用户态使用netlink的方式类似于socket的使用方式,可以支持大量的数据传输,netlink是一种异步通信机制,且支持多播。
Procfs 依赖procfs文件系统,一般对于大于一个内存页(通常为4KB)的数据大小的传输会比较麻烦。 使用procfs文件系统,可以方便的通过操作文件的方式进行数据传输。
seq_file 依赖procfs,且传递数据是单向的,仅用于内核向用户空间传输大量的数据。 对procfs传递大量数据的不足进行了改进,使得内核输出大文件信息更容易。
debugfs 依赖debugfs文件系统,仅用于简单类型的变量处理。 专门设计用来内核开发调试使用,方便内核开发者向用户空间输出调试信息。

其中内核启动参数、模块参数和sysfs、Sysctl三种方式主要用于向内核传递配置信息,这些配置信息将很可能改变内核的行为或状态,对于这三种方式,Sysctl方式最是简便,不仅提供了Sysctl这个内核接口以便在程序中调用,而且还提供了sysctl命令以便在命令行中直接执行,因此,修改内核配置信息则首选sysctl方式。

系统调用方式则是内核中实现的提供给用户使用的内核接口,如果用户程序需要使用系统资源时,便可以通过系统调用的方式让自己进入内核态,由于内核已经提供了足够多的接口,所以我们一般只使用内核提供的就够了,如果需要自己定义一个系统调用函数,则需要编写内核程序和重新编译内核了。

Procfs、Seq_file、Debugfs三种方式都是基于文件系统的,因此这三种方式在用户空间的操作接口就是文件操作接口,而且在内核中的接口也比较类似。

Netlink方式则是用途最多的一种方式,由于其不需要重新编译内核,因此要比内核启动参数和系统调用方式要方便许多。Netlink工作方式非常类似socket的工作方式,因此可传输的数据量大小要比其他方式多得多。Netlink是一种异步通信方式,所以不需要等待接收者收到消息。由于Netlink方式拥有很多优点,所以其应用场景也是很多的,比如路由daemon、防火墙、socket监视等等。

第五章 总结

本文调查研究了八种Linux内核空间与用户空间的通信方式,简要介绍了每种通信方式的工作原理,并对每种通信方式用一段简单的程序代码测试其可行性,最后对这八种方式做了一个对比,分析每种方式的优劣,并给出了相应的应用场景。随着Linux内核版本的不断更新,Linux内核空间与用户空间的通信方式也在不断的衍变,因此不断学习是一个内核开发者所必备的素质。

参考文献

[1] Torvalds L B. Free minix-like kernel sources for 386-AT[N/OL].http://groups.google.com/group/comp.os.minix/msg/2194d253268b0a1b?pli=1.1991-10-5 [2014-5-8].

[2] [美]Michael Beck. Linux内核编程指南[M]. 第三版. 清华大学出版社,2004.

[3] [美]Danile P Bovet,Marco Cesati,陈莉君. 深入理解LINUX内核[M].北京:中国电力出版社,2004.

[4] 董昱, 马鑫. 基于netlink机制内核空间与用户空间通信的分析[J]. 测控技术, 2007(09):57-58+60.

[5] 张磊,刘艳霞. Linux的虚拟内存管理和Cache机制探析[J]. 焦作大学报, 2004(04).

[6] 陈浩. Linux下用户态和内核态内存共享的实现[J]. 电脑编程技巧与维护, 2011(4).

[7] 康望星, 马光胜, 黄烨明, 等. 嵌入式Linux的中断处理技术研究[J]. 信息技术, 2005(8).

上传的附件 cloud_download Linux内核:用户空间与内核空间的数据传递的研究与实现.zip ( 8.48mb, 1次下载 )
error_outline 下载需要7点积分

发送私信

修行的路总是孤独的,因为智慧必然来自孤独

8
文章数
11
评论数
最近文章
eject