linux驱动开发总结(一)
基础性总结
1, linux驱动通常分为3大类:
* 字符设备
* 块设备
* 网络设备javascript
2, 开发环境构建:
* 交叉工具链构建
* NFS和tftp服务器安装php
3, 驱动开发中设计到的硬件:
* 数字电路知识
* ARM硬件知识
* 熟练使用万用表和示波器
* 看懂芯片手册和原理图 css
4, linux内核源代码目录结构:
* arch/: arch子目录包括了全部和体系结构相关的核心代码。它的每个子目录都表明一种支持的体系结构,例如i386就是关于intel cpu及与之相兼容体系结构的子目录。
* block/: 部分块设备驱动程序;
* crypto: 经常使用加密和散列算法(如AES、SHA等),还有一些压缩和CRC校验算法;
* documentation/: 文档目录,没有内核代码,只是一套有用的文档;
* drivers/: 放置系统全部的设备驱动程序;每种驱动程序又各占用一个子目录:如,/block 下为块设备驱动程序,好比ide(ide.c)。若是你但愿查看全部可能包含文件系统的设备是如何初始化的,你能够看 drivers/block/genhd.c中的device_setup()。
* fs/: 全部的文件系统代码和各类类型的文件操做代码,它的每个子目录支持一个文件系统, 例如fat和ext2;
* include/: include子目录包括编译核心所须要的大部分头文件。与平台无关的头文件在 include/linux子目录下,与 intel cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关scsi设备的头文件目录;
* init/: 这个目录包含核心的初始化代码(注:不是系统的引导代码),包含两个文件main.c和Version.c,这是研究核心如何工做的好的起点之一;
* ipc/: 这个目录包含核心的进程间通信的代码;
* kernel/: 主要的核心代码,此目录下的文件实现了大多数linux系统的内核函数,其中最重要的文件当属sched.c;一样,和体系结构相关的代码在arch/i386/kernel下;
* lib/: 放置核心的库代码;
* mm/:这个目录包括全部独立于 cpu 体系结构的内存管理代码,如页式存储管理内存的分配和释放等;而和体系结构相关的内存管理代码则位于arch/i386/mm/下;
* net/: 核心与网络相关的代码;
* scripts/: 描述文件,脚本,用于对核心的配置;
* security: 主要是一个SELinux的模块;
* sound: 经常使用音频设备的驱动程序等;
* usr: 实现了用于打包和压缩的cpio; html
5, 内核的五个子系统:
* 进程调试(SCHED)
* 内存管理(MM)
* 虚拟文件系统(VFS)
* 网络接口(NET)
* 进程间通讯(IPC)java
6, linux内核的编译:
* 配置内核:make menuconfig,使用后会生成一个.confiig配置文件,记录哪些部分被编译入内核,哪些部分被编译成内核模块。
* 编译内核和模块的方法:make zImage
Make modules
* 执行完上述命令后,在arch/arm/boot/目录下获得压缩的内核映像zImage,在内核各对应目录获得选中的内核模块。node
7, 在linux内核中增长程序
(直接编译进内核)要完成如下3项工做:
* 将编写的源代码拷入linux内核源代码相应目录
* 在目录的Kconifg文件中增长关于新源代码对应项目的编译配置选项
* 在目录的Makefile文件中增长对新源代码的编译条目linux
8, linux下C编程的特色:
内核下的Documentation/CodingStyle描述了linux内核对编码风格的要求。具体要求不一一列举,如下是要注意的:
* 代码中空格的应用
* 当前函数名:
GNU C预约义了两个标志符保存当前函数的名字,__FUNCTION__
保存函数在源码中的名字,__PRETTY_FUNCTION__
保存带语言特点的名字。
因为C99已经支持__func__
宏,在linux编程中应该不要使用__FUNCTION__
,应该使用__func__
。
*内建函数:不属于库函数的其余内建函数的命名一般以__builtin
开始。 程序员
9,内核模块
内核模块主要由以下几部分组成:
(1) 模块加载函数
(2) 模块卸载函数
(3) 模块许可证声明(经常使用的有Dual BSD/GPL,GPL,等)
(4) 模块参数(可选)它指的是模块被加载的时候能够传递给它的值,它自己对应模块内部的全局变量。例如P88页中讲到的一个带模块参数的例子:
insmod book.ko book_name=”GOOD BOOK” num=5000
(5) 模块导出符号(可选)导出的符号能够被其余模块使用,在使用以前只需声明一下。
(6) 模块做者等声明信息(可选)
如下是一个典型的内核模块: web
注意:标有__init的函数在连接的时候都放在.init.text段,在.initcall.init中还保存了一份函数指针,初始化的时候内核会经过这些函数指针调用__init函数,在初始化完成后释放init区段。
模块编译经常使用模版: 正则表达式
KVERS = $(shell uname -r)
注意要指明内核版本,而且内核版本要匹配——编译模块使用的内核版本要和模块欲加载到的那个内核版本要一致。
模块中常用的命令:
insmod,lsmod,rmmod
系统调用:
int open(const char *pathname,int flags,mode_t mode);
flag表示文件打开标志,如:O_RDONLY
mode表示文件访问权限,如:S_IRUSR(用户可读),S_IRWXG(组能够读、写、执行)
10,linux文件系统与设备驱动的关系
应用程序和VFS之间的接口是系统调用,而VFS与磁盘文件系统以及普通设备之间的接口是file_operation结构体成员函数。
两个重要的函数:
(1)struct file结构体定义在/linux/include/linux/fs.h(Linux 2.6.11内核)中定义。文件结构体表明一个打开的文件,系统中每一个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时建立,并传递给在文件上进行操做的任何函数。在文件的全部实例都关闭后,内核释放这个数据结构。在内核建立和驱动源码中,struct file的指针一般被命名为file或filp。
在驱动开发中,文件读/写模式mode、标志f_flags都是设备驱动关心的内容,而私有数据指针private_data在驱动中被普遍使用,大多被指向设备驱动自定义的用于描述设备的结构体。驱动程序中经常使用以下相似的代码来检测用户打开文件的读写方式:
if (file->f_mode & FMODE_WRITE)
下面的代码可用于判断以阻塞仍是非阻塞方式打开设备文件:
if (file->f_flags & O_NONBLOCK)
(2)struct inode结构体定义在linux/fs.h中
11,devfs、sysfs、udev三者的关系:
(1)devfs
linux下有专门的文件系统用来对设备进行管理,devfs和sysfs就是其中两种。在2.4内核4一直使用的是devfs,devfs挂载于/dev目录下,提供了一种相似于文件的方法来管理位于/dev目录下的全部设备,咱们知道/dev目录下的每个文件都对应的是一个设备,至于当前该设备存在与否先且不论,并且这些特殊文件是位于根文件系统上的,在制做文件系统的时候咱们就已经创建了这些设备文件,所以经过操做这些特殊文件,能够实现与内核进行交互。可是devfs文件系统有一些缺点,例如:不肯定的设备映射,有时一个设备映射的设备文件可能不一样,例如个人U盘可能对应sda有可能对应sdb;没有足够的主/次设备号,当设备过多的时候,显然这会成为一个问题;/dev目录下文件太多并且不能表示当前系统上的实际设备;命名不够灵活,不能任意指定等等。
(2)sysfs
正由于上述这些问题的存在,在linux2.6内核之后,引入了一个新的文件系统sysfs,它挂载于/sys目录下,跟devfs同样它也是一个虚拟文件系统,也是用来对系统的设备进行管理的,它把实际链接到系统上的设备和总线组织成一个分级的文件,用户空间的程序一样能够利用这些信息以实现和内核的交互,该文件系统是当前系统上实际设备树的一个直观反应,它是经过kobject子系统来创建这个信息的,当一个kobject被建立的时候,对应的文件和目录也就被建立了,位于/sys下的相关目录下,既然每一个设备在sysfs中都有惟一对应的目录,那么也就能够被用户空间读写了。用户空间的工具udev就是利用了sysfs提供的信息来实现全部devfs的功能的,但不一样的是udev运行在用户空间中,而devfs却运行在内核空间,并且udev不存在devfs那些先天的缺陷。
(3)udev
udev是一种工具,它可以根据系统中的硬件设备的情况动态更新设备文件,包括设备文件的建立,删除等。设备文件一般放在/dev目录下,使用udev后,在/dev下面只包含系统中真实存在的设备。它于硬件平台无关的,位于用户空间,须要内核sysfs和tmpfs的支持,sysfs为udev提供设备入口和uevent通道,tmpfs为udev设备文件提供存放空间。
12,linux设备模型:

在linux内核中,分别使用bus_type
,device_driver
,device
来描述总线、驱动和设备,这3个结构体定义于include/linux/device.h头文件中。驱动和设备正是经过bus_type
中的match()
函数来配对的。
13, 重要结构体解析
(1)cdev结构体
在linux2.6内核中,使用cdev结构体描述一个字符设备,定义以下:
struct cdev{
struct kobject kobj;
(2)file_operations结构体
结构体file_operations在头文件linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各类操做的函数的指针。这些函数实际会在应用程序进行linux的open(),write(),read(),close()等系统调用时最终被调用。该结构体的每一个域都对应着驱动内核模块用来处理某个被请求的事务的函数地址。源代码(2.6.28.7)以下:
struct file_operations{
struct module*owner;
loff_t (*llseek)(struct file*,loff_t,int);
ssize_t (*read)(struct file*,char__user*,size_t,loff_t*); ssize_t (*write)(struct file*,constchar__user*,size_t,loff_t*); ssize_t (*aio_read)(struct kiocb*,cons tstruct iovec*,unsigned long,loff_t); ssize_t (*aio_write)(struct kiocb*,const struct iovec*,unsigned long,loff_t); int (*readdir)(struct file*,void*,filldir_t); unsigned int (*poll)(struct file*,struct poll_table_struct*); int (*ioctl)(struc inode*,struct file*,unsigned int,unsigned long); long (*unlocked_ioctl)(struct file*,unsigned int,unsigned long); long (*compat_ioctl)(struct file*,unsigned int,unsigned long); int (*mmap)(struct file*,struct vm_area_struct*); int (*open)(struct inode*,struct file*); int (*flush)(struct file*,fl_owner_t id); int (*release)(struct inode*,struct file*); int (*fsync)(struct file*,struct dentry*,int datasync); int (*aio_fsync)(struct kiocb*,int datasync); in (*fasync)(int,struct file*,int); int (*lock)(struct file*,int,struct file_lock*); ssize_t (*sendpage)(struct file*,struct page*,int,size_t,loff_t*,int); unsigned long (*get_unmapped_area)(struct file*,unsigned long,unsigned long,unsigned long,unsigned long); in t(*check_flags)(int); int (*dir_notify)(structfile*filp,unsignedlongarg); int (*flock)(structfile*,int,structfile_lock*); ssize_t (*splice_write)(struct pipe_inode_info*,struct file*,loff_t*,size_t,unsig ned int); ssize_t (*splice_read)(struct file*,loff_t*,struct pipe_inode_info*,size_t,unsigned int); int(*setlease)(struct file*,long,struct file_lock**); };
解析:
struct module*owner;
14, 字符设备驱动程序设计基础
主设备号和次设备号(两者一块儿为设备号):
一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操做的是哪一个设备,用来区分同类型的设备。
linux内核中,设备号用dev_t来描述,2.6.28中定义以下:
typedef u_long dev_t;
在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。
能够使用下列宏从dev_t中得到主次设备号:也能够使用下列宏经过主次设备号生成dev_t:
MAJOR(dev_tdev);
MKDEV(intmajor,intminor);
MINOR(dev_tdev);
分配设备号(两种方法):
(1)静态申请:
int register_chrdev_region(dev_t from,unsigned count,const char *name);
(2)动态分配:
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);
注销设备号:
void unregister_chrdev_region(dev_t from,unsigned count);
建立设备文件:
利用cat/proc/devices查看申请到的设备名,设备号。
(1)使用mknod手工建立:mknod filename type major minor
(2)自动建立;
利用udev(mdev)来实现设备文件的自动建立,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备建立一个class,再为每一个设备调用device_create建立对应的设备。
15, 字符设备驱动程序设计
设备注册:
字符设备的注册分为三个步骤:
(1)分配
cdev:struct cdev *cdev_alloc(void);
(2)初始化
cdev:void cdev_init(struct cdev *cdev,const struct file_operations *fops);
(3)添加
cdev:int cdev_add(struct cdev *p,dev_t dev,unsigned count)
设备操做的实现:
file_operations函数集的实现。
struct file_operations xxx_ops={
.owner=THIS_MODULE,
.llseek=xxx_llseek,
.read=xxx_read,
.write=xxx_write,
.ioctl=xxx_ioctl, .open=xxx_open, .release=xxx_release, … }
特别注意:驱动程序应用程序的数据交换:
驱动程序和应用程序的数据交换是很是重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。经过数据交换,驱动程序和应用程序能够彼此了解对方的状况。可是驱动程序和应用程序属于不一样的地址空间。驱动程序不能直接访问应用程序的地址空间;一样应用程序也不能直接访问驱动程序的地址空间,不然会破坏彼此空间中的数据,从而形成系统崩溃,或者数据损坏。安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:
unsigned long copy_to_user(void__user *to,const void *from,unsigned long n); unsigned long copy_from_user(void *to,constvoid __user *from,unsigned long n); put_user(local,user); get_user(local,user);
设备注销:
void cdev_del(struct cdev *p);
16,ioctl函数说明
ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数以下:
int ioctl(int fd,ind cmd,…);
其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,后面的省略号是一些补充参数,有或没有是和cmd的意义相关的。
ioctl函数是文件结构中的一个属性份量,就是说若是你的驱动程序提供了对ioctl的支持,用户就能够在用户程序中使用ioctl函数控制设备的I/O通道。
命令的组织是有一些讲究的,由于咱们必定要作到命令和设备是一一对应的,这样才不会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的命令发给错误的设备。
因此在Linux核心中是这样定义一个命令码的:
设备类型 |
序列号 |
方向 |
数据尺寸 |
8bit |
8bit |
2bit |
13~14bit |
|
|
|
|
这样一来,一个命令就变成了一个整数形式的命令码。可是命令码很是的不直观,因此LinuxKernel中提供了一些宏,这些宏可根据便于理解的字符串生成命令码,或者是从命令码获得一些用户能够理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送方向和数据传输尺寸。
点击(此处)折叠或打开
/*used to create numbers*/
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) #defin e_IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size)) #define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size)) #define _IOWR_BAD(type,nr,size)_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size)) #define _IOC(dir,type,nr,size)\ (((dir)<<_IOC_DIRSHIFT)|\ ((type)<<_IOC_TYPESHIFT)|\ ((nr)<<_IOC_NRSHIFT)|\ ((size)<<_IOC_SIZESHIFT))
17,文件私有数据
大多数linux的驱动工程师都将文件私有数据private_data
指向设备结构体,read等个函数经过调用private_data
来访问设备结构体。这样作的目的是为了区分子设备,若是一个驱动有两个子设备(次设备号分别为0和1),那么使用private_data
就很方便。
这里有一个函数要提出来:
container_of(ptr,type,member)//经过结构体成员的指针找到对应结构体的的指针
其定义以下:
/**
*container_of-castamemberofastructureouttothecontainingstructure
*@ptr: thepointertothemember.
*@type: thetypeofthecontainerstructthisisembeddedin.
*@member: thenameofthememberwithinthestruct.
*
*/
#define container_of(ptr,type,member)({ \ const typeof(((type*)0)->member)*__mptr=(ptr); \ (type*)((char*)__mptr-offsetof(type,member));})
18,字符设备驱动的结构
能够归纳以下图:
字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工做是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation
结构体中操做函数,并实现file_operations
结构体中的read()
、write()
、ioctl()
等重要函数。如图所示为cdev结构体、file_operation
s和用户空间调用驱动的关系。
19, 自旋锁与信号量
为了不并发,防止竞争。内核提供了一组同步方法来提供对共享数据的保护。咱们的重点不是介绍这些方法的详细用法,而是强调为何使用这些方法和它们之间的差异。
Linux使用的同步机制能够说从2.0到2.6以来不断发展完善。从最初的原子操做,到后来的信号量,从大内核锁到今天的自旋锁。这些同步机制的发展伴随Linux从单处理器到对称多处理器的过分;伴随着从非抢占内核到抢占内核的过分。锁机制愈来愈有效,也愈来愈复杂。目前来讲内核中原子操做多用来作计数使用,其它状况最经常使用的是两种锁以及它们的变种:一个是自旋锁,另外一个是信号量。
自旋锁
自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来讲,防止中断处理中的并发可简单采用关闭中断的方式,不须要自旋锁)。
自旋锁最多只能被一个内核任务持有,若是一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁从新可用。要是锁未被争用,请求它的内核任务便能马上获得它而且继续进行。自旋锁能够在任什么时候刻防止多于一个的内核任务同时进入临界区,所以这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。
自旋锁的基本形式以下:
spin_lock(&mr_lock);
信号量
Linux中的信号量是一种睡眠锁。若是有一个任务试图得到一个已被持有的信号量时,信号量会将其推入等待队列,而后让其睡眠。这时处理器得到自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而即可以得到这个信号量。
信号量的睡眠特性,使得信号量适用于锁会被长时间持有的状况;只能在进程上下文中使用,由于中断上下文中是不能被调度的;另外当代码持有信号量时,不能够再持有自旋锁。
信号量基本使用形式为:
static DECLARE_MUTEX(mr_sem);
信号量和自旋锁区别
从严格意义上说,信号量和自旋锁属于不一样层次的互斥手段,前者的实现有赖于后者,在信号量自己的实现上,为了保证信号量结构存取的原子性,在多CPU中须要自旋锁来互斥。
信号量是进程级的。用于多个进程之间对资源的互斥,虽然也是在内核中,可是该内核执行路径是以进程的身份,表明进程来争夺进程。鉴于进程上下文切换的开销也很大,所以,只有当进程占用资源时间比较长时,用信号量才是较好的选择。
当所要保护的临界区访问时间比较短时,用自旋锁是很是方便的,由于它节省上下文切换的时间,可是CPU得不到自旋锁会在那里空转直到执行单元锁为止,因此要求锁不能在临界区里长时间停留,不然会下降系统的效率
由此,能够总结出自旋锁和信号量选用的3个原则:
1:当锁不能获取到时,使用信号量的开销就是进程上线文切换的时间Tc,使用自旋锁的开销就是等待自旋锁(由临界区执行的时间决定)Ts,若是Ts比较小时,应使用自旋锁比较好,若是Ts比较大,应使用信号量。
2:信号量所保护的临界区可包含可能引发阻塞的代码,而自旋锁绝对要避免用来保护包含这样的代码的临界区,由于阻塞意味着要进行进程间的切换,若是进程被切换出去后,另外一个进程企图获取本自旋锁,死锁就会发生。
3:信号量存在于进程上下文,所以,若是被保护的共享资源须要在中断或软中断状况下使用,则在信号量和自旋锁之间只能选择自旋锁,固然,若是必定要是要那个信号量,则只能经过down_trylock()方式进行,不能得到就当即返回以免阻塞
自旋锁VS信号量
需求建议的加锁方法
低开销加锁优先使用自旋锁
短时间锁定优先使用自旋锁
长期加锁优先使用信号量
中断上下文中加锁使用自旋锁
持有锁是须要睡眠、调度使用信号量
20, 阻塞与非阻塞I/O
一个驱动当它没法马上知足请求应当如何响应?一个对 read 的调用可能当没有数据时到来,而之后会期待更多的数据;或者一个进程可能试图写,可是你的设备没有准备好接受数据,由于你的输出缓冲满了。调用进程每每不关心这种问题,程序员只但愿调用 read 或 write 而且使调用返回,在必要的工做已完成后,你的驱动应当(缺省地)阻塞进程,使它进入睡眠直到请求可继续。
阻塞操做是指在执行设备操做时若不能得到资源则挂起进程,直到知足可操做的条件后再进行操做。
一个典型的能同时处理阻塞与非阻塞的globalfifo读函数以下:
21, poll方法
使用非阻塞I/O的应用程序一般会使用select()
和poll()
系统调用查询是否可对设备进行无阻塞的访问。select()
和poll()
系统调用最终会引起设备驱动中的poll()
函数被执行。
这个方法由下列的原型:
unsigned int (*poll) (struct file *filp, poll_table *wait);
这个驱动方法被调用, 不管什么时候用户空间程序进行一个 poll, select, 或者 epoll 系统调用, 涉及一个和驱动相关的文件描述符. 这个设备方法负责这 2 步:
- 对可能引发设备文件状态变化的等待队列,调用
poll_wait()
函数,将对应的等待队列头添加到poll_table
.
- 返回一个位掩码, 描述可能没必要阻塞就马上进行的操做.
poll_table
结构, 给 poll 方法的第 2 个参数, 在内核中用来实现 poll, select, 和 epoll 调用; 它在 中声明, 这个文件必须被驱动源码包含. 驱动编写者没必要要知道全部它内容而且必须做为一个不透明的对象使用它; 它被传递给驱动方法以便驱动可用每一个能唤醒进程的等待队列来加载它, 而且可改变 poll 操做状态. 驱动增长一个等待队列到poll_table
结构经过调用函数 poll_wait
:
void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
poll 方法的第 2 个任务是返回位掩码, 它描述哪一个操做可立刻被实现; 这也是直接的. 例如, 若是设备有数据可用, 一个读可能没必要睡眠而完成; poll 方法应当指示这个时间状态. 几个标志(经过 定义)用来指示可能的操做:
POLLIN
:若是设备可被不阻塞地读, 这个位必须设置.
POLLRDNORM
:这个位必须设置, 若是”正常”数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM
).
POLLOUT
:这个位在返回值中设置, 若是设备可被写入而不阻塞.
……
poll的一个典型模板以下:
static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)
{
unsigned int mask = 0; struct globalfifo_dev *dev = filp->private_data;
应用程序如何去使用这个poll呢?通常用select()
来实现,其原型为:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中,readfds, writefds, exceptfds,分别是被select()
监视的读、写和异常处理的文件描述符集合。numfds是须要检查的号码最高的文件描述符加1。
如下是一个具体的例子:
其中:
FD_ZERO(fd_set *set);
//清除一个文件描述符集set
FD_SET(int fd, fd_set *set);
//将一个文件描述符fd,加入到文件描述符集set中
FD_CLEAR(int fd, fd_set *set);
//将一个文件描述符fd,从文件描述符集set中清除
FD_ISSET(int fd, fd_set *set);
//判断文件描述符fd是否被置位。
22,并发与竞态介绍
Linux设备驱动中必须解决一个问题是多个进程对共享资源的并发访问,并发的访问会致使竞态,在当今的Linux内核中,支持SMP与内核抢占的环境下,更是充满了并发与竞态。幸运的是,Linux 提供了多钟解决竞态问题的方式,这些方式适合不一样的应用场景。例如:中断屏蔽、原子操做、自旋锁、信号量等等并发控制机制。
并发与竞态的概念
并发是指多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易致使竞态。
临界区概念是为解决竞态条件问题而产生的,一个临界区是一个不容许多路访问的受保护的代码,这段代码能够操纵共享数据或共享服务。临界区操纵坚持互斥锁原则(当一个线程处于临界区中,其余全部线程都不能进入临界区)。然而,临界区中须要解决的一个问题是死锁。
23, 中断屏蔽
在单CPU 范围内避免竞态的一种简单而省事的方法是进入临界区以前屏蔽系统的中断。CPU 通常都具备屏蔽中断和打开中断的功能,这个功能能够保证正在执行的内核执行路径不被中断处理程序所抢占,有效的防止了某些竞态条件的发送,总之,中断屏蔽将使得中断与进程之间的并发再也不发生。
中断屏蔽的使用方法:
local_irq_disable() /屏蔽本地CPU 中断/
…..
critical section /临界区受保护的数据/
…..
local_irq_enable() /打开本地CPU 中断/
因为Linux 的异步I/O、进程调度等不少重要操做都依赖于中断,中断对内核的运行很是重要,在屏蔽中断期间的全部中断都没法获得处理,所以长时间屏蔽中断是很是危险的,有可能形成数据的丢失,甚至系统崩溃的后果。这就要求在屏蔽了中断后,当前的内核执行路径要尽快地执行完临界区代码。
与local_irq_disable()
不一样的是,local_irq_save(flags)
除了进行禁止中断的操做外,还保存当前CPU 的中断状态位信息;与local_irq_enable()
不一样的是,local_irq_restore(flags)
除了打开中断的操做外,还恢复了CPU 被打断前的中断状态位信息。
24, 原子操做
原子操做指的是在执行过程当中不会被别的代码路径所中断的操做,Linux 内核提供了两类原子操做——位原子操做和整型原子操做。它们的共同点是在任何状况下都是原子的,内核代码能够安全地调用它们而不被打断。然而,位和整型变量原子操做都依赖于底层CPU 的原子操做来实现,所以这些函数的实现都与 CPU 架构密切相关。
1 整型原子操做
1)、设置原子变量的值
void atomic_set(atomic v,int i); /设置原子变量的值为 i */
atomic_t v = ATOMIC_INIT(0); /定义原子变量 v 并初始化为 0 /
2)、获取原子变量的值
int atomic_read(atomic_t v) /返回原子变量 v 的当前值*/
3)、原子变量加/减
void atomic_add(int i,atomic_t v) /原子变量增长 i */
void atomic_sub(int i,atomic_t v) /原子变量减小 i */
4)、原子变量自增/自减
void atomic_inc(atomic_t v) /原子变量增长 1 */
void atomic_dec(atomic_t v) /原子变量减小 1 */
5)、操做并测试
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
上述操做对原子变量执行自增、自减和减操做后测试其是否为 0 ,若为 0 返回true,不然返回false。注意:没有atomic_add_and_test(int i, atomic_t *v)
。
6)、操做并返回
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
上述操做对原子变量进行加/减和自增/自减操做,并返回新的值。
2 位原子操做
1)、设置位
void set_bit(nr,void addr);/设置addr 指向的数据项的第 nr 位为1 */
2)、清除位
void clear_bit(nr,void addr)/设置addr 指向的数据项的第 nr 位为0 */
3)、取反位
void change_bit(nr,void addr); /对addr 指向的数据项的第 nr 位取反操做*/
4)、测试位
test_bit(nr,void addr);/返回addr 指向的数据项的第 nr位*/
5)、测试并操做位
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr,void *addr);
int test_amd_change_bit(nr,void *addr);
25, 自旋锁
自旋锁(spin lock)是一种典型的对临界资源进行互斥访问的手段。为了得到一个自旋锁,在某CPU 上运行的代码需先执行一个原子操做,该操做测试并设置某个内存变量,因为它是原子操做,因此在该操做完成以前其余执行单元不能访问这个内存变量。若是测试结果代表锁已经空闲,则程序得到这个自旋锁并继续执行;若是测试结果代表锁仍被占用,则程序将在一个小的循环里面重复这个“测试并设置” 操做,即进行所谓的“自旋”。
理解自旋锁最简单的方法是把它当作一个变量看待,该变量把一个临界区标记为“我在这运行了,大家都稍等一会”,或者标记为“我当前不在运行,能够被使用”。
Linux中与自旋锁相关操做有:
1)、定义自旋锁
spinlock_t my_lock;
2)、初始化自旋锁
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /静态初始化自旋锁/
void spin_lock_init(spinlock_t lock); /动态初始化自旋锁*/
3)、获取自旋锁
/若得到锁马上返回真,不然自旋在那里直到该锁保持者释放/
void spin_lock(spinlock_t *lock);
/若得到锁马上返回真,不然马上返回假,并不会自旋等待/
void spin_trylock(spinlock_t *lock)
4)、释放自旋锁
void spin_unlock(spinlock_t *lock)
自旋锁的通常用法:
spinlock_t lock; /定义一个自旋锁/
spin_lock_init(&lock); /动态初始化一个自旋锁/
……
spin_lock(&lock); /获取自旋锁,保护临界区/ ……./临界区/ spin_unlock(&lock); /解锁/
自旋锁主要针对SMP 或单CPU 但内核可抢占的状况,对于单CPU 且内核不支持抢占的系统,自旋锁退化为空操做。尽管用了自旋锁能够保证临界区不受别的CPU和本地CPU内的抢占进程打扰,可是获得锁的代码路径在执行临界区的时候,还可能受到中断和底半部(BH)的影响,为了防止这种影响,就须要用到自旋锁的衍生。
获取自旋锁的衍生函数:
void spin_lock_irq(spinlock_t lock); /获取自旋锁以前禁止中断*/
void spin_lock_irqsave(spinlock_t lock, unsigned long flags);/获取自旋锁以前禁止中断,而且将先前的中断状态保存在flags 中*/ void spin_lock_bh(spinlock_t lock); /在获取锁以前禁止软中断,但不由止硬件中断*/
释放自旋锁的衍生函数:
void spin_unlock_irq(spinlock_t *lock)
void spin_unlock_irqrestore(spinlock_t *lock,unsigned long flags);
void spin_unlock_bh(spinlock_t *lock);
解锁的时候注意要一一对应去解锁。
自旋锁注意点:
(1)自旋锁其实是忙等待,所以,只有占用锁的时间极短的状况下,使用自旋锁才是合理的。
(2)自旋锁可能致使系统死锁。
(3)自旋锁锁按期间不能调用可能引发调度的函数。如:copy_from_user()、copy_to_user()、kmalloc()、msleep()等函数。
(4)拥有自旋锁的代码是不能休眠的。
26, 读写自旋锁
它容许多个读进程并发执行,可是只容许一个写进程执行临界区代码,并且读写也是不能同时进行的。
1)、定义和初始化读写自旋锁
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* 静态初始化 */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /* 动态初始化 */
2)、读锁定
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags); void read_lock_irq(rwlock_t *lock); void read_lock_bh(rwlock_t *lock);
3)、读解锁
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void read_unlock_irq(rwlock_t *lock); void read_unlock_bh(rwlock_t *lock);
在对共享资源进行读取以前,应该先调用读锁定函数,完成以后调用读解锁函数。
4)、写锁定
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags); void write_lock_irq(rwlock_t *lock); void write_lock_bh(rwlock_t *lock); void write_trylock(rwlock_t *lock);
5)、写解锁
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void write_unlock_irq(rwlock_t *lock); void write_unlock_bh(rwlock_t *lock);
在对共享资源进行写以前,应该先调用写锁定函数,完成以后应调用写解锁函数。
读写自旋锁的通常用法:
rwlock_t lock; /定义一个读写自旋锁 rwlock/
rwlock_init(&lock); /初始化/
read_lock(&lock); /读取前先获取锁/ …../临界区资源/ read_unlock(&lock); /读完后解锁/ write_lock_irqsave(&lock, flags); /写前先获取锁/ …../临界区资源/ write_unlock_irqrestore(&lock,flags); /写完后解锁/
27, 顺序锁(sequence lock)
顺序锁是对读写锁的一种优化,读执行单元在写执行单元对被顺序锁保护的资源进行写操做时仍然能够继续读,而没必要等地写执行单元完成写操做,写执行单元也没必要等待全部读执行单元完成读操做才进去写操做。可是,写执行单元与写执行单元依然是互斥的。而且,在读执行单元读操做期间,写执行单元已经发生了写操做,那么读执行单元必须进行重读操做,以便确保读取的数据是完整的,这种锁对于读写同时进行几率比较小的状况,性能是很是好的。
顺序锁有个限制,它必需要求被保护的共享资源不包含有指针,由于写执行单元可能使得指针失效,但读执行单元若是正要访问该指针,就会致使oops。
1)、初始化顺序锁
seqlock_t lock1 = SEQLOCK_UNLOCKED; /静态初始化/
seqlock lock2; /动态初始化/
seqlock_init(&lock2)
2)、获取顺序锁
void write_seqlock(seqlock_t *s1);
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags)
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock); int write_tryseqlock(seqlock_t *s1);
3)、释放顺序锁
void write_sequnlock(seqlock_t *s1);
void write_sequnlock_irqsave(seqlock_t *lock, unsigned long flags)
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);
写执行单元使用顺序锁的模式以下:
write_seqlock(&seqlock_a);
/写操做代码/
……..
write_sequnlock(&seqlock_a);
4)、读开始
unsigned read_seqbegin(const seqlock_t *s1);
unsigned read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
5)、重读
int read_seqretry(const seqlock_t *s1, unsigned iv);
int read_seqretry_irqrestore(seqlock_t *lock,unsigned int seq,unsigned long flags);
读执行单元使用顺序锁的模式以下:
unsigned int seq;
do{
seq = read_seqbegin(&seqlock_a);
/读操做代码/
…….
}while (read_seqretry(&seqlock_a, seq));
28, 信号量
信号量的使用
信号量(semaphore)是用于保护临界区的一种最经常使用的办法,它的使用方法与自旋锁是相似的,可是,与自旋锁不一样的是,当获取不到信号量的时候,进程不会自旋而是进入睡眠的等待状态。
1)、定义信号量
struct semaphore sem;
2)、初始化信号量
void sema_init(struct semaphore sem, int val); /初始化信号量的值为 val */
更经常使用的是下面这二个宏:
#define init_MUTEX(sem) sema_init(sem, 1)
#define init_MUTEX_LOCKED(sem) sem_init(sem, 0)
然而,下面这两个宏是定义并初始化信号量的“快捷方式”
DECLARE_MUTEX(name) /一个称为name信号量变量被初始化为 1 /
DECLARE_MUTEX_LOCKED(name) /一个称为name信号量变量被初始化为 0 /
3)、得到信号量
/该函数用于获取信号量,若获取不成功则进入不可中断的睡眠状态/
void down(struct semaphore *sem);
/该函数用于获取信号量,若获取不成功则进入可中断的睡眠状态/
void down_interruptible(struct semaphore *sem);
/该函数用于获取信号量,若获取不成功马上返回 -EBUSY/
int down_trylock(struct sempahore *sem);
4)、释放信号量
void up(struct semaphore sem); /释放信号量 sem ,并唤醒等待者*/
信号量的通常用法:
DECLARE_MUTEX(mount_sem); /定义一个信号量mount_sem,并初始化为 1 /
down(&mount_sem); /* 获取信号量,保护临界区*/
…..
critical section /临界区/
…..
up(&mount_sem); /释放信号量/
29, 读写信号量
读写信号量可能引发进程阻塞,可是它容许多个读执行单元同时访问共享资源,但最多只能有一个写执行单元。
1)、定义和初始化读写信号量
struct rw_semaphore my_rws; /定义读写信号量/
void init_rwsem(struct rw_semaphore sem); /初始化读写信号量*/
2)、读信号量获取
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
3)、读信号量释放
void up_read(struct rw_semaphore *sem);
4)、写信号量获取
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
5)、写信号量释放
void up_write(struct rw_semaphore *sem);
30, completion
完成量(completion)用于一个执行单元等待另一个执行单元执行完某事。
1)、定义完成量
struct completion my_completion;
2)、初始化完成量
init_completion(&my_completion);
3)、定义并初始化的“快捷方式”
DECLARE_COMPLETION(my_completion)
4)、等待完成量
void wait_for_completion(struct completion c); /等待一个 completion 被唤醒*/
5)、唤醒完成量
void complete(struct completion c); /只唤醒一个等待执行单元*/
void complete(struct completion c); /唤醒所有等待执行单元*/
31, 自旋锁VS信号量
信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,可是该内核执行路径是以进程的身份,表明进程来争夺资源的。若是竞争失败,会发送进程上下文切换,当前进程进入睡眠状态,CPU 将运行其余进程。鉴于开销比较大,只有当进程资源时间较长时,选用信号量才是比较合适的选择。然而,当所要保护的临界区访问时间比较短时,用自旋锁是比较方便的。
总结:
解决并发与竞态的方法有(按本文顺序):
(1)中断屏蔽
(2)原子操做(包括位和整型原子)
(3)自旋锁
(4)读写自旋锁
(5)顺序锁(读写自旋锁的进化)
(6)信号量
(7)读写信号量
(8)完成量
其中,中断屏蔽不多单独被使用,原子操做只能针对整数进行,所以自旋锁和信号量应用最为普遍。自旋锁会致使死循环,锁按期间内不容许阻塞,所以要求锁定的临界区小;信号量容许临界区阻塞,能够适用于临界区大的状况。读写自旋锁和读写信号量分别是放宽了条件的自旋锁 信号量,它们容许多个执行单元对共享资源的并发读。
Linux驱动开发必看-Linux启动过程
在开始步入Linux设备驱动程序的神秘世界以前,让咱们从驱动程序开发人员的角度看几个内核构成要素,熟悉一些基本的内核概念。咱们将学习内核定时器、同步机制以及内存分配方法。不过,咱们仍是得从头开始此次探索之旅。所以,本章要先浏览一下内核发出的启动信息,而后再逐个讲解一些有意思的点。
2.1 启动过程
图2-1显示了基于x86计算机Linux系统的启动顺序。第一步是BIOS从启动设备中导入主引导记录(MBR),接下来MBR中的代码查看分区表并 从活动分区读取GRUB、LILO或SYSLINUX等引导装入程序,以后引导装入程序会加载压缩后的内核映像并将控制权传递给它。内核取得控制权后,会 将自身解压缩并投入运转。
基于x86的处理器有两种操做模式:实模式和保护模式。在实模式下,用户仅能够使用1 MB内存,而且没有任何保护。保护模式要复杂得多,用户能够使用更多的高级功能(如分页)。CPU必须中途将实模式切换为保护模式。可是,这种切换是单向的,即不能从保护模式再切换回实模式。
内核初始化的第一步是执行实模式下的汇编代码,以后执行保护模式下init/main.c文件(上一章修改的源文件)中的start_kernel() 函数。start_kernel()函数首先会初始化CPU子系统,以后让内存和进程管理系统就位,接下来启动外部总线和I/O设备,最后一步是激活初始 化(init)程序,它是全部Linux进程的父进程。初始化进程执行启动必要的内核服务的用户空间脚本,而且最终派生控制台终端程序以及显示登陆 (login)提示。

图2-1 基于x86硬件上的Linux的启动过程
本节内的3级标题都是图2-2中的一条打印信息,这些信息来源于基于x86的笔记本电脑的Linux启动过程。若是在其余体系架构上启动内核,消息以及语义可能会有所不一样。
2.1.1 BIOS-provided physical RAM map
内核会解析从BIOS中读取到的系统内存映射,并率先将如下信息打印出来:
BIOS-provided physical RAM map:
BIOS-e820: 0000000000000000 - 000000000009f000 (usable)
...
BIOS-e820: 00000000ff800000 - 0000000100000000 (reserved)
实模式下的初始化代码经过使用BIOS的int 0x15服务并执行0xe820号函数(即上面的BIOS-e820字符串)来得到系统的内存映射信息。内存映射信息中包含了预留的和可用的内存,内核将 随后使用这些信息建立其可用的内存池。在附录B的B.1节,咱们会对BIOS提供的内存映射问题进行更深刻的讲解。

图2-2 内核启动信息
2.1.2 758MB LOWMEM available
896 MB之内的常规的可被寻址的内存区域被称做低端内存。内存分配函数kmalloc()就是从该区域分配内存的。高于896 MB的内存区域被称为高端内存,只有在采用特殊的方式进行映射后才能被访问。
在启动过程当中,内核会计算并显示这些内存区内总的页数。
2.1.3 Kernel command line: ro root=/dev/hda1
Linux的引导装入程序一般会给内核传递一个命令行。命令行中的参数相似于传递给C程序中main()函数的argv[]列表,惟一的不一样在于它们是 传递给内核的。能够在引导装入程序的配置文件中增长命令行参数,固然,也能够在运行过程当中修改引导装入程序的提示行[1]。若是使用的是GRUB这个引导 装入程序,因为发行版本的不一样,其配置文件多是/boot/grub/grub.conf或者是/boot/grub/menu.lst。若是使用的是 LILO,配置文件为/etc/lilo.conf。下面给出了一个grub.conf文件的例子(增长了一些注释),看了紧接着title kernel 2.6.23的那行代码以后,你会明白前述打印信息的由来。
default 0 #Boot the 2.6.23 kernel by default
timeout 5 #5 second to alter boot order or parameters
title kernel 2.6.23 #Boot Option 1
#The boot image resides in the first partition of the first disk
#under the /boot/ directory and is named vmlinuz-2.6.23. 'ro'
#indicates that the root partition should be mounted read-only.
kernel (hd0,0)/boot/vmlinuz-2.6.23 ro root=/dev/hda1
#Look under section "Freeing initrd memory:387k freed"
initrd (hd0,0)/boot/initrd
#...
命令行参数将影响启动过程当中的代码执行路径。举一个例子,假设某命令行参数为bootmode,若是该参数被设置为1,意味着你但愿在启动过程当中打印一 些调试信息并在启动结束时切换到runlevel的第3级(初始化进程的启动信息打印后就会了解runlevel的含义);若是bootmode参数被设 置为0,意味着你但愿启动过程相对简洁,而且设置runlevel为2。既然已经熟悉了init/main.c文件,下面就在该文件中增长以下修改:
static
unsigned int bootmode = 1;
static int __init
is_bootmode_setup(char *str)
{
get_option(&str, &bootmode);
return 1;
}
/* Handle parameter "bootmode=" */
__setup("bootmode=", is_bootmode_setup);
if (bootmode) {
/* Print verbose output */
/* ... */
}
/* ... */
/* If bootmode is 1, choose an init runlevel of 3, else
switch to a run level of 2 */
if (bootmode) {
argv_init[++args] = "3";
} else {
argv_init[++args] = "2";
}
/* ... */
请从新编译内核并尝试运行新的修改。
2.1.4 Calibrating delay...1197.46 BogoMIPS (lpj=2394935)
在启动过程当中,内核会计算处理器在一个jiffy时间内运行一个内部的延迟循环的次数。jiffy的含义是系统定时器2个连续的节拍之间的间隔。正如所料,该计算必须被校准到所用CPU的处理速度。校准的结果被存储在称为loops_per_jiffy的内核变量中。使用loops_per_jiffy的一种状况是某设备驱动程序但愿进行小的微秒级别的延迟的时候。
为了理解延迟—循环校准代码,让咱们看一下定义于init/calibrate.c文件中的calibrate_ delay()函数。该函数灵活地使用整型运算获得了浮点的精度。以下的代码片断(有一些注释)显示了该函数的开始部分,这部分用于获得一个 loops_per_jiffy的粗略值:
loops_per_jiffy = (1 << 12); /* Initial approximation = 4096 */
printk(KERN_DEBUG “Calibrating delay loop...“);
while ((loops_per_jiffy <<= 1) != 0) {
ticks = jiffies; /* As you will find out in the section, “Kernel
Timers," the jiffies variable contains the
number of timer ticks since the kernel
started, and is incremented in the timer
interrupt handler */
while (ticks == jiffies); /* Wait until the start of the next jiffy */
ticks = jiffies;
/* Delay */
__delay(loops_per_jiffy);
/* Did the wait outlast the current jiffy? Continue if it didn't */
ticks = jiffies - ticks;
if (ticks) break;
}
loops_per_jiffy >>= 1; /* This fixes the most significant bit and is
the lower-bound of loops_per_jiffy */
上述代码首先假定loops_per_jiffy大于4096,这能够转化为处理器速度大约为每秒100万条指令,即1 MIPS。接下来,它等待jiffy被刷新(1个新的节拍的开始),并开始运行延迟循环__delay(loops_per_jiffy)。若是这个延迟 循环持续了1个jiffy以上,将使用之前的loops_per_jiffy值(将当前值右移1位)修复当前loops_per_jiffy的最高位;否 则,该函数继续经过左移loops_per_jiffy值来探测出其最高位。在内核计算出最高位后,它开始计算低位并微调其精度:
loopbit = loops_per_jiffy;
/* Gradually work on the lower-order bits */
while (lps_precision-- && (loopbit >>= 1)) {
loops_per_jiffy |= loopbit;
ticks = jiffies;
while (ticks == jiffies); /* Wait until the start of the next jiffy */
ticks = jiffies;
/* Delay */
__delay(loops_per_jiffy);
if (jiffies != ticks) /* longer than 1 tick */
loops_per_jiffy &= ~loopbit;
}
上述代码计算出了延迟循环跨越jiffy边界时loops_per_jiffy的低位值。这个被校准的值可被用于获取BogoMIPS(其实它是一个并 非科学的处理器速度指标)。能够使用BogoMIPS做为衡量处理器运行速度的相对尺度。在1.6G Hz 基于Pentium M的笔记本电脑上,根据前述启动过程的打印信息,循环校准的结果是:loops_per_jiffy的值为2394935。得到BogoMIPS的方式以下:
BogoMIPS = loops_per_jiffy * 1秒内的jiffy数*延迟循环消耗的指令数(以百万为单位)
= (2394935 * HZ * 2) / (1000000)
= (2394935 * 250 * 2) / (1000000)
= 1197.46(与启动过程打印信息中的值一致)
在2.4节将更深刻阐述jiffy、HZ和loops_per_jiffy。
2.1.5 Checking HLT instruction
因为Linux内核支持多种硬件平台,启动代码会检查体系架构相关的bug。其中一项工做就是验证停机(HLT)指令。
x86处理器的HLT指令会将CPU置入一种低功耗睡眠模式,直到下一次硬件中断发生以前维持不变。当内核想让CPU进入空闲状态时(查看 arch/x86/kernel/process_32.c文件中定义的cpu_idle()函数),它会使用HLT指令。对于有问题的CPU而言,命令 行参数no-hlt能够禁止HLT指令。若是no-hlt被设置,在空闲的时候,内核会进行忙等待而不是经过HLT给CPU降温。
当init/main.c中的启动代码调用include/asm-your-arch/bugs.h中定义的check_bugs()时,会打印上述信息。
2.1.6 NET: Registered protocol family 2
Linux套接字(socket)层是用户空间应用程序访问各类网络协议的统一接口。每一个协议经过include/linux/socket.h文件中定义的分配给它的独一无二的系列号注册。上述打印信息中的Family 2表明af_inet(互联网协议)。
启动过程当中另外一个常见的注册协议系列是AF_NETLINK(Family 16)。网络连接套接字提供了用户进程和内核通讯的 方法。经过网络连接套接字可完成的功能还包括存取路由表和地址解析协议(ARP)表(include/linux/netlink.h文件给出了完整的用 法列表)。对于此类任务而言,网络连接套接字比系统调用更合适,由于前者具备采用异步机制、更易于实现和可动态连接的优势。
内核中常常使能的另外一个协议系列是AF_Unix或Unix-domain套接字。X Windows等程序使用它们在同一个系统上进行进程间通讯。
2.1.7 Freeing initrd memory: 387k freed
initrd是一种由引导装入程序加载的常驻内存的虚拟磁盘映像。在内核启动后,会将其挂载为初始根文件系统,这个初始根文件系统中存放着挂载实际根文 件系统磁盘分区时所依赖的可动态链接的模块。因为内核可运行于各类各样的存储控制器硬件平台上,把全部可能的磁盘驱动程序都直接放进基本的内核映像中并不 可行。你所使用的系统的存储设备的驱动程序被打包放入了initrd中,在内核启动后、实际的根文件系统被挂载以前,这些驱动程序才被加载。使用 mkinitrd命令能够建立一个initrd映像。
2.6内核提供了一种称为initramfs的新功能,它在几个方面较 initrd更为优秀。后者模拟了一个磁盘(于是被称为initramdisk或initrd),会带来Linux块I/O子系统的开销(如缓冲);前者 基本上如同一个被挂载的文件系统同样,由自身获取缓冲(所以被称做initramfs)。
不一样于initrd,基于页缓冲创建的 initramfs如同页缓冲同样会动态地变大或缩小,从而减小了其内存消耗。另外,initrd要求你的内核映像包含initrd所使用的文件系统(例 如,若是initrd为EXT2文件系统,内核必须包含EXT2驱动程序),然而initramfs不须要文件系统支持。再者,因为initramfs只 是页缓冲之上的一小层,所以它的代码量很小。
用户能够将初始根文件系统打包为一个cpio压缩包[1],并经过initrd=命令行参 数传递给内核。固然,也能够在内核配置过程当中经过INITRAMFS_SOURCE选项直接编译进内核。对于后一种方式而言,用户能够提供cpio压缩包 的文件名或者包含initramfs的目录树。在启动过程当中,内核会将文件解压缩为一个initramfs根文件系统,若是它找到了/init,它就会执 行该顶层的程序。这种获取初始根文件系统的方法对于嵌入式系统而言特别有用,由于在嵌入式系统中系统资源很是宝贵。使用mkinitramfs能够建立一 个initramfs映像,查看文档Documentation/filesystems/ramfs- rootfs-initramfs.txt可得到更多信息。
在本例中,咱们使用的是经过initrd=命令行参数向内核传递初始根文件 系统cpio压缩包的方式。在将压缩包中的内容解压为根文件系统后,内核将释放该压缩包所占据的内存(本例中为387 KB)并打印上述信息。释放后的页面会被分发给内核中的其余部分以便被申请。
在嵌入式系统开发过程当中,initrd和initramfs有时候也可被用做嵌入式设备上实际的根文件系统。
2.1.8 io scheduler anticipatory registered (default)
I/O调度器的主要目标是经过减小磁盘的定位次数来增长系统的吞吐率。在磁盘定位过程当中,磁头须要从当前的位置移动到感兴趣的目标位置,这会带来必定的 延迟。2.6内核提供了4种不一样的I/O调度器:Deadline、Anticipatory、Complete Fair Queuing以及NOOP。从上述内核打印信息能够看出,本例将Anticipatory 设置为了默认的I/O调度器。
2.1.9 Setting up standard PCI resources
启动过程的下一阶段会初始化I/O总线和外围控制器。内核会经过遍历PCI总线来探测PCI硬件,接下来再初始化其余的I/O子系统。从图2-3中咱们会看到SCSI子系统、USB控制器、视频芯片(855北桥芯片组信息中的一部分)、串行端口(本例中为8250 UART)、PS/2键盘和鼠标、软驱、ramdisk、loopback设备、IDE控制器(本例中为ICH4南桥芯片组中的一部分)、触控板、以太网控制器(本例中为e1000)以及PCMCIA控制器初始化的启动信息。图2-3中 符号指向的为I/O设备的标识(ID)。

图2-3 在启动过程当中初始化总线和外围控制器
本书会以单独的章节讨论大部分上述驱动程序子系统,请注意若是驱动程序以模块的形式被动态连接到内核,其中的一些消息也许只有在内核启动后才会被显示。
2.1.10 EXT3-fs: mounted filesystem
EXT3文件系统已经成为Linux事实上的文件系统。EXT3在退役的EXT2文件系统基础上增添了日志层,该层可用于崩溃后文件系统的快速恢复。它 的目标是不经由耗时的文件系统检查(fsck)操做便可得到一个一致的文件系统。EXT2仍然是新文件系统的工做引擎,可是EXT3层会在进行实际的磁盘 改变以前记录文件交互的日志。EXT3向后兼容于EXT2,所以,你能够在你现存的EXT2文件系统上加上EXT3或者由EXT3返回到EXT2文件系 统。
EXT3会启动一个称为kjournald的内核辅助线程(在接下来的一章中将深刻讨论内核线程)来完成日志功能。在EXT3投入运转之后,内核挂载根文件系统并作好“业务”上的准备:
EXT3-fs: mounted filesystem with ordered data mode
kjournald starting. Commit interval 5 seconds
VFS: Mounted root (ext3 filesystem).
2.1.11 INIT: version 2.85 booting
全部Linux进程的父进程init是内核完成启动序列后运行的第1个程序。在init/main.c的最后几行,内核会搜索一个不一样的位置以定位到init:
if
(ramdisk_execute_command) { /* Look for /init in initramfs */
run_init_process(ramdisk_execute_command);
}
if (execute_command) { /* You may override init and ask the kernel
to execute a custom program using the
"init=" kernel command-line argument. If
you do that, execute_command points to the
specified program */
run_init_process(execute_command);
}
/* Else search for init or sh in the usual places .. */
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
init会接受/etc/inittab的指引。它首先执行/etc/rc.sysinit中的系统初始化脚本,该脚本的一项最重要的职责就是激活对换(swap)分区,这会致使以下启动信息被打印:
Adding 1552384k swap on /dev/hda6
让咱们来仔细看看上述这段话的意思。Linux用户进程拥有3 GB的虚拟地址空间(见2.7节),构成“工做集”的页被保存在RAM中。可是,若是有太多程序须要内存资源,内核会释放一些被使用了的RAM页面并将其 存储到称为对换空间(swap space)的磁盘分区中。根据经验法则,对换分区的大小应该是RAM的2倍。在本例中,对换空间位于/dev/hda6这个磁盘分区,其大小为1 552 384 KB。
接下来,init开始运行/etc/rc.d/rcX.d/目录中的脚本,其中X是inittab中定义的运行 级别。runlevel是根据预期的工做模式所进入的执行状态。例如,多用户文本模式意味着runlevel为3,X Windows则意味着runlevel为5。所以,当你看到INIT: Entering runlevel 3这条信息的时候,init就已经开始执行/etc/rc.d/rc3.d/目录中的脚本了。这些脚本会启动动态设备命名子系统(第4章中将讨论 udev),并加载网络、音频、存储设备等驱动程序所对应的内核模块:
Starting udev: [ OK ]
Initializing hardware... network audio storage [Done]
...
最后,init发起虚拟控制台终端,你如今就能够登陆了。
2.2 内核模式和用户模式
MS-DOS等操做系统在单一的CPU模式下运行,可是一些类Unix的操做系统则使用了双模式,能够有效地实现时间共享。在Linux机器上,CPU要么处于受信任的内核模式,要么处于受限制的用户模式。除了内核自己处于内核模式之外,全部的用户进程都运行在用户模式之中。
内核模式的代码能够无限制地访问全部处理器指令集以及所有内存和I/O空间。若是用户模式的进程要享有此特权,它必须经过系统调用向设备驱动程序或其余内核模式的代码发出请求。另外,用户模式的代码容许发生缺页,而内核模式的代码则不容许。
在2.4和更早的内核中,仅仅用户模式的进程能够被上下文切换出局,由其余进程抢占。除非发生如下两种状况,不然内核模式代码能够一直独占CPU:
(1) 它自愿放弃CPU;
(2) 发生中断或异常。
2.6内核引入了内核抢占,大多数内核模式的代码也能够被抢占。
2.3 进程上下文和中断上下文
内核能够处于两种上下文:进程上下文和中断上下文。在系统调用以后,用户应用程序进入内核空间,此后内核空间针对用户空间相应进程的表明就运行于进程上 下文。异步发生的中断会引起中断处理程序被调用,中断处理程序就运行于中断上下文。中断上下文和进程上下文不可能同时发生。
运行于进程上下文的内核代码是可抢占的,但进程上下文则会一直运行至结束,不会被抢占。所以,内核会限制中断上下文的工做,不容许其执行以下操做:
(1) 进入睡眠状态或主动放弃CPU;
(2) 占用互斥体;
(3) 执行耗时的任务;
(4) 访问用户空间虚拟内存。
本书4.2节会对中断上下文进行更深刻的讨论。
2.4 内核定时器
内核中许多部分的工做都高度依赖于时间信息。Linux内核利用硬件提供的不一样的定时器以支持忙等待或睡眠等待等时间相关的服务。忙等待时,CPU会不 断运转。可是睡眠等待时,进程将放弃CPU。所以,只有在后者不可行的状况下,才考虑使用前者。内核也提供了某些便利,能够在特定的时间以后调度某函数运 行。
咱们首先来讨论一些重要的内核定时器变量(jiffies、HZ和xtime)的含义。接下来,咱们会使用Pentium时间戳计数器(TSC)测量基于Pentium的系统的运行次数。以后,咱们也分析一下Linux怎么使用实时钟(RTC)。
2.4.1 HZ和Jiffies
系统定时器能以可编程的频率中断处理器。此频率即为每秒的定时器节拍数,对应着内核变量HZ。选择合适的HZ值须要权衡。HZ值大,定时器间隔时间就小,所以进程调度的准确性会更高。可是,HZ值越大也会致使开销和电源消耗更多,由于更多的处理器周期将被耗费在定时器中断上下文中。
HZ的值取决于体系架构。在x86系统上,在2.4内核中,该值默认设置为100;在2.6内核中,该值变为1000;而在2.6.13中,它又被下降到了250。在基于ARM的平台上,2.6内核将HZ设置为100。在目前的内核中,能够在编译内核时经过配置菜单选择一个HZ值。该选项的默认值取决于体系架构的版本。
2.6.21内核支持无节拍的内核(CONFIG_NO_HZ),它会根据系统的负载动态触发定时器中断。无节拍系统的实现超出了本章的讨论范围,再也不详述。
jiffies变量记录了系统启动以来,系统定时器已经触发的次数。内核每秒钟将jiffies变量增长HZ次。所以,对于HZ值为100的系统,1个jiffy等于10ms,而对于HZ为1000的系统,1个jiffy仅为1ms。
为了更好地理解HZ和jiffies变量,请看下面的取自IDE驱动程序(drivers/ide/ide.c)的代码片断。该段代码会一直轮询磁盘驱动器的忙状态:
unsigned long timeout = jiffies + (3*HZ);
while (hwgroup->busy) {
/* ... */
if (time_after(jiffies, timeout)) {
return -EBUSY;
}
/* ... */
}
return SUCCESS;
若是忙条件在3s内被清除,上述代码将返回SUCCESS,不然,返回-EBUSY。3*HZ是3s内的jiffies数量。计算出来的超时 jiffies + 3*HZ将是3s超时发生后新的jiffies值。time_after()的功能是将目前的jiffies值与请求的超时时间对比,检测溢出。相似函数 还包括time_before()、time_before_eq()和time_after_eq()。
jiffies被定义为volatile类型,它会告诉编译器不要优化该变量的存取代码。这样就确保了每一个节拍发生的定时器中断处理程序都能更新jiffies值,而且循环中的每一步都会从新读取jiffies值。
对于jiffies向秒转换,能够查看USB主机控制器驱动程序drivers/usb/host/ehci-sched.c中的以下代码片断:
if
(stream->rescheduled) {
ehci_info(ehci, "ep%ds-iso rescheduled " "%lu times in %lu
seconds\n", stream->bEndpointAddress, is_in? "in":
"out", stream->rescheduled,
((jiffies – stream->start)/HZ));
}
上述调试语句计算出USB端点流(见第11章)被从新调度stream->rescheduled次所耗费的秒数。jiffies-stream->start是从开始到如今消耗的jiffies数量,将其除以HZ就获得了秒数值。
假定jiffies值为1000,32位的jiffies会在大约50天的时间内溢出。因为系统的运行时间能够比该时间长许多倍,所以,内核提供了另外一 个变量jiffies_64以存放64位(u64)的jiffies。连接器将jiffies_64的低32位与32位的jiffies指向同一个地址。 在32位的机器上,为了将一个u64变量赋值给另外一个,编译器须要2条指令,所以,读jiffies_64的操做不具有原子性。能够将 drivers/cpufreq/cpufreq_stats.c文件中定义的cpufreq_stats_update()做为实例来学习。
2.4.2 长延时
在内核中,以jiffies为单位进行的延迟一般被认为是长延时。一种可能但非最佳的实现长延时的方法是忙等待。实现忙等待的函数有“占着茅坑不拉屎”之嫌,它自己不利用CPU进行有用的工做,同时还不让其余程序使用CPU。以下代码将占用CPU 1秒:
unsigned long timeout = jiffies + HZ;
while (time_before(jiffies, timeout)) continue;
实现长延时的更好方法是睡眠等待而不是忙等待,在这种方式中,本进程会在等待时将处理器出让给其余进程。schedule_timeout()完成此功能:
unsigned long timeout = HZ;
schedule_timeout(timeout); /* Allow other parts of the kernel to run */
这种延时仅仅确保超时较低时的精度。因为只有在时钟节拍引起的内核调度才会更新jiffies,因此不管是在内核空间仍是在用户空间,都很难使超时的精 度比HZ更大了。另外,即便你的进程已经超时并可被调度,可是调度器仍然可能基于优先级策略选择运行队列的其余进程[1]。
用于睡眠等 待的另2个函数是wait_event_timeout()和msleep(),它们的实现都基于schedule_timeout()。 wait_event_timeout()的使用场合是:在一个特定的条件知足或者超时发生后,但愿代码继续运行。msleep()表示睡眠指定的时间 (以毫秒为单位)。
这种长延时技术仅仅适用于进程上下文。睡眠等待不能用于中断上下文,由于中断上下文不容许执行schedule() 或睡眠(4.2节给出了中断上下文能够作和不能作的事情)。在中断中进行短期的忙等待是可行的,可是进行长时间的忙等则被认为不可赦免的罪行。在中断禁 止时,进行长时间的忙等待也被看做禁忌。
为了支持在未来的某时刻进行某项工做,内核也提供了定时器API。能够经过 init_timer()动态定义一个定时器,也能够经过DEFINE_TIMER()静态建立定时器。而后,将处理函数的地址和参数绑定给一个 timer_list,并使用add_timer()注册它便可:
#include <linux/timer.h>
struct timer_list my_timer;
init_timer(&my_timer); /* Also see setup_timer() */
my_timer.expire = jiffies + n*HZ; /* n is the timeout in number of seconds */
my_timer.function = timer_func; /* Function to execute after n seconds */
my_timer.data = func_parameter; /* Parameter to be passed to timer_func */
add_timer(&my_timer); /* Start the timer */
上述代码只会让定时器运行一次。若是想让timer_func()函数周期性地执行,须要在timer_func()加上相关代码,指定其在下次超时后调度自身:
static
void timer_func(unsigned long func_parameter)
{
/* Do work to be done periodically */
/* ... */
init_timer(&my_timer);
my_timer.expire = jiffies + n*HZ;
my_timer.data = func_parameter;
my_timer.function = timer_func;
add_timer(&my_timer);
}
你能够使用mod_timer()修改my_timer的到期时间,使用del_timer()取消定时器,或使用timer_pending()以查 看my_timer当前是否处于等待状态。查看kernel/timer.c源代码,会发现schedule_timeout()内部就使用了这些 API。
clock_settime()和clock_gettime()等用户空间函数可用于得到内核定时器服务。用户应用程序能够使用setitimer()和getitimer()来控制一个报警信号在特定的超时后发生。
2.4.3 短延时
在内核中,小于jiffy的延时被认为是短延时。这种延时在进程或中断上下文均可能发生。因为不可能使用基于jiffy的方法实现短延时,以前讨论的睡眠等待将再也不能用于短的超时。这种状况下,惟一的解决途径就是忙等待。
实现短延时的内核API包括mdelay()、udelay()和ndelay(),分别支持毫秒、微秒和纳秒级的延时。这些函数的实际实现取决于体系架构,并且也并不是在全部平台上都被完整实现。
忙等待的实现方法是测量处理器执行一条指令的时间,为了延时,执行必定数量的指令。从前文可知,内核会在启动过程当中进行测量并将该值存储在 loops_per_jiffy变量中。短延时API就使用了loops_per_jiffy值来决定它们须要进行循环的数量。为了实现握手进程中1微秒 的延时,USB主机控制器驱动程序(drivers/usb/host/ehci-hcd.c)会调用udelay(),而udelay()会内部调用 loops_per_jiffy:
do
{
result = ehci_readl(ehci, ptr);
/* ... */
if (result == done) return 0;
udelay(1); /* Internally uses loops_per_jiffy */
usec--;
} while (usec > 0);
2.4.4 Pentium时间戳计数器
时间戳计数器(TSC)是Pentium兼容处理器中的一个计数器,它记录自启动以来处理器消耗的时钟周期数。因为TSC随着处理器周期速率的比例的变 化而变化,所以提供了很是高的精确度。TSC一般被用于剖析和监测代码。使用rdtsc指令可测量某段代码的执行时间,其精度达到微秒级。TSC的节拍可 以被转化为秒,方法是将其除以CPU时钟速率(可从内核变量cpu_khz读取)。
在以下代码片断中,low_tsc_ticks和high_tsc_ticks分别包含了TSC的低32位和高32位。低32位可能在数秒内溢出(具体时间取决于处理器速度),可是这已经用于许多代码的剖析了:
unsigned long low_tsc_ticks0, high_tsc_ticks0;
unsigned long low_tsc_ticks1, high_tsc_ticks1;
unsigned long exec_time;
rdtsc(low_tsc_ticks0, high_tsc_ticks0); /* Timestamp before */
printk("Hello World\n"); /* Code to be profiled */
rdtsc(low_tsc_ticks1, high_tsc_ticks1); /* Timestamp after */
exec_time = low_tsc_ticks1 - low_tsc_ticks0;
在1.8 GHz Pentium 处理器上,exec_time的结果为871(或半微秒)。
在2.6.21内核中,针对高精度定时器的支持(CONFIG_HIGH_RES_TIMERS)已经被融入了内核。它使用了硬件特定的高速定时器来提供对nanosleep()等API高精度的支持。在基于Pentium的机器上,内核借助TSC实现这一功能。
2.4.5 实时钟
RTC在非易失性存储器上记录绝对时间。在x86 PC上,RTC位于由电池供电[1]的互补金属氧化物半导体(CMOS)存储器的顶部。从第5章的图5-1能够看出传统PC体系架构中CMOS的位置。在 嵌入式系统中,RTC可能被集成处处理器中,也可能经过I2C或SPI总线在外部链接,见第8章。
使用RTC能够完成以下工做:
(1) 读取、设置绝对时间,在时钟更新时产生中断;
(2) 产生频率为2~8192 Hz之间的周期性中断;
(3) 设置报警信号。
许多应用程序须要使用绝对时间[或称墙上时间(wall time)]。jiffies是相对于系统启动后的时间,它不包含墙上时间。内核将墙上时间记录在xtime变量中,在启动过程当中,会根据从RTC读取到 的目前的墙上时间初始化xtime,在系统停机后,墙上时间会被写回RTC。你能够使用do_gettimeofday()读取墙上时间,其最高精度由硬 件决定:
#include <linux/time.h>
static struct timeval curr_time;
do_gettimeofday(&curr_time);
my_timestamp = cpu_to_le32(curr_time.tv_sec); /* Record timestamp */
用户空间也包含一系列能够访问墙上时间的函数,包括:
(1) time(),该函数返回日历时间,或重新纪元(1970年1月1日00:00:00)以来经历的秒数;
(2) localtime(),以分散的形式返回日历时间;
(3) mktime(),进行localtime()函数的反向工做;
(4) gettimeofday(),若是你的平台支持,该函数将以微秒精度返回日历时间。
用户空间使用RTC的另外一种途径是经过字符设备/dev/rtc来进行,同一时刻只有一个进程容许返回该字符设备。
在第5章和第8章,本书将更深刻讨论RTC驱动程序。另外,在第19章给出了一个使用/dev/rtc以微秒级精度执行周期性工做的应用程序示例。
2.5 内核中的并发
随着多核笔记本电脑时代的到来,对称多处理器(SMP)的使用再也不被限于高科技用户。SMP和内核抢占是多线程执行的两种场景。多个线程可以同时操做共享的内核数据结构,所以,对这些数据结构的访问必须被串行化。
接下来,咱们会讨论并发访问状况下保护共享内核资源的基本概念。咱们以一个简单的例子开始,并逐步引入中断、内核抢占和SMP等复杂概念。
2.5.1 自旋锁和互斥体
访问共享资源的代码区域称做临界区。自旋锁(spinlock)和互斥体(mutex,mutual exclusion的缩写)是保护内核临界区的两种基本机制。咱们逐个分析。
自旋锁能够确保在同时只有一个线程进入临界区。其余想进入临界区的线程必须不停地原地打转,直到第1个线程释放自旋锁。注意:这里所说的线程不是内核线程,而是执行的线程。
下面的例子演示了自旋锁的基本用法:
#include <linux/spinlock.h>
spinlock_t mylock = SPIN_LOCK_UNLOCKED; /* Initialize */
/* Acquire the spinlock. This is inexpensive if there
* is no one inside the critical section. In the face of
* contention, spinlock() has to busy-wait.
*/
spin_lock(&mylock);
/* ... Critical Section code ... */
spin_unlock(&mylock); /* Release the lock */
与自旋锁不一样的是,互斥体在进入一个被占用的临界区以前不会原地打转,而是使当前线程进入睡眠状态。若是要等待的时间较长,互斥体比自旋锁更合适,由于 自旋锁会消耗CPU资源。在使用互斥体的场合,多于2次进程切换时间均可被认为是长时间,所以一个互斥体会引发本线程睡眠,而当其被唤醒时,它须要被切换 回来。
所以,在不少状况下,决定使用自旋锁仍是互斥体相对来讲很容易:
(1) 若是临界区须要睡眠,只能使用互斥体,由于在得到自旋锁后进行调度、抢占以及在等待队列上睡眠都是非法的;
(2) 因为互斥体会在面临竞争的状况下将当前线程置于睡眠状态,所以,在中断处理函数中,只能使用自旋锁。(第4章将介绍更多的关于中断上下文的限制。)
下面的例子演示了互斥体使用的基本方法:
#include <linux/mutex.h>
/* Statically declare a mutex. To dynamically
create a mutex, use mutex_init() */
static DEFINE_MUTEX(mymutex);
/* Acquire the mutex. This is inexpensive if there
* is no one inside the critical section. In the face of
* contention, mutex_lock() puts the calling thread to sleep.
*/
mutex_lock(&mymutex);
/* ... Critical Section code ... */
mutex_unlock(&mymutex); /* Release the mutex */
为了论证并发保护的用法,咱们首先从一个仅存在于进程上下文的临界区开始,并如下面的顺序逐步增长复杂性:
(1) 非抢占内核,单CPU状况下存在于进程上下文的临界区;
(2) 非抢占内核,单CPU状况下存在于进程和中断上下文的临界区;
(3) 可抢占内核,单CPU状况下存在于进程和中断上下文的临界区;
(4) 可抢占内核,SMP状况下存在于进程和中断上下文的临界区。
旧的信号量接口
互斥体接口代替了旧的信号量接口(semaphore)。互斥体接口是从-rt树演化而来的,在2.6.16内核中被融入主线内核。
尽管如此,可是旧的信号量仍然在内核和驱动程序中普遍使用。信号量接口的基本用法以下:
#include <asm/semaphore.h> /* Architecture dependent header */
/* Statically declare a semaphore. To dynamically
create a semaphore, use init_MUTEX() */
static DECLARE_MUTEX(mysem);
down(&mysem); /* Acquire the semaphore */
/* ... Critical Section code ... */
up(&mysem); /* Release the semaphore */
1. 案例1:进程上下文,单CPU,非抢占内核
这种状况最为简单,不须要加锁,所以再也不赘述。
2. 案例2:进程和中断上下文,单CPU,非抢占内核
在这种状况下,为了保护临界区,仅仅须要禁止中断。如图2-4所示,假定进程上下文的执行单元A、B以及中断上下文的执行单元C都企图进入相同的临界区。

图2-4 进程和中断上下文进入临界区
因为执行单元C老是在中断上下文执行,它会优先于执行单元A和B,所以,它不用担忧保护的问题。执行单元A和B也没必要关心彼此会被互相打断,由于内核是 非抢占的。所以,执行单元A和B仅仅须要担忧C会在它们进入临界区的时候强行进入。为了实现此目的,它们会在进入临界区以前禁止中断:
Point A:
local_irq_disable(); /* Disable Interrupts in local CPU */
/* ... Critical Section ... */
local_irq_enable(); /* Enable Interrupts in local CPU */
可是,若是当执行到Point A的时候已经被禁止,local_irq_enable()将产生反作用,它会从新使能中断,而不是恢复以前的中断状态。能够这样修复它:
unsigned long flags;
Point A:
local_irq_save(flags); /* Disable Interrupts */
/* ... Critical Section ... */
local_irq_restore(flags); /* Restore state to what it was at Point A */
不论Point A的中断处于什么状态,上述代码都将正确执行。
3. 案例3:进程和中断上下文,单CPU,抢占内核
若是内核使能了抢占,仅仅禁止中断将没法确保对临界区的保护,由于另外一个处于进程上下文的执行单元可能会进入临界区。从新回到图2-4,如今,除了C以 外,执行单元A和B必须提防彼此。显而易见,解决该问题的方法是在进入临界区以前禁止内核抢占、中断,并在退出临界区的时候恢复内核抢占和中断。所以,执 行单元A和B使用了自旋锁API的irq变体:
unsigned long flags;
Point A:
/* Save interrupt state.
* Disable interrupts - this implicitly disables preemption */
spin_lock_irqsave(&mylock, flags);
/* ... Critical Section ... */
/* Restore interrupt state to what it was at Point A */
spin_unlock_irqrestore(&mylock, flags);
咱们不须要在最后显示地恢复Point A的抢占状态,由于内核自身会经过一个名叫抢占计数器的变量维护它。在抢占被禁止时(经过调用preempt_disable()),计数器值会增长;在 抢占被使能时(经过调用preempt_enable()),计数器值会减小。只有在计数器值为0的时候,抢占才发挥做用。
4. 案例4:进程和中断上下文,SMP机器,抢占内核
如今假设临界区执行于SMP机器上,并且你的内核配置了CONFIG_SMP和CONFIG_PREEMPT。
到目前为止讨论的场景中,自旋锁原语发挥的做用仅限于使能和禁止抢占和中断,时间的锁功能并未被彻底编译进来。在SMP机器内,锁逻辑被编译进来,并且自旋锁原语确保了SMP安全性。SMP使能的含义以下:
unsigned long flags;
Point A:
/*
- Save interrupt state on the local CPU
- Disable interrupts on the local CPU. This implicitly disables preemption.
- Lock the section to regulate access by other CPUs
*/
spin_lock_irqsave(&mylock, flags);
/* ... Critical Section ... */
/*
- Restore interrupt state and preemption to what it
was at Point A for the local CPU
- Release the lock
*/
spin_unlock_irqrestore(&mylock, flags);
在SMP系统上,获取自旋锁时,仅仅本CPU上的中断被禁止。所以,一个进程上下文的执行单元(图2-4中的执行单元A)在一个CPU上运行的同时,一 个中断处理函数(图2-4中的执行单元C)可能运行在另外一个CPU上。非本CPU上的中断处理函数必须自旋等待本CPU上的进程上下文代码退出临界区。中 断上下文须要调用spin_lock()/spin_unlock():
spin_lock(&mylock);
/* ... Critical Section ... */
spin_unlock(&mylock);
除了有irq变体之外,自旋锁也有底半部(BH)变体。在锁被获取的时候,spin_lock_bh()会禁止底半部,而spin_unlock_bh()则会在锁被释放时从新使能底半部。咱们将在第4章讨论底半部。
-rt树
实时(-rt)树,也被称做CONFIG_PREEMPT_RT补丁集,实现了内核中一些针对低延时的修改。该补丁集能够从 www.kernel.org/pub/linux/kernel/projects/rt下载,它容许内核的大部分位置可被抢占,可是用自旋锁代替了一 些互斥体。它也合并了一些高精度的定时器。数个-rt功能已经被融入了主线内核。详细的文档见http://rt.wiki.kernel.org/。
为了提升性能,内核也定义了一些针对特定环境的特定的锁原语。使能适用于代码执行场景的互斥机制将使代码更高效。下面来看一下这些特定的互斥机制。
2.5.2 原子操做
原子操做用于执行轻量级的、仅执行一次的操做,例如修改计数器、有条件的增长值、设置位等。原子操做能够确保操做的串行化,再也不须要锁进行并发访问保护。原子操做的具体实现取决于体系架构。
为了在释放内核网络缓冲区(称为skbuff)以前检查是否还有余留的数据引用,定义于net/core/skbuff.c文件中的skb_release_data()函数将进行以下操做:
1 if
(!skb->cloned ||
2 /* Atomically decrement and check if the returned value is zero */
3 !atomic_sub_return(skb->nohdr ? (1 << SKB_DATAREF_SHIFT) + 1 :
4 1,&skb_shinfo(skb)->dataref)) {
5 /* ... */
6 kfree(skb->head);
7 }
当skb_release_data()执行的时候,另外一个调用skbuff_clone()(也在net/core/skbuff.c文件中定义)的执行单元也许在同步地增长数据引用计数值:
/* ... */
/* Atomically bump up the data reference count */
atomic_inc(&(skb_shinfo(skb)->dataref));
/* ... */
原子操做的使用将确保数据引用计数不会被这两个执行单元“蹂躏”。它也消除了使用锁去保护单一整型变量的争论。
内核也支持set_bit()、clear_bit()和test_and_set_bit()操做,它们可用于原子地位修改。查看include/asm-your-arch/atomic.h文件能够看出你所在体系架构所支持的原子操做。
2.5.3 读—写锁
另外一个特定的并发保护机制是自旋锁的读—写锁变体。若是每一个执行单元在访问临界区的时候要么是读要么是写共享的数据结构,可是它们都不会同时进行读和写操做,那么这种锁是最好的选择。容许多个读线程同时进入临界区。读自旋锁能够这样定义:
rwlock_t myrwlock = RW_LOCK_UNLOCKED;
read_lock(&myrwlock); /* Acquire reader lock */
/* ... Critical Region ... */
read_unlock(&myrwlock); /* Release lock */
可是,若是一个写线程进入了临界区,那么其余的读和写都不容许进入。写锁的用法以下:
rwlock_t myrwlock = RW_LOCK_UNLOCKED;
write_lock(&myrwlock); /* Acquire writer lock */
/* ... Critical Region ... */
write_unlock(&myrwlock); /* Release lock */
net/ipx/ipx_route.c中的IPX路由代码是使用读—写锁的真实示例。一个称做ipx_routes_lock的读—写锁将保护IPX 路由表的并发访问。要经过查找路由表实现包转发的执行单元须要请求读锁。须要添加和删除路由表中入口的执行单元必须获取写锁。因为经过读路由表的状况比更 新路由表的状况多得多,使用读—写锁提升了性能。
和传统的自旋锁同样,读—写锁也有相应的irq变 体:read_lock_irqsave()、read_unlock_ irqrestore()、write_lock_irqsave()和write_unlock_irqrestore()。这些函数的含义与传统自旋 锁相应的变体类似。
2.6内核引入的顺序锁(seqlock)是一种支持写多于读的读—写锁。在一个变量的写操做比读操做多得多的状况 下,这种锁很是有用。前文讨论的jiffies_64变量就是使用顺序锁的一个例子。写线程没必要等待一个已经进入临界区的读,所以,读线程也许会发现它们 进入临界区的操做失败,所以须要重试:
u64 get_jiffies_64(void) /* Defined in kernel/time.c */
{
unsigned long seq;
u64 ret;
do {
seq = read_seqbegin(&xtime_lock);
ret = jiffies_64;
} while (read_seqretry(&xtime_lock, seq));
return ret;
}
写者会使用write_seqlock()和write_sequnlock()保护临界区。
2.6内核还引入了另外一种称为读—复制—更新(RCU)的机制。该机制用于提升读操做远多于写操做时的性能。其基本理念是读线程不须要加锁,可是写线程 会变得更加复杂,它们会在数据结构的一份副本上执行更新操做,并代替读者看到的指针。为了确保全部正在进行的读操做的完成,原子副本会一直被保持到全部 CPU上的下一次上下文切换。使用RCU的状况很复杂,所以,只有在确保你确实须要使用它而不是前文的其余原语的时候,才适宜选择它。 include/linux/ rcupdate.h文件中定义了RCU的数据结构和接口函数,Documentation/RCU/*提供了丰富的文档。
fs/dcache.c文件中包含一个RCU的使用示例。在Linux中,每一个文件都与一个目录入口信息(dentry结构体)、元数据信息(存放在 inode中)和实际的数据(存放在数据块中)关联。每次操做一个文件的时候,文件路径中的组件会被解析,相应的dentry会被获取。为了加速将来的操 做,dentry结构体被缓存在称为dcache的数据结构中。任什么时候候,对dcache进行查找的数量都远多于dcache的更新操做,所以,对 dcache的访问适宜用RCU原语进行保护。
2.5.4 调试
因为难于重现,并发相关的问 题一般很是难调试。在编译和测试代码的时候使能SMP(CONFIG_SMP)和抢占(CONFIG_PREEMPT)是一种很好的理念,即使你的产品将 运行在单CPU、禁止抢占的状况下。在Kernel hacking下有一个称为Spinlock and rw-lock debugging的配置选项(CONFIG_DEBUG_SPINLOCK),它能帮助你找到一些常见的自旋锁错误。 Lockmeter(http://oss.sgi. com/projects/lockmeter/)等工具可用于收集锁相关的统计信息。
在访问共享资源以前忘记加锁就会出现常见的并发问题。这会致使一些不一样的执行单元杂乱地“竞争”。这种问题(被称做“竞态”)可能会致使一些其余的行为。
在某些代码路径里忘记了释放锁也会出现并发问题,这会致使死锁。为了理解这个问题,让咱们分析以下代码:
spin_lock(&mylock); /* Acquire lock */
/* ... Critical Section ... */
if (error) { /* This error condition occurs rarely */
return -EIO; /* Forgot to release the lock! */
}
spin_unlock(&mylock); /* Release lock */
if (error)语句成立的话,任何要获取mylock的线程都会死锁,内核也可能所以而冻结。
若是在写完代码的数月或数年之后首次出现了问题,回过头来调试它将变得更为棘手。(在21.3.3节有一个相关的调试例子。)所以,为了不遭遇这种不快,在设计软件架构的时候,就应该考虑并发逻辑。
2.6 proc文件系统
proc文件系统(procfs)是一种虚拟的文件系统,它建立内核内部的视窗。浏览procfs时看到的数据是在内核运行过程当中产生的。procfs中的文件可被用于配置内核参数、查看内核结构体、从设备驱动程序中收集统计信息或者获取通用的系统信息。
procfs是一种虚拟的文件系统,这意味着驻留于procfs中的文件并不与物理存储设备如硬盘等关联。相反,这些文件中的数据由内核中相应的入口点按需动态建立。所以,procfs中的文件大小都显示为0。procfs一般在启动过程当中挂载在/proc目录,经过运行mount命令能够看出这一点。
为了了解procfs的能力,请查看/proc/cpuinfo、/proc/meminfo、/proc/interrupts、/proc/tty /driver /serial、/proc/bus/usb/devices和/proc/stat的内容。经过写/proc/sys/目录中的文件能够在运行时修改某 些内核参数。例如,经过向/proc/sys/kernel/printk文件回送一个新的值,能够改变内核printk日志的级别。许多实用程序(如 ps)和系统性能监视工具(如sysstat)就是经过驻留于/proc中的文件来获取信息的。
2.6内核引入的seq文件简化了大的procfs操做。附录C对此进行了描述。
2.7 内存分配
一些设备驱动程序必须意识到内存区的存在,另外,许多驱动程序须要内存分配函数的服务。本节咱们将简要地讨论这两点。
内核会以分页形式组织物理内存,而页大小则取决于具体的体系架构。在基于x86的机器上,其大小为4096B。物理内存中的每一页都有一个与之对应的struct page(定义在include/linux/ mm_types.h文件中):
在32位x86系统上,默认的内核配置会将4 GB的地址空间分红给用户空间的3 GB的虚拟内存空间和给内核空间的1 GB的空间(如图2-5所示)。这致使内核能处理的处理内存有1 GB的限制。现实状况是,限制为896 MB,由于地址空间的128 MB已经被内核数据结构占据。经过改变3 GB/1 GB的分割线,能够放宽这个限制,可是因为减小了用户进程虚拟地址空间的大小,在内存密集型的应用程序中可能会出现一些问题。

图2-5 32位PC系统上默认的地址空间分布
内核中用于映射低于896 MB物理内存的地址与物理地址之间存在线性偏移;这种内核地址被称做逻辑地址。在支持“高端内存”的状况下,在经过特定的方式映射这些区域产生对应的虚拟 地址后,内核将能访问超过896 MB的内存。全部的逻辑地址都是内核虚拟地址,而全部的虚拟地址并不是必定是逻辑地址。
所以,存在以下的内存区。
(1) ZONE_DMA(小于16 MB),该区用于直接内存访问(DMA)。因为传统的ISA设备有24条地址线,只能访问开始的16 MB,所以,内核将该区献给了这些设备。
(2) ZONE_NORMAL(16~896 MB),常规地址区域,也被称做低端内存。用于低端内存页的struct page结构中的“虚拟”字段包含了对应的逻辑地址。
(3) ZONE_HIGH(大于896 MB),仅仅在经过kmap()映射页为虚拟地址后才能访问。(经过kunmap()可去除映射。)相应的内核地址为虚拟地址而非逻辑地址。若是相应的页 未被映射,用于高端内存页的struct page结构体的“虚拟”字段将指向NULL。
kmalloc()是一个用于从ZONE_NORMAL区域返回连续内存的内存分配函数,其原型以下:
void *kmalloc(int count, int flags);
count是要分配的字节数,flags是一个模式说明符。支持的全部标志列在include/linux./gfp.h文件中(gfp是get free page的缩写),以下为经常使用标志。
(1) GFP_KERNEL,被进程上下文用来分配内存。若是指定了该标志,kmalloc()将被容许睡眠,以等待其余页被释放。
(2) GFP_ATOMIC,被中断上下文用来获取内存。在这种模式下,kmalloc()不容许进行睡眠等待,以得到空闲页,所以GFP_ATOMIC分配成功的可能性比用GFP_KERNEL低。
因为kmalloc()返回的内存保留了之前的内容,将它暴露给用户空间可到会致使安全问题,所以咱们能够使用kzalloc()得到被填充为0的内存。
若是须要分配大的内存缓冲区,并且也不要求内存在物理上有联系,能够用vmalloc()代替kmalloc():
void *vmalloc(unsigned long count);
count是要请求分配的内存大小。该函数返回内核虚拟地址。
vmalloc()须要比kmalloc()更大的分配空间,可是它更慢,并且不能从中断上下文调用。另外,不能用vmalloc()返回的物理上不连 续的内存执行DMA。在设备打开时,高性能的网络驱动程序一般会使用vmalloc()来分配较大的描述符环行缓冲区。
内核还提供了一些更复杂的内存分配技术,包括后备缓冲区(look aside buffer)、slab和mempool;这些概念超出了本章的讨论范围,再也不细述。
2.8 查看源代码
内存启动始于执行arch/x86/boot/目录中的实模式汇编代码。查看arch/x86/kernel/setup_32.c文件能够看出保护模式的内核怎样获取实模式内核收集的信息。
第一条信息来自于init/main.c中的代码,深刻挖掘init/calibrate.c能够对BogoMIPS校准理解得更清楚,而include/asm-your-arch/bugs.h则包含体系架构相关的检查。
内核中的时间服务由驻留于arch/your-arch/kernel/中的体系架构相关的部分和实现于kernel/timer.c中的通用部分组成。从include/linux/time*.h头文件中能够获取相关的定义。
jiffies定义于linux/jiffies.h文件中。HZ的值与处理器相关,能够从include/asm-your-arch/ param.h找到。
内存管理源代码存放在顶层mm/目录中。
表2-1给出了本章中主要的数据结构以及其在源代码树中定义的位置。表2-2则列出了本章中主要内核编程接口及其定义的位置。
表2-1 数据结构小结

表2-2 内核编程接口小结



Linux 驱动之模块参数--Linux设备驱动程序
模块参数
不少状况下,咱们指望经过参数来控制咱们的驱动的行为,好比因为系统的不一样,而为了保证咱们驱动有较好的移植性,咱们有时候指望经过传递参数来控制咱们驱动的行为,这样不一样的系统中,驱动可能有不一样的行为控制。
为了知足这种需求,内核容许对驱动程序指定参数,而这些参数可在加载驱动的过程当中动态的改变
参数的来源主要有两个
这个宏必须放在任何函数以外,一般实在源文件的头部
模块参数传递的方式
对于如何向模块传递参数,Linux kernel 提供了一个简单的框架。其容许驱动程序声明参数,而且用户在系统启动或模块装载时为参数指定相应值,在驱动程序里,参数的用法如同全局变量。
使用下面的宏时须要包含头文件<linux/moduleparam.h>
宏
module_param(name, type, perm);
module_param_array(name, type, num_point, perm); module_param_named(name_out, name_in, type, perm); module_param_string(name, string, len, perm); MODULE_PARM_DESC(name, describe);
参数类型
内核支持的模块参数类型以下
参数 |
描述 |
bool |
布尔类型(true/false),关联的变量类型应该死int |
intvbool |
bool的反值,例如赋值位true,可是实际值位false |
int |
整型 |
long |
长整型 |
short |
短整型 |
uint |
无符号整型 |
ulong |
无符号长整形型 |
ushort |
无符号短整型 |
charp |
字符指针类型,内核会为用户提供的字符串分配内存,并设置相应指针 |
关于数组类型怎么传递,咱们后面会谈到
注意
若是咱们须要的类型不在上面的清单中,模块代码中的钩子可以让咱们来指定这些类型。
具体的细节请参阅moduleparam.h文件。全部的模块参数都应该给定一个默认值;
insmod只会在用户明确设定了参数值的状况下才会改变参数的值,模块能够根据默认值来判断是否一个显示给定的值
访问权限
perm访问权限与linux文件爱你访问权限相同的方式管理,
如0644,或使用stat.h中的宏如S_IRUGO表示。
咱们鼓励使用stat.h中存在的定义。这个值用来控制谁可以访问sysfs中对模块参数的表述。
若是制定0表示彻底关闭在sysfs中相对应的项,不然的话,模块参数会在/sys/module中出现,并设置为给定的访问许可。
若是指定S_IRUGO,则任何人都可读取该参数,但不能修改
若是指定S_IRUGO | S_IWUSR 则容许root修改该值
注意
若是一个参数经过sysfs而被修改,则若是模块修改了这个参数的值同样,可是内核不会以任何方式通知模块,大多数状况下,咱们不该该让模块参数是可写的,除非咱们打算检测这种修改并作出相应的动做。
若是你只有ko文件却没有源码,想知道模块中到底有哪些模块参数,不着急,只须要用
modinfo -p ${modulename}
就能够看到个究竟啦。
对于已经加载到内核里的模块,若是想改变这些模块的模块参数该咋办呢?简单,只须要输入
echo -n ${value} > /sys/module/${modulename}/parameters/${param}
来修改便可。
示例
传递全局参数
在模块里面, 声明一个变量(全局变量),用来接收用户加载模块时传递的参数
module_param(name, type, perm);
参数 |
描述 |
name |
用来接收参数的变量名 |
type |
参数的数据类型 |
perm |
用于sysfs入口项系的访问可见性掩码 |
示例–传递int
这些宏不会声明变量,所以在使用宏以前,必须声明变量,典型地用法以下:
static int value = 0;
module_param(value, int, 0644); MODULE_PARM_DESC(value_int, "Get an value from user...\n");
使用
sudo insmod param.ko value=100
来进行加载
示例–传递charp
static char *string = "gatieme";
module_param(string, charp, 0644); MODULE_PARM_DESC(string, "Get an string(char *) value from user...\n");
使用
sudo insmod param.ko string="hello"
在模块内部变量的名字和加载模块时传递的参数名字不一样
前面那种状况下,外部参数的名字和模块内部的名字必须一致,那么有没有其余的绑定方法,能够是咱们的参数传递更加灵活呢?
使模块源文件内部的变 量名与外部的参数名有不一样的名字,经过module_param_named()定义。
module_param_named(name_out, name_in, type, perm);
参数 |
描述 |
name_out |
加载模块时,参数的名字 |
name_in |
模块内部变量的名字 |
type |
参数类型 |
perm |
访问权限 |
使用
static int value_in = 0;
module_param_named(value_out, value_in, int, 0644);
MODULE_PARM_DESC(value_in, "value_in named var_out...\n");
加载
sudo insmod param.ko value_out=200
传递字符串
加载模块的时候, 传递字符串到模块的一个全局字符数组里面
module_param_string(name, string, len, perm);
参数 |
描述 |
name |
在加载模块时,参数的名字 |
string |
模块内部的字符数组的名字 |
len |
模块内部的字符数组的大小 |
perm |
访问权限 |
static char buffer[20] = "gatieme";
module_param_string(buffer, buffer, sizeof(buffer), 0644); MODULE_PARM_DESC(value_charp, "Get an string buffer from user...\n");
传递数组
加载模块的时候, 传递参数到模块的数组中
module_param_array(name, type, num_point, perm);
参数 |
描述 |
name |
模块的数组名,也是外部制定的数组名 |
type |
模块数组的数据类型 |
num_point |
用来获取用户在加载模块时传递的参数个数,为NULL时,表示不关心用户传递的参数个数 |
perm |
访问权限 |
使用
static int array[3];
int num; module_param_array(array, int, &num, 0644); MODULE_PARM_DESC(array, "Get an array from user...\n");
指定描述信息
MODULE_PARM_DESC(name, describe);
参数 |
描述 |
name |
参数变量名 |
describe |
描述信息的字符串 |
使用modinfo查看参数
modinfo -p param.ko
param驱动源码
驱动源码param.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
Makefile
obj-m := param.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: make -C $(KERNELDIR) M=$(PWD) modules clean: make -C $(KERNELDIR) M=$(PWD) clean
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
参数传递过程
sudo insmod param.ko value=100 value_out=200 string="gatieme" buffer="Hello-World" array=100,200,300

dmesg查看

sudo rmmod param

使用modinfo查看参数
modinfo -p param.ko

动态修改模块参数
首先查看一下sysfs目录下的本模块参数信息
ls /sys/module/param/parameters

动态修改

Linux 驱动开发以内核模块开发 (一)—— 内核模块机制基础
1、内核模块的概念
一、什么是模块?
内核模块是一些可让操做系统内核在须要时载入和执行的代码,同时在不须要的时候能够卸载。这是一个好的功能,扩展了操做系统的内核功能,却不须要从新启动系统,是一种动态加载的技术。
特色:动态加载,随时载入,随时卸载,扩展功能
二、内核模块的加载做用
内核模块只是向linux内核预先注册本身,以便于未来的请求使用;由目标代码组成,没有造成完整的可执行程序。只是告诉内核,它有了新增的功能,而并不立刻使用(执行),只是等待应用程序的调用;而应用程序在加载后就开始执行。
三、内核模块所用函数
内核模块代码编写没有外部的函数库能够用,只能使用内核导出的函数。而应用程序习惯于使用外部的库函数,在编译时将程序与库函数连接在一块儿。例如对比printf( ) and printk( )。
所以驱动所用头文件均来自内核源代码,应用程序所用头文件来自库函数。
四、内核模块代码运行空间
内核代码运行在内核空间,而应用程序在用户空间。应用程序的运行会造成新的进程,而内核模块通常不会。每当应用程序执行系统调用时,linux执行模式从用户空间切换到内核空间。
2、linux内核模块的框架
最少两个入口点
*模块加载函数 module_init()
*模块卸载函数 module_exit()
module_init() and module_exit()两个宏定义声明模块的加载函数和卸载函数,这个定义在linux3.14/include/linux/init.h中。内容为:
#define module_init(x) __initcall(x)
//在内核启动或模块加载时执行
#define module_exit(x) __exitcall(x)
//在模块卸载时执行
每个模块只能有一个module_init 和一个module_exit。
下面咱们对比一下应用程序,看看应用程序与内核模块的区别:
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
|
#include <linux/module.h> //全部内核模块都必须包含这个头文件
#include<linux/kernel.h> //使用内核信息优先级时要包含这个
#include<linux/init.h> //一些初始化的函数如module_init()
static int hello_init(void)
{
printk("hello_init");
}
static void hello_exit(void)
{
printk("hello_exit \n");
}
MODULE_LICENSE("GPL"); //模块许可声明
module_init(hello_init); 加载时候调用该函数insmod
module_exit(hello_exit);卸载时候 rmmod
|
应用程序和内核模块对比总结以下
|
应用程序 |
模块 |
入口函数 |
main |
加载时候调用hello_init |
函数的调用 |
/lib |
全部函数能够直接调用 |
运行空间 |
用户空间 |
内核空间 |
资源的释放 |
系统自动释放
kill -9 pid 手动释放
|
手动释放 |
3、Linux 内核模块的编译和加载
其实内核的编译在前面就已经讲过了,如今回顾一下:
linux3.14内核的Makefile分为5个组成部分:
Makefile 最顶层的Makefile
.config 内核的当前配置文件,编译时成为定层Makefile的一部分
arch/$(ARCH)/Makefile 与体系结构相关的Makefile
s/ Makefile.* 一些Makefile的通用规则
kbuild Makefile 各级目录下的大概约500个文件,编译时根据上层Makefile传下来的宏定义和其余编译规则,将源代码编译成模块或者编入内核
顶层的Makefile文件读取 .config文件的内容,并整体上负责build内核和模块。Arch Makefile则提供补充体系结构相关的信息。 s目录下的Makefile文件包含了全部用来根据kbuild Makefile 构建内核所需的定义和规则。(其中.config的内容是在make menuconfig的时候,经过Kconfig文件配置的结果,上面已经说过)
对于大部份内核模块或设备驱动的开发者和使用者来讲,最常接触到的就是各层目录下
基于kbuild架构的kbuild Makefile文件。主要部分有:
一、目标定义
目标定义就是用来定义哪些内容要作为模块编译,哪些要编译连接进内核。
最简单的只有一行,如
obj-y += foo.o
表示要由foo.c或者foo.s文件编译获得foo.o并连接进内核,而obj-m则表示该文件要做为模块编译。除了y,m之外的obj-x形式的目标都不会被编译。
因为既能够编译成模块,也能够编译进内核,更常见的作法是根据.config文件的CONFIG_ 变量来决定文件的编译方式,如:
obj-$(CONFIG_HELLO_MODULE) += hello.o
除了obj-形式的目标之外,还有lib-y library库,hostprogs-y 主机程序等目标,可是基本都应用在特定的目录和场合下
二、多目标
一个内核模块由多个源文件编译而成,这是Makefile有所不一样。
采用模块名加 –objs后缀或者 –y后缀的形式来定义模块的组成文件。
如如下例子:
obj-$(CONFIG_EXT2_FS) += ext2.o
ext2-y := balloc.o bitmap.o
ext2-$(CONFIG_EXT2_FS_XATTR) += xattr.o
模 块的名字为ext2,由balloc.o和bitmap.o两个目标文件最终连接生成ext2.o 直至ext2.ko文件,是否包括xattr.o取决于内核配置文件的配置状况。若是CONFIG_EXT2_FS的值是y也没有关系,在此过程当中生成的 ext2.o将被连接进built-in.o最终连接进内核。这里须要注意的一点是,该kbuild Makefile所在的目录中不该该再包含和模块名相同的源文件如ext2.c/ext2.s
或者写成如-objs的形式:
obj-$(CONFIG_ISDN) += isdn.o
isdn-objs := isdn_net_lib.o isdn_v110.o isdn_common.o
三、目录的迭代
obj-$(CONFIG_EXT2_FS) += ext2/
若是CONFIG_EXT2_FS 的值为y或m,kbuild将会将ext2目录列入向下迭代的目标中,可是其做用也仅限于此,具体ext2目录下的文件是要做为模块编译仍是链入内核,仍是有ext2目录下的Makefile文件的内容来决定的
四、不一样的模块编译方式
编译模块的时候,你能够将模块放在代码树中,用Make modules的方式来编译你的模块,你也能够将模块相关文件目录放在代码树之外的位置,用以下命令来编译模块:
make -C path/to/kernel/src M=$PWD modules
-C指定内核源码的根目录,$PWD 或 `PWD` 是当前目录的环境变量,告诉kbuild回到当前目录来执行build操做。
五、模块安装
当你须要将模块安装到非默认位置的时候,你能够用INSTALL_MOD_PATH 指定一个前缀,如:
make INSTALL_MOD_PATH=/foo modules_install
模块将被安装到 /foo/lib/modules目录下
注:内核模块是.ko后缀,使内核模块和普通的目标文件区别开。
Linux 驱动开发以内核模块开发 (二)—— 内核模块编译 Makefile 入门
1、模块的编译
咱们在前面内核编译中驱动移植那块,讲到驱动编译分为静态编译和动态编译;静态编译即为将驱动直接编译进内核,动态编译即为将驱动编译成模块。
而动态编译又分为两种:
a -- 内部编译
在内核源码目录内编译
b -- 外部编译
在内核源码的目录外编译
2、具体编译过程分析
注:本次编译是外部编译,使用的内核源码是Ubuntu 的源代码,而非开发板所用linux 3.14内核源码,运行平台为X86。
对于一个普通的linux设备驱动模块,如下是一个经典的makefile代码,使用下面这个makefile能够完成大部分驱动的编译,使用时只须要修改一下要编译生成的驱动名称便可。只需修改obj-m的值。
ifneq ($(KERNELRELEASE),)
obj-m:=hello.o
else
KDIR := /lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.symvers *.cmd *.cmd.o
endif
|
一、makefile 中的变量
先说明如下makefile中一些变量意义:
(1)KERNELRELEASE 在linux内核源代码中的顶层makefile中有定义
(2)shell pwd 取得当前工做路径
(3)shell uname -r 取得当前内核的版本号
(4)KDIR 当前内核的源代码目录。
关于linux源码的目录有两个,分别为
"/lib/modules/$(shell uname -r)/build"
"/usr/src/linux-header-$(shell uname -r)/"
但若是编译过内核就会知道,usr目录下那个源代码通常是咱们本身下载后解压的,而lib目录下的则是在编译时自动copy过去的,二者的文件结构彻底同样,所以有时也将内核源码目录设置成/usr/src/linux-header-$(shell uname -r)/。关于内核源码目录能够根据本身的存放位置进行修改。
(5)make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
这就是编译模块了:
a -- 首先改变目录到-C选项指定的位置(即内核源代码目录),其中保存有内核的顶层makefile;
b -- M=选项让该makefile在构造modules目标以前返回到模块源代码目录;而后,modueles目标指向obj-m变量中设定的模块;在上面的例子中,咱们将该变量设置成了hello.o。
二、make 的的执行步骤
a -- 第一次进来的时候,宏“KERNELRELEASE”未定义,所以进入 else;
b -- 记录内核路径,记录当前路径;
因为make 后面没有目标,因此make会在Makefile中的第一个不是以.开头的目标做为默认的目标执行。默认执行all这个规则
c -- make -C $(KDIR) M=$(PWD) modules
-C 进入到内核的目录执行Makefile ,在执行的时候KERNELRELEASE就会被赋值,M=$(PWD)表示返回当前目录,再次执行makefile,modules 编译成模块的意思
因此这里实际运行的是
make -C /lib/modules/2.6.13-study/build M=/home/fs/code/1/module/hello/ modules
d -- 再次执行该makefile,KERNELRELEASE就有值了,就会执行obj-m:=hello.o
obj-m:表示把hello.o 和其余的目标文件连接成hello.ko模块文件,编译的时候还要先把hello.c编译成hello.o文件
能够看出make在这里一共调用了3次
1)-- make
2)-- linux内核源码树的顶层makedile调用,产生。o文件
3)-- linux内核源码树makefile调用,把.o文件连接成ko文件
三、编译多文件
如有多个源文件,则采用以下方法:
obj-m := hello.o
hello-objs := file1.o file2.o file3.o
3、内部编译简单说明
若是把hello模块移动到内核源代码中。例如放到/usr/src/linux/driver/中, KERNELRELEASE就有定义了。
在/usr/src/linux/Makefile中有KERNELRELEASE=$(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION)$(LOCALVERSION)。
这时候,hello模块也再也不是单独用make编译,而是在内核中用make modules进行编译,此时驱动模块便和内核编译在一块儿。
Linux 驱动开发以内核模块开发 (三)—— 模块传参
1、module_param() 定义
一般在用户态下编程,即应用程序,能够经过main()的来传递命令行参数,而编写一个内核模块,则经过module_param() 来传参。
module_param()宏是Linux 2.6内核中新增的,该宏被定义在include/linux/moduleparam.h文件中,具体定义以下:
#define module_param(name, type, perm) module_param_named(name, name, type, perm)
因此咱们经过宏module_param()定义一个模块参数:
module_param(name, type, perm);
参数的意义:
name 既是用户看到的参数名,又是模块内接受参数的变量;
type 表示参数的数据类型,是下列之一:byte, short, ushort, int, uint, long, ulong, charp, bool, invbool;
perm 指定了在sysfs中相应文件的访问权限。访问权限与linux文件访问权限相同的方式管理,如0644,或使用stat.h中的宏如S_IRUGO表示。
0表示彻底关闭在sysfs中相对应的项。
2、module_param() 使用方法
module_param()宏不会声明变量,所以在使用宏以前,必须声明变量,典型地用法以下:
static unsigned int int_var = 0;
module_param(int_var, uint, S_IRUGO);
这些必须写在模块源文件的开头部分。即int_var是全局的。也能够使模块源文件内部的变量名与外部的参数名有不一样的名字,经过module_param_named()定义。
a -- module_param_named()
module_param_named(name, variable, type, perm);
name 外部(用户空间)可见的参数名;
variable 源文件内部的全局变量名;
type 类型
perm 权限
而module_param经过module_param_named实现,只不过name与variable相同。
例如:
static unsigned int max_test = 9;
module_param_name(maximum_line_test, max_test, int, 0);
b -- 字符串参数
若是模块参数是一个字符串时,一般使用charp类型定义这个模块参数。内核复制用户提供的字符串到内存,而且相对应的变量指向这个字符串。
例如:
static char *name;
module_param(name, charp, 0);
另外一种方法是经过宏module_param_string()让内核把字符串直接复制到程序中的字符数组内。
module_param_string(name, string, len, perm);
这里,name是外部的参数名,string是内部的变量名,len是以string命名的buffer大小(能够小于buffer的大小,可是没有意义),perm表示sysfs的访问权限(或者perm是零,表示彻底关闭相对应的sysfs项)。
例如:
static char species[BUF_LEN];
module_param_string(specifies, species, BUF_LEN, 0);
c -- 数组参数
数组参数, 用逗号间隔的列表提供的值, 模块加载者也支持. 声明一个数组参数, 使用:
module_param_array(name, type, num, perm);
name 数组的名子(也是参数名),
type 数组元素的类型,
num 一个整型变量,
perm 一般的权限值.
若是数组参数在加载时设置, num被设置成提供的数的个数. 模块加载者拒绝比数组能放下的多的值。
3、使用实例
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/moduleparam.h>
- MODULE_LICENSE ("GPL");
-
- static char *who = "world";
- static int times = 1;
- module_param (times, int, S_IRUSR);
- module_param (who, charp, S_IRUSR);
-
- static int hello_init (void)
- {
- int i;
- for (i = 0; i < times; i++)
- printk (KERN_ALERT "(%d) hello, %s!\n", i, who);
- return 0;
- }
-
- static void hello_exit (void)
- {
- printk (KERN_ALERT "Goodbye, %s!\n", who);
- }
-
- module_init (hello_init);
- module_exit (hello_exit);
编译生成可执行文件hello
# insmod hello.ko who="world" times=5
- #(1) hello, world!
- #(2) hello, world!
- #(3) hello, world!
- #(4) hello, world!
- #(5) hello, world!
- # rmmod hello
- # Goodbye,world!
注:
a -- 若是加载模块hello时,没有输入任何参数,那么who的初始值为"world",times的初始值为1
b -- 同时向指针传递字符串的时候,不能传递这样的字符串 who="hello world!".即字符串中间不能有空格
c --/sys/module/hello/parameters 该目录下生成变量对应的文件节点
Linux 驱动开发以内核模块开发(四)—— 符号表的导出
Linux内核头文件提供了一个方便的方法用来管理符号的对模块外部的可见性,所以减小了命名空间的污染(命名空间的名称可能会与内核其余地方定义的名称冲突),而且适当信息隐藏。 若是你的模块须要输出符号给其余模块使用,应当使用下面的宏定义:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name); //只适用于包含GPL许可权的模块;
这两个宏均用于将给定的符号导出到模块外. _GPL版本的宏定义只能使符号对GPL许可的模块可用。 符号必须在模块文件的全局部分导出,不能在函数中导出,这是由于上述这两个宏将被扩展成一个特殊用途的声明,而该变量必须是全局的。这个变量存储于模块的一个特殊的可执行部分(一个"ELF段" ),在装载时,内核经过这个段来寻找模块导出的变量(感兴趣的读者能够看<linux/module.h>获知更详细的信息)。
1、宏定义EXPORT_SYMBOL分析
一、源码
- <include/linux/moudule.h>
-
- …….
-
- #ifndef MODULE_SYMBOL_PREFIX
- #define MODULE_SYMBOL_PREFIX ""
- #endif
-
- …….
-
- struct kernel_symbol
- {
- unsignedlong value;
- constchar *name;
-
- };
-
- ……
-
- #define __EXPORT_SYMBOL(sym,sec) \
- externtypeof(sym) sym; \
- __CRC_SYMBOL(sym,sec) \
- staticconst char __kstrtab_##sym[] \
- __attribute__((section(“__ksymtab_strings”),aligned(1))) \
- =MODULE_SYMBOL_PREFIX#sym; \
- staticconst struct kernel_symbol __ksymtab_##sym \
- __used \
- __attribute__((section(“__ksymatab”sec),unused)) \
- ={(unsignedlong)&sym,_kstrab_#sym}
-
- #define EXPORT_SYMBOL(sym) \
- __EXPOTR_SYMBOL(sym,””)
-
- #define EXPORT_SYMBOL_GPL(sym) \
- __EXPOTR_SYMBOL(sym,”_gpl”)
-
- #define EXPORT_SYMBOL(sym) \
- __EXPOTR_SYMBOL(sym,”_gpl_future”)
在分析前,先了解以下相关知识:
1)#运算符,##运算符
一般在宏定义中使用#来建立字符串 #abc就表示字符串”abc”等。
##运算符称为预处理器的粘合剂,用来替换粘合两个不一样的符号,
如:#define xName (n) x##n
则xName(4) 则变为x4
2)gcc的 __attribute__ 属性:
__attribute__((section(“section_name”)))的做用是将指定的函数或变量放入到名为”section_name”的段中。
__attribute__属性添加能够在函数或变量定义的时候直接加入在定义语句中。
如:
int myvar__attribute__((section("mydata"))) = 0;
表示定义了整形变量myvar=0;而且将该变量存放到名为”mydata”的section中
关于gcc_attribute详解能够参考:http://blog.sina.com.cn/s/blog_661314940100qujt.html
二、EXPORT_SYMBOL的做用是什么?
EXPORT_SYMBOL标签内定义的函数或者符号对所有内核代码公开,不用修改内核代码就能够在您的内核模块中直接调用,即便用EXPORT_SYMBOL能够将一个函数以符号的方式导出给其余模块使用。
这里要和System.map作一下对比:System.map 中的是链接时的函数地址。链接完成之后,在2.6内核运行过程当中,是不知道哪一个符号在哪一个地址的。
EXPORT_SYMBOL的符号,是把这些符号和对应的地址保存起来,在内核运行的过程当中,能够找到这些符号对应的地址。而模块在加载过程当中,其本质就是能动态链接到内核,若是在模块中引用了内核或其它模块的符号,就要EXPORT_SYMBOL这些符号,这样才能找到对应的地址链接。
2、 EXPORT_SYMBOL使用方法
第1、在模块函数定义以后使用EXPORT_SYMBOL(函数名)
第2、在调用该函数的模块中使用extern对之声明
第3、首先加载定义该函数的模块,再加载调用该函数的模块
要调用别的模块实现的函数接口和全局变量,就要导出符号 /usr/src/linux-headers-2.6.32-33-generic/Module.symvers
A |
B |
static int num =10;
static void show(void)
{
printk("%d \n",num);
}
EXPORT_SYMBOL(show);
|
extern void show(void);
|
函数A先将show() 函数导出,函数B 使用extern 对其声明,要注意:
a -- 编译a模块后,要将 Module.symvers 拷贝到b模块下
b -- 而后才能编译b模块
c -- 加载:先加载a模块,再加载b模块
d -- 卸载:先卸载b模块,再卸载a模块
3、示例
代码a ,hello.c
- #include <linux/module.h>
-
- static int num =10;
- static void show(void)
- {
- printk("show(),num = %d\n",num);
- }
- static int hello_init(void)
- {
- printk("hello_init");
- return 0;
- }
- static void hello_exit(void)
- {
- printk("hello_exit \n");
- }
- EXPORT_SYMBOL(show);
- MODULE_LICENSE("GPL");
- module_init(hello_init);
- module_exit(hello_exit);
代码b show.c
- #include <linux/module.h>
-
- extern void show(void);
-
- static int show_init(void)
- {
- printk("show_init");
- show();
- return 0;
- }
- static void show_exit(void)
- {
- printk("show_exit \n");
- }
-
- MODULE_LICENSE("GPL");
-
- module_init(show_init);
- module_exit(show_exit);<strong>
- </strong>
编译后加载模块,卸载模块,能够用
dmesg 查看内核打印信息。
Linux 设备驱动开发 —— Tasklets 机制浅析
一 、Tasklets 机制基础知识点
一、Taklets 机制概念
Tasklets 机制是linux中断处理机制中的软中断延迟机制。一般用于减小中断处理的时间,将本应该是在中断服务程序中完成的任务转化成软中断完成。
为了最大程度的避免中断处理时间过长而致使中断丢失,有时候咱们须要把一些在中断处理中不是很是紧急的任务放在后面执行,而让中断处理程序尽快返回。在老版本的 linux 中一般将中断处理分为 top half handler 、 bottom half handler 。利用 top half handler 处理中断必须处理的任务,而 bottom half handler 处理不是太紧急的任务。
可是 linux2.6 之后的 linux 采起了另一种机制,就是软中断来代替 bottom half handler 的处理。而 tasklet 机制正是利用软中断来完成对驱动 bottom half 的处理。 Linux2.6 中软中断一般只有固定的几种: HI_SOFTIRQ( 高优先级的 tasklet ,一种特殊的 tasklet) 、 TIMER_SOFTIRQ (定时器)、 NET_TX_SOFTIRQ (网口发送)、 NET_RX_SOFTIRQ (网口接收) 、 BLOCK_SOFTIRQ (块设备)、 TASKLET_SOFTIRQ (普通 tasklet )。固然也能够经过直接修改内核本身加入本身的软中断,可是通常来讲这是不合理的,软中断的优先级比较高,若是不是在内核处理频繁的任务不建议使用。一般驱动用户使用 tasklet 足够了。
机制流程:当linux接收到硬件中断以后,经过 tasklet 函数来设定软中断被执行的优先程度从而致使软中断处理函数被优先执行的差别性。
特色:tasklet的优先级别较低,并且中断处理过程当中能够被打断。但被打断以后,还能进行自我恢复,断点续运行。
二、Tasklets 解决什么问题?
a -- tasklet是I/O驱动程序中实现可延迟函数的首选方法;
b -- tasklet和工做队列是延期执行工做的机制,其实现基于软中断,但他们更易于使用,于是更适合与设备驱动程序...tasklet是“小进程”,执行一些迷你任务,对这些人物使用全功能进程可能比较浪费。
c -- tasklet是并行可执行(可是是锁密集型的)软件中断和旧下半区的一种混合体,这里既谈不上并行性,也谈不上性能。引入tasklet是为了替代原来的下半区。
软中断是将操做推迟到将来时刻执行的最有效的方法。但该延期机制处理起来很是复杂。由于多个处理器能够同时且独立的处理软中断,同一个软中断的处理程序能够在几个CPU上同时运行。对软中断的效率来讲,这是一个关键,多处理器系统上的网络实现显然受惠于此。但处理程序的设计必须是彻底可重入且线程安全的。另外,临界区必须用自旋锁保护(或其余IPC机制),而这须要大量审慎的考虑。
我本身的理解,因为软中断以ksoftirqd的形式与用户进程共同调度,这将关系到OS总体的性能,所以软中断在Linux内核中也仅仅就几个(网络、时钟、调度以及Tasklet等),在内核编译时肯定。软中断这种方法显然不是面向硬件驱动的,而是驱动更上一层:不关心如何从具体的网卡接收数据包,可是从全部的网卡接收的数据包都要通过内核协议栈的处理。并且软中断比较“硬”——数量固定、编译时肯定、操做函数必须可重入、须要慎重考虑锁的问题,不适合驱动直接调用,所以Linux内核为驱动直接提供了一种使用软中断的方法,就是tasklet。
软中断和 tasklet 的关系以下图:

上图能够看出, ksoftirqd 是一个后台运行的内核线程,它会周期的遍历软中断的向量列表,若是发现哪一个软中断向量被挂起了( pend ),就执行对应的处理函数,对于 tasklet 来讲,此处理函数就是 tasklet_action ,这个处理函数在系统启动时初始化软中断的就挂接了。Tasklet_action 函数,遍历一个全局的 tasklet_vec 链表(此链表对于 SMP 系统是每一个 CPU 都有一个),此链表中的元素为 tasklet_struct 。下面将介绍各个函数
2、tasklet数据结构
tasklet经过软中断实现,软中断中有两种类型属于tasklet,分别是级别最高的HI_SOFTIRQ和TASKLET_SOFTIRQ。
Linux内核采用两个PER_CPU的数组tasklet_vec[]和tasklet_hi_vec[]维护系统种的全部tasklet(kernel/softirq.c),分别维护TASKLET_SOFTIRQ级别和HI_SOFTIRQ级别的tasklet:
- struct tasklet_head
- {
- struct tasklet_struct *head;
- struct tasklet_struct *tail;
- };
-
- static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
- static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

tasklet的核心结构体以下(include/linux/interrupt.h):
- struct tasklet_struct
- {
- struct tasklet_struct *next;
- unsigned long state;
- atomic_t count;
- void (*func)(unsigned long);
- unsigned long data;
- };
各成员的含义以下:
a -- next指针:指向下一个tasklet的指针。
b -- state:定义了这个tasklet的当前状态。这一个32位的无符号长整数,当前只使用了bit[1]和bit[0]两个状态位。其中,bit[1]=1表示这个tasklet当前正在某个CPU上被执行,它仅对SMP系统才有意义,其做用就是为了防止多个CPU同时执行一个tasklet的情形出现;bit[0]=1表示这个tasklet已经被调度去等待执行了。对这两个状态位的宏定义以下所示(interrupt.h)
- enum
- {
- TASKLET_STATE_SCHED,
- TASKLET_STATE_RUN
- };
TASKLET_STATE_SCHED置位表示已经被调度(挂起),也意味着tasklet描述符被插入到了tasklet_vec和tasklet_hi_vec数组的其中一个链表中,能够被执行。TASKLET_STATE_RUN置位表示该tasklet正在某个CPU上执行,单个处理器系统上并不校验该标志,由于不必检查特定的tasklet是否正在运行。
c -- 原子计数count:对这个tasklet的引用计数值。NOTE!只有当count等于0时,tasklet代码段才能执行,也即此时tasklet是被使能的;若是count非零,则这个tasklet是被禁止的。任何想要执行一个tasklet代码段的人都首先必须先检查其count成员是否为0。
d -- 函数指针func:指向以函数形式表现的可执行tasklet代码段。
e -- data:函数func的参数。这是一个32位的无符号整数,其具体含义可供func函数自行解释,好比将其解释成一个指向某个用户自定义数据结构的地址值。
3、tasklet操做接口
tasklet对驱动开放的经常使用操做包括:
a -- 初始化,tasklet_init(),初始化一个tasklet描述符。
b -- 调度,tasklet_schedule()和tasklet_hi_schedule(),将taslet置位TASKLET_STATE_SCHED,并尝试激活所在的软中断。
c -- 禁用/启动,tasklet_disable_nosync()、tasklet_disable()、task_enable(),经过count计数器实现。
d -- 执行,tasklet_action()和tasklet_hi_action(),具体的执行软中断。
e -- 杀死,tasklet_kill()
即驱动程序在初始化时,经过函数task_init创建一个tasklet,而后调用函数tasklet_schedule将这个tasklet放在 tasklet_vec链表的头部,并唤醒后台线程ksoftirqd。当后台线程ksoftirqd运行调用__do_softirq时,会执行在中断向量表softirq_vec里中断号TASKLET_SOFTIRQ对应的tasklet_action函数,而后tasklet_action遍历 tasklet_vec链表,调用每一个tasklet的函数完成软中断操做。
一、tasklet_int()函数实现以下(kernel/softirq.c)
用来初始化一个指定的tasklet描述符
- void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
- {
- t->next = NULL;
- t->state = 0;
- atomic_set(&t->count, 0);
- t->func = func;
- t->data = data;
- }
二、tasklet_schedule()函数
与tasklet_hi_schedule()函数的实现很相似,这里只列tasklet_schedule()函数的实现(kernel/softirq.c),都挺明白就不描述了:
- static inline void tasklet_schedule(struct tasklet_struct *t)
- {
- if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
- __tasklet_schedule(t);
- }
-
- void __tasklet_schedule(struct tasklet_struct *t)
- {
- unsigned long flags;
- local_irq_save(flags);
- t->next = NULL;
- *__this_cpu_read(tasklet_vec.tail) = t;
- __this_cpu_write(tasklet_vec.tail, &(t->next));
- raise_softirq_irqoff(TASKLET_SOFTIRQ);
- local_irq_restore(flags);
- }
该函数的参数t指向要在当前CPU上被执行的tasklet。对该函数的NOTE以下:
a -- 调用test_and_set_bit()函数将待调度的tasklet的state成员变量的bit[0]位(也即TASKLET_STATE_SCHED位)设置为1,该函数同时还返回TASKLET_STATE_SCHED位的原有值。所以若是bit[0]为的原有值已经为1,那就说明这个tasklet已经被调度到另外一个CPU上去等待执行了。因为一个tasklet在某一个时刻只能由一个CPU来执行,所以tasklet_schedule()函数什么也不作就直接返回了。不然,就继续下面的调度操做。
b -- 首先,调用local_irq_save()函数来关闭当前CPU的中断,以保证下面的步骤在当前CPU上原子地被执行。
c -- 而后,将待调度的tasklet添加到当前CPU对应的tasklet队列的首部。
d -- 接着,调用__cpu_raise_softirq()函数在当前CPU上触发软中断请求TASKLET_SOFTIRQ。
e -- 最后,调用local_irq_restore()函数来开当前CPU的中断。
三、tasklet_disable()函数、task_enable()函数以及tasklet_disable_nosync()函数(include/linux/interrupt.h)
使能与禁止操做每每老是成对地被调用的
- static inline void tasklet_disable_nosync(struct tasklet_struct *t)
- {
- atomic_inc(&t->count);
- smp_mb__after_atomic_inc();
- }
-
- static inline void tasklet_disable(struct tasklet_struct *t)
- {
- tasklet_disable_nosync(t);
- tasklet_unlock_wait(t);
- smp_mb();
- }
-
- static inline void tasklet_enable(struct tasklet_struct *t)
- {
- smp_mb__before_atomic_dec();
- atomic_dec(&t->count);
- }
四、tasklet_action()函数在softirq_init()函数中被调用:
- void __init softirq_init(void)
- {
- ...
-
- open_softirq(TASKLET_SOFTIRQ, tasklet_action);
- open_softirq(HI_SOFTIRQ, tasklet_hi_action);
- }
tasklet_action()函数
- static void tasklet_action(struct softirq_action *a)
- {
- struct tasklet_struct *list;
- local_irq_disable();
- list = __this_cpu_read(tasklet_vec.head);
- __this_cpu_write(tasklet_vec.head, NULL);
- __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);
- local_irq_enable();
-
- while (list)
- {
- struct tasklet_struct *t = list;
- list = list->next;
- if (tasklet_trylock(t))
- {
- if (!atomic_read(&t->count))
- {
- if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
- BUG();
- t->func(t->data);
- tasklet_unlock(t);
- continue;
- }
-
- tasklet_unlock(t);
- }
-
- local_irq_disable();
- t->next = NULL;
- *__this_cpu_read(tasklet_vec.tail) = t;
- __this_cpu_write(tasklet_vec.tail, &(t->next));
- __raise_softirq_irqoff(TASKLET_SOFTIRQ);
- local_irq_enable();
- }
- }
注释以下:
①首先,在当前CPU关中断的状况下,“原子”地读取当前CPU的tasklet队列头部指针,将其保存到局部变量list指针中,而后将当前CPU的tasklet队列头部指针设置为NULL,以表示理论上当前CPU将再也不有tasklet须要执行(但最后的实际结果却并不必定如此,下面将会看到)。
②而后,用一个while{}循环来遍历由list所指向的tasklet队列,队列中的各个元素就是将在当前CPU上执行的tasklet。循环体的执行步骤以下:
a -- 用指针t来表示当前队列元素,即当前须要执行的tasklet。
b -- 更新list指针为list->next,使它指向下一个要执行的tasklet。
c -- 用tasklet_trylock()宏试图对当前要执行的tasklet(由指针t所指向)进行加锁
若是加锁成功(当前没有任何其余CPU正在执行这个tasklet),则用原子读函atomic_read()进一步判断count成员的值。若是count为0,说明这个tasklet是容许执行的,因而:
(1)先清除TASKLET_STATE_SCHED位;
(2)而后,调用这个tasklet的可执行函数func;
(3)执行barrier()操做;
(4)调用宏tasklet_unlock()来清除TASKLET_STATE_RUN位。
(5)最后,执行continue语句跳过下面的步骤,回到while循环继续遍历队列中的下一个元素。若是count不为0,说明这个tasklet是禁止运行的,因而调用tasklet_unlock()清除前面用tasklet_trylock()设置的TASKLET_STATE_RUN位。
若是tasklet_trylock()加锁不成功,或者由于当前tasklet的count值非0而不容许执行时,咱们必须将这个tasklet从新放回到当前CPU的tasklet队列中,以留待这个CPU下次服务软中断向量TASKLET_SOFTIRQ时再执行。为此进行这样几步操做:
(1)先关CPU中断,以保证下面操做的原子性。
(2)把这个tasklet从新放回到当前CPU的tasklet队列的首部;
(3)调用__cpu_raise_softirq()函数在当前CPU上再触发一次软中断请求TASKLET_SOFTIRQ;
(4)开中断。
c -- 最后,回到while循环继续遍历队列。
五、tasklet_kill()实现
- void tasklet_kill(struct tasklet_struct *t)
- {
- if (in_interrupt())
- printk("Attempt to kill tasklet from interruptn");
- while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
- {
- do {
- yield();
- } while (test_bit(TASKLET_STATE_SCHED, &t->state));
- }
-
- tasklet_unlock_wait(t);
- clear_bit(TASKLET_STATE_SCHED, &t->state);
- }
4、一个tasklet调用例子
找了一个tasklet的例子看一下(drivers/usb/atm,usb摄像头),在其自举函数usbatm_usb_probe()中调用了tasklet_init()初始化了两个tasklet描述符用于接收和发送的“可延迟操做处理”,但此是并无将其加入到tasklet_vec[]或tasklet_hi_vec[]中:
- tasklet_init(&instance->rx_channel.tasklet,
- usbatm_rx_process, (unsigned long)instance);
- tasklet_init(&instance->tx_channel.tasklet,
- usbatm_tx_process, (unsigned long)instance);
在其发送接口usbatm_atm_send()函数调用tasklet_schedule()函数将所初始化的tasklet加入到当前cpu的tasklet_vec链表尾部,并尝试调用do_softirq_irqoff()执行软中断TASKLET_SOFTIRQ:
- static int usbatm_atm_send(struct atm_vcc *vcc, struct sk_buff *skb)
- {
- ...
-
- tasklet_schedule(&instance->tx_channel.tasklet);
-
- ...
- }
在其断开设备的接口usbatm_usb_disconnect()中调用tasklet_disable()函数和tasklet_enable()函数从新启动其收发tasklet(具体缘由不详,这个地方可能就是由这个须要,暂时重启收发tasklet):
- void usbatm_usb_disconnect(struct usb_interface *intf)
- {
- ...
-
- tasklet_disable(&instance->rx_channel.tasklet);
- tasklet_disable(&instance->tx_channel.tasklet);
-
- ...
-
- tasklet_enable(&instance->rx_channel.tasklet);
- tasklet_enable(&instance->tx_channel.tasklet);
-
- ...
- }
在其销毁接口usbatm_destroy_instance()中调用tasklet_kill()函数,强行将该tasklet踢出调度队列。
从上述过程以及tasklet的设计能够看出,tasklet总体是这么运行的:驱动应该在其硬中断处理函数的末尾调用tasklet_schedule()接口激活该tasklet;内核常常调用do_softirq()执行软中断,经过softirq执行tasket,以下图所示。图中灰色部分为禁止硬中断部分,为保护软中断pending位图和tasklet_vec链表数组,count的改变均为原子操做,count确保SMP架构下同时只有一个CPU在执行该tasklet:

进程上下文、中断上下文及原子上下文
谈论进程上下文 、中断上下文 、 原子上下文以前,有必要讨论下两个概念:
a -- 上下文
上下文是从英文context翻译过来,指的是一种环境。相对于进程而言,就是进程执行时的环境;
具体来讲就是各个变量和数据,包括全部的寄存器变量、进程打开的文件、内存信息等。
b -- 原子
原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操做(atomic operation)意为"不可被中断的一个或一系列操做" ;
1、为何会有上下文这种概念
内核空间和用户空间是现代操做系统的两种工做模式,内核模块运行在内核空间,而用户态应用程序运行在用户空间。它们表明不一样的级别,而对系统资源具备不一样的访问权限。内核模块运行在最高级别(内核态),这个级下全部的操做都受系统信任,而应用程序运行在较低级别(用户态)。在这个级别,处理器控制着对硬件的直接访问以及对内存的非受权访问。内核态和用户态有本身的内存映射,即本身的地址空间。
其中处理器总处于如下状态中的一种:
内核态,运行于进程上下文,内核表明进程运行于内核空间;
内核态,运行于中断上下文,内核表明硬件运行于内核空间;
用户态,运行于用户空间。
系统的两种不一样运行状态,才有了上下文的概念。用户空间的应用程序,若是想请求系统服务,好比操做某个物理设备,映射设备的地址到用户空间,必须经过系统调用来实现。(系统调用是操做系统提供给用户空间的接口函数)。
经过系统调用,用户空间的应用程序就会进入内核空间,由内核表明该进程运行于内核空间,这就涉及到上下文的切换,用户空间和内核空间具备不一样的 地址映射,通用或专用的寄存器组,而用户空间的进程要传递不少变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户 空间继续执行,
2、进程上下文
所谓的进程上下文,就是一个进程在执行的时候,CPU的全部寄存器中的值、进程的状态以及堆栈上的内容,当内核须要切换到另外一个进程时,它 须要保存当前进程的全部状态,即保存当前进程的进程上下文,以便再次执行该进程时,可以恢复切换时的状态,继续执行。
一个进程的上下文能够分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
用户级上下文: 正文、数据、用户堆栈以及共享存储区;
寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。
当发生进程调度时,进行进程切换就是上下文切换(context switch)。
操做系统必须对上面提到的所有信息进行切换,新调度的进程才能运行。而系统调用进行的是模式切换(mode switch)。模式切换与进程切换比较起来,容易不少,并且节省时间,由于模式切换最主要的任务只是切换进程寄存器上下文的切换。
进程上下文主要是异常处理程序和内核线程。内核之因此进入进程上下文是由于进程自身的一些工做须要在内核中作。例如,系统调用是为当前进程服务的,异常一般是处理进程致使的错误状态等。因此在进程上下文中引用current是有意义的。
3、中断上下文
硬件经过触发信号,向CPU发送中断信号,致使内核调用中断处理程序,进入内核空间。这个过程当中,硬件的一些变量和参数也要传递给内核, 内核经过这些参数进行中断处理。
因此,“中断上下文”就能够理解为硬件传递过来的这些参数和内核须要保存的一些环境,主要是被中断的进程的环境。
内核进入中断上下文是由于中断信号而致使的中断处理或软中断。而中断信号的发生是随机的,中断处理程序及软中断并不能事先预测发生中断时当前运行的是哪一个进程,因此在中断上下文中引用current是能够的,但没有意义。
事实上,对于A进程但愿等待的中断信号,可能在B进程执行期间发生。例如,A进程启动写磁盘操做,A进程睡眠后B进程在运行,当磁盘写完后磁盘中断信号打断的是B进程,在中断处理时会唤醒A进程。
4、进程上下文 VS 中断上下文
内核能够处于两种上下文:进程上下文和中断上下文。
在系统调用以后,用户应用程序进入内核空间,此后内核空间针对用户空间相应进程的表明就运行于进程上下文。
异步发生的中断会引起中断处理程序被调用,中断处理程序就运行于中断上下文。
中断上下文和进程上下文不可能同时发生。
运行于进程上下文的内核代码是可抢占的,但中断上下文则会一直运行至结束,不会被抢占。所以,内核会限制中断上下文的工做,不容许其执行以下操做:
a -- 进入睡眠状态或主动放弃CPU
因为中断上下文不属于任何进程,它与current没有任何关系(尽管此时current指向被中断的进程),因此中断上下文一旦睡眠或者放弃CPU,将没法被唤醒。因此也叫原子上下文(atomic context)。
b -- 占用互斥体
为了保护中断句柄临界区资源,不能使用mutexes。若是得到不到信号量,代码就会睡眠,会产生和上面相同的状况,若是必须使用锁,则使用spinlock。
c -- 执行耗时的任务
中断处理应该尽量快,由于内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。在中断处理例程中执行耗时任务时,应该交由中断处理例程底半部来处理。
d -- 访问用户空间虚拟内存
由于中断上下文是和特定进程无关的,它是内核表明硬件运行在内核空间,因此在中断上下文没法访问用户空间的虚拟地址
e -- 中断处理例程不该该设置成reentrant(可被并行或递归调用的例程)
由于中断发生时,preempt和irq都被disable,直到中断返回。因此中断上下文和进程上下文不同,中断处理例程的不一样实例,是不容许在SMP上并发运行的。
f -- 中断处理例程能够被更高级别的IRQ中断
若是想禁止这种中断,能够将中断处理例程定义成快速处理例程,至关于告诉CPU,该例程运行时,禁止本地CPU上全部中断请求。这直接致使的结果是,因为其余中断被延迟响应,系统性能降低。
5、原子上下文
内核的一个基本原则就是:在中断或者说原子上下文中,内核不能访问用户空间,并且内核是不能睡眠的。也就是说在这种状况下,内核是不能调用有可能引发睡眠的任何函数。通常来说原子上下文指的是在中断或软中断中,以及在持有自旋锁的时候。内核提供 了四个宏来判断是否处于这几种状况里:
- #define in_irq() (hardirq_count()) //在处理硬中断中
- #define in_softirq() (softirq_count()) //在处理软中断中
- #define in_interrupt() (irq_count()) //在处理硬中断或软中断中
- #define in_atomic() ((preempt_count() & ~PREEMPT_ACTIVE) != 0) //包含以上全部状况
这四个宏所访问的count都是thread_info->preempt_count。这个变量实际上是一个位掩码。最低8位表示抢占计数,一般由spin_lock/spin_unlock修改,或程序员强制修改,同时代表内核允许的最大抢占深度是256。
8-15位是软中断计数,一般由local_bh_disable/local_bh_enable修改,同时代表内核允许的最大软中断深度是256。
16-27位是硬中断计数,一般由enter_irq/exit_irq修改,同时代表内核允许的最大硬中断深度是4096。
第28位是PREEMPT_ACTIVE标志。用代码表示就是:
PREEMPT_MASK: 0x000000ff
SOFTIRQ_MASK: 0x0000ff00
HARDIRQ_MASK: 0x0fff0000
凡是上面4个宏返回1获得地方都是原子上下文,是不允许内核访问用户空间,不允许内核睡眠的,不允许调用任何可能引发睡眠的函数。并且表明thread_info->preempt_count不是0,这就告诉内核,在这里面抢占被禁用。
但 是,对于in_atomic()来讲,在启用抢占的状况下,它工做的很好,能够告诉内核目前是否持有自旋锁,是否禁用抢占等。可是,在没有启用抢占的状况 下,spin_lock根本不修改preempt_count,因此即便内核调用了spin_lock,持有了自旋锁,in_atomic()仍然会返回 0,错误的告诉内核目前在非原子上下文中。因此凡是依赖in_atomic()来判断是否在原子上下文的代码,在禁抢占的状况下都是有问题的。
Linux的mmap内存映射机制解析
在讲述文件映射的概念时,不可避免的要牵涉到虚存(SVR 4的VM).实际上,文件映射是虚存的中心概念, 文件映射一方面给用户提供了一组措施,好似用户将文件映射到本身地址空间的某个部分,使用简单的内存访问指令读写文件;另外一方面,它也能够用于内核的基本组织模式,在这种模式种,内核将整个地址空间视为诸如文件之类的一组不一样对象的映射.中的传统文件访问方式是,首先用open系统调用打开文件,而后使用read, write以及lseek等调用进行顺序或者随即的I/O.这种方式是很是低效的,每一次I/O操做都须要一次系统调用.另外,若是若干个进程访问同一个文件,每一个进程都要在本身的地址空间维护一个副本,浪费了内存空间.而若是可以经过必定的机制将页面映射到进程的地址空间中,也就是说首先经过简单的产生某些内存管理数据结构完成映射的建立.当进程访问页面时产生一个缺页中断,内核将页面读入内存而且更新页表指向该页面.并且这种方式很是方便于同一副本的共享.
VM是面向对象的方法设计的,这里的对象是指内存对象:内存对象是一个软件抽象的概念,它描述内存区与后备存储之间的映射.系统能够使用多种类型的后备存储,好比交换空间,本地或者远程文件以及帧缓存等等. VM系统对它们统一处理,采用同一操做集操做,好比读取页面或者回写页面等.每种不一样的后备存储均可以用不一样的方法实现这些操做.这样,系统定义了一套统一的接口,每种后备存储给出本身的实现方法.这样,进程的地址空间就被视为一组映射到不一样数据对象上的的映射组成.全部的有效地址就是那些映射到数据对象上的地址.这些对象为映射它的页面提供了持久性的后备存储.映射使得用户能够直接寻址这些对象.
值得提出的是, VM体系结构独立于Unix系统,全部的Unix系统语义,如正文,数据及堆栈区均可以建构在基本VM系统之上.同时, VM体系结构也是独立于存储管理的,存储管理是由操做系统实施的,如:究竟采起什么样的对换和请求调页算法,到底是采起分段仍是分页机制进行存储管理,到底是如何将虚拟地址转换成为物理地址等等(Linux中是一种叫Three Level Page Table的机制),这些都与内存对象的概念无关.
1、Linux中VM的实现.
一个进程应该包括一个mm_struct(memory manage struct), 该结构是进程虚拟地址空间的抽象描述,里面包括了进程虚拟空间的一些管理信息: start_code, end_code, start_data, end_data, start_brk, end_brk等等信息.另外,也有一个指向进程虚存区表(vm_area_struct: virtual memory area)的指针,该链是按照虚拟地址的增加顺序排列的.在Linux进程的地址空间被分做许多区(vma),每一个区(vma)都对应虚拟地址空间上一段连续的区域, vma是能够被共享和保护的独立实体,这里的vma就是前面提到的内存对象.
下面是vm_area_struct的结构,其中,前半部分是公共的,与类型无关的一些数据成员,如:指向mm_struct的指针,地址范围等等,后半部分则是与类型相关的成员,其中最重要的是一个指向vm_operation_struct向量表的指针vm_ops, vm_pos向量表是一组虚函数,定义了与vma类型无关的接口.每个特定的子类,即每种vma类型都必须在向量表中实现这些操做.这里包括了: open, close, unmap, protect, sync, nopage, wppage, swapout这些操做.
- struct vm_area_struct {
-
- struct mm_struct * vm_mm;
- unsigned long vm_start;
- unsigned long vm_end;
- struct vm_area_struct *vm_next;
- pgprot_t vm_page_prot;
- unsigned long vm_flags;
- short vm_avl_height;
- struct vm_area_struct * vm_avl_left;
- struct vm_area_struct * vm_avl_right;
- struct vm_area_struct *vm_next_share;
- struct vm_area_struct **vm_pprev_share;
-
- struct vm_operations_struct * vm_ops;
- unsigned long vm_pgoff;
- struct file * vm_file;
- unsigned long vm_raend;
- void * vm_private_data;
- };
vm_ops: open, close, no_page, swapin, swapout……
2、驱动中的mmap()函数解析
设备驱动的mmap实现主要是将一个物理设备的可操做区域(设备空间)映射到一个进程的虚拟地址空间。这样就能够直接采用指针的方式像访问内存的方式访问设备。在驱动中的mmap实现主要是完成一件事,就是实际物理设备的操做区域到进程虚拟空间地址的映射过程。同时也须要保证这段映射的虚拟存储器区域不会被进程当作通常的空间使用,所以须要添加一系列的保护方式。
- static int mem_mmap(struct file* filp,struct vm_area_struct *vma)
- {
-
- struct mem_dev *dev = filp->private_data;
-
-
- vma->vm_flags |= VM_IO;
-
- vma->vm_flags |= VM_RESERVED;
-
-
- if(remap_pfn_range(vma,
- vma->vm_start,
- virt_to_phys(dev->data)>>PAGE_SHIFT,
- dev->size,
- vma->vm_page_prot
- ))
- return -EAGAIN;
-
- return 0;
- }
具体的实现分析以下:
vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;
上面的两个保护机制就说明了被映射的这段区域具备映射IO的类似性,同时保证这段区域不能随便的换出。就是创建一个物理页与虚拟页之间的关联性。具体原理是虚拟页和物理页之间是以页表的方式关联起来,虚拟内存一般大于物理内存,在使用过程当中虚拟页经过页表关联一切对应的物理页,当物理页不够时,会选择性的牺牲一些页,也就是将物理页与虚拟页之间切断,重现关联其余的虚拟页,保证物理内存够用。在设备驱动中应该具体的虚拟页和物理页之间的关系应该是长期的,应该保护起来,不能随便被别的虚拟页所替换。具体也可参看关于虚拟存储器的文章。
接下来就是创建物理页与虚拟页之间的关系,即采用函数remap_pfn_range(),具体的参数以下:
int remap_pfn_range(structvm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot)
一、struct vm_area_struct是一个虚拟内存区域结构体,表示虚拟存储器中的一个内存区域。其中的元素vm_start是指虚拟存储器中的起始地址。
二、addr也就是虚拟存储器中的起始地址,一般能够选择addr = vma->vm_start。
三、pfn是指物理存储器的具体页号,一般经过物理地址获得对应的物理页号,具体采用virt_to_phys(dev->data)>>PAGE_SHIFT.首先将虚拟内存转换到物理内存,而后获得页号。>>PAGE_SHIFT一般为12,这是由于每一页的大小恰好是4K,这样右移12至关于除以4096,获得页号。
四、size区域大小
五、区域保护机制。
返回值,若是成功返回0,不然正数。
3、系统调用mmap函数解析
介绍完VM的基本概念后,咱们能够讲述mmap和munmap系统调用了.mmap调用实际上就是一个内存对象vma的建立过程,
一、mmap函数
Linux提供了内存映射函数mmap,它把文件内容映射到一段内存上(准确说是虚拟内存上),经过对这段内存的读取和修改,实现对文件的读取和修改 。普通文件被映射到进程地址空间后,进程能够向访问普通内存同样对文件进行访问,没必要再调用read(),write()等操做。
先来看一下mmap的函数声明:
- 头文件:
- <unistd.h>
- <sys/mman.h>
-
- 原型: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offsize);
-
mmap的做用是映射文件描述符fd指定文件的 [off,off + len]区域至调用进程的[addr, addr + len]的内存区域, 以下图所示:

mmap系统调用的实现过程是
1.先经过文件系统定位要映射的文件;
2.权限检查,映射的权限不会超过文件打开的方式,也就是说若是文件是以只读方式打开,那么则不容许创建一个可写映射;
3.建立一个vma对象,并对之进行初始化;
4.调用映射文件的mmap函数,其主要工做是给vm_ops向量表赋值;
5.把该vma链入该进程的vma链表中,若是能够和先后的vma合并则合并;
6.若是是要求VM_LOCKED(映射区不被换出)方式映射,则发出缺页请求,把映射页面读入内存中.
二、munmap函数
munmap(void * start, size_t length):
该调用能够看做是mmap的一个逆过程.它将进程中从start开始length长度的一段区域的映射关闭,若是该区域不是刚好对应一个vma,则有可能会分割几个或几个vma.
msync(void * start, size_t length, int flags):
把映射区域的修改回写到后备存储中.由于munmap时并不保证页面回写,若是不调用msync,那么有可能在munmap后丢失对映射区的修改.其中flags能够是MS_SYNC, MS_ASYNC, MS_INVALIDATE, MS_SYNC要求回写完成后才返回, MS_ASYNC发出回写请求后当即返回, MS_INVALIDATE使用回写的内容更新该文件的其它映射.该系统调用是经过调用映射文件的sync函数来完成工做的.
brk(void * end_data_segement):
将进程的数据段扩展到end_data_segement指定的地址,该系统调用和mmap的实现方式十分类似,一样是产生一个vma,而后指定其属性.不过在此以前须要作一些合法性检查,好比该地址是否大于mm->end_code, end_data_segement和mm->brk之间是否还存在其它vma等等.经过brk产生的vma映射的文件为空,这和匿名映射产生的vma类似,关于匿名映射不作进一步介绍.库函数malloc就是经过brk实现的.
4、实例解析
下面这个例子显示了把文件映射到内存的方法,源代码是:
- #include <sys/mman.h> /* for mmap and munmap */
- #include <sys/types.h> /* for open */
- #include <sys/stat.h> /* for open */
- #include <fcntl.h> /* for open */
- #include <unistd.h> /* for lseek and write */
- #include <stdio.h>
-
- int main(int argc, char **argv)
- {
- int fd;
- char *mapped_mem, * p;
- int flength = 1024;
- void * start_addr = 0;
-
- fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
- flength = lseek(fd, 1, SEEK_END);
- write(fd, "\0", 1);
- lseek(fd, 0, SEEK_SET);
- mapped_mem = mmap(start_addr, flength, PROT_READ,
- MAP_PRIVATE,
- fd, 0);
-
-
- printf("%s\n", mapped_mem);
- close(fd);
- munmap(mapped_mem, flength);
- return 0;
- }
编译运行此程序:
gcc -Wall mmap.c
./a.out text_filename
上面的方法由于用了PROT_READ,因此只能读取文件里的内容,不能修改,若是换成PROT_WRITE就能够修改文件的内容了。又因为 用了MAAP_PRIVATE因此只能此进程使用此内存区域,若是换成MAP_SHARED,则能够被其它进程访问,好比下面的
- #include <sys/mman.h> /* for mmap and munmap */
- #include <sys/types.h> /* for open */
- #include <sys/stat.h> /* for open */
- #include <fcntl.h> /* for open */
- #include <unistd.h> /* for lseek and write */
- #include <stdio.h>
- #include <string.h> /* for memcpy */
-
- int main(int argc, char **argv)
- {
- int fd;
- char *mapped_mem, * p;
- int flength = 1024;
- void * start_addr = 0;
-
- fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
- flength = lseek(fd, 1, SEEK_END);
- write(fd, "\0", 1);
- lseek(fd, 0, SEEK_SET);
- start_addr = 0x80000;
- mapped_mem = mmap(start_addr, flength, PROT_READ|PROT_WRITE,
- MAP_SHARED,
- fd, 0);
-
- * 使用映射区域. */
- printf("%s\n", mapped_mem);
- while((p = strstr(mapped_mem, "Hello"))) {
- memcpy(p, "Linux", 5);
- p += 5;
- }
-
- close(fd);
- munmap(mapped_mem, flength);
- return 0;
- }
5、mmap和共享内存对比
共享内存容许两个或多个进程共享一给定的存储区,由于数据不须要来回复制,因此是最快的一种进程间通讯机制。共享内存能够经过mmap()映射普通文件(特殊状况下还能够采用匿名映射)机制实现,也能够经过系统V共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通讯,每每还与信号灯等同步机制共同使用。
对好比下:
mmap机制:就是在磁盘上创建一个文件,每一个进程存储器里面,单独开辟一个空间来进行映射。若是多进程的话,那么不会对实际的物理存储器(主存)消耗太大。
shm机制:每一个进程的共享内存都直接映射到实际物理存储器里面。
一、mmap保存到实际硬盘,实际存储并无反映到主存上。优势:储存量能够很大(多于主存);缺点:进程间读取和写入速度要比主存的要慢。
二、shm保存到物理存储器(主存),实际的储存量直接反映到主存上。优势,进程间访问速度(读写)比磁盘要快;缺点,储存量不能很是大(多于主存)
使用上看:若是分配的存储量不大,那么使用shm;若是存储量大,那么使用mmap。
Linux 下的DMA浅析
DMA是一种无需CPU的参与就可让外设和系统内存之间进行双向数据传输的硬件机制。使用DMA能够使系统CPU从实际的I/O数据传输过程当中摆脱出来,从而大大提升系统的吞吐率。DMA常常与硬件体系结构特别是外设的总线技术密切相关。
1、DMA控制器硬件结构
DMA容许外围设备和主内存之间直接传输 I/O 数据, DMA 依赖于系统。每一种体系结构DMA传输不一样,编程接口也不一样。
数据传输能够以两种方式触发:一种软件请求数据,另外一种由硬件异步传输。
a -- 软件请求数据
调用的步骤能够归纳以下(以read为例):
(1)在进程调用 read 时,驱动程序的方法分配一个 DMA 缓冲区,随后指示硬件传送它的数据。进程进入睡眠。
(2)硬件将数据写入 DMA 缓冲区并在完成时产生一个中断。
(3)中断处理程序得到输入数据,应答中断,最后唤醒进程,该进程如今能够读取数据了。
b -- 由硬件异步传输
在 DMA 被异步使用时发生的。以数据采集设备为例:
(1)硬件发出中断来通知新的数据已经到达。
(2)中断处理程序分配一个DMA缓冲区。
(3)外围设备将数据写入缓冲区,而后在完成时发出另外一个中断。
(4)处理程序利用DMA分发新的数据,唤醒任何相关进程。
网卡传输也是如此,网卡有一个循环缓冲区(一般叫作 DMA 环形缓冲区)创建在与处理器共享的内存中。每个输入数据包被放置在环形缓冲区中下一个可用缓冲区,而且发出中断。而后驱动程序将网络数据包传给内核的其它部分处理,并在环形缓冲区中放置一个新的 DMA 缓冲区。
驱动程序在初始化时分配DMA缓冲区,并使用它们直到中止运行。
2、DMA通道使用的地址
DMA通道用dma_chan结构数组表示,这个结构在kernel/dma.c中,列出以下:
- struct dma_chan {
- int lock;
- const char *device_id;
- };
-
- static struct dma_chan dma_chan_busy[MAX_DMA_CHANNELS] = {
- [4] = { 1, "cascade" },
- };
若是dma_chan_busy[n].lock != 0表示忙,DMA0保留为DRAM更新用,DMA4用做级联。DMA 缓冲区的主要问题是,当它大于一页时,它必须占据物理内存中的连续页。
因为DMA须要连续的内存,于是在引导时分配内存或者为缓冲区保留物理 RAM 的顶部。在引导时给内核传递一个"mem="参数能够保留 RAM 的顶部。例如,若是系统有 32MB 内存,参数"mem=31M"阻止内核使用最顶部的一兆字节。稍后,模块能够使用下面的代码来访问这些保留的内存:
dmabuf = ioremap( 0x1F00000 /* 31M */, 0x100000 /* 1M */);
分配 DMA 空间的方法,代码调用 kmalloc(GFP_ATOMIC) 直到失败为止,而后它等待内核释放若干页面,接下来再一次进行分配。最终会发现由连续页面组成的DMA 缓冲区的出现。
一个使用 DMA 的设备驱动程序一般会与链接到接口总线上的硬件通信,这些硬件使用物理地址,而程序代码使用虚拟地址。基于 DMA 的硬件使用总线地址而不是物理地址,有时,接口总线是经过将 I/O 地址映射到不一样物理地址的桥接电路链接的。甚至某些系统有一个页面映射方案,可以使任意页面在外围总线上表现为连续的。
当驱动程序须要向一个 I/O 设备(例如扩展板或者DMA控制器)发送地址信息时,必须使用 virt_to_bus 转换,在接受到来自链接到总线上硬件的地址信息时,必须使用 bus_to_virt 了。
3、DMA操做函数
写一个DMA驱动的主要工做包括:DMA通道申请、DMA中断申请、控制寄存器设置、挂入DMA等待队列、清除DMA中断、释放DMA通道
由于 DMA 控制器是一个系统级的资源,因此内核协助处理这一资源。内核使用 DMA 注册表为 DMA 通道提供了请求/释放机制,而且提供了一组函数在 DMA 控制器中配置通道信息。
如下具体分析关键函数(linux/arch/arm/mach-s3c2410/dma.c)
- int s3c2410_request_dma(const char *device_id, dmach_t channel,
- dma_callback_t write_cb, dma_callback_t read_cb) (s3c2410_dma_queue_buffer);
-
- int s3c2410_dma_queue_buffer(dmach_t channel, void *buf_id,
- dma_addr_t data, int size, int write) (s3c2410_dma_stop);
-
- int s3c2410_dma_stop(dmach_t channel)
-
- int s3c2410_dma_flush_all(dmach_t channel)
-
- void s3c2410_free_dma(dmach_t channel)
4、DMA映射
一个DMA映射就是分配一个 DMA 缓冲区并为该缓冲区生成一个可以被设备访问的地址的组合操做。通常状况下,简单地调用函数virt_to_bus 就设备总线上的地址,但有些硬件映射寄存器也被设置在总线硬件中。映射寄存器(mapping register)是一个相似于外围设备的虚拟内存等价物。在使用这些寄存器的系统上,外围设备有一个相对较小的、专用的地址区段,能够在此区段执行 DMA。经过映射寄存器,这些地址被重映射到系统 RAM。映射寄存器具备一些好的特性,包括使分散的页面在设备地址空间看起来是连续的。但不是全部的体系结构都有映射寄存器,特别地,PC 平台没有映射寄存器。
在某些状况下,为设备设置有用的地址也意味着须要构造一个反弹(bounce)缓冲区。例如,当驱动程序试图在一个不能被外围设备访问的地址(一个高端内存地址)上执行 DMA 时,反弹缓冲区被建立。而后,按照须要,数据被复制到反弹缓冲区,或者从反弹缓冲区复制。
根据 DMA 缓冲区指望保留的时间长短,PCI 代码区分两种类型的 DMA 映射:
a -- 一致 DMA 映射
它们存在于驱动程序的生命周期内。一个被一致映射的缓冲区必须同时可被 CPU 和外围设备访问,这个缓冲区被处理器写时,可当即被设备读取而没有cache效应,反之亦然,使用函数pci_alloc_consistent创建一致映射。
b -- 流式 DMA映射
流式DMA映射是为单个操做进行的设置。它映射处理器虚拟空间的一块地址,以至它能被设备访问。应尽量使用流式映射,而不是一致映射。这是由于在支持一致映射的系统上,每一个 DMA 映射会使用总线上一个或多个映射寄存器。具备较长生命周期的一致映射,会独占这些寄存器很长时间――即便它们没有被使用。使用函数dma_map_single创建流式映射。
一、创建一致 DMA 映射
函数pci_alloc_consistent处理缓冲区的分配和映射,函数分析以下(在include/asm-generic/pci-dma-compat.h中):
- static inline void *pci_alloc_consistent(struct pci_dev *hwdev,
- size_t size, dma_addr_t *dma_handle)
- {
- return dma_alloc_coherent(hwdev == NULL ? NULL : &hwdev->dev,
- size, dma_handle, GFP_ATOMIC);
- }
结构dma_coherent_mem定义了DMA一致性映射的内存的地址、大小和标识等。结构dma_coherent_mem列出以下(在arch/i386/kernel/pci-dma.c中):
- struct dma_coherent_mem {
- void *virt_base;
- u32 device_base;
- int size;
- int flags;
- unsigned long *bitmap;
- };
函数dma_alloc_coherent分配size字节的区域的一致内存,获得的dma_handle是指向分配的区域的地址指针,这个地址做为区域的物理基地址。dma_handle是与总线同样的位宽的无符号整数。 函数dma_alloc_coherent分析以下(在arch/i386/kernel/pci-dma.c中):
- void *dma_alloc_coherent(struct device *dev, size_t size,
- dma_addr_t *dma_handle, int gfp)
- {
- void *ret;
-
- struct dma_coherent_mem *mem = dev ? dev->dma_mem : NULL;
- int order = get_order(size);
-
- gfp &= ~(__GFP_DMA | __GFP_HIGHMEM);
-
- if (mem) {
-
- int page = bitmap_find_free_region(mem->bitmap, mem->size,
- order);
- if (page >= 0) {
- *dma_handle = mem->device_base + (page << PAGE_SHIFT);
- ret = mem->virt_base + (page << PAGE_SHIFT);
- memset(ret, 0, size);
- return ret;
- }
- if (mem->flags & DMA_MEMORY_EXCLUSIVE)
- return NULL;
- }
-
-
- if (dev == NULL || (dev->coherent_dma_mask < 0xffffffff))
- gfp |= GFP_DMA;
-
- ret = (void *)__get_free_pages(gfp, order);
-
- if (ret != NULL) {
- memset(ret, 0, size);
- *dma_handle = virt_to_phys(ret);
- }
- return ret;
- }
当再也不须要缓冲区时(一般在模块卸载时),应该调用函数 pci_free_consitent 将它返还给系统。
二、创建流式 DMA 映射
在流式 DMA 映射的操做中,缓冲区传送方向应匹配于映射时给定的方向值。缓冲区被映射后,它就属于设备而再也不属于处理器了。在缓冲区调用函数pci_unmap_single撤销映射以前,驱动程序不该该触及其内容。
在缓冲区为 DMA 映射时,内核必须确保缓冲区中全部的数据已经被实际写到内存。可能有些数据还会保留在处理器的高速缓冲存储器中,所以必须显式刷新。在刷新以后,由处理器写入缓冲区的数据对设备来讲也许是不可见的。
若是欲映射的缓冲区位于设备不能访问的内存区段时,某些体系结构仅仅会操做失败,而其它的体系结构会建立一个反弹缓冲区。反弹缓冲区是被设备访问的独立内存区域,反弹缓冲区复制原始缓冲区的内容。
函数pci_map_single映射单个用于传送的缓冲区,返回值是能够传递给设备的总线地址,若是出错的话就为 NULL。一旦传送完成,应该使用函数pci_unmap_single 删除映射。其中,参数direction为传输的方向,取值以下:
PCI_DMA_TODEVICE 数据被发送到设备。
PCI_DMA_FROMDEVICE若是数据将发送到 CPU。
PCI_DMA_BIDIRECTIONAL数据进行两个方向的移动。
PCI_DMA_NONE 这个符号只是为帮助调试而提供。
函数pci_map_single分析以下(在arch/i386/kernel/pci-dma.c中)
- static inline dma_addr_t pci_map_single(struct pci_dev *hwdev,
- void *ptr, size_t size, int direction)
- {
- return dma_map_single(hwdev == NULL ? NULL : &hwdev->dev, ptr, size,
- (enum ma_data_direction)direction);
- }
函数dma_map_single映射一块处理器虚拟内存,这块虚拟内存能被设备访问,返回内存的物理地址,函数dma_map_single分析以下(在include/asm-i386/dma-mapping.h中):
- static inline dma_addr_t dma_map_single(struct device *dev, void *ptr,
- size_t size, enum dma_data_direction direction)
-
- {
- BUG_ON(direction == DMA_NONE);
-
- flush_write_buffers();
- return virt_to_phys(ptr);
-
- }
三、分散/集中映射
分散/集中映射是流式 DMA 映射的一个特例。它将几个缓冲区集中到一块儿进行一次映射,并在一个 DMA 操做中传送全部数据。这些分散的缓冲区由分散表结构scatterlist来描述,多个分散的缓冲区的分散表结构组成缓冲区的struct scatterlist数组。
分散表结构列出以下(在include/asm-i386/scatterlist.h):
- struct scatterlist {
- struct page *page;
- unsigned int offset;
- dma_addr_t dma_address;
- unsigned int length;
- };
每个缓冲区的地址和长度会被存储在 struct scatterlist 项中,但在不一样的体系结构中它们在结构中的位置是不一样的。下面的两个宏定义来解决平台移植性问题,这些宏定义应该在一个pci_map_sg 被调用后使用:
- #define sg_dma_address(sg) �sg)->dma_address)
- #define sg_dma_len(sg) �sg)->length)
函数pci_map_sg完成分散/集中映射,其返回值是要传送的 DMA 缓冲区数;它可能会小于 nents(也就是传入的分散表项的数量),由于可能有的缓冲区地址上是相邻的。一旦传输完成,分散/集中映射经过调用函数pci_unmap_sg 来撤销映射。 函数pci_map_sg分析以下(在include/asm-generic/pci-dma-compat.h中):
- static inline int pci_map_sg(struct pci_dev *hwdev, struct scatterlist *sg,
- int nents, int direction)
- {
- return dma_map_sg(hwdev == NULL ? NULL : &hwdev->dev, sg, nents,
- (enum dma_data_direction)direction);
- }
- include/asm-i386/dma-mapping.h
- static inline int dma_map_sg(struct device *dev, struct scatterlist *sg,
- int nents, enum dma_data_direction direction)
- {
- int i;
-
- BUG_ON(direction == DMA_NONE);
-
- for (i = 0; i < nents; i++ ) {
- BUG_ON(!sg[i].page);
-
- sg[i].dma_address = page_to_phys(sg[i].page) + sg[i].offset;
- }
-
- flush_write_buffers();
- return nents;
- }
5、DMA池
许多驱动程序须要又多又小的一致映射内存区域给DMA描述子或I/O缓存buffer,这使用DMA池比用dma_alloc_coherent分配的一页或多页内存区域好,DMA池用函数dma_pool_create建立,用函数dma_pool_alloc从DMA池中分配一块一致内存,用函数dmp_pool_free放内存回到DMA池中,使用函数dma_pool_destory释放DMA池的资源。
结构dma_pool是DMA池描述结构,列出以下:
- struct dma_pool {
- struct list_head page_list;
- spinlock_t lock;
- size_t blocks_per_page;
- size_t size;
- struct device *dev;
- size_t allocation;
- char name [32];
- wait_queue_head_t waitq;
- struct list_head pools;
- };
函数dma_pool_create给DMA建立一个一致内存块池,其参数name是DMA池的名字,用于诊断用,参数dev是将作DMA的设备,参数size是DMA池里的块的大小,参数align是块的对齐要求,是2的幂,参数allocation返回没有跨越边界的块数(或0)。
函数dma_pool_create返回建立的带有要求字符串的DMA池,若建立失败返回null。对被给的DMA池,函数dma_pool_alloc被用来分配内存,这些内存都是一致DMA映射,可被设备访问,且没有使用缓存刷新机制,由于对齐缘由,分配的块的实际尺寸比请求的大。若是分配非0的内存,从函数dma_pool_alloc返回的对象将不跨越size边界(如不跨越4K字节边界)。这对在个体的DMA传输上有地址限制的设备来讲是有利的。
函数dma_pool_create分析以下(在drivers/base/dmapool.c中):
- struct dma_pool *dma_pool_create (const char *name, struct device *dev,
- size_t size, size_t align, size_t allocation)
- {
- struct dma_pool *retval;
-
- if (align == 0)
- align = 1;
- if (size == 0)
- return NULL;
- else if (size < align)
- size = align;
- else if ((size % align) != 0) {
- size += align + 1;
- size &= ~(align - 1);
- }
-
- if (allocation == 0) {
- if (PAGE_SIZE < size)
- allocation = size;
- else
- allocation = PAGE_SIZE;
-
- } else if (allocation < size)
- return NULL;
-
- if (!(retval = kmalloc (sizeof *retval, SLAB_KERNEL)))
- return retval;
-
- strlcpy (retval->name, name, sizeof retval->name);
-
- retval->dev = dev;
-
- INIT_LIST_HEAD (&retval->page_list);
- spin_lock_init (&retval->lock);
- retval->size = size;
- retval->allocation = allocation;
- retval->blocks_per_page = allocation / size;
- init_waitqueue_head (&retval->waitq);
-
- if (dev) {
- down (&pools_lock);
- if (list_empty (&dev->dma_pools))
-
- device_create_file (dev, &dev_attr_pools);
-
- list_add (&retval->pools, &dev->dma_pools);
- up (&pools_lock);
- } else
- INIT_LIST_HEAD (&retval->pools);
-
- return retval;
- }
函数dma_pool_alloc从DMA池中分配一块一致内存,其参数pool是将产生块的DMA池,参数mem_flags是GFP_*位掩码,参数handle是指向块的DMA地址,函数dma_pool_alloc返回当前没用的块的内核虚拟地址,并经过handle给出它的DMA地址,若是内存块不能被分配,返回null。
函数dma_pool_alloc包裹了dma_alloc_coherent页分配器,这样小块更容易被总线的主控制器使用。这可能共享slab分配器的内容。
函数dma_pool_alloc分析以下(在drivers/base/dmapool.c中):
- void *dma_pool_alloc (struct dma_pool *pool, int mem_flags, dma_addr_t *handle)
- {
- unsigned long flags;
- struct dma_page *page;
- int map, block;
- size_t offset;
- void *retval;
-
- restart:
- spin_lock_irqsave (&pool->lock, flags);
- list_for_each_entry(page, &pool->page_list, page_list) {
- int i;
-
-
- for (map = 0, i = 0;
- i < pool->blocks_per_page;
- i += BITS_PER_LONG, map++) {
- if (page->bitmap [map] == 0)
- continue;
- block = ffz (~ page->bitmap [map]);
- if ((i + block) < pool->blocks_per_page) {
- clear_bit (block, &page->bitmap [map]);
-
- offset = (BITS_PER_LONG * map) + block;
- offset *= pool->size;
- goto ready;
- }
- }
- }
- if (!(page = pool_alloc_page (pool, SLAB_ATOMIC))) {
- if (mem_flags & __GFP_WAIT) {
- DECLARE_WAITQUEUE (wait, current);
-
- current->state = TASK_INTERRUPTIBLE;
- add_wait_queue (&pool->waitq, &wait);
- spin_unlock_irqrestore (&pool->lock, flags);
-
- schedule_timeout (POOL_TIMEOUT_JIFFIES);
-
- remove_wait_queue (&pool->waitq, &wait);
- goto restart;
- }
- retval = NULL;
- goto done;
- }
-
- clear_bit (0, &page->bitmap [0]);
- offset = 0;
- ready:
- page->in_use++;
- retval = offset + page->vaddr;
- *handle = offset + page->dma;
- #ifdef CONFIG_DEBUG_SLAB
- memset (retval, POOL_POISON_ALLOCATED, pool->size);
- #endif
- done:
- spin_unlock_irqrestore (&pool->lock, flags);
- return retval;
- }
6、一个简单的使用DMA 例子
示例:下面是一个简单的使用DMA进行传输的驱动程序,它是一个假想的设备,只列出DMA相关的部分来讲明驱动程序中如何使用DMA的。
函数dad_transfer是设置DMA对内存buffer的传输操做函数,它使用流式映射将buffer的虚拟地址转换到物理地址,设置好DMA控制器,而后开始传输数据。
- int dad_transfer(struct dad_dev *dev, int write, void *buffer,
- size_t count)
- {
- dma_addr_t bus_addr;
- unsigned long flags;
-
-
- dev->dma_dir = (write ? PCI_DMA_TODEVICE : PCI_DMA_FROMDEVICE);
- dev->dma_size = count;
-
- bus_addr = pci_map_single(dev->pci_dev, buffer, count,
- dev->dma_dir);
- dev->dma_addr = bus_addr;
-
-
- writeb(dev->registers.command, DAD_CMD_DISABLEDMA);
-
- writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD);
- writel(dev->registers.addr, cpu_to_le32(bus_addr));
- writel(dev->registers.len, cpu_to_le32(count));
-
-
- writeb(dev->registers.command, DAD_CMD_ENABLEDMA);
- return 0;
- }
函数dad_interrupt是中断处理函数,当DMA传输完时,调用这个中断函数来取消buffer上的DMA映射,从而让内核程序能够访问这个buffer。
- void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs)
- {
- struct dad_dev *dev = (struct dad_dev *) dev_id;
-
-
-
-
- pci_unmap_single(dev->pci_dev, dev->dma_addr, dev->dma_size,
- dev->dma_dir);
-
-
- ...
- }
函数dad_open打开设备,此时应申请中断号及DMA通道
- int dad_open (struct inode *inode, struct file *filp)
- {
- struct dad_device *my_device;
-
-
- if ( (error = request_irq(my_device.irq, dad_interrupt,
- SA_INTERRUPT, "dad", NULL)) )
- return error;
-
- if ( (error = request_dma(my_device.dma, "dad")) ) {
- free_irq(my_device.irq, NULL);
- return error;
- }
-
- return 0;
- }
在与open 相对应的 close 函数中应该释放DMA及中断号。
- void dad_close (struct inode *inode, struct file *filp)
- {
- struct dad_device *my_device;
- free_dma(my_device.dma);
- free_irq(my_device.irq, NULL);
- ……
- }
函数dad_dma_prepare初始化DMA控制器,设置DMA控制器的寄存器的值,为 DMA 传输做准备。
- int dad_dma_prepare(int channel, int mode, unsigned int buf,
- unsigned int count)
- {
- unsigned long flags;
-
- flags = claim_dma_lock();
- disable_dma(channel);
- clear_dma_ff(channel);
- set_dma_mode(channel, mode);
- set_dma_addr(channel, virt_to_bus(buf));
- set_dma_count(channel, count);
- enable_dma(channel);
- release_dma_lock(flags);
-
- return 0;
- }
函数dad_dma_isdone用来检查 DMA 传输是否成功结束。
- int dad_dma_isdone(int channel)
- {
- int residue;
- unsigned long flags = claim_dma_lock ();
- residue = get_dma_residue(channel);
- release_dma_lock(flags);
- return (residue == 0);
- }
Linux 设备驱动的固件加载
做为一个驱动做者, 你可能发现你面对一个设备必须在它能支持工做前下载固件到它里面. 硬件市场的许多地方的竞争是如此得强烈, 以致于甚至一点用做设备控制固件的 EEPROM 的成本制造商都不肯意花费. 所以固件发布在随硬件一块儿的一张 CD 上, 而且操做系统负责传送固件到设备自身.
硬件愈来愈复杂,硬件的许多功能使用了程序实现,与直接硬件实现相比,固件拥有处理复琐事物的灵活性和便于升级、维护等优势。固件(firmware)就是这样的一段在设备硬件自身中执行的程序,经过固件标准驱动程序才能实现特定机器的操做,如:光驱、刻录机等都有内部的固件。
固件通常存放在设备上的flash存储器中,但出于成本和灵活性考虑,许多设备都将固件的映像(image)以文件的形式存放在硬盘中,设备驱动程序初始化时再装载到设备内部的存储器中。这样,方便了固件的升级,并省略了设备的flash存储器。
1、驱动和固件的区别
从计算机领域来讲,驱动和固件历来没有过明确的定义,就好像今天咱们说内存,大部分人用来表示SDRAM,但也有人把Android里的“固化的Flash/Storage"称为“内存”,你不能说这样说就错了,由于这确实是一种“内部存储”。
但在Linux Kernel中,Driver和Firmware是有明确含义的,
一、驱动
Driver是控制被操做系统管理的外部设备(Device)的代码段。不少时候Driver会被实现为LKM,但这不是必要条件。driver经过driver_register()注册到总线(bus_type)上,表明系统具有了驱动某种设备(device)的能力。当某个device被注册到一样的总线的时候(一般是总线枚举的时候发现了这个设备),总线驱动会对driver和device会经过必定的策略进行binding(即进行匹配),若是Binding成功,总线驱动会调用driver的probe()函数,把设备的信息(例如端口,中断号等)传递给驱动,驱动就能够对真实的物理部件进行初始化,并把对该设备的控制接口注册到Linux的其余子系统上(例如字符设备,v4l2子系统等)。这样操做系统的其余部分就能够经过这些通用的接口来访问设备了。
二、固件
Firmware,是表示运行在非“控制处理器”(指不直接运行操做系统的处理器,例如外设中的处理器,或者被用于bare metal的主处理器的其中一些核)中的程序。这些程序不少时候使用和操做系统所运行的处理器彻底不一样的指令集。这些程序以二进制形式存在于Linux内核的源代码树中,生成目标系统的时候,一般拷贝在/lib/firmware目录下。当driver对device进行初始化的时候,经过request_firmware()等接口,在一个用户态helper程序的帮助下,能够把指定的firmware加载到内存中,由驱动传输到指定的设备上。
因此,总的来讲,其实driver和firmware没有什么直接的关系,但firmware一般由驱动去加载。咱们讨论的那个OS,通常不须要理解firmware是什么,只是把它当作数据。firmware是什么,只有使用这些数据的那个设备才知道。比如你用一个电话,电话中有一个软件,这个软件你彻底不关心如何工做的,你换这个软件的时候,就能够叫这个软件是“固件”,但若是你用了一个智能手机,你要细细关系什么是上面的应用程序,Android平台,插件之类的细节内容,你可能就不叫这个东西叫“固件”了。
如何解决固件问题呢?你可能想解决固件问题使用这样的一个声明:
static char my_firmware[] = { 0x34, 0x78, 0xa4, ... };
可是, 这个方法几乎确定是一个错误. 将固件编码到一个驱动扩大了驱动的代码, 使固件升级困难, 而且很是可能产生许可问题. 供应商不可能已经发布固件映象在 GPL 之下, 所以和 GPL-许可的代码混合经常是一个错误. 为此, 包含内嵌固件的驱动不可能被接受到主流内核或者被 Linux 发布者包含.
2、内核固件接口
正确的方法是当你须要它时从用户空间获取它. 可是, 请抵制试图从内核空间直接打开包含固件的文件的诱惑; 那是一个易出错的操做, 而且它安放了策略(以一个文件名的形式)到内核. 相反, 正确的方法时使用固件接口, 它就是为此而建立的:
- #include <linux/firmware.h>
-
- int request_firmware(const struct firmware **fw, char *name, struct device *device);
函数request_firmware向用户空间请求提供一个名为name固件映像文件并等待完成。参数device为固件装载的设备。文件内容存入request_firmware 返回,若是固件请求成功,返回0。该函数从用户空间获得的数据未作任何检查,用户在编写驱动程序时,应对固件映像作数据安全检查,检查方向由设备固件提供商肯定,一般有检查标识符、校验和等方法。
调用 request_firmware 要求用户空间定位并提供一个固件映象给内核; 咱们一下子看它如何工做的细节. name 应当标识须要的固件; 正常的用法是供应者提供的固件文件名. 某些象 my_firmware.bin 的名子是典型的. 若是固件被成功加载, 返回值是 0(负责经常使用的错误码被返回), 而且 fw 参数指向一个这些结构:
- struct firmware {
- size_t size;
- u8 *data;
- };
那个结构包含实际的固件, 它如今可被下载到设备中. 当心这个固件是来自用户空间的未被检查的数据; 你应当在发送它到硬件以前运用任何而且全部的你可以想到的检查来讲服你本身它是正确的固件映象. 设备固件经常包含标识串, 校验和, 等等; 在信任数据前所有检查它们.
在你已经发送固件到设备前, 你应当释放 in-kernel 结构, 使用:
- void release_firmware(struct firmware *fw);
由于 request_firmware 请求用户空间来帮忙, 它保证在返回前睡眠. 若是你的驱动当它必须请求固件时不在睡眠的位置, 异步的替代方法可能要使用:
- int request_firmware_nowait(struct module *module,
- char *name, struct device *device, void *context,
- void (*cont)(const struct firmware *fw, void *context));
这里额外的参数是 moudle( 它将一直是 THIS_MODULE), context (一个固件子系统不使用的私有数据指针), 和 cont. 若是都进行顺利, request_firmware_nowait 开始固件加载过程而且返回 0. 在未来某个时间, cont 将用加载的结果被调用. 若是因为某些缘由固件加载失败, fw 是 NULL.
3、固件如何工做
固件子系统使用 sysfs 和热插拔机制. 当调用 request_firmware, 一个新目录在 /sys/class/firmware 下使用你的驱动的名子被建立. 那个目录包含 3 个属性:
loading
这个属性应当被加载固件的用户空间进程设置为 1. 当加载进程完成, 它应当设为 0. 写一个值 -1 到 loading 会停止固件加载进程.
data
data 是一个二进制的接收固件数据自身的属性. 在设置 loading 后, 用户空间进程应当写固件到这个属性.
device
这个属性是一个符号链接到 /sys/devices 下面的被关联入口项.
一旦建立了 sysfs 入口项, 内核为你的设备产生一个热插拔事件. 传递给热插拔处理者的环境包括一个变量 FIRMWARE, 它被设置为提供给 request_firmware 的名子. 这个处理者应当定位固件文件, 而且拷贝它到内核使用提供的属性. 若是这个文件没法找到, 处理者应当设置 loading 属性为 -1.
若是一个固件请求在 10 秒内没有被服务, 内核就放弃并返回一个失败状态给驱动. 超时周期可经过 sysfs 属性 /sys/class/firmware/timeout 属性改变.
使用 request_firmware 接口容许你随你的驱动发布设备固件. 当正确地集成到热插拔机制, 固件加载子系统容许设备简化工做"在盒子以外" 显然这是处理问题的最好方法.
可是, 请容许咱们提出多一条警告: 设备固件没有制造商的许可不该当发布. 许多制造商会赞成在合理的条款下许可它们的固件, 若是客气地请求; 一些其余的可能不何在. 不管如何, 在没有许可时拷贝和发布它们的固件是对版权法的破坏而且招致麻烦.
4、固件接口函数的使用方法
当驱动程序须要使用固件驱动时,在驱动程序的初始化化过程当中须要加下以下的代码:
- if(request_firmware(&fw_entry, $FIRMWARE, device) == 0)
-
- copy_fw_to_device(fw_entry->data, fw_entry->size);
- release(fw_entry);
用户还须要在用户空间提供脚本经过文件系统sysfs中的文件data将固件映像文件读入到内核的缓冲区中。脚本样例列出以下:
- #变量$DEVPATH(固件设备的路径)和$FIRMWARE(固件映像名)应已在环境变量中提供
-
- HOTPLUG_FW_DIR=/usr/lib/hotplug/firmware/ #固件映像文件所在目录
-
- echo 1 > /sys/$DEVPATH/loading
- cat $HOTPLUG_FW_DIR/$FIRMWARE > /sysfs/$DEVPATH/data
- echo 0 > /sys/$DEVPATH/loading
5、固件请求函数request_firmware
函数request_firmware请求从用户空间拷贝固件映像文件到内核缓冲区。该函数的工做流程列出以下:
a -- 在文件系统sysfs中建立文件/sys/class/firmware/xxx/loading和data,"xxx"表示固件的名字,给文件loading和data附加读写函数,设置文件属性,文件loading表示开/关固件映像文件装载功能;文件data的写操做将映像文件的数据写入内核缓冲区,读操做从内核缓冲区读取数据。
b -- 将添加固件的uevent事件(即"add")经过内核对象模型发送到用户空间。
c -- 用户空间管理uevent事件的后台进程udevd接收到事件后,查找udev规则文件,运行规则所定义的动做,与固件相关的规则列出以下:
- $ /etc/udev/rules.d/50-udev-default.rules
- ……
- # firmware class requests
- SUBSYSTEM=="firmware", ACTION=="add", RUN+="firmware.sh"
- ……
从上述规则能够看出,固件添加事件将引发运行脚本firmware.sh。
d -- 脚本firmware.sh打开"装载"功能,同命令"cat 映像文件 > /sys/class/firmware/xxx/data"将映像文件数据写入到内核的缓冲区。
e -- 映像数据拷贝完成后,函数request_firmware从文件系统/sysfs注销固件设备对应的目录"xxx"。若是请求成功,函数返回0。
f -- 用户就将内核缓冲区的固件映像数据拷贝到固件的内存中。而后,调用函数release_firmware(fw_entry)释放给固件映像分配的缓冲区。
函数request_firmware列出以下(在drivers/base/firmware_class.c中):
- int request_firmware(const struct firmware **firmware_p, const char *name,
- struct device *device)
- {
- int uevent = 1;
- return _request_firmware(firmware_p, name, device, uevent);
- }
-
- static int _request_firmware(const struct firmware **firmware_p, const char *name,
- struct device *device, int uevent)
- {
- struct device *f_dev;
- struct firmware_priv *fw_priv;
- struct firmware *firmware;
- struct builtin_fw *builtin;
- int retval;
-
- if (!firmware_p)
- return -EINVAL;
-
- *firmware_p = firmware = kzalloc(sizeof(*firmware), GFP_KERNEL);
- ……
-
-
- for (builtin = __start_builtin_fw; builtin != __end_builtin_fw;
- builtin++) {
- if (strcmp(name, builtin->name))
- continue;
- dev_info(device, "firmware: using built-in firmware %s\n", name);
- firmware->size = builtin->size;
- firmware->data = builtin->data;
- return 0;
- }
- ……
-
- retval = fw_setup_device(firmware, &f_dev, name, device, uevent);
- if (retval)
- goto error_kfree_fw;
-
- fw_priv = dev_get_drvdata(f_dev);
-
- if (uevent) {
- if (loading_timeout > 0) {
- fw_priv->timeout.expires = jiffies + loading_timeout * HZ;
- add_timer(&fw_priv->timeout);
- }
-
- kobject_uevent(&f_dev->kobj, KOBJ_ADD);
- wait_for_completion(&fw_priv->completion);
- set_bit(FW_STATUS_DONE, &fw_priv->status);
- del_timer_sync(&fw_priv->timeout);
- } else
- wait_for_completion(&fw_priv->completion);
-
- mutex_lock(&fw_lock);
-
- if (!fw_priv->fw->size || test_bit(FW_STATUS_ABORT, &fw_priv->status)) {
- retval = -ENOENT;
- release_firmware(fw_priv->fw);
- *firmware_p = NULL;
- }
- fw_priv->fw = NULL;
- mutex_unlock(&fw_lock);
- device_unregister(f_dev);
- goto out;
-
- error_kfree_fw:
- kfree(firmware);
- *firmware_p = NULL;
- out:
- return retval;
- }
函数fw_setup_device在文件系统sysfs中建立固件设备的目录和文件,其列出以下:
- static int fw_setup_device(struct firmware *fw, struct device **dev_p,
- const char *fw_name, struct device *device,
- int uevent)
- {
- struct device *f_dev;
- struct firmware_priv *fw_priv;
- int retval;
-
- *dev_p = NULL;
- retval = fw_register_device(&f_dev, fw_name, device);
- if (retval)
- goto out;
-
- ……
- fw_priv = dev_get_drvdata(f_dev);
-
- fw_priv->fw = fw;
- retval = sysfs_create_bin_file(&f_dev->kobj, &fw_priv->attr_data);
- ……
-
- retval = device_create_file(f_dev, &dev_attr_loading);
- ……
-
- if (uevent)
- f_dev->uevent_suppress = 0;
- *dev_p = f_dev;
- goto out;
-
- error_unreg:
- device_unregister(f_dev);
- out:
- return retval;
- }
函数fw_register_device注册设备,在文件系统sysfs中建立固件设备对应的设备类,存放固件驱动程序私有数据。其列出以下:
- static int fw_register_device(struct device **dev_p, const char *fw_name,
- struct device *device)
- {
- int retval;
- struct firmware_priv *fw_priv = kzalloc(sizeof(*fw_priv),
- GFP_KERNEL);
- struct device *f_dev = kzalloc(sizeof(*f_dev), GFP_KERNEL);
-
- *dev_p = NULL;
-
- ……
- init_completion(&fw_priv->completion);
- fw_priv->attr_data = firmware_attr_data_tmpl;
- strlcpy(fw_priv->fw_id, fw_name, FIRMWARE_NAME_MAX);
-
- fw_priv->timeout.function = firmware_class_timeout;
- fw_priv->timeout.data = (u_long) fw_priv;
- init_timer(&fw_priv->timeout);
-
- fw_setup_device_id(f_dev, device);
- f_dev->parent = device;
- f_dev->class = &firmware_class;
- dev_set_drvdata(f_dev, fw_priv);
- f_dev->uevent_suppress = 1;
- retval = device_register(f_dev);
- if (retval) {
- dev_err(device, "%s: device_register failed\n", __func__);
- goto error_kfree;
- }
- *dev_p = f_dev;
- return 0;
- ……
- }
- static struct bin_attribute firmware_attr_data_tmpl = {
- .attr = {.name = "data", .mode = 0644},
- .size = 0,
- .read = firmware_data_read,
- .write = firmware_data_write,
- };
-
- static struct class firmware_class = {
- .name = "firmware",
- .dev_uevent = firmware_uevent,
- .dev_release = fw_dev_release,
- };
linux驱动之--fops的关联
1.各类驱动形式不过是表象,本质仍是把fops注册到inode中。
2.一直没有找到确实的“证据”不过仍是有点线索的:device_create->device_create_vargs->
dev_set_drvdata(dev, drvdata)把fops设置到了dev->p->driver_data中 device_register->device_add->devtmpfs_create_node->vfs_mknod这里应该就是终点了
注:前半部分把fops函数数组放到了dev->device_private->driver_data中,后半部分vfs_mknod(nd.path.dentry->d_inode,dentry, mode, dev->devt);创建了设备号与inode名称的映射关系,这样经过文件名能够找到设备号,经过设备号就能找到dev结构,经过dev->device_private->driver_data就能解析出fops,从而给系统调用open时创建file operation
linux平台驱动其实不是真正的“驱动”它只不过作点初始化硬件的事情(在probe函数里)真正操做设备的函数在device结构里。
这里体现了C++类的影子
3.简单点说系统调用open会创建一个file结构体,而且经过文件名和路径找到inode结构,并提取i_fop给fops

4.至于提取的过程

static int chrdev_open(struct inode *inode, struct file *filp)
{
struct cdev *p;
struct cdev *new = NULL;
int ret = 0;
spin_lock(&cdev_lock);
p = inode->i_cdev;
if (!p) {
struct kobject *kobj;
int idx;
spin_unlock(&cdev_lock);
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, kobj);
spin_lock(&cdev_lock);
/* Check i_cdev again in case somebody beat us to it while
we dropped the lock. */
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new;
list_add(&inode->i_devices, &p->list);
new = NULL;
} else if (!cdev_get(p))
ret = -ENXIO;
} else if (!cdev_get(p))
ret = -ENXIO;
spin_unlock(&cdev_lock);
cdev_put(new);
if (ret)
return ret;
ret = -ENXIO;
filp->f_op = fops_get(p->ops);
if (!filp->f_op)
goto out_cdev_put;
if (filp->f_op->open) {
ret = filp->f_op->open(inode,filp);
if (ret)
goto out_cdev_put;
}
return 0;
out_cdev_put:
cdev_put(p);
return ret;
}
注:貌似只有 open(!/dev/testchar!, O_RDWR) 打开才是这样的,由于/dev目录下都是字符的驱动,是否是使用cdev的都不在sysfs内呢?
在linux设备模型浅析之设备篇中有段描述:device_add定义在drivers/base/core.c中
int device_add(struct device *dev)
{
................
if (MAJOR(dev->devt)) {
error = device_create_file(dev, &devt_attr); //若是存在设备号则添加dev_t属性,这样udev就能读取设备号属性从而在/dev/目录下建立设备节点,这样kobj和cdev也关联了
if (error)
goto ueventattrError;
注:因此我一直追求的目标貌似在这里,是udev把device里包含的fops关联到cedv里,而后chrdev_open就瓜熟蒂落了!!
补充点内容:device结构有个device_private用来放一些不想对外开放的东西,其中还有个driver_data。因此是这样的device->p->driver_data
通常状况下这里放的是file_operations可是也未必,对于platform来讲有2个函数void *dev_get_drvdata(const struct device *dev)
和void dev_set_drvdata(struct device *dev, void *data)
注册的时候set,至于之后怎么用就不必定了,好比LED的驱动,使用get函数又取出数据,放在了attr里导出到用户空间使用
Linux驱动开发之主设备号找驱动,次设备号找设备
1、引言
好久前接触linux驱动就知道主设备号找驱动,次设备号找设备。这句到底怎么理解呢,如何在驱动中实现呢,在介绍该实现以前先看下内核中主次设备号的管理:
2、Linux内核主次设备号的管理
Linux的设备管理是和文件系统紧密结合的,各类设备都以文件的形式存放在/dev目录下,称为设备文件。应用程序能够打开、关闭和读写这些设备文件,完成对设备的操做,就像操做普通的数据文件同样。为了管理这些设备,系统为设备编了号,每一个设备号又分为主设备号和次设备号。主设备号用来区分不一样种类的设备,而次设备号用来区分同一类型的多个设备。对于经常使用设备,Linux有约定俗成的编号,如终端类设备的主设备号是4。
设备号的内部表示
在内核中,dev_t 类型( 在 <linux/types.h>头文件有定义 ) 用来表示设备号,包括主设备号和次设备号两部分。对于 2.6.x内核,dev_t是个32位量,其中高12位用来表示主设备号,低20位用来表示次设备号。
在 linux/types.h 头文件里定义有
typedef __kernel_dev_t dev_t;
typedef __u32 __kernel_dev_t;
主设备号和次设备号的获取
为了写出可移植的驱动程序,不能假定主设备号和次设备号的位数。不一样的机型中,主设备号和次设备号的位数多是不一样的。应该使用MAJOR宏获得主设备号,使用MINOR宏来获得次设备号。下面是两个宏的定义:(linux/kdev_t.h)
#define MINORBITS 20 /*次设备号*/
#define MINORMASK ((1U << MINORBITS) - 1) /*次设备号掩码*/
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) /*dev右移20位获得主设备号*/
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) /*与次设备掩码与,获得次设备号*/
MAJOR宏将dev_t向右移动20位,获得主设备号;MINOR宏将dev_t的高12位清零,获得次设备号。相反,能够将主设备号和次设备号转换为设备号类型(dev_t),使用宏MKDEV能够完成这个功能。
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
MKDEV宏将主设备号(ma)左移20位,而后与次设备号(mi)相或,获得设备号
3、主设备号找驱动、次设备号找设备的内核实现
Linux内核容许多个驱动共享一个主设备号,但更多的设备都遵循一个驱动对一个主设备号的原则。
内核维护着一个以主设备号为key的全局哈希表,而哈希表中数据部分则为与该主设备号设备对应的驱动程序(只有一个次设备)的指针或者多个同类设备驱动程序组成的数组的指针(设备共享主设备号)。根据所编写的驱动程序,能够从内核那里获得一个直接指向设备驱动的指针,或者使用次设备号做为索引的数组来找到设备驱动程序。但不管哪一种方式,内核自身几乎不知道次设备号的什么事情。以下图所示:

图1:应用程序调用open时经过主次设备号找到相应驱动
来看内核中一个简单的字符设备驱动的例子,其主设备号为1,根据LANANA标准,该设备有10个不一样的次设备号。每一个都提供了一个不一样的功能,这些都与内存访问操做有关。下面列出一些次设备号,以及相关的文件名和含义。
表1 用于主设备号1的各个从设备号
从设备号 |
文件 |
含义 |
1 |
/dev/mem |
物理内存 |
2 |
/dev/kmem |
内核虚拟地址空间 |
3 |
/dev/null |
比特位桶 |
4 |
/dev/port |
访问I/O端口 |
5 |
/dev/zero |
WULL字符源 |
8 |
/dev/random |
非肯定性随机数发生器 |
一些设备是咱们熟悉的,特别是/dev/null。根据设备描述咱们能够很清楚地知道尽管这些从设备都涉及到内存访问,但所实现功能有很大差异。而后来看下图1中主设备号为1的memory_fops中定义了哪些函数指针。代码以下:
driver/char/mem.c
static const struct file_operations memory_fops = { .open = memory_open, .llseek = noop_llseek, }; |
其中函数memory_open最为关键,其做用是根据次设备号找到次设备的驱动程序。
static int memory_open(struct inode *inode, struct file *filp) { int minor; const struct memdev *dev; minor = iminor(inode); /* get the minor device number commented by guoqingbo */ if (minor >= ARRAY_SIZE(devlist)) return -ENXIO; dev = &devlist[minor];/* select the specific file_operations */ if (!dev->fops) return -ENXIO; filp->f_op = dev->fops; if (dev->dev_info) filp->f_mapping->backing_dev_info = dev->dev_info; /* Is /dev/mem or /dev/kmem ? */ if (dev->dev_info == &directly_mappable_cdev_bdi) filp->f_mode |= FMODE_UNSIGNED_OFFSET; if (dev->fops->open) //open the device return dev->fops->open(inode, filp); return 0; } |
该函数用到的图1中的devlist数组定义以下:
static const struct memdev { const char *name; mode_t mode; const struct file_operations *fops; struct backing_dev_info *dev_info; } devlist[] = { [1] = { "mem", 0, &mem_fops, &directly_mappable_cdev_bdi }, #ifdef CONFIG_DEVKMEM [2] = { "kmem", 0, &kmem_fops, &directly_mappable_cdev_bdi }, #endif [3] = { "null", 0666, &null_fops, NULL }, #ifdef CONFIG_DEVPORT [4] = { "port", 0, &port_fops, NULL }, #endif [5] = { "zero", 0666, &zero_fops, &zero_bdi }, [7] = { "full", 0666, &full_fops, NULL }, [8] = { "random", 0666, &random_fops, NULL }, [9] = { "urandom", 0666, &urandom_fops, NULL }, [11] = { "kmsg", 0, &kmsg_fops, NULL }, #ifdef CONFIG_CRASH_DUMP [12] = { "oldmem", 0, &oldmem_fops, NULL }, #endif }; |
经过上面代码及图1可看出,memory_open实际上实现了一个分配器(根据次设备号区分各个设备,而且选择适当的file_operations),图2说明了打开内存设备时,文件操做是如何改变的。所涉及的函数逐渐反映了设备的具体特性。最初只知道用于打开设备的通常函数,而后由打开与内存相关设备文件的具体函数所替代。接下来根据选择的次设备号,进一步细化函数指针 ,为不一样的次设备号最终选定函数指针。

图2:设备驱动程序函数指针的选择过程
Linux 用户空间与内核空间数据交换方式
引言
通常地,在使用虚拟内存技术的多任务系统上,内核和应用有不一样的地址空间,所以,在内核和应用之间以及在应用与应用之间进行数据交换须要专门的机制来实现,众所周知,进程间通讯(IPC)机制就是为实现应用与应用之间的数据交换而专门实现的,大部分读者可能对进程间通讯比较了解,但对应用与内核之间的数据交换机制可能了解甚少
本文将详细介绍 Linux 系统下内核与应用进行数据交换的各类方式,包括内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs 和 relayfs。
系统调用
Linux内核提供了多个函数和宏用于内核空间和用户空间传递数据。
主要有:access_ok(),copy_to_user(),copy_from_user,put_user,get_user。
1.access_ok()
函数原型:int access_ok(int type,unsigned long addr,unsigned long size)
函数access_ok()用于检查指定地址是否能够访问。参数type为访问方式,能够为VERIFY_READ(可读),VERIFY_WRITE(可写)。addr为要操做的地址,size为要操做的空间大小(以字节计算)。函数返回1,表示能够访问,0表示不能够访问。
2.copy_to_user()和copy_from_user()
函数原型:unsigned long copy_to_user(void *to,const void *from,unsigned long len)
unsigned long copy_from_user(void *to,const void *from,unsigned long len)
这两个函数用于内核空间与用户空间的数据交换。copy_to_user()用于把数据从内核空间拷贝至用户空间,copy_from_user()用于把数据从用户空间拷贝至内核空间。第一个参数to为目标地址,第二个参数from为源地址,第三个参数len为要拷贝的数据个数,以字节计算。这两个函数在内部调用access_ok()进行地址检查。返回值为未能拷贝的字节数。
3.get_user()和put_user()
函数原型:int get_user(x,p)
int put_user(x,p)
这是两个宏,用于一个基本数据(1,2,4字节)的拷贝。get_user()用于把数据从用户空间拷贝至内核空间,put_user()用于把数据从内核空间拷贝至用户空间。x为内核空间的数据,p为用户空间的指针。这两个宏会调用access_ok()进行地址检查。拷贝成功,返回0,不然返回-EFAULT。
4.还有两个函数__copy_to_user()和__copy_from_user(),功能与copy_to_user()和copy_from_user()相同,只是不进行地址检查。还有两个宏__get_user()和__put_user(),功能与get_user()和put_user()相同,也不进行地址检查。
(一般状况下,应用程序经过内核接口访问驱动程序,所以,驱动程序须要和应用程序交换数据。Linux将存储器分为“内核空间”和“用户空间”。操做系统和驱动程序在内核空间运行,应用程序在用户空间运行,二者不能简单地使用指针传递数据。由于Linux系统使用了虚拟内存机制,用户空间的内存可能被换出,当内核空间使用用户空间指针时,对应的数据可能不在内存中。Linux内核提供了多个函数和宏用于内核空间和用户空间传递数据。)
内核开发者常常须要向用户空间应用输出一些调试信息,在稳定的系统中可能根本不须要这些调试信息,可是在开发过程当中,为了搞清楚内核的行为,调试信息很是必要,printk多是用的最多的,但它并非最好的,调试信息只是在开发中用于调试,而printk将一直输出,所以开发完毕后须要清除没必要要 的printk语句,另外若是开发者但愿用户空间应用可以改变内核行为时,printk就没法实现。所以,须要一种新的机制,那只有在须要的时候使用,它在须要时经过在一个虚拟文件系统中建立一个或多个文件来向用户空间应用提供调试信息。
有几种方式能够实现上述要求:
(1)使用procfs,在/proc建立文件输出调试信息,可是procfs对于大于一个内存页(对于x86是4K)的输出比较麻烦,并且速度慢,有时回出现一些意想不到的问题。
(2)使用sysfs(2.6内核引入的新的虚拟文件系统),在不少状况下,调试信息能够存放在那里,可是sysfs主要用于系统管理,它但愿每个文件对应内核的一个变量,若是使用它输出复杂的数据结构或调试信息是很是困难的。
(3)使用libfs建立一个新的文件系统,该方法极其灵活,开发者能够为新文件系统设置一些规则,使用libfs使得建立新文件系统更加简单,可是仍然超出了一个开发者的想象。
(4)为了使得开发者更加容易使用这样的机制,Greg Kroah-Hartman开发了debugfs(在2.6.11中第一次引入),它是一个虚拟文件系统,专门用于输出调试信息,该文件系统很是小,很容易使用,能够在配置内核时选择是否构件到内核中,在不选择它的状况下,使用它提供的API的内核部分不须要作任何改动。
使用debugfs的开发者首先须要在文件系统中建立一个目录,下面函数用于在debugfs文件系统下建立一个目录:
struct dentry *debugfs_create_dir(const char *name, struct dentry *parent);
参数name是要建立的目录名,参数parent指定建立目录的父目录的dentry,若是为NULL,目录将建立在debugfs文件系统的根目录下。若是返回为-ENODEV,表示内核没有把debugfs编译到其中,若是返回为NULL,表示其余类型的建立失败,若是建立目录成功,返回指向该 目录对应的dentry条目的指针。
下面函数用于在debugfs文件系统中建立一个文件:
struct dentry *debugfs_create_file(const char *name, mode_t mode, struct dentry *parent,
void *data, struct file_operations *fops);
参数name指定要建立的文件名,参数mode指定该文件的访问许可,参数parent指向该文件所在目录,参数data为该文件特定的一些数据, 参数fops为实如今该文件上进行文件操做的fiel_operations结构指针,在不少状况下,由seq_file提供的文件操做实现就足够了,所以使用debugfs很容易,固然,在一些状况下,开发者可能仅须要使用用户应用能够控制的变量来调试,debugfs也提供了4个这样的API方便开发者使用:
struct dentry *debugfs_create_u8(const char *name, mode_t mode, struct dentry *parent, u8 *value);
struct dentry *debugfs_create_u16(const char *name, mode_t mode, struct dentry *parent, u16 *value);
struct dentry *debugfs_create_u32(const char *name, mode_t mode, struct dentry *parent, u32 *value);
struct dentry *debugfs_create_bool(const char *name, mode_t mode, struct dentry *parent, u32 *value);
参数name和mode指定文件名和访问许可,参数value为须要让用户应用控制的内核变量指针。
当内核模块卸载时,Debugfs并不会自动清除该模块建立的目录或文件,所以对于建立的每个文件或目录,开发者必须调用下面函数清除:
void debugfs_remove(struct dentry *dentry);
参数dentry为上面建立文件和目录的函数返回的dentry指针。
在下面给出了一个使用debufs的示例模块debugfs_exam.c,为了保证该模块正确运行,必须让内核支持debugfs, debugfs是一个调试功能,所以它位于主菜单Kernel hacking,而且必须选择Kernel debugging选项才能选择,它的选项名称为Debug Filesystem。为了在用户态使用debugfs,用户必须mount它,下面是在做者系统上的使用输出:
$ mkdir -p /debugfs
$ mount -t debugfs debugfs /debugfs
$ insmod ./debugfs_exam.ko
$ ls /debugfs
debugfs-exam
$ ls /debugfs/debugfs-exam
u8_var u16_var u32_var bool_var
$ cd /debugfs/debugfs-exam
$ cat u8_var
0
$ echo 200 > u8_var
$ cat u8_var
200
$ cat bool_var
N
$ echo 1 > bool_var
$ cat bool_var
Y
//kernel module: debugfs_exam.c
#include <linux/config.h>
#include <linux/module.h>
#include <linux/debugfs.h>
#include <linux/types.h>
/*dentry:目录项,是Linux文件系统中某个索引节点(inode)的连接。这个索引节点能够是文件,也能够是目录。
Linux用数据结构dentry来描述fs中和某个文件索引节点相连接的一个目录项(能是文件,也能是目录)。
(1)未使用(unused)状态:该dentry对象的引用计数d_count的值为0,但其d_inode指针仍然指向相关
的的索引节点。该目录项仍然包含有效的信息,只是当前没有人引用他。这种dentry对象在回收内存时可能会被释放。
(2)正在使用(inuse)状态:处于该状态下的dentry对象的引用计数d_count大于0,且其d_inode指向相关
的inode对象。这种dentry对象不能被释放。
(3)负(negative)状态:和目录项相关的inode对象不复存在(相应的磁盘索引节点可能已被删除),dentry
对象的d_inode指针为NULL。但这种dentry对象仍然保存在dcache中,以便后续对同一文件名的查找可以快速完成。
这种dentry对象在回收内存时将首先被释放。
*/
static struct dentry *root_entry, *u8_entry, *u16_entry, *u32_entry, *bool_entry;
static u8 var8;
static u16 var16;
static u32 var32;
static u32 varbool;
static int __init exam_debugfs_init(void)
{
root_entry = debugfs_create_dir("debugfs-exam", NULL);
if (!root_entry) {
printk("Fail to create proc dir: debugfs-exam\n");
return 1;
}
u8_entry = debugfs_create_u8("u8-var", 0644, root_entry, &var8);
u16_entry = debugfs_create_u16("u16-var", 0644, root_entry, &var16);
u32_entry = debugfs_create_u32("u32-var", 0644, root_entry, &var32);
bool_entry = debugfs_create_bool("bool-var", 0644, root_entry, &varbool);
return 0;
}
static void __exit exam_debugfs_exit(void)
{
debugfs_remove(u8_entry);
debugfs_remove(u16_entry);
debugfs_remove(u32_entry);
debugfs_remove(bool_entry);
debugfs_remove(root_entry);
}
module_init(exam_debugfs_init);
module_exit(exam_debugfs_exit);
MODULE_LICENSE("GPL");
procfs是比较老的一种用户态与内核态的数据交换方式,内核的不少数据都是经过这种方式出口给用户的,内核的不少参数也是经过这种方式来让用户方便设置的。除了sysctl出口到/proc下的参数,procfs提供的大部份内核参数是只读的。实际上,不少应用严重地依赖于procfs,所以它几乎是必不可少的组件。本节将讲解如何使用procfs。
Procfs提供了以下API:
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。不然它应当为proc_mkdir 返回的struct proc_dir_entry结构的指针。
extern
void
remove_proc_entry(
const
char
*
name,
struct
proc_dir_entry
*
parent)
该函数用于删除上面函数建立的proc条目,参数name给出要删除的proc条目的名称,参数parent指定创建的proc条目所在的目录。
struct
proc_dir_entry
*
proc_mkdir(
const
char
*
name,
struct
proc_dir_entry
*
parent)
该函数用于建立一个proc目录,参数name指定要建立的proc目录的名称,参数parent为该proc目录所在的目录。
extern
struct
proc_dir_entry
*
proc_mkdir_mode(
const
char
*
name, mode_t mode,
struct
proc_dir_entry
*
parent)
struct
proc_dir_entry
*
proc_symlink(
const
char
*
name,
struct
proc_dir_entry
*
parent,
const
char
*
dest)
该函数用于创建一个proc条目的符号连接,参数name给出要创建的符号连接proc条目的名称,参数parent指定符号链接所在的目录,参数dest指定连接到的proc条目名称。
struct
proc_dir_entry
*
create_proc_read_entry(
const
char
*
name, mode_t mode,
struct
proc_dir_entry
*
base
,
read_proc_t
*
read_proc,
void
*
data);
该函数用于创建一个规则的只读proc条目,参数name给出要创建的proc条目的名称,参数mode给出了创建的该proc条目的访问权限,参 数base指定创建的proc条目所在的目录,参数read_proc给出读去该proc条目的操做函数,参数data为该proc条目的专用数据,它将 保存在该proc条目对应的struct file结构的private_data字段中。
struct
proc_dir_entry
*
create_proc_info_entry(
const
char
*
name, mode_t mode,
struct
proc_dir_entry
*
base
,
get_info_t
*
get_info);
该函数用于建立一个info型的proc条目,参数name给出要创建的proc条目的名称,参数mode给出了创建的该proc条目的访问权限, 参数base指定创建的proc条目所在的目录,参数get_info指定该proc条目的get_info操做函数。实际上get_info等同于 read_proc,若是proc条目没有定义个read_proc,对该proc条目的read操做将使用get_info取代,所以它在功能上很是相似于函数create_proc_read_entry。
struct
proc_dir_entry
*
proc_net_create(
const
char
*
name, mode_t mode, get_info_t
*
get_info)
该函数用于在/proc/net目录下建立一个proc条目,参数name给出要创建的proc条目的名称,参数mode给出了创建的该proc条目的访问权限,参数get_info指定该proc条目的get_info操做函数。
struct
proc_dir_entry
*
proc_net_fops_create(
const
char
*
name, mode_t mode,
struct
file_operations
*
fops)
该函数也用于在/proc/net下建立proc条目,可是它也同时指定了对该proc条目的文件操做函数。
void
proc_net_remove(
const
char
*
name)
该函数用于删除前面两个函数在/proc/net目录下建立的proc条目。参数name指定要删除的proc名称。
除了这些函数,值得一提的是结构struct proc_dir_entry,为了建立一了可写的proc条目并指定该proc条目的写操做函数,必须设置上面的这些建立proc条目的函数返回的指针 指向的struct proc_dir_entry结构的write_proc字段,并指定该proc条目的访问权限有写权限。
为了使用这些接口函数以及结构struct proc_dir_entry,用户必须在模块中包含头文件linux/proc_fs.h。
在源代码包中给出了procfs示例程序procfs_exam.c,它定义了三个proc文件条目和一个proc目录条目,读者在插入该模块后应当看到以下结构:
$ ls
/
proc
/
myproctest
aint astring bigprocfile
$
读者能够经过cat和echo等文件操做函数来查看和设置这些proc文件。特别须要指出,bigprocfile是一个大文件(超过一个内存
页),对于这种大文件,procfs有一些限制,由于它提供的缓存,只有一个页,所以必须特别当心,并对超过页的部分作特别的考虑,处理起来比较复杂而且
很容易出错,全部procfs并不适合于大数据量的输入输出,后面一节seq_file就是由于这一缺陷而设计的,固然seq_file依赖于 procfs的一些基础功能。
//
kernel module: procfs_exam.c
#include
<
linux
/
config.h
>
#include
<
linux
/
kernel.h
>
#include
<
linux
/
module.h
>
#include
<
linux
/
proc_fs.h
>
#include
<
linux
/
sched.h
>
#include
<
linux
/
types.h
>
#include
<
asm
/
uaccess.h
>
#define
STR_MAX_SIZE 255
static
int
int_var;
static
char
string_var[
256
];
static
char
big_buffer[
65536
];
static
int
big_buffer_len
=
0
;
static
struct
proc_dir_entry
*
myprocroot;
static
int
first_write_flag
=
1
;
int
int_read_proc(
char
*
page,
char
**
start, off_t off,
int
count,
int
*
eof,
void
*
data)
{
count
=
sprintf(page,
"
%d
"
,
*
(
int
*
)data);
return
count;
}
int
int_write_proc(
struct
file
*
file,
const
char
__user
*
buffer,unsigned
long
count,
void
*
data)
{
unsigned
int
c
=
0
, len
=
0
, val, sum
=
0
;
int
*
temp
=
(
int
*
)data;
while
(count) {
if
(get_user(c, buffer))
//
从用户空间中获得数据
return
-
EFAULT;
len
++
;
buffer
++
;
count
--
;
if
(c
==
10
||
c
==
0
)
break
;
val
=
c
-
'
0
'
;
if
(val
>
9
)
return
-
EINVAL;
sum
*=
10
;
sum
+=
val;
}
*
temp
=
sum;
return
len;
}
int
string_read_proc(
char
*
page,
char
**
start, off_t off,
int
count,
int
*
eof,
void
*
data)
{
count
=
sprintf(page,
"
%s
"
, (
char
*
)data);
return
count;
}
int
string_write_proc(
struct
file
*
file,
const
char
__user
*
buffer, unsigned
long
count,
void
*
data)
{
if
(count
>
STR_MAX_SIZE) {
count
=
255
;
}
copy_from_user(data, buffer, count);
return
count;
}
int
bigfile_read_proc(
char
*
page,
char
**
start, off_t off,
int
count,
int
*
eof,
void
*
data)
{
if
(off
>
big_buffer_len) {
*
eof
=
1
;
return
0
;
}
if
(count
>
PAGE_SIZE) {
count
=
PAGE_SIZE;
}
if
(big_buffer_len
-
off
<
count) {
count
=
big_buffer_len
-
off;
}
memcpy(page, data, count);
*
start
=
page;
return
count;
}
int
bigfile_write_proc(
struct
file
*
file,
const
char
__user
*
buffer, unsigned
long
count,
void
*
data)
{
char
*
p
=
(
char
*
)data;
if
(first_write_flag) {
big_buffer_len
=
0
;
first_write_flag
=
0
;
}
if
(
65536
-
big_buffer_len
<
count) {
count
=
65536
-
big_buffer_len;
first_write_flag
=
1
;
}
copy_from_user(p
+
big_buffer_len, buffer, count);
big_buffer_len
+=
count;
return
count;
}
static
int
__init procfs_exam_init(
void
)
{
#ifdef CONFIG_PROC_FS
struct
proc_dir_entry
*
entry;
myprocroot
=
proc_mkdir(
"
myproctest
"
, NULL);
entry
=
create_proc_entry(
"
aint
"
,
0644
, myprocroot);
if
(entry) {
entry
->
data
=
&
int_var;
entry
->
read_proc
=
&
int_read_proc;
entry
->
write_proc
=
&
int_write_proc;
}
entry
=
create_proc_entry(
"
astring
"
,
0644
, myprocroot);
if
(entry) {
entry
->
data
=
&
string_var;
entry
->
read_proc
=
&
string_read_proc;
entry
->
write_proc
=
&
string_write_proc;
}
entry
=
create_proc_entry(
"
bigprocfile
"
,
0644
, myprocroot);
if
(entry) {
entry
->
data
=
&
big_buffer;
entry
->
read_proc
=
&
bigfile_read_proc;
entry
->
write_proc
=
&
bigfile_write_proc;
}
#else
printk(
"
This module requires the kernel to support procfs,\n
"
);
#endif
return
0
;
}
staticvoid __exit procfs_exam_exit(void)
{
#ifdef CONFIG_PROC_FS
remove_proc_entry("aint", myprocroot);
remove_proc_entry("astring", myprocroot);
remove_proc_entry("bigprocfile", myprocroot);
remove_proc_entry("myproctest", NULL);
#endif
}
module_init(procfs_exam_init);
module_exit(procfs_exam_exit);
MODULE_LICENSE("GPL");
通常地,内核经过在procfs文件系统下创建文件来向用户空间提供输出信息,用户空间能够经过任何文本阅读应用查看该文件信息,可是procfs 有一个缺陷,若是输出内容大于1个内存页,须要屡次读,所以处理起来很难,另外,若是输出太大,速度比较慢,有时会出现一些意想不到的状况, Alexander Viro实现了一套新的功能,使得内核输出大文件信息更容易,该功能出如今2.4.15(包括2.4.15)之后的全部2.4内核以及2.6内核中,尤为 是在2.6内核中,已经大量地使用了该功能。
要想使用seq_file功能,开发者须要包含头文件linux/seq_file.h,并定义与设置一个seq_operations结构(相似于file_operations结构):
struct
seq_operations {
void
*
(
*
start) (
struct
seq_file
*
m, loff_t
*
pos);
void
(
*
stop) (
struct
seq_file
*
m,
void
*
v);
void
*
(
*
next) (
struct
seq_file
*
m,
void
*
v, loff_t
*
pos);
int
(
*
show) (
struct
seq_file
*
m,
void
*
v);
};
start函数用于指定seq_file文件的读开始位置,返回实际读开始位置,若是指定的位置超过文件末尾,应当返回NULL,start函数能够有一个特殊的返回SEQ_START_TOKEN,它用于让show函数输出文件头,但这只能在pos为0时使用,next函数用于把seq_file文件的当前读位置移动到下一个读位置,返回实际的下一个读位置,若是已经到达文件末尾,返回NULL,stop函数用于在读完seq_file文件后调用,它相似于文件操做close,用于作一些必要的清理,如释放内存等,show函数用于格式化输出,若是成功返回0,不然返回出错码。
Seq_file也定义了一些辅助函数用于格式化输出:
/*
函数seq_putc用于把一个字符输出到seq_file文件
*/
int
seq_putc(
struct
seq_file
*
m,
char
c);
/*
函数seq_puts则用于把一个字符串输出到seq_file文件
*/
int
seq_puts(
struct
seq_file
*
m,
const
char
*
s);
/*
函数seq_escape相似于seq_puts,只是,它将把第一个字符串参数中出现的包含在第二个字符串参数
中的字符按照八进制形式输出,也即对这些字符进行转义处理
*/
int
seq_escape(
struct
seq_file
*
,
const
char
*
,
const
char
*
);
/*
函数seq_printf是最经常使用的输出函数,它用于把给定参数按照给定的格式输出到seq_file文件
*/
int
seq_printf(
struct
seq_file
*
,
const
char
*
, ...)__attribute__ ((format(printf,
2
,
3
)));
/*
函数seq_path则用于输出文件名,字符串参数提供须要转义的文件名字符,它主要供文件系统使用
*/
int
seq_path(
struct
seq_file
*
,
struct
vfsmount
*
,
struct
dentry
*
,
char
*
);
在定义告终构struct seq_operations以后,用户还须要把打开seq_file文件的open函数,以便该结构与对应于seq_file文件的struct file结构关联起来,例如,struct seq_operations定义为:
struct
seq_operations exam_seq_ops
=
{
.start
=
exam_seq_start,
.stop
=
exam_seq_stop,
.next
=
exam_seq_next,
.show
=
exam_seq_show
};
那么,open函数应该以下定义:
static
int
exam_seq_open(
struct
inode
*
inode,
struct
file
*
file)
{
return
seq_open(file,
&
exam_seq_ops);
};
注意,函数seq_open是seq_file提供的函数,它用于把struct seq_operations结构与seq_file文件关联起来。
最后,用户须要以下设置struct file_operations结构:
struct
file_operations exam_seq_file_ops
=
{
.owner
=
THIS_MODULE,
.open
=
exm_seq_open,
.read
=
seq_read,
.llseek
=
seq_lseek,
.release
=
seq_release
};
注意,用户仅须要设置open函数,其它的都是seq_file提供的函数。
而后,用户建立一个/proc文件并把它的文件操做设置为exam_seq_file_ops便可:
struct
proc_dir_entry
*
entry;
entry
=
create_proc_entry(
"
exam_seq_file
"
,
0
, NULL);
if
(entry)
entry
->
proc_fops
=
&
exam_seq_file_ops;
对于简单的输出,seq_file用户并不须要定义和设置这么多函数与结构,它仅需定义一个show函数,而后使用single_open来定义open函数就能够,如下是使用这种简单形式的通常步骤:
1.定义一个show函数
int
exam_show(
struct
seq_file
*
p,
void
*
v)
{
…
}
2. 定义open函数
int
exam_single_open(
struct
inode
*
inode,
struct
file
*
file)
{
return
(single_open(file, exam_show, NULL));
}
注意要使用single_open而不是seq_open。
3. 定义struct file_operations结构
struct
file_operations exam_single_seq_file_operations
=
{
.open
=
exam_single_open,
.read
=
seq_read,
.llseek
=
seq_lseek,
.release
=
single_release,
};
注意,若是open函数使用了single_open,release函数必须为single_release,而不是seq_release。 下面给出了一个使用seq_file的具体例子seqfile_exam.c,它使用seq_file提供了一个查看当前系统运行的全部进程的/proc接口,在编译并插入该模块后,用户经过命令"cat /proc/exam_esq_file"能够查看系统的全部进程。
//
kernel module: seqfile_exam.c
#include
<
linux
/
config.h
>
#include
<
linux
/
module.h
>
#include
<
linux
/
proc_fs.h
>
#include
<
linux
/
seq_file.h
>
#include
<
linux
/
percpu.h
>
#include
<
linux
/
sched.h
>
static
struct
proc_dir_entry
*
entry;
static
void
*
l_start(
struct
seq_file
*
m, loff_t
*
pos)
{
loff_t index
=
*
pos;
if
(index
==
0
) {
seq_printf(m,
"
Current all the processes in system:\n
"
"
%-24s%-5s\n
"
,
"
name
"
,
"
pid
"
);
return
&
init_task;
}
else
{
return
NULL;
}
}
static
void
*
l_next(
struct
seq_file
*
m,
void
*
p, loff_t
*
pos)
{
task_t
*
task
=
(task_t
*
)p;
task
=
next_task(task);
if
((
*
pos
!=
0
)
&&
(task
==
&
init_task)) {
return
NULL;
}
++*
pos;
return
task;
}
static
void
l_stop(
struct
seq_file
*
m,
void
*
p)
{
}
static
int
l_show(
struct
seq_file
*
m,
void
*
p)
{
task_t
*
task
=
(task_t
*
)p;
seq_printf(m,
"
%-24s%-5d\n
"
, task
->
comm, task
->
pid);
return
0
;
}
static
struct
seq_operations exam_seq_op
=
{
.start
=
l_start,
.next
=
l_next,
.stop
=
l_stop,
.show
=
l_show
};
static
int
exam_seq_open(
struct
inode
*
inode,
struct
file
*
file)
{
return
seq_open(file,
&
exam_seq_op);
}
static
struct
file_operations exam_seq_fops
=
{
.open
=
exam_seq_open,
.read
=
seq_read,
.llseek
=
seq_lseek,
.release
=
seq_release,
};
static
int
__init exam_seq_init(
void
)
{
entry
=
create_proc_entry(
"
exam_esq_file
"
,
0
, NULL);
if
(entry)
entry
->
proc_fops
=
&
exam_seq_fops;
return
0
;
}
static
void
__exit exam_seq_exit(
void
)
{
remove_proc_entry(
"
exam_esq_file
"
, NULL);
}
module_init(exam_seq_init);
module_exit(exam_seq_exit);
MODULE_LICENSE(
"
GPL
"
);
relayfs是一个快速的转发(relay)数据的文件系统,它以其功能而得名。它为那些须要从内核空间转发大量数据到用户空间的工具和应用提供了快速有效的转发机制。
Channel是relayfs文件系统定义的一个主要概念,每个channel由一组内核缓存组成,每个CPU有一个对应于该channel 的内核缓存,每个内核缓存用一个在relayfs文件系统中的文件文件表示,内核使用relayfs提供的写函数把须要转发给用户空间的数据快速地写入当前CPU上的channel内核缓存,用户空间应用经过标准的文件I/O函数在对应的channel文件中能够快速地取得这些被转发出的数据mmap 来。写入到channel中的数据的格式彻底取决于内核中建立channel的模块或子系统。
relayfs的用户空间API:
relayfs实现了四个标准的文件I/O函数,open、mmap、poll和close.
open(),打开一个channel在某一个CPU上的缓存对应的文件。
mmap(),把打开的channel缓存映射到调用者进程的内存空间。
read (),读取channel缓存,随后的读操做将看不到被该函数消耗的字节,若是channel的操做模式为非覆盖写,那么用户空间应用在有内核模块写时仍 能够读取,可是若是channel的操做模式为覆盖式,那么在读操做期间若是有内核模块进行写,结果将没法预知,所以对于覆盖式写的channel,用户 应当在确认在channel的写彻底结束后再进行读。
poll(),用于通知用户空间应用转发数据跨越了子缓存的边界,支持的轮询标志有POLLIN、POLLRDNORM和POLLERR。
close(),关闭open函数返回的文件描述符,若是没有进程或内核模块打开该channel缓存,close函数将释放该channel缓存。
注意:用户态应用在使用上述API时必须保证已经挂载了relayfs文件系统,但内核在建立和使用channel时不须要relayfs已经挂载。下面命令将把relayfs文件系统挂载到/mnt/relay。
mount -t relayfs relayfs /mnt/relay
relayfs内核API:
relayfs提供给内核的API包括四类:channel管理、写函数、回调函数和辅助函数。
Channel管理函数包括:
relay_open(base_filename, parent, subbuf_size, n_subbufs, overwrite, callbacks)
relay_close(chan)
relay_flush(chan)
relay_reset(chan)
relayfs_create_dir(name, parent)
relayfs_remove_dir(dentry)
relay_commit(buf, reserved, count)
relay_subbufs_consumed(chan, cpu, subbufs_consumed)
写函数包括:
relay_write(chan, data, length)
__relay_write(chan, data, length)
relay_reserve(chan, length)
回调函数包括:
subbuf_start(buf, subbuf, prev_subbuf_idx, prev_subbuf)
buf_mapped(buf, filp)
buf_unmapped(buf, filp)
辅助函数包括:
relay_buf_full(buf)
subbuf_start_reserve(buf, length)
前面已经讲过,每个channel由一组channel缓存组成,每一个CPU对应一个该channel的缓存,每个缓存又由一个或多个子缓存组成,每个缓存是子缓存组成的一个环型缓存。
函数relay_open用于建立一个channel并分配对应于每个CPU的缓存,用户空间应用经过在relayfs文件系统中对应的文件能够 访问channel缓存,参数base_filename用于指定channel的文件名,relay_open函数将在relayfs文件系统中建立 base_filename0..base_filenameN-1,即每个CPU对应一个channel文件,其中N为CPU数,缺省状况下,这些文件将创建在relayfs文件系统的根目录下,但若是参数parent非空,该函数将把channel文件建立于parent目录下,parent目录使 用函数relay_create_dir建立,函数relay_remove_dir用于删除由函数relay_create_dir建立的目录,谁建立的目录,谁就负责在不用时负责删除。参数subbuf_size用于指定channel缓存中每个子缓存的大小,参数n_subbufs用于指定 channel缓存包含的子缓存数,所以实际的channel缓存大小为(subbuf_size x n_subbufs),参数overwrite用于指定该channel的操做模式,relayfs提供了两种写模式,一种是覆盖式写,另外一种是非覆盖式 写。使用哪种模式彻底取决于函数subbuf_start的实现,覆盖写将在缓存已满的状况下无条件地继续从缓存的开始写数据,而无论这些数据是否已经 被用户应用读取,所以写操做决不失败。在非覆盖写模式下,若是缓存满了,写将失败,但内核将在用户空间应用读取缓存数据时经过函数 relay_subbufs_consumed()通知relayfs。若是用户空间应用没来得及消耗缓存中的数据或缓存已满,两种模式都将致使数据丢失,惟一的区别是,前者丢失数据在缓存开头,然后者丢失数据在缓存末尾。一旦内核再次调用函数relay_subbufs_consumed(),已满的缓存将再也不满,于是能够继续写该缓存。当缓存满了之后,relayfs将调用回调函数buf_full()来通知内核模块或子系统。当新的数据太大没法写 入当前子缓存剩余的空间时,relayfs将调用回调函数subbuf_start()来通知内核模块或子系统将须要使用新的子缓存。内核模块须要在该回调函数中实现下述功能:
初始化新的子缓存;
若是1正确,完成当前子缓存;
若是2正确,返回是否正确完成子缓存切换;
在非覆盖写模式下,回调函数subbuf_start()应该以下实现:
static
int
subbuf_start(
struct
rchan_buf
*
buf,
void
*
subbuf,
void
*
prev_subbuf,
unsigned
int
prev_padding)
{
if
(prev_subbuf)
*
((unsigned
*
)prev_subbuf)
=
prev_padding;
if
(relay_buf_full(buf))
return
0
;
subbuf_start_reserve(buf,
sizeof
(unsigned
int
));
return
1
;
}
若是当前缓存满,即全部的子缓存都没读取,该函数返回0,指示子缓存切换没有成功。当子缓存经过函数relay_subbufs_consumed ()被读取后,读取者将负责通知relayfs,函数relay_buf_full()在已经有读者读取子缓存数据后返回0,在这种状况下,子缓存切换成 功进行。
在覆盖写模式下, subbuf_start()的实现与非覆盖模式相似:
static
int
subbuf_start(
struct
rchan_buf
*
buf,
void
*
subbuf,
void
*
prev_subbuf, unsigned
int
prev_padding)
{
if
(prev_subbuf)
*
((unsigned
*
)prev_subbuf)
=
prev_padding;
subbuf_start_reserve(buf,
sizeof
(unsigned
int
));
return
1
;
}
只是不作relay_buf_full()检查,由于此模式下,缓存是环行的,能够无条件地写。所以在此模式下,子缓存切换一定成功,函数 relay_subbufs_consumed() 也无须调用。若是channel写者没有定义subbuf_start(),缺省的实现将被使用。 能够经过在回调函数subbuf_start()中调用辅助函数subbuf_start_reserve()在子缓存中预留头空间,预留空间能够保存任 何须要的信息,如上面例子中,预留空间用于保存子缓存填充字节数,在subbuf_start()实现中,前一个子缓存的填充值被设置。前一个子缓存的填 充值和指向前一个子缓存的指针一道做为subbuf_start()的参数传递给subbuf_start(),只有在子缓存完成后,才能知道填充值。 subbuf_start()也被在channel建立时分配每个channel缓存的第一个子缓存时调用,以便预留头空间,但在这种状况下,前一个子 缓存指针为NULL。
内核模块使用函数relay_write()或__relay_write()往channel缓存中写须要转发的数据,它们的区别是前者失效了本 地中断,然后者只抢占失效,所以前者能够在任何内核上下文安全使用,然后者应当在没有任何中断上下文将写channel缓存的状况下使用。这两个函数没有 返回值,所以用户不能直接肯定写操做是否失败,在缓存满且写模式为非覆盖模式时,relayfs将经过回调函数buf_full来通知内核模块。
函数relay_reserve()用于在channel缓存中预留一段空间以便之后写入,在那些没有临时缓存而直接写入channel缓存的内核 模块可能须要该函数,使用该函数的内核模块在实际写这段预留的空间时能够经过调用relay_commit()来通知relayfs。当全部预留的空间全 部写完并经过relay_commit通知relayfs后,relayfs将调用回调函数deliver()通知内核模块一个完整的子缓存已经填满。因为预留空间的操做并不在写channel的内核模块彻底控制之下,所以relay_reserve()不能很好地保护缓存,所以当内核模块调用 relay_reserve()时必须采起恰当的同步机制。
当内核模块结束对channel的使用后须要调用relay_close() 来关闭channel,若是没有任何用户在引用该channel,它将和对应的缓存所有被释放。
函数relay_flush()强制在全部的channel缓存上作一个子缓存切换,它在channel被关闭前使用来终止和处理最后的子缓存。
函数relay_reset()用于将一个channel恢复到初始状态,于是没必要释放现存的内存映射并从新分配新的channel缓存就能够使用channel,可是该调用只有在该channel没有任何用户在写的状况下才能够安全使用。
回调函数buf_mapped() 在channel缓存被映射到用户空间时被调用。
回调函数buf_unmapped()在释放该映射时被调用。内核模块能够经过它们触发一些内核操做,如开始或结束channel写操做。
在源代码包中给出了一个使用relayfs的示例程序relayfs_exam.c,它只包含一个内核模块,对于复杂的使用,须要应用程序配合。该模块实现了相似于文章中seq_file示例实现的功能。
固然为了使用relayfs,用户必须让内核支持relayfs,而且要mount它,下面是做者系统上的使用该模块的输出信息:
$ mkdir -p /relayfs
$ insmod ./relayfs-exam.ko
$ mount -t relayfs relayfs /relayfs
$ cat /relayfs/example0
…
$
relayfs是一种比较复杂的内核态与用户态的数据交换方式,本例子程序只提供了一个较简单的使用方式,对于复杂的使用,请参考relayfs用例页面http://relayfs.sourceforge.net/examples.html。
//
kernel module: relayfs-exam.c
#include
<
linux
/
module.h
>
#include
<
linux
/
relayfs_fs.h
>
#include
<
linux
/
string
.h
>
#include
<
linux
/
sched.h
>
#define
WRITE_PERIOD (HZ * 60)
static
struct
rchan
*
chan;
static
size_t subbuf_size
=
65536
;
static
size_t n_subbufs
=
4
;
static
char
buffer[
256
];
void
relayfs_exam_write(unsigned
long
data);
static
DEFINE_TIMER(relayfs_exam_timer, relayfs_exam_write,
0
,
0
);
void
relayfs_exam_write(unsigned
long
data)
{
int
len;
task_t
*
p
=
NULL;
len
=
sprintf(buffer,
"
Current all the processes:\n
"
);
len
+=
sprintf(buffer
+
len,
"
process name\t\tpid\n
"
);
relay_write(chan, buffer, len);
for_each_process(p) {
len
=
sprintf(buffer,
"
%s\t\t%d\n
"
, p
->
comm, p
->
pid);
relay_write(chan, buffer, len);
}
len
=
sprintf(buffer,
"
\n\n
"
);
relay_write(chan, buffer, len);
relayfs_exam_timer.expires
=
jiffies
+
WRITE_PERIOD;
add_timer(
&
relayfs_exam_timer);
}
/*
* subbuf_start() relayfs callback.
*
* Defined so that we can 1) reserve padding counts in the sub-buffers, and
* 2) keep a count of events dropped due to the buffer-full condition.
*/
static
int
subbuf_start(
struct
rchan_buf
*
buf,
void
*
subbuf,
void
*
prev_subbuf,
unsigned
int
prev_padding)
{
if
(prev_subbuf)
*
((unsigned
*
)prev_subbuf)
=
prev_padding;
if
(relay_buf_full(buf))
return
0
;
subbuf_start_reserve(buf,
sizeof
(unsigned
int
));
return
1
;
}
/*
* relayfs callbacks
*/
static
struct
rchan_callbacks relayfs_callbacks
=
{
.subbuf_start
=
subbuf_start,
};
/*
*
* module init - creates channel management control files
*
* Returns 0 on success, negative otherwise.
*/
static
int
init(
void
)
{
chan
=
relay_open(
"
example
"
, NULL, subbuf_size,
n_subbufs,
&
relayfs_callbacks);
if
(
!
chan) {
printk(
"
relay channel creation failed.\n
"
);
return
1
;
}
relayfs_exam_timer.expires
=
jiffies
+
WRITE_PERIOD;
add_timer(
&
relayfs_exam_timer);
return
0
;
}
static
void
cleanup(
void
)
{
del_timer_sync(
&
relayfs_exam_timer);
if
(chan) {
relay_close(chan);
chan
=
NULL;
}
}
module_init(init);
module_exit(cleanup);
MODULE_LICENSE(
"
GPL
"
);
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是一个基于内存的文件系统,实际上它基于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
"
);
sysctl是一种用户应用来设置和得到运行时内核的配置参数的一种有效方式,经过这种方式,用户应用能够在内核运行的任什么时候刻来改变内核的配置参数,也能够在任什么时候候得到内核的配置参数,一般,内核的这些配置参数也出如今proc文件系统的/proc/sys目录下,用户应用能够直接经过这个目录下的文件来实现内核配置的读写操做,例如,用户能够经过
cat /proc/sys/net/ipv4/ip_forward
来得知内核IP层是否容许转发IP包,用户能够经过
echo 1 > /proc/sys/net/ipv4/ip_forward
把内核 IP 层设置为容许转发 IP 包,即把该机器配置成一个路由器或网关。 通常地,全部的 Linux 发布也提供了一个系统工具 sysctl,它能够设置和读取内核的配置参数,可是该工具依赖于 proc 文件系统,为了使用该工具,内核必须支持 proc 文件系统。下面是使用 sysctl 工具来获取和设置内核配置参数的例子:
# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
# sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
注意,参数 net.ipv4.ip_forward 实际被转换到对应的 proc 文件/proc/sys/net/ipv4/ip_forward,选项 -w 表示设置该内核配置参数,没有选项表示读内核配置参数,用户能够使用 sysctl -a 来读取全部的内核配置参数,对应更多的 sysctl 工具的信息,请参考手册页 sysctl(8)。
可是 proc 文件系统对 sysctl 不是必须的,在没有 proc 文件系统的状况下,仍然能够,这时须要使用内核提供的系统调用 sysctl 来实现对内核配置参数的设置和读取。
在源代码中给出了一个实际例子程序,它说明了如何在内核和用户态使用sysctl。头文件 sysctl-exam.h 定义了 sysctl 条目 ID,用户态应用和内核模块须要这些 ID 来操做和注册 sysctl 条目。内核模块在文件 sysctl-exam-kern.c 中实现,在该内核模块中,每个 sysctl 条目对应一个 struct ctl_table 结构,该结构定义了要注册的 sysctl 条目的 ID(字段 ctl_name),在 proc 下的名称(字段procname),对应的内核变量(字段data,注意该该字段的赋值必须是指针),条目容许的最大长度(字段maxlen,它主要用于字符串内核变量,以便在对该条目设置时,对超过该最大长度的字符串截掉后面超长的部分),条目在proc文件系统下的访问权限(字段mode),在经过 proc设置时的处理函数(字段proc_handler,对于整型内核变量,应当设置为&proc_dointvec,而对于字符串内核变量,则设置为 &proc_dostring),字符串处理策略(字段strategy,通常这是为&sysctl_string)。
sysctl 条目能够是目录,此时 mode 字段应当设置为 0555,不然经过 sysctl 系统调用将没法访问它下面的 sysctl 条目,child 则指向该目录条目下面的全部条目,对于在同一目录下的多个条目,没必要一一注册,用户能够把它们组织成一个 struct ctl_table 类型的数组,而后一次注册就能够,但此时必须把数组的最后一个结构设置为NULL,即
{
.ctl_name = 0
}
注册sysctl条目使用函数register_sysctl_table(struct ctl_table *, int),第一个参数为定义的struct ctl_table结构的sysctl条目或条目数组指针,第二个参数为插入到sysctl条目表中的位置,若是插入到末尾,应当为0,若是插入到开头,则为非0。内核把全部的sysctl条目都组织成sysctl表。
当模块卸载时,须要使用函数unregister_sysctl_table(struct ctl_table_header *)解注册经过函数register_sysctl_table注册的sysctl条目,函数register_sysctl_table在调用成功时返 回结构struct ctl_table_header,它就是sysctl表的表头,解注册函数使用它来卸载相应的sysctl条目。 用户态应用sysctl-exam-user.c经过sysctl系统调用来查看和设置前面内核模块注册的sysctl条目(固然若是用户的系统内核已经支持proc文件系统,能够直接使用文件操做应用如cat, echo等直接查看和设置这些sysctl条目)。
下面是做者运行该模块与应用的输出结果示例:
# insmod .
/
sysctl
-
exam
-
kern.ko
# cat
/
proc
/
sys
/
mysysctl
/
myint
0
# cat
/
proc
/
sys
/
mysysctl
/
mystring
# .
/
sysctl
-
exam
-
user
mysysctl.myint
=
0
mysysctl.mystring
=
""
# .
/
sysctl
-
exam
-
user
100
"
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
);
}
系统调用是内核提供给应用程序的接口,应用对底层硬件的操做大部分都是经过调用系统调用来完成的,例如获得和设置系统时间,就须要分别调用 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。
在源代码包中给出了一个使用这种方式实现用户态与内核态数据交换的典型例子,它包含了三个文件:头文件 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 是一种特殊的 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);
}
本系列文章包括两篇,他们文周详地地介绍了Linux系统下用户空间和内核空间数据交换的九种方式,包括内核启动参数、模块参数和sysfs、
sysctl、系统调用、netlink、procfs、seq_file、debugfs和relayfs,并给出具体的例子帮助读者掌控这些技术的使
用。
本文是该系列文章的第二篇,他介绍了procfs、seq_file、debugfs和relayfs,并结合给出的例子程式周详地说明了他们怎么使用。
一、内核启动参数
Linux 提供了一种经过 bootloader 向其传输启动参数的功能,内核研发者能经过这种方式来向内核传输数据,从而控制内核启动行为。
一般的使用方式是,定义一个分析参数的函数,然后使用内核提供的宏 __setup把他注册到内核中,该宏定义在 linux/init.h 中,所以要使用他必须包含该头文件:
__setup("para_name=", parse_func)
para_name 为参数名,parse_func
为分析参数值的函数,他负责把该参数的值转换成相应的内核变量的值并设置那个内核变量。内核为整数参数值的分析提供了函数 get_option 和
get_options,前者用于分析参数值为一个整数的状况,然后者用于分析参数值为逗号分割的一系列整数的状况,对于参数值为字符串的状况,须要研发
者自定义相应的分析函数。在原始码包中的内核程式kern-boot-params.c
说明了三种状况的使用。该程式列举了参数为一个整数、逗号分割的整数串及字符串三种状况,读者要想测试该程式,须要把该程式拷贝到要使用的内核的源码目
录树的一个目录下,为了不和内核其余部分混淆,做者建议在内核源码树的根目录下建立一个新目录,如 examples,而后把该程式拷贝到
examples 目录下并从新命名为 setup_example.c,而且为该目录建立一个 Makefile 文件:
obj-y = setup_example.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
来查看该程式的输出。
二、模块参数和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’
三、sysctl
Sysctl是一种用户应用来设置和得到运行时内核的设置参数的一种有效方式,经过这种方式,用户应用能在内核运行的全部时刻来改动内核的设置参
数,也能在全部时候得到内核的设置参数,一般,内核的这些设置参数也出目前proc文件系统的/proc/sys目录下,用户应用能直接经过这个目录
下的文件来实现内核设置的读写操做,例如,用户能经过
Cat /proc/sys/net/ipv4/ip_forward
来得知内核IP层是否容许转发IP包,用户能经过
echo 1 > /proc/sys/net/ipv4/ip_forward
把内核 IP 层设置为容许转发 IP 包,即把该机器设置成一个路由器或网关。
通常地,全部的 Linux 发布也提供了一个系统工具 sysctl,他能设置和读取内核的设置参数,不过该工具依赖于 proc 文件系统,为了使用该工具,内核必须支持 proc 文件系统。下面是使用 sysctl 工具来获取和设置内核设置参数的例子:
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
$ sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
注意,参数 net.ipv4.ip_forward 实际被转换到对应的 proc
文件/proc/sys/net/ipv4/ip_forward,选项 -w 表示设置该内核设置参数,没有选项表示读内核设置参数,用户能使用
sysctl -a 来读取全部的内核设置参数,对应更多的 sysctl 工具的信息,请参考手册页 sysctl(8)。
不过 proc 文件系统对 sysctl 不是必须的,在没有 proc 文件系统的状况下,仍然能,这时须要使用内核提供的系统调用 sysctl 来实现对内核设置参数的设置和读取。
在
原始码包
中
给出了一个实际例子程式,他说明了怎么在内核和用户态使用sysctl。头文件 sysctl-exam.h 定义了 sysctl 条目
ID,用户态应用和内核模块须要这些 ID 来操做和注册 sysctl 条目。内核模块在文件 sysctl-exam-kern.c
中实现,在该内核模块中,每个 sysctl 条目对应一个 struct ctl_table 结构,该结构定义了要注册的 sysctl 条目的
ID(字段 ctl_name),在 proc
下的名称(字段procname),对应的内核变量(字段data,注意该该字段的赋值必须是指针),条目容许的最大长度(字段maxlen,他主要用于
字符串内核变量,以便在对该条目设置时,对超过该最大长度的字符串截掉后面超长的部分),条目在proc文件系统下的访问权限(字段mode),在经过
proc设置时的处理函数(字段proc_handler,对于整型内核变量,应当设置为&proc_dointvec,而对于字符串内核变量,
则设置为 &proc_dostring),字符串处理策略(字段strategy,通常这是为&sysctl_string)。
Sysctl 条目能是目录,此时 mode 字段应当设置为 0555,不然经过 sysctl 系统调用将没法访问他下面的 sysctl
条目,child 则指向该目录条目下面的全部条目,对于在同一目录下的多个条目,没必要一一注册,用户能把他们组织成一个 struct
ctl_table 类型的数组,而后一次注册就能,但此时必须把数组的最后一个结构设置为NULL,即
{
.ctl_name = 0
}
注册sysctl条目使用函数register_sysctl_table(struct ctl_table *,
int),第一个参数为定义的struct
ctl_table结构的sysctl条目或条目数组指针,第二个参数为插入到sysctl条目表中的位置,若是插入到末尾,应当为0,若是插入到开头,
则为非0。内核把全部的sysctl条目都组织成sysctl表。
当模块卸载时,须要使用函数unregister_sysctl_table(struct ctl_table_header
*)解注册经过函数register_sysctl_table注册的sysctl条目,函数register_sysctl_table在调用成功时返
回结构struct ctl_table_header,他就是sysctl表的表头,解注册函数使用他来卸载相应的sysctl条目。
用户态应用sysctl-exam-user.c经过sysctl系统调用来查看和设置前面内核模块注册的sysctl条目(固然若是用户的系统内核已
支持proc文件系统,能直接使用文件操做应用如cat, echo等直接查看和设置这些sysctl条目)。
下面是做者运行该模块和应用的输出结果示例:
$ insmod ./sysctl-exam-kern.ko
$ cat /proc/sys/mysysctl/myint
0
$ cat /proc/sys/mysysctl/mystring
$ ./sysctl-exam-user
mysysctl.myint = 0
mysysctl.mystring = ""
$ ./sysctl-exam-user 100 "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
$
四、系统调用
系统调用是内核提供给应用程式的接口,应用对底层硬件的操做大部分都是经过调用系统调用来完成的,例如获得和设置系统时间,就须要分别调用
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。
在原始码包中给出了一个使用这种方式实现用户态和内核态数据交换的典型例子,他包含了三个文件:
头文件 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
$
五、netlink
Netlink 是一种特别的 socket,他是 Linux 所特有的,相似于 BSD 中的AF_ROUTE
但又远比他的功能强大,目前在最新的 Linux 内核(2.6.14)中使用netlink 进行应用和内核通讯的应用很是多,包括:路由
daemon(NETLINK_ROUTE),1-wire 子系统(NETLINK_W1),用户态 socket
协议(NETLINK_USERSOCK),防火墙(NETLINK_FIREWALL),socket
监视(NETLINK_INET_DIAG),netfilter 日志(NETLINK_NFLOG),ipsec
安全策略(NETLINK_XFRM),SELinux 事件通知(NETLINK_SELINUX),iSCSI
子系统(NETLINK_ISCSI),进程审计(NETLINK_AUDIT),转发信息表查询(NETLINK_FIB_LOOKUP),
netlink connector(NETLINK_CONNECTOR),netfilter
子系统(NETLINK_NETFILTER),IPv6 防火墙(NETLINK_IP6_FW),DECnet
路由信息(NETLINK_DNRTMSG),内核事件向用户态通知(NETLINK_KOBJECT_UEVENT),通用
netlink(NETLINK_GENERIC)。
Netlink 是一种在内核和用户应用间进行双向数据传输的很是好的方式,用户态应用使用标准的 socket API 就能使用 netlink 提供的强大功能,内核态须要使用专门的内核 API 来使用 netlink。
Netlink 相对于系统调用,ioctl 及 /proc 文件系统而言具备如下好处:
1,为了使用 netlink,用户仅须要在 include/linux/netlink.h 中增长一个新类型的 netlink
协议定义便可, 如
#define NETLINK_MYTEST 17
而后,内核和用户态应用就能即时经过 socket API 使用该 netlink
协议类型进行数据交换。但系统调用须要增长新的系统调用,ioctl 则须要增长设备或文件, 那须要很多代码,proc 文件系统则须要在
/proc 下添加新的文件或目录,那将使原本就混乱的 /proc 更加混乱。
2.
netlink是一种异步通讯机制,在内核和用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接
收队列,而没必要等待接收者收到消息,但系统调用和 ioctl 则是同步通讯机制,若是传递的数据太长,将影响调度粒度。
3.使用 netlink 的内核部分能采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖,但系统调用就有依赖,并且新的系统调用的实现必须静态地链接到内核中,他没法在模块中实现,使用新系统调用的应用在编译时须要依赖内核。
4.netlink 支持多播,内核模块或应用能把消息多播给一个netlink组,属于该neilink
组的全部内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性,全部对内核事件感兴趣的应用都能收到该子系统发送的内核事件,在
后面的文章中将介绍这一机制的使用。
5.内核能使用 netlink 首先发起会话,但系统调用和 ioctl 只能由用户应用发起调用。
6.netlink 使用标准的 socket API,所以很是容易使用,但系统调用和 ioctl则须要专门的培训才能使用。
用户态使用 netlink
用户态应用使用标准的socket APIs, socket(), bind(), sendmsg(), recvmsg() 和
close() 就能很是容易地使用 netlink socket,查询手册页能了解这些函数的使用细节,本文只是讲解使用 netlink
的用户应该怎么使用这些函数。注意,使用 netlink 的应用必须包含头文件 linux/netlink.h。固然 socket
须要的头文件也必不可少,sys/socket.h。
为了建立一个 netlink socket,用户须要使用以下参数调用 socket():
socket(AF_NETLINK, SOCK_RAW, netlink_type)
第一个参数必须是 AF_NETLINK 或 PF_NETLINK,在 Linux
中,他们俩实际为一个东西,他表示要使用netlink,第二个参数必须是SOCK_RAW或SOCK_DGRAM,
第三个参数指定netlink协议类型,如前面讲的用户自定义协议类型NETLINK_MYTEST,
NETLINK_GENERIC是个通用的协议类型,他是专门为用户使用的,所以,用户能直接使用他,而没必要再添加新的协议类型。内核预约义的协议类
型有:
#define NETLINK_ROUTE 0 /* Routing/device hook */
#define NETLINK_W1 1 /* 1-wire subsystem */
#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
#define NETLINK_FIREWALL 3 /* Firewalling hook */
#define NETLINK_INET_DIAG 4 /* INET socket monitoring */
#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
#define NETLINK_XFRM 6 /* ipsec */
#define NETLINK_SELINUX 7 /* SELinux event notifications */
#define NETLINK_ISCSI 8 /* Open-iSCSI */
#define NETLINK_AUDIT 9 /* auditing */
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12 /* netfilter subsystem */
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14 /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
#define NETLINK_GENERIC 16
对于每个netlink协议类型,能有多达 32多播组,每个多播组用一个位表示,netlink 的多播特性使得发送消息给同一个组仅须要一次系统调用,于是对于须要多拨消息的应用而言,大大地下降了系统调用的次数。
函数 bind() 用于把一个打开的 netlink socket 和 netlink 源 socket 地址绑定在一块儿。netlink socket 的地址结构以下:
struct sockaddr_nl
{
sa_family_t nl_family;
unsigned short nl_pad;
__u32 nl_pid;
__u32 nl_groups;
};
字段 nl_family 必须设置为 AF_NETLINK 或着 PF_NETLINK,字段 nl_pad
当前没有使用,所以要老是设置为 0,字段 nl_pid 为接收或发送消息的进程的 ID,若是但愿内核处理消息或多播消息,就把该字段设置为
0,不然设置为处理消息的进程 ID。字段 nl_groups 用于指定多播组,bind 函数用于把调用进程加入到该字段指定的多播组,若是设置为
0,表示调用者不加入全部多播组。
传递给 bind 函数的地址的 nl_pid 字段应当设置为本进程的进程 ID,这至关于 netlink socket 的本地地址。不过,对于一个进程的多个线程使用 netlink socket 的状况,字段 nl_pid 则能设置为其余的值,如:
pthread_self()
所以字段 nl_pid 实际上未必是进程 ID,他只是用于区分不一样的接收者或发送者的一个标识,用户能根据本身须要设置该字段。函数 bind 的调用方式以下:
bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl));
fd为前面的 socket 调用返回的文件描述符,参数 nladdr 为 struct sockaddr_nl 类型的地址。
为了发送一个 netlink 消息给内核或其余用户态应用,须要填充目标 netlink socket 地址
,此时,字段 nl_pid 和 nl_groups 分别表示接收消息者的进程 ID 和多播组。若是字段 nl_pid 设置为 0,表示消息接收者为内核或多播组,若是 nl_groups为 0,表示该消息为单播消息,不然表示多播消息。
使用函数 sendmsg 发送 netlink 消息时还须要引用结构 struct msghdr、struct nlmsghdr 和 struct iovec,结构 struct msghdr 需以下设置:
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
其中 nladdr 为消息接收者的 netlink 地址。
struct nlmsghdr 为 netlink socket 本身的消息头,这用于多路复用和多路分解 netlink
定义的全部协议类型及其余一些控制,netlink
的内核实现将利用这个消息头来多路复用和多路分解已其余的一些控制,所以他也被称为netlink 控制块。所以,应用在发送 netlink
消息时必须提供该消息头。
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message */
__u16 nlmsg_type; /* Message type*/
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};
字段 nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度及该结构的大小,字段 nlmsg_type
用于应用内部定义消息的类型,他对 netlink 内核实现是透明的,所以大部分状况下设置为 0,字段 nlmsg_flags
用于设置消息标志,可用的标志包括:
/* Flags values */
#define NLM_F_REQUEST 1 /* It is request message. */
#define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK 4 /* Reply with ack, with zero or error code */
#define NLM_F_ECHO 8 /* Echo this request */
/* Modifiers to GET request */
#define NLM_F_ROOT 0x100 /* specify tree root */
#define NLM_F_MATCH 0x200 /* return all matching */
#define NLM_F_ATOMIC 0x400 /* atomic GET */
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
/* Modifiers to NEW request */
#define NLM_F_REPLACE 0x100 /* Override existing */
#define NLM_F_EXCL 0x200 /* Do not touch, if it exists */
#define NLM_F_CREATE 0x400 /* Create, if it does not exist */
#define NLM_F_APPEND 0x800 /* Add to end of list */
标志NLM_F_REQUEST用于表示消息是个请求,全部应用首先发起的消息都应设置该标志。
标志NLM_F_MULTI 用于指示该消息是个多部分消息的一部分,后续的消息能经过宏NLMSG_NEXT来得到。
宏NLM_F_ACK表示该消息是前一个请求消息的响应,顺序号和进程ID能把请求和响应关联起来。
标志NLM_F_ECHO表示该消息是相关的一个包的回传。
标志NLM_F_ROOT 被许多 netlink
协议的各类数据获取操做使用,该标志指示被请求的数据表应当总体返回用户应用,而不是个条目一个条目地返回。有该标志的请求一般致使响应消息设置
NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,所以,须要在字段 nlmsg_type 中指定协议类型。
标志 NLM_F_MATCH 表示该协议特定的请求只须要一个数据子集,数据子集由指定的协议特定的过滤器来匹配。
标志 NLM_F_ATOMIC 指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改。
标志 NLM_F_DUMP 未实现。
标志 NLM_F_REPLACE 用于取代在数据表中的现有条目。
标志 NLM_F_EXCL_ 用于和 CREATE 和 APPEND 配合使用,若是条目已存在,将失败。
标志 NLM_F_CREATE 指示应当在指定的表中建立一个条目。
标志 NLM_F_APPEND 指示在表末尾添加新的条目。
内核须要读取和修改这些标志,对于通常的使用,用户把他设置为 0 就能,只是一些高级应用(如 netfilter 和路由 daemon
须要他进行一些复杂的操做),字段 nlmsg_seq 和 nlmsg_pid 用于应用追踪消息,前者表示顺序号,后者为消息来源进程
ID。下面是个示例:
#define MAX_MSGSIZE 1024
char buffer[] = "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消息处理函数,当有消
息到达这个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
看到所有的内容,同时内核也把该消息发送给用户态接收程式,用户态接收程式将把接收到的内容输出到屏幕上。
六、procfs
procfs是比较老的一种用户态和内核态的数据交换方式,内核的很是多数据都是经过这种方式出口给用户的,内核的很是多参数也是经过这种方式来让用户
方便设置的。除了sysctl出口到/proc下的参数,procfs提供的大部份内核参数是只读的。实际上,很是多应用严重地依赖于procfs,所以他
几乎是必不可少的组件。前面部分的几个例子实际上已使用他来出口内核数据,不过并无讲解怎么使用,本节将讲解怎么使用procfs。
Procfs提供了以下API:
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。不然他应当为proc_mkdir
返回的struct proc_dir_entry结构的指针。
extern void remove_proc_entry(const char *name, struct proc_dir_entry *parent)
该函数用于删除上面函数建立的proc条目,参数name给出要删除的proc条目的名称,参数parent指定创建的proc条目所在的目录。
struct proc_dir_entry *proc_mkdir(const char * name, struct proc_dir_entry *parent)
该函数用于建立一个proc目录,参数name指定要建立的proc目录的名称,参数parent为该proc目录所在的目录。
extern struct proc_dir_entry *proc_mkdir_mode(const char *name, mode_t mode,
struct proc_dir_entry *parent);
struct proc_dir_entry *proc_symlink(const char * name,
struct proc_dir_entry * parent, const char * dest)
该函数用于创建一个proc条目的符号连接,参数name给出要创建的符号连接proc条目的名称,参数parent指定符号链接所在的目录,参数dest指定连接到的proc条目名称。
struct proc_dir_entry *create_proc_read_entry(const char *name,
mode_t mode, struct proc_dir_entry *base,
read_proc_t *read_proc, void * data)
该函数用于创建一个规则的只读proc条目,参数name给出要创建的proc条目的名称,参数mode给出了创建的该proc条目的访问权限,参
数base指定创建的proc条目所在的目录,参数read_proc给出读去该proc条目的操做函数,参数data为该proc条目的专用数据,他将
保存在该proc条目对应的struct file结构的private_data字段中。
struct proc_dir_entry *create_proc_info_entry(const char *name,
mode_t mode, struct proc_dir_entry *base, get_info_t *get_info)
该函数用于建立一个info型的proc条目,参数name给出要创建的proc条目的名称,参数mode给出了创建的该proc条目的访问权限,
参数base指定创建的proc条目所在的目录,参数get_info指定该proc条目的get_info操做函数。实际上get_info等同于
read_proc,若是proc条目没有定义个read_proc,对该proc条目的read操做将使用get_info取代,所以他在功能上很是类
似于函数create_proc_read_entry。
struct proc_dir_entry *proc_net_create(const char *name,
mode_t mode, get_info_t *get_info)
该函数用于在/proc/net目录下建立一个proc条目,参数name给出要创建的proc条目的名称,参数mode给出了创建的该proc条目的访问权限,参数get_info指定该proc条目的get_info操做函数。
struct proc_dir_entry *proc_net_fops_create(const char *name,
mode_t mode, struct file_operations *fops)
该函数也用于在/proc/net下建立proc条目,不过他也同时指定了对该proc条目的文件操做函数。
void proc_net_remove(const char *name)
该函数用于删除前面两个函数在/proc/net目录下建立的proc条目。参数name指定要删除的proc名称。
除了这些函数,值得一提的是结构struct
proc_dir_entry,为了建立一了可写的proc条目并指定该proc条目的写操做函数,必须设置上面的这些建立proc条目的函数返回的指针
指向的struct proc_dir_entry结构的write_proc字段,并指定该proc条目的访问权限有写权限。
为了使用这些接口函数及结构struct proc_dir_entry,用户必须在模块中包含头文件linux/proc_fs.h。
在原始码包中给出了procfs示例程式procfs_exam.c,他定义了三个proc文件条目和一个proc目录条目,读者在插入该模块后应当看到以下结构:
$ ls /proc/myproctest
aint astring bigprocfile
$
读者能经过cat和echo等文件操做函数来查看和设置这些proc文件。特别须要指出,bigprocfile是个大文件(超过一个内存
页),对于这种大文件,procfs有一些限制,由于他提供的缓存,只有一个页,所以必须特别当心,并对超过页的部分作特别的考虑,处理起来比较复杂而且
很是容易出错,全部procfs并不适合于大数据量的输入输出,后面一节seq_file就是由于这一缺陷而设计的,固然seq_file依赖于
procfs的一些基础功能。
七、seq_file
通常地,内核经过在procfs文件系统下创建文件来向用户空间提供输出信息,用户空间能经过全部文本阅读应用查看该文件信息,不过procfs
有一个缺陷,若是输出内容大于1个内存页,须要屡次读,所以处理起来很是难,另外,若是输出太大,速度比较慢,有时会出现一些意想不到的状况,
Alexander
Viro实现了一套新的功能,使得内核输出大文件信息更容易,该功能出目前2.4.15(包括2.4.15)之后的全部2.4内核及2.6内核中,尤为
是在2.6内核中,已大量地使用了该功能。
要想使用seq_file功能,研发者须要包含头文件linux/seq_file.h,并定义和设置一个seq_operations结构(相似于file_operations结构):
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
start函数用于指定seq_file文件的读开始位置,返回实际读开始位置,若是指定的位置超过文件末尾,应当返回NULL,start函数可
以有一个特别的返回SEQ_START_TOKEN,他用于让show函数输出文件头,但这只能在pos为0时使用,next函数用于把seq_file
文件的当前读位置移动到下一个读位置,返回实际的下一个读位置,若是已到达文件末尾,返回NULL,stop函数用于在读完seq_file文件后调
用,他相似于文件操做close,用于作一些必要的清理,如释放内存等,show函数用于格式化输出,若是成功返回0,不然返回出错码。
Seq_file也定义了一些辅助函数用于格式化输出:
int seq_putc(struct seq_file *m, char c);
函数seq_putc用于把一个字符输出到seq_file文件。
int seq_puts(struct seq_file *m, const char *s);
函数seq_puts则用于把一个字符串输出到seq_file文件。
int seq_escape(struct seq_file *, const char *, const char *);
函数seq_escape相似于seq_puts,只是,他将把第一个字符串参数中出现的包含在第二个字符串参数中的字符按照八进制形式输出,也即对这些字符进行转义处理。
int seq_printf(struct seq_file *, const char *, ...)
__attribute__ ((format (printf,2,3)));
函数seq_printf是最经常使用的输出函数,他用于把给定参数按照给定的格式输出到seq_file文件。
int seq_path(struct seq_file *, struct vfsmount *, struct dentry *, char *);
函数seq_path则用于输出文件名,字符串参数提供须要转义的文件名字符,他主要供文件系统使用。
在定义告终构struct seq_operations以后,用户还须要把打开seq_file文件的open函数,以便该结构和对应于seq_file文件的struct file结构关联起来,例如,struct seq_operations定义为:
struct seq_operations exam_seq_ops = {
.start = exam_seq_start,
.stop = exam_seq_stop,
.next = exam_seq_next,
.show = exam_seq_show
};
那么,open函数应该以下定义:
static int exam_seq_open(struct inode *inode, struct file *file)
{
return seq_open(file, &exam_seq_ops);
};
注意,函数seq_open是seq_file提供的函数,他用于把struct seq_operations结构和seq_file文件关联起来。
最后,用户须要以下设置struct file_operations结构:
struct file_operations exam_seq_file_ops = {
.owner = THIS_MODULE,
.open = exm_seq_open,
.read = seq_read,
.llseek = seq_lseek,
.release = seq_release
};
注意,用户仅须要设置open函数,其余的都是seq_file提供的函数。
而后,用户建立一个/proc文件并把他的文件操做设置为exam_seq_file_ops便可:
struct proc_dir_entry *entry;
entry = create_proc_entry("exam_seq_file", 0, NULL);
if (entry)
entry->proc_fops = &exam_seq_file_ops;
对于简单的输出,seq_file用户并没必要定义和设置这么多函数和结构,他仅需定义一个show函数,而后使用single_open来定义open函数就能,如下是使用这种简单形式的通常步骤:
1.定义一个show函数
int exam_show(struct seq_file *p, void *v)
{
…
}
2. 定义open函数
int exam_single_open(struct inode *inode, struct file *file)
{
return(single_open(file, exam_show, NULL));
}
注意要使用single_open而不是seq_open。
3. 定义struct file_operations结构
struct file_operations exam_single_seq_file_operations = {
.open = exam_single_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
注意,若是open函数使用了single_open,release函数必须为single_release,而不是seq_release。
在原始码包中给出了一个使用seq_file的具体例子seqfile_exam.c,他使用seq_file提供了一个查看当前系统运行的全部进程的
/proc接口,在编译并插入该模块后,用户经过命令"cat /proc/ exam_esq_file"能查看系统的全部进程。
回页首
3、debugfs
内核研发者常常须要向用户空间应用输出一些调试信息,在稳定的系统中可能根本没必要这些调试信息,不过在研发过程当中,为了搞清晰内核的行为,调试信
息很是必要,printk多是用的最多的,但他并非最佳的,调试信息只是在研发中用于调试,而printk将一直输出,所以研发完毕后须要清除没必要要
的printk语句,另外若是研发者但愿用户空间应用可以改动内核行为时,printk就没法实现。所以,须要一种新的机制,那只有在须要的时候使用,他
在须要时经过在一个虚拟文件系统中建立一个或多个文件来向用户空间应用提供调试信息。
有几种方式能实现上述需求:
使用procfs,在/proc建立文件输出调试信息,不过procfs对于大于一个内存页(对于x86是4K)的输出比较麻烦,并且速度慢,有时回出现一些意想不到的问题。
使用sysfs(2.6内核引入的新的虚拟文件系统),在很是多状况下,调试信息能存放在那里,不过sysfs主要用于系统管理,他但愿每个文件对应内核的一个变量,若是使用他输出复杂的数据结构或调试信息是很是困难的。
使用libfs建立一个新的文件系统,该方法极其灵活,研发者能为新文件系统设置一些规则,使用libfs使得建立新文件系统更加简单,不过仍然超出了一个研发者的想象。
为了使得研发者更加容易使用这样的机制,Greg
Kroah-Hartman研发了debugfs(在2.6.11中第一次引入),他是个虚拟文件系统,专门用于输出调试信息,该文件系统很是小,很是容
易使用,能在设置内核时选择是否构件到内核中,在不选择他的状况下,使用他提供的API的内核部分没必要作全部改动。
使用debugfs的研发者首先须要在文件系统中建立一个目录,下面函数用于在debugfs文件系统下建立一个目录:
struct dentry *debugfs_create_dir(const char *name, struct dentry *parent);
参数name是要建立的目录名,参数parent指定建立目录的父目录的dentry,若是为NULL,目录将建立在debugfs文件系统的根目
录下。若是返回为-ENODEV,表示内核没有把debugfs编译到其中,若是返回为NULL,表示其余类型的建立失败,若是建立目录成功,返回指向该
目录对应的dentry条目的指针。
下面函数用于在debugfs文件系统中建立一个文件:
struct dentry *debugfs_create_file(const char *name, mode_t mode,
struct dentry *parent, void *data,
struct file_operations *fops);
参数name指定要建立的文件名,参数mode指定该文件的访问许可,参数parent指向该文件所在目录,参数data为该文件特定的一些数据,
参数fops为实目前该文件上进行文件操做的fiel_operations结构指针,在很是多状况下,由seq_file(前面章节已讲过)提供的文件
操做实现就足够了,所以使用debugfs很是容易,固然,在一些状况下,研发者可能仅须要使用用户应用能控制的变量来调试,debugfs也提供了4个
这样的API方便研发者使用:
struct dentry *debugfs_create_u8(const char *name, mode_t mode,
struct dentry *parent, u8 *value);
struct dentry *debugfs_create_u16(const char *name, mode_t mode,
struct dentry *parent, u16 *value);
struct dentry *debugfs_create_u32(const char *name, mode_t mode,
struct dentry *parent, u32 *value);
struct dentry *debugfs_create_bool(const char *name, mode_t mode,
struct dentry *parent, u32 *value);
参数name和mode指定文件名和访问许可,参数value为须要让用户应用控制的内核变量指针。
当内核模块卸载时,Debugfs并不会自动清除该模块建立的目录或文件,所以对于建立的每个文件或目录,研发者必须调用下面函数清除:
void debugfs_remove(struct dentry *dentry);
参数dentry为上面建立文件和目录的函数返回的dentry指针。
在原始码包中给出了一个使用debufs的示例模块debugfs_exam.c,为了确保该模块正确运行,必须让内核支持debugfs,
debugfs是个调试功能,所以他位于主菜单Kernel hacking,而且必须选择Kernel
debugging选项才能选择,他的选项名称为Debug
Filesystem。为了在用户态使用debugfs,用户必须mount他,下面是在做者系统上的使用输出:
$ mkdir -p /debugfs
$ mount -t debugfs debugfs /debugfs
$ insmod ./debugfs_exam.ko
$ ls /debugfs
debugfs-exam
$ ls /debugfs/debugfs-exam
u8_var u16_var u32_var bool_var
$ cd /debugfs/debugfs-exam
$ cat u8_var
0
$ echo 200 > u8_var
$ cat u8_var
200
$ cat bool_var
N
$ echo 1 > bool_var
$ cat bool_var
Y
八、relayfs
relayfs是个快速的转发(relay)数据的文件系统,他以其功能而得名。他为那些须要从内核空间转发大量数据到用户空间的工具和应用提供了快速有效的转发机制。
Channel是relayfs文件系统定义的一个主要概念,每个channel由一组内核缓存组成,每个CPU有一个对应于该channel
的内核缓存,每个内核缓存用一个在relayfs文件系统中的文件文件表示,内核使用relayfs提供的写函数把须要转发给用户空间的数据快速地写入
当前CPU上的channel内核缓存,用户空间应用经过标准的文件I/O函数在对应的channel文件中能快速地取得这些被转发出的数据mmap
来。写入到channel中的数据的格式彻底取决于内核中建立channel的模块或子系统。
relayfs的用户空间API:
relayfs实现了四个标准的文件I/O函数,open、mmap、poll和close
o open(),o 打开一个channel在某一个CPU上的缓存对应的文件。
o mmap(),o 把打开的channel缓存映射到调用者进程的内存空间。
o read
(),o 读取channel缓存,o 随后的读操做将看不o 到被该函数消耗的字节,o 若是channel的操做模式为非覆盖写,o 那么用户空间应用在有内核模块写时仍
能读取,o 不o 过若是channel的操做模式为覆盖式,o 那么在读操做期间若是有内核模块进行写,o 结果将没法预知,o 所以对于覆盖式写的channel,o 用户
应当在确认在channel的写彻底结束后再进行读。
o poll(),o 用于通知用户空间应用转发数据跨越了子缓存的边界,o 支持的轮询标o 志有POLLIN、POLLRDNORM和POLLERR。
o close(),o 关闭open函数返回的文件描述符,o 若是没有进程或内核模块打开该channel缓存,o close函数将释放该channel缓存。
注意:用户态应用在使用上述API时必须确保已挂载了relayfs文件系统,但内核在建立和使用channel时没必要relayfs已挂载。下面命令将把relayfs文件系统挂载到/mnt/relay。
mount -t relayfs relayfs /mnt/relay
relayfs内核API:
relayfs提供给内核的API包括四类:channel管理、写函数、回调函数和辅助函数。
Channel管理函数包括:
o relay_open(base_filename, parent, subbuf_size, n_subbufs, overwrite, callbacks)
o relay_close(chan)
o relay_flush(chan)
o relay_reset(chan)
o relayfs_create_dir(name, parent)
o relayfs_remove_dir(dentry)
o relay_commit(buf, reserved, count)
o relay_subbufs_consumed(chan, cpu, subbufs_consumed)
写函数包括:
o relay_write(chan, data, length)
o __relay_write(chan, data, length)
o relay_reserve(chan, length)
回调函数包括:
o subbuf_start(buf, subbuf, prev_subbuf_idx, prev_subbuf)
o buf_mapped(buf, filp)
o buf_unmapped(buf, filp)
辅助函数包括:
o relay_buf_full(buf)
o subbuf_start_reserve(buf, length)
前面已讲过,每个channel由一组channel缓存组成,每一个CPU对应一个该channel的缓存,每个缓存又由一个或多个子缓存组成,每个缓存是子缓存组成的一个环型缓存。
函数relay_open用于建立一个channel并分配对应于每个CPU的缓存,用户空间应用经过在relayfs文件系统中对应的文件能
访问channel缓存,参数base_filename用于指定channel的文件名,relay_open函数将在relayfs文件系统中建立
base_filename0..base_filenameN-1,即每个CPU对应一个channel文件,其中N为CPU数,缺省状况下,这些文
件将创建在relayfs文件系统的根目录下,但若是参数parent非空,该函数将把channel文件建立于parent目录下,parent目录使
用函数relay_create_dir建立,函数relay_remove_dir用于删除由函数relay_create_dir建立的目录,谁建立
的目录,谁就负责在不用时负责删除。参数subbuf_size用于指定channel缓存中每个子缓存的大小,参数n_subbufs用于指定
channel缓存包含的子缓存数,所以实际的channel缓存大小为(subbuf_size x
n_subbufs),参数overwrite用于指定该channel的操做模式,relayfs提供了两种写模式,一种是覆盖式写,另外一种是非覆盖式
写。使用哪种模式彻底取决于函数subbuf_start的实现,覆盖写将在缓存已满的状况下无条件地继续从缓存的开始写数据,而无论这些数据是否已
被用户应用读取,所以写操做决不失败。在非覆盖写模式下,若是缓存满了,写将失败,但内核将在用户空间应用读取缓存数据时经过函数
relay_subbufs_consumed()通知relayfs。若是用户空间应用没来得及消耗缓存中的数据或缓存已满,两种模式都将致使数据丢
失,惟一的差异是,前者丢失数据在缓存开头,然后者丢失数据在缓存末尾。一旦内核再次调用函数relay_subbufs_consumed(),已满的
缓存将再也不满,于是能继续写该缓存。当缓存满了之后,relayfs将调用回调函数buf_full()来通知内核模块或子系统。当新的数据太大没法写
入当前子缓存剩余的空间时,relayfs将调用回调函数subbuf_start()来通知内核模块或子系统将须要使用新的子缓存。内核模块须要在该回
调函数中实现下述功能:
初始化新的子缓存;
若是1正确,完成当前子缓存;
若是2正确,返回是否正确完成子缓存转换;
在非覆盖写模式下,回调函数subbuf_start()应该以下实现:
static int subbuf_start(struct rchan_buf *buf,
void *subbuf,
void *prev_subbuf,
unsigned int prev_padding)
{
if (prev_subbuf)
*((unsigned *)prev_subbuf) = prev_padding;
if (relay_buf_full(buf))
return 0;
subbuf_start_reserve(buf, sizeof(unsigned int));
return 1;
}
若是当前缓存满,即全部的子缓存都没读取,该函数返回0,指示子缓存转换没有成功。当子缓存经过函数relay_subbufs_consumed
()被读取后,读取者将负责通知relayfs,函数relay_buf_full()在已有读者读取子缓存数据后返回0,在这种状况下,子缓存转换成
功进行。
在覆盖写模式下, subbuf_start()的实现和非覆盖模式相似:
static int subbuf_start(struct rchan_buf *buf,
void *subbuf,
void *prev_subbuf,
unsigned int prev_padding)
{
if (prev_subbuf)
*((unsigned *)prev_subbuf) = prev_padding;
subbuf_start_reserve(buf, sizeof(unsigned int));
return 1;
}
只是不作relay_buf_full()检查,由于此模式下,缓存是环行的,能无条件地写。所以在此模式下,子缓存转换一定成功,函数
relay_subbufs_consumed() 也无须调用。若是channel写者没有定义subbuf_start(),缺省的实现将被使用。
能经过在回调函数subbuf_start()中调用辅助函数subbuf_start_reserve()在子缓存中预留头空间,预留空间能保存任
何须要的信息,如上面例子中,预留空间用于保存子缓存填充字节数,在subbuf_start()实现中,前一个子缓存的填充值被设置。前一个子缓存的填
充值和指向前一个子缓存的指针一道做为subbuf_start()的参数传递给subbuf_start(),只有在子缓存完成后,才能知道填充值。
subbuf_start()也被在channel建立时分配每个channel缓存的第一个子缓存时调用,以便预留头空间,但在这种状况下,前一个子
缓存指针为NULL。
内核模块使用函数relay_write()或__relay_write()往channel缓存中写须要转发的数据,他们的差异是前者失效了本
地中断,然后者只抢占失效,所以前者能在全部内核上下文安全使用,然后者应当在没有全部中断上下文将写channel缓存的状况下使用。这两个函数没有
返回值,所以用户不能直接肯定写操做是否失败,在缓存满且写模式为非覆盖模式时,relayfs将经过回调函数buf_full来通知内核模块。
函数relay_reserve()用于在channel缓存中预留一段空间以便之后写入,在那些没有临时缓存而直接写入channel缓存的内核
模块可能须要该函数,使用该函数的内核模块在实际写这段预留的空间时能经过调用relay_commit()来通知relayfs。当全部预留的空间全
部写完并经过relay_commit通知relayfs后,relayfs将调用回调函数deliver()通知内核模块一个完整的子缓存已填满。由
于预留空间的操做并不在写channel的内核模块彻底控制之下,所以relay_reserve()不能很是好地保护缓存,所以当内核模块调用
relay_reserve()时必须采起恰当的同步机制。
当内核模块结束对channel的使用后须要调用relay_close() 来关闭channel,若是没有全部用户在引用该channel,他将和对应的缓存所有被释放。
函数relay_flush()强制在全部的channel缓存上作一个子缓存转换,他在channel被关闭前使用来终止和处理最后的子缓存。
函数relay_reset()用于将一个channel恢复到初始状态,于是没必要释放现存的内存映射并从新分配新的channel缓存就能使用channel,不过该调用只有在该channel没有全部用户在写的状况下才能安全使用。
回调函数buf_mapped() 在channel缓存被映射到用户空间时被调用。
回调函数buf_unmapped()在释放该映射时被调用。内核模块能经过他们触发一些内核操做,如开始或结束channel写操做。
在原始码包中给出了一个使用relayfs的示例程式relayfs_exam.c,他只包含一个内核模块,对于复杂的使用,须要应用程式配合。该模块实现了相似于文章中seq_file示例实现的功能。
固然为了使用relayfs,用户必须让内核支持relayfs,而且要mount他,下面是做者系统上的使用该模块的输出信息:
$ mkdir -p /relayfs
$ insmod ./relayfs-exam.ko
$ mount -t relayfs relayfs /relayfs
$ cat /relayfs/example0
…
$
relayfs是一种比较复杂的内核态和用户态的数据交换方式,本例子程式只提供了一个较简单的使用方式,对于复杂的使用,请参考relayfs用例页面
http://relayfs.sourceforge.net/examples.html
。
小结
本文是该系列文章最后一篇,他周详地讲解了其他四种用户空间和内核空间的数据交换方式,并经过实际例子程式向读者讲解了怎么在内核研发中使用这些技
术,其中seq_file是单向的,即只能向内核传递,而不能从内核获取,而另外三种方式均能进行双向数据交换,即既能从用户应用传递给内核,又能
从内核传递给应用态应用。procfs通常用于向用户出口少许的数据信息,或用户经过他设置内核变量从而控制内核行为。seq_file实际上依赖于
procfs,所以为了使用seq_file,必须使内核支持procfs。debugfs用于内核研发者调试使用,他比其余集中方式都方便,不过仅用于
简单类型的变量处理。relayfs是一种很是复杂的数据交换方式,要想准确使用并不容易,不过若是使用得当,他远比procfs和seq_file功能
强大。
linux用户空间和内核空间交换数据
在研究dahdi驱动的时候,见到了一些get_user,put_user的函数,不知道其来由,故而搜索了这篇文章,前面对linux内存的框架描述不是很清晰,描述的有一点乱,若是没有刚性需求,建议不用怎么关注,倒不如直接看那几个图片。对我很是有用的地方就是几个函数的介绍,介绍的比较详细,对应用有需求的能够着重看一个这几个函数。
Linux 内存
在 Linux 中,用户内存和内核内存是独立的,在各自的地址空间实现。地址空间是虚拟的,就是说地址是从物理内存中抽象出来的(经过一个简短描述的过程)。因为地址空间是虚拟的,因此能够存在不少。事实上,内核自己驻留在一个地址空间中,每一个进程驻留在本身的地址空间。这些地址空间由虚拟内存地址组成,容许一些带有独立地址空间的进程指向一个相对较小的物理地址空间(在机器的物理内存中)。不只仅是方便,并且更安全。由于每一个地址空间是独立且隔离的,所以很安全。
可是与安全性相关联的成本很高。由于每一个进程(和内核)会有相同地址指向不一样的物理内存区域,不可能当即共享内存。幸运的是,有一些解决方案。用户进程能够经过 Portable Operating System Interface for UNIX? (POSIX) 共享的内存机制(shmem)共享内存,但有一点要说明,每一个进程可能有一个指向相同物理内存区域的不一样虚拟地址。
虚拟内存到物理内存的映射经过页表完成,这是在底层软件中实现的(见图 1)。硬件自己提供映射,可是内核管理表及其配置。注意这里的显示,进程可能有一个大的地址空间,可是不多见,就是说小的地址空间的区域(页面)经过页表指向物理内存。这容许进程仅为随时须要的网页指定大的地址空间。
图 1. 页表提供从虚拟地址到物理地址的映射

因为缺少为进程定义内存的能力,底层物理内存被过分使用。经过一个称为 paging(然而,在 Linux 中一般称为 swap)的进程,不多使用的页面将自动移到一个速度较慢的存储设备(好比磁盘),来容纳须要被访问的其它页面(见图 2 )。这一行为容许,在将不多使用的页面迁移到磁盘来提升物理内存使用的同时,计算机中的物理内存为应用程序更容易须要的页面提供服务。注意,一些页面能够指向文件,在这种状况下,若是页面是脏(dirty)的,数据将被冲洗,若是页面是干净的(clean),直接丢掉。
图 2. 经过将不多使用的页面迁移到速度慢且便宜的存储器,交换使物理内存空间获得了更好的利用

MMU-less 架构
不是全部的处理器都有 MMU。所以,uClinux 发行版(微控制器 Linux)支持操做的一个地址空间。该架构缺少 MMU 提供的保护,可是容许 Linux 运行另外一类处理器。
选择一个页面来交换存储的过程被称为一个页面置换算法,能够经过使用许多算法(至少是最近使用的)来实现。该进程在请求存储位置时发生,存储位置的页面不在存储器中(在存储器管理单元 [MMU] 中无映射)。这个事件被称为一个页面错误 并被硬件(MMU)删除,出现页面错误中断后该事件由防火墙管理。该栈的详细说明见 图 3。
Linux 提供一个有趣的交换实现,该实现提供许多有用的特性。Linux 交换系统容许建立和使用多个交换分区和优先权,这支持存储设备上的交换层次结构,这些存储设备提供不一样的性能参数(例如,固态磁盘 [SSD] 上的一级交换和速度较慢的存储设备上的较大的二级交换)。为 SSD 交换附加一个更高的优先级使其能够使用直至耗尽;直到那时,页面才能被写入优先级较低的交换分区。
图 3. 地址空间和虚拟 - 物理地址映射的元素

并非全部的页面都适合交换。考虑到响应中断的内核代码或者管理页表和交换逻辑的代码,显然,这些页面决不能被换出,所以它们是固定的,或者是永久地驻留在内存中。尽管内核页面不须要进行交换,然而用户页面须要,可是它们能够被固定,经过 mlock(或 mlockall)函数来锁定页面。这就是用户空间内存访问函数的目的。若是内核假设一个用户传递的地址是有效的且是可访问的,最终可能会出现内核严重错误(kernel panic)(例如,由于用户页面被换出,而致使内核中的页面错误)。该应用程序编程接口(API)确保这些边界状况被妥善处理。
内核 API
如今,让咱们来研究一下用户操做用户内存的内核 API。请注意,这涉及内核和用户空间接口,而下一部分将研究其余的一些内存 API。用户空间内存访问函数在表 1 中列出。
表 1. 用户空间内存访问 API
函数 |
描述 |
access_ok |
检查用户空间内存指针的有效性 |
get_user |
从用户空间获取一个简单变量 |
put_user |
输入一个简单变量到用户空间 |
clear_user |
清除用户空间中的一个块,或者将其归零。 |
copy_to_user |
将一个数据块从内核复制到用户空间 |
copy_from_user |
将一个数据块从用户空间复制到内核 |
strnlen_user |
获取内存空间中字符串缓冲区的大小 |
strncpy_from_user |
从用户空间复制一个字符串到内核 |
正如您所指望的,这些函数的实现架构是独立的。例如在 x86 架构中,您能够使用 ./linux/arch/x86/lib/usercopy_32.c 和 usercopy_64.c 中的源代码找到这些函数以及在 ./linux/arch/x86/include/asm/uaccess.h 中定义的字符串。
当数据移动函数的规则涉及到复制调用的类型时(简单 VS. 汇集),这些函数的做用如图 4 所示。
图 4. 使用 User Space Memory Access API 进行数据移动

access_ok 函数
您能够使用 access_ok 函数在您想要访问的用户空间检查指针的有效性。调用函数提供指向数据块的开始的指针、块大小和访问类型(不管这个区域是用来读仍是写的)。函数原型定义以下:
access_ok( type, addr, size );
type 参数能够被指定为 VERIFY_READ 或 VERIFY_WRITE。VERIFY_WRITE 也能够识别内存区域是否可读以及可写(尽管访问仍然会生成 -EFAULT)。该函数简单检查地址多是在用户空间,而不是内核。
get_user 函数
要从用户空间读取一个简单变量,能够使用 get_user 函数,该函数适用于简单数据类型,好比,char 和 int,可是像结构体这类较大的数据类型,必须使用 copy_from_user 函数。该原型接受一个变量(存储数据)和一个用户空间地址来进行 Read 操做:
get_user( x, ptr );
get_user 函数将映射到两个内部函数其中的一个。在系统内部,这个函数决定被访问变量的大小(根据提供的变量存储结果)并经过 __get_user_x 造成一个内部调用。成功时该函数返回 0,通常状况下,get_user 和 put_user 函数比它们的块复制副本要快一些,若是是小类型被移动的话,应该用它们。
put_user 函数
您能够使用 put_user 函数来将一个简单变量从内核写入用户空间。和 get_user 同样,它接受一个变量(包含要写的值)和一个用户空间地址做为写目标:
put_user( x, ptr );
和 get_user 同样,put_user 函数被内部映射到 put_user_x 函数,成功时,返回 0,出现错误时,返回 -EFAULT。
clear_user 函数
clear_user 函数被用于将用户空间的内存块清零。该函数采用一个指针(用户空间中)和一个型号进行清零,这是以字节定义的:
clear_user( ptr, n );
在内部,clear_user 函数首先检查用户空间指针是否可写(经过 access_ok),而后调用内部函数(经过内联组装方式编码)来执行 Clear 操做。使用带有 repeat 前缀的字符串指令将该函数优化成一个很是紧密的循环。它将返回不可清除的字节数,若是操做成功,则返回 0。
copy_to_user 函数
copy_to_user 函数将数据块从内核复制到用户空间。该函数接受一个指向用户空间缓冲区的指针、一个指向内存缓冲区的指针、以及一个以字节定义的长度。该函数在成功时,返回 0,不然返回一个非零数,指出不能发送的字节数。
copy_to_user( to, from, n );
检查了向用户缓冲区写入的功能以后(经过 access_ok),内部函数 __copy_to_user 被调用,它反过来调用 __copy_from_user_inatomic(在 ./linux/arch/x86/include/asm/uaccess_XX.h 中。其中 XX 是 32 或者 64 ,具体取决于架构。)在肯定了是否执行 一、2 或 4 字节复制以后,该函数调用 __copy_to_user_ll,这就是实际工做进行的地方。在损坏的硬件中(在 i486 以前,WP 位在管理模式下不可用),页表能够随时替换,须要将想要的页面固定到内存,使它们在处理时不被换出。i486 以后,该过程只不过是一个优化的副本。
copy_from_user 函数
copy_from_user 函数将数据块从用户空间复制到内核缓冲区。它接受一个目的缓冲区(在内核空间)、一个源缓冲区(从用户空间)和一个以字节定义的长度。和 copy_to_user 同样,该函数在成功时,返回 0 ,不然返回一个非零数,指出不能复制的字节数。
copy_from_user( to, from, n );
该函数首先检查从用户空间源缓冲区读取的能力(经过 access_ok),而后调用 __copy_from_user,最后调用 __copy_from_user_ll。今后开始,根据构架,为执行从用户缓冲区到内核缓冲区的零拷贝(不可用字节)而进行一个调用。优化组装函数包含管理功能。
strnlen_user 函数
strnlen_user 函数也能像 strnlen 那样使用,但前提是缓冲区在用户空间可用。strnlen_user 函数带有两个参数:用户空间缓冲区地址和要检查的最大长度。
strnlen_user( src, n );
strnlen_user 函数首先经过调用 access_ok 检查用户缓冲区是否可读。若是是 strlen 函数被调用,max length 参数则被忽略。
strncpy_from_user 函数
strncpy_from_user 函数将一个字符串从用户空间复制到一个内核缓冲区,给定一个用户空间源地址和最大长度。
strncpy_from_user( dest, src, n );
因为从用户空间复制,该函数首先使用 access_ok 检查缓冲区是否可读。和 copy_from_user 同样,该函数做为一个优化组装函数(在 ./linux/arch/x86/lib/usercopy_XX.c 中)实现。
内存映射的其余模式
上面部分探讨了在内核和用户空间之间移动数据的方法(使用内核初始化操做)。Linux 还提供一些其余的方法,用于在内核和用户空间中移动数据。尽管这些方法未必可以提供与用户空间内存访问函数相同的功能,可是它们在地址空间之间映射内存的功能是类似的。
在用户空间,注意,因为用户进程出如今单独的地址空间,在它们之间移动数据必须通过某种进程间通讯机制。Linux 提供各类模式(好比,消息队列),可是最着名的是 POSIX 共享内存(shmem)。该机制容许进程建立一个内存区域,而后同一个或多个进程共享该区域。注意,每一个进程可能在其各自的地址空间中映射共享内存区域到不一样地址。所以须要相对的寻址偏移(offset addressing)。
mmap 函数容许一个用户空间应用程序在虚拟地址空间中建立一个映射,该功能在某个设备驱动程序类中是常见的,容许将物理设备内存映射到进程的虚拟地址空间。在一个驱动程序中,mmap 函数经过 remap_pfn_range 内核函数实现,它提供设备内存到用户地址空间的线性映射。
结束语
本文讨论了 Linux 中的内存管理主题,而后讨论了使用这些概念的用户空间内存访问函数。在用户空间和内核空间之间移动数据并无表面上看起来那么简单,可是 Linux 包含一个简单的 API 集合,跨平台为您管理这个复杂的任务。
内核空间与用户空间的通讯方式
下面总结了7种方式,主要对之前不是很熟悉的方式作了编程实现,以便加深印象。
1.使用API:这是最常使用的一种方式了
A.get_user(x,ptr):在内核中被调用,获取用户空间指定地址的数值并保存到内核变量x中。
B.put_user(x,ptr):在内核中被调用,将内核空间的变量x的数值保存到到用户空间指定地址处。
C.Copy_from_user()/copy_to_user():主要应用于设备驱动读写函数中,经过系统调用触发。
2.使用proc文件系统:和sysfs文件系统相似,也能够做为内核空间和用户空间交互的手段。
/proc 文件系统是一种虚拟文件系统,经过他能够做为一种linux内核空间和用户空间的。与普通文件不一样,这里的虚拟文件的内容都是动态建立的。
使用/proc文件系统的方式很简单。调用create_proc_entry,返回一个proc_dir_entry指针,而后去填充这个指针指向的结构就行了,我下面的这个测试用例只是填充了其中的read_proc属性。
下面是一个简单的测试用例,经过读虚拟出的文件能够获得内核空间传递过来的“proc ! test by qiankun!”字符串。
3.使用sysfs文件系统+kobject:其实这个之前是编程实现过得,可是那天太紧张忘记了,T_T。每一个在内核中注册的kobject都对应着sysfs系统中的一个目录。能够经过读取根目录下的sys目录中的文件来得到相应的信息。除了sysfs文件系统和proc文件系统以外,一些其余的虚拟文件系统也能一样达到这个效果。
4.netlink:netlink socket提供了一组相似于BSD风格的API,用于用户态和内核态的IPC。相比于其余的用户态和内核态IPC机制,netlink有几个好处:1.使用自定义一种协议完成数据交换,不须要添加一个文件等。2.能够支持多点传送。3.支持内核先发起会话。4.异步通讯,支持缓存机制。
对于用户空间,使用netlink比较简单,由于和使用socket很是的相似,下面说一下内核空间对netlink的使用,主要说一下最重要的create函数,函数原型以下:
extern struct sock *netlink_kernel_create(struct net *net,
int unit,unsigned int groups,
void (*input)(struct sk_buff *skb),
struct mutex *cb_mutex,
struct module *module);
第一个参数通常传入&init_net。
第二个参数指的是netlink的类型,系统定义了16个,咱们若是使用的话最好本身定义。这个需和用户空间所使用的建立socket的第三个参数一致,才能够完成通讯。
第四个参数指的是一个回调函数,当接受到一个消息的时候会调用这个函数。回调函数的参数为struct sk_buff类型的结构体。经过分析其结构成员能够获得传递过来的数据
第六个参数通常传入的是THIS_MODULE。指当前模块。
下面是对netlink的一个简单测试,将字符串“netlink test by qiankun”经过netlink输出到内核,内核再把字符串返回。Netlink类型使用的是22.
5.文件:应该说这是一种比较笨拙的作法,不过确实能够这样用。当处于内核空间的时候,直接操做文件,将想要传递的信息写入文件,而后用户空间能够读取这个文件即可以获得想要的数据了。下面是一个简单的测试程序,在内核态中,程序会向“/home/melody/str_from_kernel”文件中写入一条字符串,而后咱们在用户态读取这个文件,就能够获得内核态传输过来的数据了。
6.使用mmap系统调用:能够将内核空间的地址映射到用户空间。在之前作嵌入式的时候用到几回。一方面能够在driver中修改Struct file_operations结构中的mmap函数指针来从新实现一个文件对应的映射操做。另外一方面,也能够直接打开/dev/mem文件,把物理内存中的某一页映射到进程空间中的地址上。
其实,除了重写Struct file_operations中mmap函数,咱们还能够重写其余的方法如ioctl等,来达到驱动内核空间和用户空间通讯的方式。
7.信号:
从内核空间向进程发送信号。这个却是常常遇到,用户程序出现重大错误,内核发送信号杀死相应进程。
socket阻塞与非阻塞模式
阻塞模式
Windows套接字在阻塞和非阻塞两种模式下执行I/O操做。在阻塞模式下,在I/O操做完成前,执行的操做函数一直等候而不会当即返回,该函数所在的线程会阻塞在这里。相反,在非阻塞模式下,套接字函数会当即返回,而无论I/O是否完成,该函数所在的线程会继续运行。
在阻塞模式的套接字上,调用任何一个Windows Sockets API都会耗费不肯定的等待时间。图所示,在调用recv()函数时,发生在内核中等待数据和复制数据的过程。
当调用recv()函数时,系统首先查是否有准备好的数据。若是数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,而后该函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态。

当使用socket()函数建立套接字时,默认的套接字都是阻塞的。这意味着当调用Windows Sockets API不能当即完成时,线程处于等待状态,直到操做完成。
并非全部Windows Sockets API以阻塞套接字为参数调用都会发生阻塞。例如,以阻塞模式的套接字为参数调用bind()、listen()函数时,函数会当即返回。将可能阻塞套接字的Windows Sockets API调用分为如下四种:
1.输入操做
recv()、recvfrom()、WSARecv()和WSARecvfrom()函数。以阻塞套接字为参数调用该函数接收数据。若是此时套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。
2.输出操做
send()、sendto()、WSASend()和WSASendto()函数。以阻塞套接字为参数调用该函数发送数据。若是套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。
3.接受链接
accept()和WSAAcept()函数。以阻塞套接字为参数调用该函数,等待接受对方的链接请求。若是此时没有链接请求,线程就会进入睡眠状态。
4.外出链接
connect()和WSAConnect()函数。对于TCP链接,客户端以阻塞套接字为参数,调用该函数向服务器发起链接。该函数在收到服务器的应答前,不会返回。这意味着TCP链接总会等待至少到服务器的一次往返时间。
使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当但愿可以当即发送和接收数据,且处理的套接字数量比较少的状况下,使用阻塞模式来开发网络程序比较合适。
阻塞模式套接字的不足表现为,在大量创建好的套接字线程之间进行通讯时比较困难。当使用“生产者-消费者”模型开发网络程序时,为每一个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。其最大的缺点是当但愿同时处理大量套接字时,将无从下手,其扩展性不好。
非阻塞模式
把套接字设置为非阻塞模式,即通知系统内核:在调用Windows Sockets API时,不要让线程睡眠,而应该让函数当即返回。在返回时,该函数返回一个错误代码。图所示,一个非阻塞模式套接字屡次调用recv()函数的过程。前三次调用recv()函数时,内核数据尚未准备好。所以,该函数当即返回WSAEWOULDBLOCK错误代码。第四次调用recv()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。

当使用socket()函数和WSASocket()函数建立套接字时,默认都是阻塞的。在建立套接字以后,经过调用ioctlsocket()函数,将该套接字设置为非阻塞模式。Linux下的函数是:fcntl().
套接字设置为非阻塞模式后,在调用Windows Sockets API函数时,调用函数会当即返回。大多数状况下,这些函数调用都会调用“失败”,并返回WSAEWOULDBLOCK错误代码。说明请求的操做在调用期间内没有时间完成。一般,应用程序须要重复调用该函数,直到得到成功返回代码。
须要说明的是并不是全部的Windows Sockets API在非阻塞模式下调用,都会返回WSAEWOULDBLOCK错误。例如,以非阻塞模式的套接字为参数调用bind()函数时,就不会返回该错误代码。固然,在调用WSAStartup()函数时更不会返回该错误代码,由于该函数是应用程序第一调用的函数,固然不会返回这样的错误代码。
要将套接字设置为非阻塞模式,除了使用ioctlsocket()函数以外,还能够使用WSAAsyncselect()和WSAEventselect()函数。当调用该函数时,套接字会自动地设置为非阻塞方式。
因为使用非阻塞套接字在调用函数时,会常常返回WSAEWOULDBLOCK错误。因此在任什么时候候,都应仔细检查返回代码并做好对“失败”的准备。应用程序接二连三地调用这个函数,直到它返回成功指示为止。上面的程序清单中,在While循环体内不断地调用recv()函数,以读入1024个字节的数据。这种作法很浪费系统资源。
要完成这样的操做,有人使用MSG_PEEK标志调用recv()函数查看缓冲区中是否有数据可读。一样,这种方法也很差。由于该作法对系统形成的开销是很大的,而且应用程序至少要调用recv()函数两次,才能实际地读入数据。较好的作法是,使用套接字的“I/O模型”来判断非阻塞套接字是否可读可写。
非阻塞模式套接字与阻塞模式套接字相比,不容易使用。使用非阻塞模式套接字,须要编写更多的代码,以便在每一个Windows Sockets API函数调用中,对收到的WSAEWOULDBLOCK错误进行处理。所以,非阻塞套接字便显得有些难于使用。
非阻塞套接字在控制创建的多个链接,在数据的收发量不均,时间不定时,明显具备优点。
这种套接字在使用上存在必定难度,但只要排除了这些困难,它在功能上仍是很是强大的。一般状况下,可考虑使用套接字的“I/O模型”,它有助于应用程序经过异步方式,同时对一个或多个套接字的通讯加以管理。
Linux设备的阻塞式和非阻塞式访问
一、休眠
休眠的概念:
休眠的进程会被搁置在一边,等待未来的某个事件发生。
当进程休眠时,它期待某个条件将来为真,当一个休眠的进程被唤醒
是,它必须再次检查它所等待的条件的确为真。
休眠有简单休眠、高级休眠、手工休眠等。
1.1简单休眠
Linux内核中最简单的休眠方式称为是wait_event的宏,它在休眠的同时
也要检查进程等待的条件。
如下是几种简单的休眠宏:
1)、wait_event(wq, condition)
* wait_event - sleep until a condition gets true
* @wq: the waitqueue to wait on
* @condition: a C expression for the event to wait for
* @condition:任意一个布尔表达式
2)、wait_event_timeout(wq, condition, ret)
* wait_event_timeout - sleep until a condition gets true or a timeout elapses
* @wq: the waitqueue to wait on
* @condition: a C expression for the event to wait for
* @timeout: timeout, in jiffies
3)、wait_event_interruptible(wq, condition)
* wait_event_interruptible - sleep until a condition gets true or a signal is received
* @wq: the waitqueue to wait on
* @condition: a C expression for the event to wait for
4)、wait_event_interruptible_timeout(wq, condition, timeout)
* wait_event_interruptible_timeout - sleep until a condition gets true or a timeout elapses
* @wq: the waitqueue to wait on
* @condition: a C expression for the event to wait for
* @timeout: timeout, in jiffies
唤醒休眠函数
wake_up()
wake_up_interruptible()
wake_up()会唤醒等待在queue上的全部进程,wake_up_interruptible()
只会唤醒那些执行可中断休眠的进程
若是要确保只有一个进程能看到非零值,则必须以原子的方式进行检查。
if (condition)\
break; \
__wait_event(wq, condition);\
} while (0)
1.2高级休眠
1.3手工休眠
在早期的Linux版本中出现,若是愿意仍能够沿用这种休眠方式,但容易出错。
在源码中进行了以下解释:
DEFINE_WAIT(name) //创建并初始化一个等待队列入口
void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state);
void finish_wait(wait_queue_head_t *q, wait_queue_t *wait);
二、Linux设备的阻塞式和非阻塞式访问
linux驱动为上层用户空间访问设备提供了阻塞和非阻塞两种不一样
的访问模式。
阻塞操做的概念:
在执行设备操做时若不能得到资源则挂起进程,知道知足可操做的
条件后再进行操做,被挂起的进程进入休眠状态。
非阻塞操做的概念:
在不能进行设备操做时并不挂起,它或者放弃,或者不停的查询,
直到能够操做为止。只有read、write和open文件操做受非阻塞标志的影响。
阻塞的进程会进入休眠状态,所以必须确保有一个地方可以唤醒休
眠的进程,为确保唤醒发生,需总体理解本身的代码,唤醒休眠进程的
地方最可能发生在中断,由于硬件资源状态变化每每伴随一个中断。
使用非阻塞I/O的应用程序一般会使用select()和poll()系统调用查询是否对
设备进行无阻塞的访问。select()和poll()的系统调用最终会引起设备驱动中的poll
函数执行。select()和poll()系统调用的本质是同样的。
深刻浅出:Linux设备驱动中的阻塞和非阻塞I/O
今天写的是Linux设备驱动中的阻塞和非阻塞I/0,何谓阻塞与非阻塞I/O?简单来讲就是对I/O操做的两种不一样的方式,驱动程序能够灵活的支持用户空间对设备的这两种访问方式。
1、基本概念:
阻塞操做 : 是指在执行设备操做时,若不能得到资源,则挂起进程直到知足操做条件后再进行操做。被挂起的进程进入休眠, 被从调度器移走,直到条件知足。
非阻塞操做 :在不能进行设备操做时,并不挂起,它或者放弃,或者不停地查询,直到能够进行操做。非阻塞应用程序一般使用select系统调用查询是否能够对设备进行无阻塞的访问最终会引起设备驱动中 poll函数执行。
2、轮询操做
阻塞的读取一个字符:
char buf;
fd = open("/dev/ttyS1",O_RDWR);
.....
res = read(fd,&buf,1); //当串口上有输入时才返回,没有输入则进程挂起睡眠
if(res == 1)
{
printf("%c/n",buf);
}
char buf;
fd = open("/dev/ttyS1",O_RDWR);
.....
res = read(fd,&buf,1); //当串口上有输入时才返回,没有输入则进程挂起睡眠
if(res == 1)
{
printf("%c/n",buf);
}
非阻塞的读一个字符:
char buf;
fd = open("/dev/ttyS1",O_RDWR|O_NONBLOCK);//O_NONBLOCK 非阻塞标识
.....
while(read(fd,&buf,1)!=1);//串口上没有输入则返回,因此循环读取
printf("%c/n",buf);
char buf;
fd = open("/dev/ttyS1",O_RDWR|O_NONBLOCK);//O_NONBLOCK 非阻塞标识
.....
while(read(fd,&buf,1)!=1);//串口上没有输入则返回,因此循环读取
printf("%c/n",buf);
阻塞操做经常用等待队列来实现,而非阻塞操做用轮询的方式来实现。非阻塞I/O的操做在应用层一般会用到select()和poll()系统调用查询是否可对设备进行无阻塞访问。select()和poll()系统调用最终会引起设备驱动中的poll()函数被调用。这里对队列就很少介绍了,你们能够看看数据结构里面的知识点。
应用层的select()原型为:
int select(int numfds,fd_set *readfds,fd_set *writefds,fd_set *exceptionfds,struct timeval *timeout);
numfds 的值为须要检查的号码最高的文件描述符加1,若select()在等待timeout时间后,若没有文件描述符准备好则返回。
int select(int numfds,fd_set *readfds,fd_set *writefds,fd_set *exceptionfds,
struct timeval *timeout); numfds 的值为须要检查的号码最高的文件描述符加1,若select()在等待timeout时间后,若没有文件描述符准备好则返回。
应用程序为:
#inlcude------
main()
{
int fd,num;
char rd_ch[BUFFER_LEN];
fd_set rfds,wfds;
fd=open("/dev/globalfifo",O_RDWR|O_NONBLOCK);
if(fd != -1)
{
if(ioctl(fd,FIFO_CLEAR,0) < 0)
{
printf("ioctl cmd failed /n");
}
while(1)
{
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(fd,&rfds);
FD_SET(fd,&wfds);
select(fd+1,&rfds,&wfds,null,null);
}
}
}
#inlcude------
main()
{
int fd,num;
char rd_ch[BUFFER_LEN];
fd_set rfds,wfds;
fd=open("/dev/globalfifo",O_RDWR|O_NONBLOCK);
if(fd != -1)
{
if(ioctl(fd,FIFO_CLEAR,0) < 0)
{
printf("ioctl cmd failed /n");
}
while(1)
{
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(fd,&rfds);
FD_SET(fd,&wfds);
select(fd+1,&rfds,&wfds,null,null);
}
}
}
下面说说设备驱动中的poll()函数,函数原型以下:
static unsigned int poll(struct file *file, struct socket *sock,poll_table *wait)
static unsigned int poll(struct file *file, struct socket *sock,poll_table *wait)
对可能引发设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头添加到poll_table
返回表示是否能对设备进行无阻塞读,写访问的掩码
这里还要提到poll_wait()函数,不少人会觉得是和wait_event()同样的函数,会阻塞的等待某件事情的发生,其实这个函数并不会引发阻塞,它的工做是把当前的进程增添到wait参数指定的等待列表poll_table中去,poll_wait()函数原型以下:
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
从中能够看出是将等待队列头wait_address添加到p所指向的结构体中(poll_table)
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
从中能够看出是将等待队列头wait_address添加到p所指向的结构体中(poll_table)
驱动函数中的poll()函数典型模板以下:
static unsigned int xxx_poll(struct file *filp,struct socket *sock,
poll_table *wait)
{
unsigned int mask = 0;
struct xxx_dev *dev = filp->private_data;//得到设备结构体指针
...
poll_wait(filp,&dev->r_wait,wait);//加读等待队列头到poll_table
poll_wait(filp,&dev->w_wait,wait);//加写等待队列头到poll_table
...
if(...)//可读
mask |= POLLIN | POLLRDNORM;
if(...)//可写
mask |= POLLOUT | POLLRDNORM;
...
return mask;
}
static unsigned int xxx_poll(struct file *filp,struct socket *sock,
poll_table *wait)
{
unsigned int mask = 0;
struct xxx_dev *dev = filp->private_data;//得到设备结构体指针
...
poll_wait(filp,&dev->r_wait,wait);//加读等待队列头到poll_table
poll_wait(filp,&dev->w_wait,wait);//加写等待队列头到poll_table
...
if(...)//可读
mask |= POLLIN | POLLRDNORM;
if(...)//可写
mask |= POLLOUT | POLLRDNORM;
...
return mask;
}
3、支持轮询操做的globalfifo驱动
在globalfifo的poll()函数中,首先将设备结构体重的r_wait和w_wait等待队列头加到等待队列表,globalfifo设备驱动的poll()函数以下:
static unsigned int gloablfif0_poll(struct file *filp,poll_table *wait)
{
unsigned int mask = 0;
struct globalfifo_dev *dev = filp->private_data;
down(&dev->sem);
poll_wait(filp,&dev->r_wait , wait) ;
poll_wait(filp,&dev->r_wait , wait) ;
if(dev->current_len != 0)
{
mask |= POLLIN | POLLRDNORM;
}
if(dev->current_len != GLOBALFIFO_SIZE)
{
mask |= POLLOUT | POLLWRNORM;
}
up(&dev->sem);
return mask;
}
static unsigned int gloablfif0_poll(struct file *filp,poll_table *wait)
{
unsigned int mask = 0;
struct globalfifo_dev *dev = filp->private_data;
down(&dev->sem);
poll_wait(filp,&dev->r_wait , wait) ;
poll_wait(filp,&dev->r_wait , wait) ;
if(dev->current_len != 0)
{
mask |= POLLIN | POLLRDNORM;
}
if(dev->current_len != GLOBALFIFO_SIZE)
{
mask |= POLLOUT | POLLWRNORM;
}
up(&dev->sem);
return mask;
}
4、总结
阻塞与非阻塞操做:
定义并初始化等待对列头;
定义并初始化等待队列;
把等待队列添加到等待队列头
设置进程状态(TASK_INTERRUPTIBLE(能够被信号打断)和TASK_UNINTERRUPTIBLE(不能被信号打断))
调用其它进程
poll机制:
把等待队列头加到poll_table
返回表示是否能对设备进行无阻塞读,写访问的掩码
linux设备驱动中的阻塞与非阻塞
这两天在搞linux驱动的阻塞和非阻塞,困扰了两天,看了很多博客,有了点本身的想法,也不知是否对错,但仍是写写吧,让各位大神给我指点指点。
首先说说什么是阻塞和非阻塞的概念:阻塞操做就是指进程在操做设备时,因为不能获取资源或者暂时不能操做设备时,系统就会把进程挂起,被挂起的进程会进入休眠状态而且会从调度器的运行队列移走,放到等待队列中,而后一直休眠,直到该进程知足可操做的条件,再被唤醒,继续执行以前的操做。非阻塞操做的进程在不能进行设备操做时,并不会挂起,要么放弃,要么不停地执行,直到能够进行操做为止。
咱们都知道,在应用中,打开一个设备文件时,指定了是以阻塞仍是非阻塞打开(缺省是阻塞方式),而后后面的读写一切都是交由驱动来实现,那么驱动是如何实现read()和write()的阻塞呢!下面以读写一个内存块为例子,当该内存写满了,不能写的时候,调用write()函数该怎么处理,当该内存已经读取完了,空了的时候,调用read()函数,又改如何处理(该代码简化了,只为说明问题,不能正常编译使用):
wait_queue_head_t read_queue; //定义读等待队列头部
wait_queue_head_t write_queue; //定义写等待队列头部
struct semaphore sem; //定义信号量,用于互斥访问公共资源
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
if(down_interruptible(&sem))
return -ERESTARTSYS; //使用 down_interruptible,给公共资源上锁,以防出现并发引发的竞态问题
while (!have_data) //have_data用来判断缓冲区中是否有数据,若是有数据,直接跳过该while语句,执行下面的 // copy_to_user
{
up(&sem); //因为没有数据,不能进行读取数据操做,要释放锁,解锁,这里的解锁很重要,要是没有解锁,很容 //易进入死锁,具体怎样,下面再分析
if(filp->f_flags & O_NONBLOCK) //判断该文件时以阻塞方式仍是非阻塞方式打开
return -EAGAIN; //因为是非阻塞打开,直接返回
wait_event_interruptible(read_queue,have_date);//阻塞方式代开,该语句会让进程进入休眠状态,而后等待其余进程 //的唤醒而且have_data=true时,才会被彻底唤醒,执行下面的语句
if(down_interruptible(&sem)) //因为能够进行读取了,因此在此给公共资源上锁
return -ERESTARTSYS;
if (copy_to_user(buf, (void*)(dev->data + p), count)) { //实现数据从内核空间读取到用户空间,完成读取操做
..................
}
have_data = false; //标记该数据已经读取完毕
up(&sem); //释放锁
wake_up(&write_queue); //读取完毕,缓冲区有空间能够写入了,就唤醒写进程,让写进程把数据写入
return ;
}
下面分析write函数,其原理和实现也是和read函数同样,都是先给公共资源上锁,再判断是阻塞访问仍是非阻塞访问,若是是非阻塞访问,且资源不能获取时,直接返回,若果时阻塞且不能获取资源时,就进入休眠,等待其余进程的唤醒。
static ssize_t mem_write(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
if(down_interruptible(&sem))
return -ERESTARTSYS; //使用 down_interruptible,给公共资源上锁,以防出现并发引发的竞态问题
while (have_data) //have_data用来判断缓冲区中是否有数据,若是有数据,表示缓冲区已经满了,不能写入,
//若是have_data是false,即没有数据,缓冲区是空的,能够写入数据,就执行下面的copy_from_user
{
up(&sem); //因为有数据,不能进行写入数据操做,要释放锁,解锁 if(filp->f_flags & O_NONBLOCK) //判断该文件时以阻塞方式仍是非阻塞方式打开
return -EAGAIN; //因为是非阻塞打开,直接返回
wait_event_interruptible(write_queue,!have_date);//阻塞方式代开,该语句会让进程进入休眠状态,而后等待其余进程 //的唤醒而且have_data=false时,才会被彻底唤醒,执行下面的语句
if(down_interruptible(&sem)) //因为能够进行写入操做了,因此在此给公共资源上锁
return -ERESTARTSYS;
if (copy_from_user((dev->data + p), buf,count)) { //实现数据从内核空间读取到用户空间,完成读取操做
..................
}
have_data = true; //标记该数据已经读取完毕
up(&sem); //释放锁
wake_up(&read_queue); //写入数据完毕,缓冲区有数据能够读取了,就唤醒读进程,让读进程开始读取数据
return ;
}
以上是驱动中的读取和写入操做,当写进程发现数据已满,不能写入时,且上层应用是以阻塞的方式打开设备文件时,因此必需要写入数据才能返回,不然不能返回,那么就有两种实现机制,要不就是不停地忙等待,等待设备能够写入时,便写入,而后返回,但是这样作的话,很是影响CPU的执行效率,大大下降了CPU的性能,因此linux内核中采起了等待队列的实现方式,就是当一个阻塞进程写入数据时,发现不能写入时,会把这个进程挂起,放到等待队列中休眠,而后一直在休眠,直到有个读进程,把缓冲区的数据读取完毕后,而后读进程会把写进程唤醒,告诉写进程缓冲区能够写入数据了,因而写进程继续写入操做,而且返回。举个例子,小明饿了,要吃饭,因而跑去妈妈那里,说要吃饭,妈妈说放没有作好,你说小明是继续在这里一直等着妈妈把饭作好,仍是先去睡一觉好呢,若是我是小明,我就先去睡一觉,而后妈妈把饭作好了,就把小明叫醒,小明,能够吃饭了,因而小明起来,跑去吃饭。当读进程阻塞时,也是这样,就不分析了。
如今说说为何每次进去阻塞前都要把锁释放掉,而后唤醒时再次上锁,咱们试想一下,假如读进程发现缓冲区为空,不能读取时,准备进入休眠了,没有把锁释放,效果会怎样,就至关于读进程带着锁睡着了,一旦读进程带着锁睡着了,写进程来了,但是写进程由于不能获取锁,就不能访问临界区的资源,更不能往缓冲区里面写入数据,因此缓冲区会一直为空,且写进程也会不停地在那里休眠,等到读进程释放锁,但是读进程睡着了,不能释放锁,写进程也休眠了,不能唤醒读进程,因而就发生了死锁了。这就比如小明他爸爸藏了一个还魂丹在保险箱里,有一天,他爸爸晕倒了,但是没有告诉小明锁放在那里,因而小明只能在保险箱外面,看着他爸爸晕过去,却无能为力了.....
阻塞和非阻塞
阻塞函数在完成其指定的任务之前不容许程序调用另外一个函数。例如,程序执行一个读数据的函数调用时,在此函数完成读操做之前将不会执行下一程序语句。当服务器运行到accept语句时,而没有客户链接服务请求到来,服务器就会中止在accept语句上等待链接服务请求的到来。这种状况称为阻塞(blocking)。而非阻塞操做则能够当即完成。好比,若是你但愿服务器仅仅注意检查是否有客户在等待链接,有就接受链接,不然就继续作其余事情,则能够经过将Socket设置为非阻塞方式来实现。非阻塞socket在没有客户在等待时就使accept调用当即返回。
#include
#include
……
sockfd = socket(AF_INET,SOCK_STREAM,0);
fcntl(sockfd,F_SETFL,O_NONBLOCK);
……
经过设置socket为非阻塞方式,能够实现"轮询"若干Socket。当企图从一个没有数据等待处理的非阻塞Socket读入数据时,函数将当即返回,返回值为-1,并置errno值为EWOULDBLOCK。可是这种"轮询"会使CPU处于忙等待方式,从而下降性能,浪费系统资源。而调用select()会有效地解决这个问题,它容许你把进程自己挂起来,而同时使系统内核监听所要求的一组文件描述符的任何活动,只要确认在任何被监控的文件描述符上出现活动,select()调用将返回指示该文件描述符已准备好的信息,从而实现了为进程选出随机的变化,而没必要由进程自己对输入进行测试而浪费CPU开销。Select函数原型为:
int select(int numfds,fd_set *readfds,fd_set *writefds,
fd_set *exceptfds,struct timeval *timeout);
其中readfds、writefds、exceptfds分别是被select()监视的读、写和异常处理的文件描述符集合。若是你但愿肯定是否能够从标准输入和某个socket描述符读取数据,你只须要将标准输入的文件描述符0和相应的sockdtfd加入到readfds集合中;numfds的值是须要检查的号码最高的文件描述符加1,这个例子中numfds的值应为sockfd+1;当select返回时,readfds将被修改,指示某个文件描述符已经准备被读取,你能够经过FD_ISSSET()来测试。为了实现fd_set中对应的文件描述符的设置、复位和测试,它提供了一组宏:
FD_ZERO(fd_set *set)----清除一个文件描述符集;
FD_SET(int fd,fd_set *set)----将一个文件描述符加入文件描述符集中;
FD_CLR(int fd,fd_set *set)----将一个文件描述符从文件描述符集中清除;
FD_ISSET(int fd,fd_set *set)----试判断是否文件描述符被置位。
Timeout参数是一个指向struct timeval类型的指针,它能够使select()在等待timeout长时间后没有文件描述符准备好即返回。struct timeval数据结构为:
struct timeval {
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
怎样能使accept函数当即返回?
能够使用ioctlsocket。
用 selece,若是返回侦听套接字可读,说明有链接请求要处理
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
关于socket的阻塞与非阻塞模式以及它们之间的优缺点,这已经没什么可言的;我打个很简单的比方,若是你调用socket send函数时;
若是是阻塞模式下:
send先比较待发送数据的长度len和套接字s的发送缓冲的长度,若是len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;若是len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据,若是是就等待协议把数据发送完,若是协议尚未开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么 send就比较s的发送缓冲区的剩余空间和len,若是len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完,若是len小于剩余空间大小send就仅仅把buf中的数据copy到剩余空间里
若是是非阻塞模式下:
在调用socket send函数时,若是能写到socket缓冲区时,就写数据并返回实际写的字节数目,固然这个返回的实际值可能比你所要写的数据长度要小些(On nonblocking stream oriented sockets, the number of bytes written can be between 1 and the requested length, depending on buffer availability on both the client and server computers),若是不可写的话,就直接返回SOCKET_ERROR了,因此没有等待的过程。。
通过上面的介绍后,下面介绍如何设置socket的非阻塞模式:
http://www.cnblogs.com/dawen/archive/2011/05/18/2050330.html
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int iMode = 1; //0:阻塞
ioctlsocket(socketc,FIONBIO, (u_long FAR*) &iMode);//非阻塞设置
rs=recvfrom(socketc,rbuf,sizeof(rbuf),0,(SOCKADDR*)&addr,&len);
int ioctlsocket (
SOCKET s,
long cmd,
u_long FAR* argp
);
-
s
-
[in] A descriptor identifying a socket.
-
cmd
-
[in] The command to perform on the socket
s.
-
argp
-
[in/out] A pointer to a parameter for
cmd.
不知道你们有没有遇到过这种状况,当socket进行TCP链接的时候(也就是调用connect时),一旦网络不通,或者是ip地址无效,就可能使整个线程阻塞。通常为30秒(我测的是20秒)。若是设置为非阻塞模式,能很好的解决这个问题,咱们能够这样来设置非阻塞模式:调用ioctlsocket函数:
unsigned long flag=1;
if (ioctlsocket(sock,FIONBIO,&flag)!=0)
{
closesocket(sock);
return false;
}
如下是对ioctlsocket函数的相关解释:
int PASCAL FAR ioctlsocket( SOCKET s, long cmd, u_long FAR* argp);
s:一个标识套接口的描述字。
cmd:对套接口s的操做命令。
argp:指向cmd命令所带参数的指针。
注释:
本函数可用于任一状态的任一套接口。它用于获取与套接口相关的操做参数,而与具体协议或通信子系统无关。支持下列命令:
FIONBIO:容许或禁止套接口s的非阻塞模式。argp指向一个无符号长整型。如容许非阻塞模式则非零,如禁止非阻塞模式则为零。当建立一个套接口时,它就处于阻塞模式(也就是说非阻塞模式被禁止)。这与BSD套接口是一致的。WSAAsynSelect()函数将套接口自动设置为非阻塞模式。若是已对一个套接口进行了WSAAsynSelect() 操做,则任何用ioctlsocket()来把套接口从新设置成阻塞模式的试图将以WSAEINVAL失败。为了把套接口从新设置成阻塞模式,应用程序必须首先用WSAAsynSelect()调用(IEvent参数置为0)来禁至WSAAsynSelect()。
FIONREAD:肯定套接口s自动读入的数据量。argp指向一个无符号长整型,其中存有ioctlsocket()的返回值。若是s是SOCKET_STREAM类型,则FIONREAD返回在一次recv()中所接收的全部数据量。这一般与套接口中排队的数据总量相同。若是S是SOCK_DGRAM 型,则FIONREAD返回套接口上排队的第一个数据报大小。
SIOCATMARK:确实是否全部的带外数据都已被读入。这个命令仅适用于SOCK_STREAM类型的套接口,且该套接口已被设置为能够在线接收带外数据(SO_OOBINLINE)。如无带外数据等待读入,则该操做返回TRUE真。不然的话返回FALSE假,下一个recv()或recvfrom()操做将检索“标记”前一些或全部数据。应用程序可用SIOCATMARK操做来肯定是否有数据剩下。若是在“紧急”(带外)数据前有常规数据,则按序接收这些数据(请注意,recv()和recvfrom()操做不会在一次调用中混淆常规数据与带外数据)。argp指向一个BOOL型数,ioctlsocket()在其中存入返回值。
此时已经设置非阻塞模式,可是并无设置connect的链接时间,咱们能够经过调用select语句来实现这个功能。如下代码设定了是链接时间为5秒,若是还未能连上,则直接返回。
struct timeval timeout ;
fd_set r;
int ret;
connect( sock, (LPSOCKADDR)sockAddr, sockAddr.Size());
FD_ZERO(&r);
FD_SET(sock,&r);
timeout.tv_sec = 5;
timeout.tv_usec =0;
ret = select(0,0,&r,0,&timeout);
if ( ret <= 0 )
{
closesocket(sock);
return false;
}
如下是对select函数的解释:
int select (
int nfds,
fd_set FAR * readfds,
fd_set FAR * writefds,
fd_set FAR * exceptfds,
const struct timeval FAR * timeout
);
第一个参数nfds沒有用,仅仅为与伯克利Socket兼容而提供。
readfds指定一個Socket数组(应该是一个,但这里主要是表现为一个Socket数组),select检查该数组中的全部Socket。若是成功返回,则readfds中存放的是符合‘可读性’条件的数组成员(如缓冲区中有可读的数据)。
writefds指定一个Socket数组,select检查该数组中的全部Socket。若是成功返回,则writefds中存放的是符合‘可写性’条件的数组成员(如链接成功)。
exceptfds指定一个Socket数组,select检查该数组中的全部Socket。若是成功返回,则cxceptfds中存放的是符合‘有异常’条件的数组成员(如链接接失败)。
timeout指定select执行的最长时间,若是在timeout限定的时间内,readfds、writefds、exceptfds中指定的Socket沒有一个符合要求,就返回0。
若是对 Connect 进行非阻塞调用,则可读意味着已经成功链接,链接不成功则不可读。因此经过这样的设定,咱们就可以实现对connect链接时间的修改。可是,应该注意,这样的设置并不能保证在限定时间内链接不上就说明网络不通。好比咱们设的时间是5秒,可是因为种种缘由,可能第6秒就能链接上,可是函数在5秒后就返回了。
- 学习原子操做和互斥信号量,实现互斥机制,同一时刻只能一个应用程序使用驱动程序
- 学习阻塞和非阻塞操做
当设备被一个程序打开时,存在被另外一个程序打开的可能,若是两个或多个程序同时对设备文件进行写操做,这就是说咱们的设备资源同时被多个进程使用,对共享资源(硬件资源、和软件上的全局变量、静态变量等)的访问则很容易致使竞态。
显然这不是咱们想要的,因此本节引入互斥的概念:实现同一时刻,只能一个应用程序使用驱动程序
互斥其实现很简单,就是采用一些标志,当文件被一个进程打开后,就会设置该标志,使其余进程没法打开设备文件。
1.其中的标志须要使用函数来操做,不能直接经过判断变量来操做标志
好比:
if (-- canopen != 0) //当canopen==0,表示没有进程访问驱动,当canopen<0:表示有进程访问
编译汇编来看,分了3段: 读值、减一、判断
若是恰好在读值的时候发生了中断,有另外一个进程访问时,那么也会访问成功,也会容易致使访问竞态。
1.1因此采用某种函数来实现,保证执行过程不被其余行为打断,有两种类型函数能够实现:
原子操做(像原子同样不可再细分不可被中途打断)
当多个进程同时访问同一个驱动时,只能有一个进程访问成功,其它进程会退出
互斥信号量操做
好比:A、B进程同时访问同一个驱动时,只有A进程访问成功了,B进程进入休眠等待状态,当A进程执行完毕释放后,等待状态的B进程又来访问,保证一个一个进程都能访问
2. 原子操做详解
原子操做指的是在执行过程当中不会被别的代码路径所中断的操做。
原子操做函数以下:
1)atomic_t v = ATOMIC_INIT(0); //定义原子变量v并初始化为0
2)atomic_read(atomic_t *v); //返回原子变量的值
3)void atomic_inc(atomic_t *v); //原子变量增长1
4)void atomic_dec(atomic_t *v); //原子变量减小1
5)int atomic_dec_and_test(atomic_t *v); //自减操做后测试其是否为0,为0则返回true,不然返回false。
2.1修改驱动程序
定义原子变量:
/*定义原子变量canopen并初始化为1 */
atomic_t canopen = ATOMIC_INIT(1);
在.open成员函数里添加:
/*自减操做后测试其是否为0,为0则返回true,不然返回false */
if(!atomic_dec_and_test(&canopen))
{
atomic_inc(&canopen); //++,复位
return -1;
}
在. release成员函数里添加:
atomic_inc(&canopen); //++,复位
2.2修改测试程序:
int main(int argc,char **argv)
{
int oflag;
unsigned int val=0;
fd=open("/dev/buttons",O_RDWR);
if(fd<0)
{printf("can't open, fd=%d\n",fd);
return -1;}
while(1)
{
read( fd, &ret, 1); //读取驱动层数据
printf("key_vale=0X%x\r\n",ret);
}
return 0;
}
2.3 测试效果
以下图,能够看到第一个进程访问驱动成功,后面的就不再能访问成功了

3.互斥信号量详解
互斥信号量(semaphore)是用于保护临界区的一种经常使用方法,只有获得信号量的进程才能执行临界区代码。
当获取不到信号量时,进程进入休眠等待状态。
信号量函数以下:
/*注意: 在2.6.36版本后这个函数DECLARE_MUTEX修改为DEFINE_SEMAPHORE了*/
1)static DECLARE_MUTEX(button_lock); //定义互斥锁button_lock,被用来后面的down和up用
2)void down(struct semaphore * sem); // 获取不到就进入不被中断的休眠状态(down函数中睡眠)
3)int down_interruptible(struct semaphore * sem); //获取不到就进入可被中断的休眠状态(down函数中睡眠)
4)int down_trylock(struct semaphore * sem); //试图获取信号量,获取不到则马上返回正数
5)void up(struct semaphore * sem); //释放信号量
3.1修改驱动程序(以down函数获取为例)
(1)定义互斥锁变量:
/*定义互斥锁button_lock,被用来后面的down()和up()使用 */
static DECLARE_MUTEX(button_lock);
(2)在.open成员函数里添加:
/* 获取不到就进入不被中断的休眠状态(down函数中睡眠) */
down(&button_lock);
(3)在. release成员函数里添加:
/* 释放信号量 */
up(&button_lock);
3.2修改测试程序:
int main(int argc,char **argv)
{
int oflag;
unsigned int val=0;
fd=open("/dev/buttons",O_RDWR);
if(fd<0)
{printf("can't open, fd=%d\n",fd);
return -1;}
else
{
printf("can open,PID=%d\n",getpid()); //打开成功,打印pid进程号
}
while(1)
{
read( fd, &ret, 1); //读取驱动层数据
printf("key_vale=0X%x\r\n",ret);
}
return 0;
}
3.3 测试效果
以下图所示,3个进程同时访问时,只有一个进程访问成功,其它2个进程进入休眠等待状态

以下图所示,多个信号量访问时, 会一个一个进程来排序访问

4.阻塞与非阻塞
4.1阻塞操做
进程进行设备操做时,使用down()函数,若获取不到资源则挂起进程,将被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被知足。
在read读取按键时, 一直等待按键按下才返回数据
4.2非阻塞操做
进程进行设备操做时,使用down_trylock()函数,若获取不到资源并不挂起,直接放弃。
在read读取按键时, 无论有没有数据都要返回
4.3 怎么来判断阻塞与非阻塞操做?
在用户层open时,默认为阻塞操做,若是添加了” O_NONBLOCK”,表示使open()、read()、write()不被阻塞
实例:
fd=open("/dev/buttons",O_RDWR); //使用阻塞操做
fd = open("/dev/buttons ", O_RDWR | O_NONBLOCK); //使用非阻塞操做
而后在驱动设备中,经过file_operations成员函数.open、.read、.write带的参数file->f_flags 来查看用户层访问时带的参数
实例:
if( file->f_flags & O_NONBLOCK ) //非阻塞操做,获取不到则退出
{
... ...
}
else //阻塞操做,获取不到则进入休眠
{
... ...
}
4.4修改应用程序,经过判断file->f_flags来使用阻塞操做仍是非阻塞操做
(1)定义互斥锁变量:
/*定义互斥锁button_lock,被用来后面的down()和up()使用 */
static DECLARE_MUTEX(button_lock);
(2)在.open成员函数里添加:
if( file->f_flags & O_NONBLOCK ) //非阻塞操做
{
if(down_trylock(&button_lock) ) //尝试获取信号量,获取不到则退出
return -1;
}
else //阻塞操做
{
down(&button_lock); //获取信号量,获取不到则进入休眠
}
(3)在. release成员函数里添加:
/*释放信号量*/
up(&button_lock);
4.5 写阻塞测试程序 fifth_blocktext.c
代码以下:
int main(int argc,char **argv)
{
int oflag;
unsigned int val=0;
fd=open("/dev/buttons",O_RDWR); //使用阻塞操做
if(fd<0)
{printf("can't open, fd=%d\n",fd);
return -1;}
else
{
printf("can open,PID=%d\n",getpid()); //打开成功,打印pid进程号
}
while(1)
{
val=read( fd, &ret, 1); //读取驱动层数据
printf("key_vale=0X%x,retrun=%d\r\n",ret,val);
}
return 0;
}
4.6 非阻塞测试效果
以下图所示:

4.7写阻塞测试程序 fifth_nonblock.c
代码以下:
int main(int argc,char **argv)
{
int oflag;
unsigned int val=0;
fd=open("/dev/buttons",O_RDWR | O_NONBLOCK); //使用非阻塞操做
if(fd<0)
{printf("can't open, fd=%d\n",fd);
return -1;}
else
{
printf("can open,PID=%d\n",getpid()); //打开成功,打印pid进程号
}
while(1)
{
val=read( fd, &ret, 1); //读取驱动层数据
printf("key_vale=0X%x,retrun=%d\r\n",ret,val);
sleep(3); //延时3S
}
return 0;
}
4.8 阻塞测试效果
以下图所示:

本节目标:
分析在linux中的中断是如何运行的,以及中断3大结构体:irq_desc、irq_chip、irqaction
在裸板程序中(参考stmdb和ldmia详解):
1.按键按下,
2.cpu发生中断,
3.强制跳到异常向量入口执行(0x18中断地址处)
3.1使用stmdb将寄存器值保存在栈顶(保护现场)
3.2执行中断服务函数
3.3 使用ldmia将栈顶处数据读出到寄存器中,并使pc=lr(恢复现场)
ldmia sp!, { r0-r12,pc }^
//^表示将spsr的值复制到cpsr,由于异常返回后须要恢复异常发生前的工做状态
在linux中:
须要先设置异常向量地址(参考linux应用手册P412):
在ARM裸板中异常向量基地址是0x00000000,以下图:

而linux内核中异常向量基地址是0xffff0000(虚拟地址),
位于代码arch/cam/kernel/traps.c,代码以下:
void __init trap_init(void)
{
/* CONFIG_VECTORS_BASE :内核配置项,在.config文件中,设置的是0Xffff0000*/
/* vectors =0xffff0000*/
unsigned long vectors = CONFIG_VECTORS_BASE;
... ...
/*将异常向量地址复制到0xffff0000处*/
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);
... ...
}
上面代码中主要是将__vectors_end - __vectors_start之间的代码复制到vectors (0xffff0000)处,
__vectors_start为何是异常向量基地址?
经过搜索,找到它在arch/arm/kernel/entry_armv.S中定义:
__vectors_start:
swi SYS_ERROR0 //复位异常,复位时会执行
b vector_und + stubs_offset //undefine未定义指令异常
ldr pc, .LCvswi + stubs_offset //swi软件中断异常
b vector_pabt + stubs_offset //指令预取停止abort
b vector_dabt + stubs_offset //数据访问停止abort
b vector_addrexcptn + stubs_offset //没有用到
b vector_irq + stubs_offset //irq异常
b vector_fiq + stubs_offset //fig异常
其中stubs_offset是连接地址的偏移地址, vector_und、vector_pabt等表示要跳转去执行的代码
1.以vector_irq中断为例, vector_irq是个宏,它在哪里定义呢?
它仍是在arch/arm/kernel/entry_armv.S中定义,以下所示:
vector_stub irq, IRQ_MODE, 4//irq:名字 IRQ_MODE:0X12 4:偏移量
上面的vector_stub 根据参数irq, IRQ_MODE, 4来定义” vector_ irq”这个宏(其它宏也是这样定义的)
2.vector_stub又是怎么实现出来的定义不一样的宏呢?
咱们找到vector_stub这个定义:
.macro vector_stub, name, mode, correction=0 //定义vector_stub有3个参数
.align 5
vector_\name: //定义不一样的宏,好比vector_ irq
.if \correction //判断correction参数是否为0
sub lr, lr, #\correction //计算返回地址
.endif
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr
mrs lr, spsr //读出spsr
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@ 进入管理模式
mrs r0, cpsr //读出cpsr
eor r0, r0, #(\mode ^ SVC_MODE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f //lr等于进入模式以前的spsr,&0X0F就等于模式位
mov r0, sp
ldr lr, [pc, lr, lsl #2]
movs pc, lr @ branch to handler in SVC mode
3.所以咱们将上面__vectors_start里的b vector_irq + stubs_offset 中断展开以下:
.macro vector_stub, name, mode, correction=0 //定义vector_stub有3个参数
.align 5
vector_stub irq, IRQ_MODE, 4 //这三个参数值代入 vector_stub中
vector_ irq: //定义 vector_ irq
/*计算返回地址(在arm流水线中,lr=pc+8,可是pc+4只译码没有执行,因此lr=lr-4) */
sub lr, lr, #4
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@保存r0和lr和spsr
stmia sp, {r0, lr} //存入sp栈里
mrs lr, spsr //读出spsr
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@ 进入管理模式
mrs r0, cpsr //读出cpsr
eor r0, r0, #(\mode ^ SVC_MODE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f //lr等于进入模式以前的spsr,&0X0F就等于模式位
mov r0, sp
ldr lr, [pc, lr, lsl #2] //若是进入中断前是usr,则取出PC+4*0的内容,即__irq_usr @若是进入中断前是svc,则取出PC+4*3的内容,即__irq_svc
movs pc, lr //跳转到下面某处,且目标寄存器是pc,指令S结尾,最后会恢复cpsr.
.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f
从上面代码中的注释能够看出:
- 1).将发生异常前的各个寄存器值保存在SP栈里,如果中断异常,则PC=PC-4,也就是CPU下个要运行的位置处
- 2).而后根据进入中断前的工做模式不一样,程序下一步将跳转到_irq_usr 、或__irq_svc等位置。
4.咱们先选择__irq_usr做为下一步跟踪的目标:
4.1其中__irq_usr的实现以下(arch\arm\kernel\entry-armv.S):
__irq_usr:
usr_entry //保存数据到栈里
get_thread_info tsk
irq_handler //调用irq_handler
b ret_to_user
4.2.irq_handler的实现过程,arch\arm\kernel\entry-armv.S
.macro irq_handler
get_irqnr_preamble r5, lr
get_irqnr_and_base r0, r6, r5, lr // get_irqnr_and_base:获取中断号,r0=中断号
movne r1, sp //r1等于sp (发生中断以前的各个寄存器的基地址)
adrne lr, 1b
bne asm_do_IRQ //调用asm_do_IRQ, irq=r0 regs=r1
irq_handler最终调用asm_do_IRQ
4.3 asm_do_IRQ实现过程,arch/arm/kernel/irq.c
该函数和裸板中断处理同样的,完成3件事情:
1).分辨是哪一个中断;
2).经过desc_handle_irq(irq, desc)调用对应的中断处理函数;
3).清中断
asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs) //irq:中断号 *regs:发生中断前的各个寄存器基地址
{
struct pt_regs *old_regs = set_irq_regs(regs);
/*根据irq中断号,找到哪一个中断, *desc =irq_desc[irq]*/
struct irq_desc *desc = irq_desc + irq; // irq_desc是个数组(位于kernel/irq/handle.c)
if (irq >= NR_IRQS)
desc = &bad_irq_desc;
irq_enter();
desc_handle_irq(irq, desc); // desc_handle_irq根据中断号和desc,调用函数指针,进入中断处理,
irq_finish(irq);
irq_exit();
set_irq_regs(old_regs);
}
上面主要是执行desc_handle_irq函数进入中断处理
其中desc_handle_irq代码以下:
desc->handle_irq(irq, desc);//至关于执行irq_desc[irq]-> handle_irq(irq, irq_desc[irq]);
它会执行handle_irq成员函数,这个成员handle_irq又是在哪里被赋值的?
搜索handle_irq,找到它位于kernel/irq/chip.c,__set_irq_handler函数下:
void __set_irq_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,const char *name)
{
... ...
desc = irq_desc + irq; //在irq_desc结构体数组中找到对应的中断
... ...
desc->handle_irq = handle; //使handle_irq成员指向handle参数函数
}
继续搜索__set_irq_handler函数,它被set_irq_handler函数调用:
static inline void set_irq_handler(unsigned int irq, irq_flow_handler_t handle)
{
__set_irq_handler(irq, handle, 0, NULL);
}
继续搜索set_irq_handler函数,以下图

发现它在s3c24xx_init_irq(void)函数中被屡次使用,显然在中断初始化时,屡次进入__set_irq_handler函数,并在irq_desc数组中构造了不少项 handle_irq函数
咱们来看看irq_desc中断描述结构体到底有什么内容:
struct irq_desc {
irq_flow_handler_t handle_irq; //指向中断函数, 中断产生后,就会执行这个handle_irq
struct irq_chip *chip; //指向irq_chip结构体,用于底层的硬件访问,下面会介绍
struct msi_desc *msi_desc;
void *handler_data;
void *chip_data;
struct irqaction *action; /* IRQ action list */ //action链表,用于中断处理函数
unsigned int status; /* IRQ status */
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* For detecting broken IRQs */
unsigned int irqs_unhandled;
spinlock_t lock;
... ...
const char *name; //产生中断的硬件名字
} ;
其中的成员*chip的结构体,用于底层的硬件访问, irq_chip类型以下:
struct irq_chip {
const char *name;
unsigned int (*startup)(unsigned int irq); //启动中断
void (*shutdown)(unsigned int irq); //关闭中断
void (*enable)(unsigned int irq); //使能中断
void (*disable)(unsigned int irq); //禁止中断
void (*ack)(unsigned int irq); //响应中断,就是清除当前中断使得能够再接收下个中断
void (*mask)(unsigned int irq); //屏蔽中断源
void (*mask_ack)(unsigned int irq); //屏蔽和响应中断
void (*unmask)(unsigned int irq); //开启中断源
... ...
int (*set_type)(unsigned int irq, unsigned int flow_type); //将对应的引脚设置为中断类型的引脚
... ...
#ifdef CONFIG_IRQ_RELEASE_METHOD
void (*release)(unsigned int irq, void *dev_id); //释放中断服务函数
#endif
};
其中的成员struct irqaction *action,主要是用来存用户注册的中断处理函数,
一个中断能够有多个处理函数 ,当一个中断有多个处理函数,说明这个是共享中断.
所谓共享中断就是一个中断的来源有不少,这些来源共享同一个引脚。
因此在irq_desc结构体中的action成员是个链表,以action为表头,如果一个以上的链表就是共享中断
irqaction结构定义以下:
struct irqaction {
irq_handler_t handler; //等于用户注册的中断处理函数,中断发生时就会运行这个中断处理函数
unsigned long flags; //中断标志,注册时设置,好比上升沿中断,降低沿中断等
cpumask_t mask; //中断掩码
const char *name; //中断名称,产生中断的硬件的名字
void *dev_id; //设备id
struct irqaction *next; //指向下一个成员
int irq; //中断号,
struct proc_dir_entry *dir; //指向IRQn相关的/proc/irq/
};
上面3个结构体的关系以下图所示:

咱们来看看s3c24xx_init_irq()函数是怎么初始化中断的,之外部中断0为例(位于s3c24xx_init_irq函数):
s3c24xx_init_irq()函数中部分代码以下:
/*其中IRQ_EINT0=16, 因此irqno=16 */
for (irqno = IRQ_EINT0; irqno <= IRQ_EINT3; irqno++)
{
irqdbf("registering irq %d (ext int)\n", irqno);
/*在set_irq_chip函数中会执行:
desc = irq_desc + irq;
desc->chip = chip;*/
set_irq_chip(irqno, &s3c_irq_eint0t4); //因此(irq_desc+16)->chip= &s3c_irq_eint0t4
/* set_irq_handler 会调用__set_irq_handler 函数*/
set_irq_handler(irqno, handle_edge_irq); //因此(irq_desc+16)-> handle_irq = handle_edge_irq
set_irq_flags(irqno, IRQF_VALID);
}
初始化了外部中断0后,当外部中断0触发,就会进入咱们以前分析的asm_do_IRQ函数中,调用(irq_desc+16)-> handle_irq也就是handle_edge_irq函数。
咱们来分析下handle_edge_irq函数是如何执行中断服务的:
void fastcall handle_edge_irq(unsigned int irq, struct irq_desc *desc)
{
const unsigned int cpu = smp_processor_id();
spin_lock(&desc->lock);
desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);
/*判断这个中断是否正在运行(INPROGRESS)或者禁止(DISABLED)*/
if (unlikely((desc->status & (IRQ_INPROGRESS | IRQ_DISABLED)) || !desc->action))
{
desc->status |= (IRQ_PENDING | IRQ_MASKED);
mask_ack_irq(desc, irq); //屏蔽中断
goto out_unlock;
}
kstat_cpu(cpu).irqs[irq]++; //计数中断次数
/* Start handling the irq */
desc->chip->ack(irq); //开始处理这个中断
/* Mark the IRQ currently in progress.*/
desc->status |= IRQ_INPROGRESS; //标记当前中断正在运行
do {
struct irqaction *action = desc->action;
irqreturn_t action_ret;
if (unlikely(!action)) { //判断链表是否为空
desc->chip->mask(irq);
goto out_unlock;
}
if (unlikely((desc->status &
(IRQ_PENDING | IRQ_MASKED | IRQ_DISABLED)) ==
(IRQ_PENDING | IRQ_MASKED))) {
desc->chip->unmask(irq);
desc->status &= ~IRQ_MASKED;
}
desc->status &= ~IRQ_PENDING;
spin_unlock(&desc->lock);
action_ret = handle_IRQ_event(irq, action); //真正的处理过程
if (!noirqdebug)
note_interrupt(irq, desc, action_ret);
spin_lock(&desc->