日志标签 ‘内核模块’

对/proc文件系统进行读写操作

2011年5月19日

本博客之前的文章中多次涉及到/proc文件系统,下面的几条命令都在曾经的文章中出现过:

cat /proc/interrupts
cat /proc/devices
cat /proc/kallsyms | grep super_blocks

第一条命令用于查看系统内已注册的中断信息,包括中断号、已接受的手段请求和驱动器名称等;第二条命令用于查看系统内已注册的字符设备和块设备信息,包括设备号和设备名称;第三条命令用于在内核符号表中检索super_blocks符号的的地址,kallsyms文件包括内核中所有的标示符及其地址。

1.概述

proc即process的缩写,最初的proc文件系统只是存放进程的相关信息。但现在的/proc文件系统除此之外还包含系统的状态信息和配置信息。
通过ls命令就可以查看/proc文件系统所包含的内容。

edsionte@edsionte-desktop:/proc$ ls
1      1290   1469  1541  1627   19612  29    49    9          dri              mdstat          sys
10     13     1471  1544  1630   19613  3     5     908        driver           meminfo         sysrq-trigger
1013   1301   1474  1548  1632   19629  30    50    913        edsionte_procfs  misc            sysvipc
…………

其中以数字为名的目录即为系统中正在运行的进程信息,数字即为进程的pid。比如我们可以进入init进程的目录,查看它的地址空间:

edsionte@edsionte-desktop:/proc/1$ sudo cat maps
[sudo] password for edsionte:
00110000-00263000 r-xp 00000000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00263000-00264000 ---p 00153000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00264000-00266000 r--p 00153000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00266000-00267000 rw-p 00155000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00267000-0026a000 rw-p 00000000 00:00 0
0026a000-00272000 r-xp 00000000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
00272000-00273000 r--p 00007000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
00273000-00274000 rw-p 00008000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
00471000-0048b000 r-xp 00000000 08:07 1048610    /sbin/init
…………

除了查看进程的相关信息,我们还可以通过打印相关文件来查看系统的当前运行状态。比如查看当前内存的使用情况:

edsionte@edsionte-desktop:/proc$ cat meminfo
MemTotal:         961368 kB
MemFree:          145264 kB
Buffers:           31648 kB
Cached:           297716 kB
SwapCached:        14436 kB
…………

总之,/proc文件系统相当于内核的一个快照,该目录下的所有信息都是动态的从正在运行的内核中读取。

基于这种原因,/proc文件系统就成为了用户和内核之间交互的接口。一方面,用户可以从/proc文件系统中读取很多内核释放出来的信息;另一方面,内核也可以在恰当的时候从用户那里得到输入信息,从而改变内核的相关状态和配置。

相比传统的文件系统,/proc是一种特殊的文件系统,即虚拟文件系统。这里的虚拟是强调/proc文件系统下的所有文件都存在于内存中而不是磁盘上。也就是说/proc文件系统只占用内存空间,而不占用系统的外存空间。

2.用户态和内核态之间的数据通信

既然内核的数据以/proc文件系统的形式呈现给用户,也就是说内核的信息以文件的形式存在于该文件系统中,那么/proc文件系统就应当提供一组接口对其内的文件进行读写操作。接下来我们以一个实际的内核模块程序easyProc.c为例,说明/proc文件系统的常用接口。该程序中依次创建了几个虚拟文件,然后在用户态对这些文件进行读写测试。

2.0数据结构

每个虚拟文件都对应一个proc_dir_entry类型的数据结构,该结构具体定义如下:

struct proc_dir_entry {
	const char *name;			// virtual file name
	mode_t mode;				// mode permissions
	uid_t uid;				// File's user id
	gid_t gid;				// File's group id
	struct inode_operations *proc_iops;	// Inode operations functions
	struct file_operations *proc_fops;	// File operations functions
	struct proc_dir_entry *parent;		// Parent directory
	...
	read_proc_t *read_proc;			// /proc read function
	write_proc_t *write_proc;		// /proc write function
	void *data;				// Pointer to private data
	atomic_t count;				// use count
	...
};

除了保存该虚拟文件的基本信息外,该结构中还有read_proc和write_proc两个字段,下文中将有详细说明。

2.1创建目录

/proc文件系统中创建一个目录对应的函数接口如下:
struct proc_dir_entry *proc_mkdir(const char *name,struct proc_dir_entry *parent);
其中name为要创建的目录名;parent为这个目录的父目录,当要创建的目录位于/proc下时此参数为空。比如我们使用该函数在/proc下创建一个目录edsionte_procfs。

#define MODULE_NAME "edsionte_procfs"
struct proc_dir_entry *example_dir;
	example_dir = proc_mkdir(MODULE_NAME, NULL);
	if (example_dir == NULL) {
		rv = -ENOMEM;
		goto out;
	}
2.2创建普通文件

在/proc文件系统中创建一个虚拟文件可以使用如下的函数:

 static inline struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent) ;

该函数中name为要创建的文件名;mode为创建文件的属性;parent指向该文件父目录的指针,如果创建的虚拟文件位于/proc下,则这个参数为NULL。

比如我们通过该函数在/proc/edsionte_procfs目录下创建一个虚拟文件foo,其权限为644。其中example_dir指向我们刚创建的目录文件edsionte_procfs。

struct proc_dir_entry  *foo_file;
	foo_file = create_proc_entry("foo", 0644, example_dir);
	if (foo_file == NULL) {
		rv = -ENOMEM;
		goto no_foo;
	}
2.3.创建符号链接文件

当我们需要在/proc文件系统下创建一个符号链接文件时,可使用如下接口:

struct proc_dir_entry *proc_symlink(const char *name, struct proc_dir_entry *parent, const char *dest);

name参数为要创建的符号链接文件名;parent为该符号链接文件的父目录;dest为符号链接所指向的目标文件。

下面的代码演示了如何通过该函数来对已存在的虚拟文件jiffies创建符号链接文件jiffies_too:

	symlink = proc_symlink("jiffies_too", example_dir, "jiffies");
	if (symlink == NULL) {
		rv = -ENOMEM;
		goto no_symlink;
	}

我们内核模块加载函数中完成上述几个虚拟文件的创建工作。

2.4.删除文件或目录

既然有创建虚拟文件的函数,必然也就有删除虚拟文件的函数接口:

void remove_proc_entry(const char *name, struct proc_dir_entry *parent);

该函数中的参数name和parent与上述函数的参数意义相同。
在示例程序中,我们在卸载函数中完成上述几个文件的删除工作:

	remove_proc_entry("jiffies_too", example_dir);
	remove_proc_entry("foo", example_dir);
	remove_proc_entry("MODULE_NAME", NULL);
2.5读写proc文件

如果只是创建了虚拟文件,那么它并不能被读写。为此,我们必须为每个虚拟文件挂接读写函数,如果该虚拟文件是只读的,那么只需挂载相应的读函数。

正如上面所述,每个虚拟文件对应的proc_dir_entry结构都有read_proc和write_proc两个字段,它们均为函数指针,其各自的类型定义如下:

 typedef int (read_proc_t)(char *page, char **start, off_t off, int count, int *eof, void *data);
  typedef int (write_proc_t)(struct file *file, const char __user *buffer, unsigned long count, void *data);

如果要实现对虚拟文件的读写,则需要实现上述两个函数接口。对于我们的示例程序,我们的实现方法如下:

static int proc_read_foobar(char *page, char **start, off_t off, int count, int *eof, void *data)
{
	int len;
	struct fb_data_t *fb_data = (struct fb_data_t *)data;

	//将fb_data的数据写入page
	len = sprintf(page, "%s = %s\n", fb_data->name, fb_data->value);

	return len;
}

static int proc_write_foobar(struct file *file, const char *buffer, unsigned long count, void *data)
{
	int len;
	struct fb_data_t *fb_data = (struct fb_data_t *)data;

	if (count > FOOBAR_LEN)
		len = FOOBAR_LEN;
	else
		len = count;

	//写函数的核心语句,将用户态的buffer写入内核态的value中
	if (copy_from_user(fb_data->value, buffer, len))
		return -EFAULT;

	fb_data->value[len] = '\0';

	return len;
}

当用户读我们刚创建的虚拟文件时,该文件对应的read_proc函数将被调用。该函数将数据写入内核的缓冲区中。上述读函数的例子中,缓冲区即为page。当用户给虚拟文件写数据时,write_proc函数将被调用,该函数从缓冲区buffer中读取count个字节的数据。

3.测试

接下来我们将进行一系列的读写测试。由于我们只为jiffies与其符号链接文件jiffies_too实现了读回调函数,因此它们为只读文件,当对这两个文件进行写操作时就会出现错误;对于foo和bar文件,我们为其实现了读、写函数,因此既可以对它们进行读操作也可以进行写操作。

root@edsionte-desktop:/proc/edsionte_procfs# cat jiffies
jiffies = 833619
root@edsionte-desktop:/proc/edsionte_procfs# cat jiffies_too
jiffies = 834442
root@edsionte-desktop:/proc/edsionte_procfs# cat bar
bar = bar
root@edsionte-desktop:/proc/edsionte_procfs# cat foo
foo = foo
root@edsionte-desktop:/proc/edsionte_procfs# echo "time" > jiffies
bash: echo: 写操作出错: 输入/输出错误
root@edsionte-desktop:/proc/edsionte_procfs# echo "time" > jiffies_too
bash: echo: 写操作出错: 输入/输出错误
root@edsionte-desktop:/proc/edsionte_procfs# echo "hello" >> bar
root@edsionte-desktop:/proc/edsionte_procfs# cat bar
bar = hello

示例程序可以在此下载完成代码。

参考:

1.边干边学-Linux内核指导;作者: 李善平;出版社: 浙江大学出版社;

2.使用 /proc 文件系统来访问 Linux 内核的内容;
http://www.ibm.com/developerworks/cn/linux/l-proc.html

字符设备驱动再学习

2011年1月15日

本学期一直在学习linux下到设备驱动开发,字符设备驱动是设备驱动开发中最基本和重要的一部分。前几天的考试让我意识到对这部分的内容理解的还不是很清楚,因此,很有必要再次理解学习字符设备驱动。

本文以全局内存字符设备globalmem为例,说明字符设备驱动的结构以及编写方法。

1.字符设备的数据结构

在linux内核中使用struct cdev来表示一个字符设备,如下:

//在linux/include/linux/cdev.h中
  12struct cdev {
  13        struct kobject kobj;
  14        struct module *owner;
  15        const struct file_operations *ops;
  16        struct list_head list;
  17        dev_t dev;
  18        unsigned int count;
  19};

下面对该数据结构的字段作简单解释:

owner:该设备的驱动程序所属的内核模块,一般设置为THIS_MODULE;
ops:文件操作结构体指针,file_operations结构体中包含一系列对设备进行操作的函数接口;
dev:设备号。dev_t封装了unsigned int,该类型前12位为主设备号,后20位为次设备号;

cdev结构是内核对字符设备驱动的标准描述。在实际的设备驱动开发中,通常使用自定义的结构体来描述一个特定的字符设备。这个自定义的结构体中必然会包含cdev结构,另外还要包含一些描述这个具体设备某些特性到字段。比如:

struct globalmem_dev
{
	struct cdev cdev; /*cdev struct which the kernel has defined*/
	unsigned char mem[GLOBALMEM_SIZE]; /*globalmem memory*/
};

该结构体用来描述一个具有全局内存的字符设备。

2.分配和释放设备号

在linux中,对于每一个设备,必须有一个惟一的设备号与之相对应。通常会有多个设备共用一个主设备号,而具体每个设备都唯一拥有一个次设备号。总的来看,每个设备都唯一的拥有一个设备号。前面已经提到,内核使用dev_t类型来表示一个设备号,对于设备号有以下几个常用的宏:

//在linux/include/linux/kdev_t.h中
7#define MAJOR(dev)      ((unsigned int) ((dev) >> MINORBITS))
8#define MINOR(dev)      ((unsigned int) ((dev) & MINORMASK))
9#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

上述三个宏的功能分别为:通过设备号获取主设备号,通过设备号获取次设备号,通过主次设备好获取设备号。

在设备驱动程序中,一般会首先向系统申请设备号。linux中设备号的申请都是一段连续的设备号,这些连续的设备号都有共同的主设备号。设备号的申请有两种方法,若提前设定了主设备号则再接着申请若干个连续的次设备即可;若未指定主设备号则直接向系统动态申请未被占用到设备号。由此可以看出,如果使用第一种方法,则可能会出现设备号已被系统中的其他设备占用的情况。

上出两种申请设备号的方法分别对应以下两个申请函数:

  //在linux/fs/char_dev.c中
 196int register_chrdev_region(dev_t from, unsigned count, const char *name)
 232int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
 233                        const char *name)

上述两个函数都可以申请一段连续的设备号。前者适用已知起始设备号的情况(通过MADEV(major,0)可以获得主设备号为major的起始设备号);后者使用于动态申请设备号的情况。如果想申请一个设备号,则将函数中的参数count设为1即可。关于这两个函数的详细源码分析,可参考这里

3.Linux字符设备驱动的组成

实现一个基本的字符设备驱动需要完成以下几部分:字符设备驱动模块的加载卸载函数和实现file_operations结构中的成员函数。

3.1.file_operations结构体

file_operations结构体中包含许多函数指针,这些函数指针是字符设备驱动和内核的接口。,实现该结构中的这些函数也是整个字符设备驱动程序的核心工作。file_operations结构中的每个函数都对应一个具体的功能,也就是对设备的不同操作。不过,这些函数是在内核模块中实现的,最终会被加载到内核中和内核一起运行。因此,用户态下的程序是不能直接使用这些函数对相应设备进行操作的。

学过系统调用后,你就会知道,比如当应用程序通过系统调用read对设备文件进行读操作时,最终的功能落实者还是设备驱动中实现的globalmem_read函数。而将系统调用read和globalmem_read函数扯上关系的则是struct file_operations。具体的操作是:

static const struct file_operations globalmem_fops =
{
	.owner = THIS_MODULE,
	.read = globalmem_read,
	.write = globalmem_write,
	.open = globalmem_open,
	.release = globalmem_release,
};

3.2.实现加载和卸载函数

由于字符设备驱动程序是以内核模块的形式加载到内核的,因此该程序中必须有内核模块的加载和卸载函数。通常,字符设备驱动程序的加载函数完成的工作有设备号的申请、cdev的注册。具体的过程可参考下图:

globalmem_init流程图(点击看大图)

从上述的图中可以看到,在内核模块加载函数中主要完成了字符设备号的申请。将字符设备注册到系统中是通过加载函数中的globalmem_setup_cdev函数来完成的。该函数具体完成的工作可以参考下图:

globalmem_setup_cdev流程图

结合上图,接下来参看globalmem_setup_cdev函数的具体代码。由cdev_init中,除了初始化cdev结构中的字段,最重要的是将globalmem_fops传递给cdev中的ops。

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
	int ret;
	int devno = MKDEV(globalmem_major, index);

	cdev_init(&dev->cdev, &globalmem_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &globalmem_fops;
	ret = cdev_add(&dev->cdev, devno, 1);
	if(ret){
		printk("adding globalmem error");
	}
}

通过上述的几步,就可以完成字符设备驱动加载函数。对于字符设备卸载函数而言,所作的工作就是加载函数功能的逆向:将cdev从系统中注销;释放设备结构体所占用的内存空间;释放设备号。具体可参看代码:

static void __exit globalmem_exit(void)
{
	/*free struct cdev*/
	cdev_del(&dev->cdev);
	/*free the memory of struct globalmem_dev*/
	kfree(dev);
	/*free the devno*/
	unregister_chrdev_region(MKDEV(globalmem_major,0), 1);
}

3.3.对file_operaions成员函数的实现
最基本的成员函数包括open、release、read和write等函数。对这些函数的具体实现还要根据具体的设备要求来完成。在本文所述的全局内存字符设备驱动中,我们要实现的是功能是在用户程序中对这字符设备中的这块全局内存进行读写操作。读写函数的具体功能可参考下图:

对于open和release可以不做具体实现,当用户态程序打开或释放设备文件时,会自动调用内核中通用的打开和释放函数。

这样,一个基本的字符设备驱动程序就完成了。本文所述实例是一个有代表性的通用模型,可以在理解本程序的基础上继续增加其他功能。

内核同步方法-原子操作

2010年10月20日

Latest Update:2010/10/23

原子操作

原子操作用于执行轻量级、仅执行一次的操作,比如修改计数器,某些条件下的增加值或设置位等。原子操作指的是指令以原子的方式执行。之所以用原子来修饰这种操作方式是因为它们均不可再分。也就是说,原子操作要么一次性执行完毕,要么就不执行,这些操作的执行过程是不可被打断的。原子操作的具体实现取决于体系架构,以下代码如无特别声明,均来自linux/arch/x86/include/asm/atomic.h中

内核中提供了两组原子操作的接口:原子整形操作和原子位操作。前者是一组对整形数据的操作,后者则是针对单独的位操作。通过上面的叙述我们可以知道,这写操作接口完成的动作都是原子的。

原子整形操作

0.数据结构

在linux/include/linux/types.h中有如下定义:

 190typedef struct {
 191        int counter;
 192} atomic_t;

原子类型内部只有一个整型的成员counter。

1.初始化/设置原子变量的值

  15#define ATOMIC_INIT(i)  { (i) }

  35static inline void atomic_set(atomic_t *v, int i)
  36{
  37        v->counter = i;
  38}

初始化宏的源码很明显的说明了如何初始化一个原子变量。我们在定义一个原子变量时可以这样的使用它:

  atomic_t v=ATOMIC_INIT(0);

atomic_set函数可以原子的设置一个变量的值。

2.获取原子变量的值

  23static inline int atomic_read(const atomic_t *v)
  24{
  25        return (*(volatile int *)&(v)->counter);
  26}

返回原子型变量v中的counter值。关键字volatile保证&(v->counter)的值固定不变,以确保这个函数每次读入的都是原始地址中的值。

3.原子变量的加与减

   47static inline void atomic_add(int i, atomic_t *v)
  48{
  49        asm volatile(LOCK_PREFIX "addl %1,%0"
  50                     : "+m" (v->counter)
  51                     : "ir" (i));
  52}

  61static inline void atomic_sub(int i, atomic_t *v)
  62{
  63        asm volatile(LOCK_PREFIX "subl %1,%0"
  64                     : "+m" (v->counter)
  65                     : "ir" (i));
  66}

加减操作中使用了内联汇编语句。

linux中的汇编语句都采用AT&T指令格式。带有C表达式的内联汇编格式为:__asm__ __volatile__(“InstructionList” :Output :Input :Clobber/Modify);其中asm(或__asm__)用来声明一个内联汇编语句;关键字volatile是可选的,选择此关键字意味着向编译器声明“不要动我所写的指令,我需要原封不动的保留每一条指令”,否则,编译器可能会对指令进行优化。InstructionList即我们所加的汇编语句。指令后面的3个部分(Output, Input,Clobber/Modify)是可选的,分别指输出操作数,输入操作数,修改位。

下面我们针对上述加减操作对上述内联汇编语句做简单的解释。

在加函数中,对变量加操作的原子型表现在使用单条的汇编语句完成。指令中的%1,%0分别代表输入操作数和输出操作数,它们用来标示变量。内联汇编语句中的操作数从0开始进行编号,并且优先为输出操作数编号,然后再依次为输入操作数编号。在指令后的输出部分,“+m”中的+表示输出变量是可读可写的,m表示输出操作数存储于内存单元中,而不是在寄存器中。在输入部分,“ir”中的i表示输入操作数是一个直接操作数,r表示存储与寄存器中。输入部分和输出部分中的括号内的C语言格式的变量分别对应汇编语句中的输入操作数和输出操作数。

汇编语句addl %1,%0就是将i加至v->counter上,这个加的过程是原子的。

了解了加操作,那么减操作也就不难理解了,这里不再赘述。

4.原子变量的自加自减

  93static inline void atomic_inc(atomic_t *v)
  94{
  95        asm volatile(LOCK_PREFIX "incl %0"
  96                     : "+m" (v->counter));
  97}

 105static inline void atomic_dec(atomic_t *v)
 106{
 107        asm volatile(LOCK_PREFIX "decl %0"
 108                     : "+m" (v->counter));
 109}

从内联的汇编语句中可以看到,自加自减均采用单条汇编指令,直接在输出数%0上加1或减1。这里的输出数%0的值即是v->counter变量的值。

5.操作并测试

atomic_sub_and_test函数实现的功能是原子的从v减去i,如果结果等于0,则返回1;否则返回0。

 77static inline int atomic_sub_and_test(int i, atomic_t *v)
  78{
  79        unsigned char c;
  80
  81        asm volatile(LOCK_PREFIX "subl %2,%0; sete %1"
  82                     : "+m" (v->counter), "=qm" (c)
  83                     : "ir" (i) : "memory");
  84        return c;
  85}

通过上述源码可以看到,第一条指令实现减操作;接着根据ZF的值设置c变量的值。sete命令的作用是,如果ZF=1,也就是说减的结果为0,将设置c变量为1;否则c的值为0。执行完毕后返回相应的c值。

 119static inline int atomic_dec_and_test(atomic_t *v)
 120{
 121        unsigned char c;
 122
 123        asm volatile(LOCK_PREFIX "decl %0; sete %1"
 124                     : "+m" (v->counter), "=qm" (c)
 125                     : : "memory");
 126        return c != 0;
 127}

 137static inline int atomic_inc_and_test(atomic_t *v)
 138{
 139        unsigned char c;
 140
 141        asm volatile(LOCK_PREFIX "incl %0; sete %1"
 142                     : "+m" (v->counter), "=qm" (c)
 143                     : : "memory");
 144        return c != 0;
 145}

上数两个函数的作用是自减1或自加1后,再测试v值是否为0,如果是0,则返回1;否则返回0。

也许你会认为上述的加/减并测试并不是原子的。我个人的认为是:第一条加/减操作的确是原子的,当此条语句执行完毕后,会产生相应的ZF值。如果此时在执行sete之前产生了中断,那么肯定会保存当前寄存器等值。因此,即便中间产生了中断,返回的也是中断前加/减后的结果。

6.操作并返回

 173static inline int atomic_add_return(int i, atomic_t *v)
 174{
 175        int __i;
 176#ifdef CONFIG_M386
 177        unsigned long flags;
 178        if (unlikely(boot_cpu_data.x86 <= 3))
 179                goto no_xadd;
 180#endif
 181        /* Modern 486+ processor */
 182        __i = i;
 183        asm volatile(LOCK_PREFIX "xaddl %0, %1"
 184                     : "+r" (i), "+m" (v->counter)
 185                     : : "memory");
 186        return i + __i;
 187
 188#ifdef CONFIG_M386
 189no_xadd: /* Legacy 386 processor */
 190        raw_local_irq_save(flags);
 191        __i = atomic_read(v);
 192        atomic_set(v, i + __i);
 193        raw_local_irq_restore(flags);
 194        return i + __i;
 195#endif
 196}

首先if判断语句判断当前的处理器是否为386或386以下,如果是则直接执行no_xadd处开始的代码;否则紧接着判断语句执行。此处的unlikely表示”经常不“。因为现在的处理器大都高于386,所以编译器一般都按照高于386来处理,以达到优化源代码的目的。搞清楚这段代码的逻辑关系,那么接下来的工作就不困难了。

如果CPU高于386,则它将执行xaddl命令,该命令的作用是先交换源和目的操作数,再相加。这里%0,%1分别代表i和v->counter。首先将i值用__i保存;操作数交换后,%0,%1的值依次为v->counter和i;相加并保存到%1;最后%0,%1的结果就是v->counter,i+v->counter,返回的结果i+__i的结果就是i+v->counter。这里涉及到i和v->counter的值时均指初值。

理解了上述过程,再看其他的操作返回函数:

 205static inline int atomic_sub_return(int i, atomic_t *v)
 206{
 207        return atomic_add_return(-i, v);
 208}

可以看到,atomic_sub_return函数是对atomic_add_return函数的封装。而下面两个函数则又是对上述两个函数的封装。

 210#define atomic_inc_return(v)  (atomic_add_return(1, v))
 211#define atomic_dec_return(v)  (atomic_sub_return(1, v))

上述就是对整形原子操作的分析。

与中断有关的数据结构

2010年10月6日

Latest Update:2010/10/12

1.概述

通过前文,我们已经知道了中断通常由上下两部分组成。在上部分,也就是中断处理程序,完成中断请求的响应以及完成那些对时间要求紧迫的工作;而在下部分,通常完成那些被推后的工作,因为这部分工作对时间的要求相对宽松一些。通过了解上下两部分的工作情况,可以更好的理解中断这个概念。从下半部分执行机制来看——不管是tasklet还是工作队列——这些推后的工作总是在上半部分被调用,然后交给内核在适当的时间来完成。那么,中断上部分具体是如何工作的?内核对中断是如何处理的?

在开始分析之前,需要说明的是接下来分析的属于内核对外设产生的中断的处理情况,异常的处理过程在本文的最后会有简单解释。

所有的中断的处理程序在init_IRQ函数中都被初始化为interrupt[i]。interrupt数组中每一项均指向一个代码片段:

pushl $n-256
/*省略部分代码*/
jmp common_interrupt

该代码片段除了将中断向量号压入堆栈,还会跳到一个公共处理程序common_interrup:

//在linux/arch/x86/kernel/entry_32.S
 863 common_interrupt:
 864         addl $-0x80,(%esp)      /* Adjust vector into the [-256,-1] range *     /
 865         SAVE_ALL
 866         TRACE_IRQS_OFF
 867         movl %esp,%eax
 868         call do_IRQ
 869         jmp ret_from_intr

这段公共处理程序会将中断发生前的所有寄存器的值压入堆栈,也就是保存被中断任务的现场。然后调用do_IRQ函数,在do_IRQ函数中会调用(并非直接调用那么简单)到handle_IRQ_event函数,在此函数中会执行实际的中断服务例程。当中断服务例程执行完毕后,会返回到上面的那段汇编程序中,转入ret_from_intr代码段从中断返回。

在上述文字描述的基础上,可以通过下图进一步加深中断的处理过程:

从上面的概述中,我们可以很快定位我们未来要分析的函数:do_IRQ()和hand_IRQ_event();在分析上述函数之前,我们很有必要先分析一下关于中断的三个重要的数据结构。

2.struct irq_desc

在一开始我们分析中断的时候,谈及到了中断向量。在内核中,每个中断向量都有相应的有一个irq_desc结构体(稍早内核版本中为irq_desc_t)来描述一个中断向量(也就是中断源),具体代码如下:

  31struct irq_desc;
  32typedef void (*irq_flow_handler_t)(unsigned int irq,
  33                                            struct irq_desc *desc);

 175struct irq_desc {
 176        unsigned int            irq;
 177        struct timer_rand_state *timer_rand_state;
 178        unsigned int            *kstat_irqs;
 179#ifdef CONFIG_INTR_REMAP
 180        struct irq_2_iommu      *irq_2_iommu;
 181#endif
 182        irq_flow_handler_t      handle_irq;
 183        struct irq_chip         *chip;
 184        struct msi_desc         *msi_desc;
 185        void                    *handler_data;
 186        void                    *chip_data;
 187        struct irqaction        *action;
 188        unsigned int            status;
 189
 190        unsigned int            depth;
 191        unsigned int            wake_depth;
 192        unsigned int            irq_count;
 193        unsigned long           last_unhandled;
 194        unsigned int            irqs_unhandled;
 195        raw_spinlock_t          lock;
 196#ifdef CONFIG_SMP
 197        cpumask_var_t           affinity;
 198        const struct cpumask    *affinity_hint;
 199        unsigned int            node;
 200#ifdef CONFIG_GENERIC_PENDING_IRQ
 201        cpumask_var_t           pending_mask;
 202#endif
 203#endif
 204        atomic_t                threads_active;
 205        wait_queue_head_t       wait_for_threads;
 206#ifdef CONFIG_PROC_FS
 207        struct proc_dir_entry   *dir;
 208#endif
 209        const char              *name;
 210}

 216#ifndef CONFIG_SPARSE_IRQ
 217extern struct irq_desc irq_desc[NR_IRQS];
 218#endif

所有这样的描述符组织在一起形成irq_desc[NR_IRQS]数组。下面我们对上述结构体的部分字符进行解释;

irq:通过数据类型可知这便是这个描述符所对应的中断号;
handle_irq:指向该IRQ线的公共服务程序;
chip:它是一个struct irq_chip类型的指针,是中断控制器的描述符,与平台有关,下文有详细描述;
handler_data:用于handler_irq的参数;
chip_data:用于chip的参数;
action:一个struct irqaction类型的指针(下文有该结构的详细描述);它指向一个单链表,该单链表是由该中断线上所有中断服务程序(对应struct irqaction)所连接起来的;
status:描述中断线当前的状态;
depth:中断线被激活时,值为0;其值为正数时,表示被禁止的次数;
irq_count:记录该中断线发生中断的次数;
irqs_unhandled:该IRQ线上未处理中断发生的次数;
name: /proc/interrupts 中显示的中断名称;

3.struct irqaction

当多个设备共享一条IRQ线时,因为每个设备都要有各自的ISR。为了能够正确处理此条IRQ线上的中断处理程序(也就是区分每个设备),就需要我们使用irqaction结构体。在这个结构体中,会有专门的handler字段指向该设备的真正的ISR。共享同一条IRQ线上的多个这样的结构体会连接成了一个单链表,即所谓的中断请求队列。中断产生时,该IRQ线的中断请求队列上所有的ISR都会被依次调用,因此每个设备的ISR必须判断当前的中断是否是自己所属的设备产生的。irqaction结构具体的定义如下:

  98typedef irqreturn_t (*irq_handler_t)(int, void *);

 113struct irqaction {
 114        irq_handler_t handler;
 115        unsigned long flags;
 116        const char *name;
 117        void *dev_id;
 118        struct irqaction *next;
 119        int irq;
 120        struct proc_dir_entry *dir;
 121        irq_handler_t thread_fn;
 122        struct task_struct *thread;
 123        unsigned long thread_flags;
 124};

handler:指向一个具体的硬件设备的中断服务例程,可以从此指针的类型发现与前文我们所定义的中断处理函数声明相同;
flags:对应request_irq函数中所传递的第三个参数,可取IRQF_DISABLED、IRQF_SAMPLE_RANDOM和IRQF_SHARED其中之一;
name:对应于request_irq函数中所传递的第四个参数,可通过/proc/interrupts文件查看到;
next:指向下一个irqaction结构体;
dev_id:对应于request_irq函数中所传递的第五个参数,可取任意值,但必须唯一能够代表发出中断请求的设备,通常取描述该设备的结构体;
irq:中断号

如果一个IRQ线上有中断请求,那么内核将依次次调用在该中断线上注册的每一个中断服务程序,但是并不是所有中断服务程序都被执行。一般硬件设备都会提供一个状态寄存器,以便中断服务程序进行检查是否应该为这个硬件服务。也就是说在整个中断请求队列中,最多会有一个ISR被执行,也就是该ISR对应的那个设备产生了中断请求时;不过当该IRQ线上某个设备未找到匹配的ISR时,那这个中断就不会被处理。此时irq_desc结构中的irqs_unhandled字段就会加1。

4.struct irq_chip

struct irq_chip是一个中断控制器的描述符。通常不同的体系结构就有一套自己的中断处理方式。内核为了统一的处理中断,提供了底层的中断处理抽象接口,对于每个平台都需要实现底层的接口函数。这样对于上层的中断通用处理程序就无需任何改动。这样的结构体就好比一个插板,我们可以使用各种插头。至于插板内部是如何实现的,使用者并不需要过于关心。

比如经典的中断控制器是2片级联的8259A,那么得15个irq_desc描述符,每一个描述符的irq_chip都指向描述8259A的i8259A_irq_type变量(arch/alpha/kernel/irq_i8259.c):

 86struct irq_chip i8259a_irq_type = {
  87        .name           = "XT-PIC",
  88        .startup        = i8259a_startup_irq,
  89        .shutdown       = i8259a_disable_irq,
  90        .enable         = i8259a_enable_irq,
  91        .disable        = i8259a_disable_irq,
  92        .ack            = i8259a_mask_and_ack_irq,
  93        .end            = i8259a_end_irq,
  94};

这一点类似于VFS中所采用的原理:通过struct file_operations提供统一的接口,每种文件系统都必须具体实现这个结构体中所提供的接口。struct irq_chip具体代码如下:

111struct irq_chip {
112        const char      *name;
113        unsigned int    (*startup)(unsigned int irq);
114        void            (*shutdown)(unsigned int irq);
115        void            (*enable)(unsigned int irq);
116        void            (*disable)(unsigned int irq);
117
118        void            (*ack)(unsigned int irq);
119        void            (*mask)(unsigned int irq);
120        void            (*mask_ack)(unsigned int irq);
121        void            (*unmask)(unsigned int irq);
122        void            (*eoi)(unsigned int irq);
123
124        void            (*end)(unsigned int irq);
125        int             (*set_affinity)(unsigned int irq,
126                                        const struct cpumask *dest);
127        int             (*retrigger)(unsigned int irq);
128        int             (*set_type)(unsigned int irq, unsigned int flow_type);
129        int             (*set_wake)(unsigned int irq, unsigned int on);
130
131        void            (*bus_lock)(unsigned int irq);
132        void            (*bus_sync_unlock)(unsigned int irq);
133
134        /* Currently used only by UML, might disappear one day.*/
135#ifdef CONFIG_IRQ_RELEASE_METHOD
136        void            (*release)(unsigned int irq, void *dev_id);
137#endif
138        /*
139         * For compatibility, ->typename is copied into ->name.
140         * Will disappear.
141         */
142        const char      *typename;
143};

name:中断控制器的名字;
Startup:启动中断线;
Shutdown:关闭中断线;
Enable:允许中断;
Disable:禁止中断;

5.数据结构之间的关系

首先我们通过下图来了解上述三个数据结构的关系:

通过上述分析,我们可以大致的知道中断处理程序的调用过程:在do_IRQ函数中,通过对irq_desc结构体中handler_irq字段的引用,调用handler_irq所指向的公共服务程序;在这个公共服务程序中会调用hand_IRQ_event函数;在hand_IRQ_event函数中,通过对irqaction结构体中handler字段的引用最终调用我们所写的中断处理程序。

另外,通过上述分析,我们知道struct irq_chip描述了中断最底层的部分;而struct irqacton则描述最上层具体的中断处理函数;而与中断向量所对应的struct desc则类似一个中间层,将中断中的硬件相关的部分和软件相关的部分连接起来。

6.内核对异常的处理

与中断不同,各种异常都有固定的中断向量(0~31)以及固定的异常处理程序。因此,当异常发生时,将直接跳转到相应的服务程序中执行。

中断下半部-工作队列

2010年10月5日

本文包含那些内容?
工作队列和tasklet的区别;中断上下文;工作队列的使用;
本文适合那些人阅读?
想了解linuxer;学习驱动开发的beginner;学习内核模块编程beginner;其他super linux NBer;
参考书籍:
Linux内核设计与实现
Linux操作系统原理与应用

为什么还需要工作队列?


工作队列(work queue)是另外一种将中断的部分工作推后的一种方式,它可以实现一些tasklet不能实现的工作,比如工作队列机制可以睡眠。这种差异的本质原因是,在工作队列机制中,将推后的工作交给一个称之为工作者线程(worker thread)的内核线程去完成(单核下一般会交给默认的线程events/0)。因此,在该机制中,当内核在执行中断的剩余工作时就处在进程上下文(process context)中。也就是说由工作队列所执行的中断代码会表现出进程的一些特性,最典型的就是可以重新调度甚至睡眠。

对于tasklet机制(中断处理程序也是如此),内核在执行时处于中断上下文(interrupt context)中。而中断上下文与进程毫无瓜葛,所以在中断上下文中就不能睡眠。

因此,选择tasklet还是工作队列来完成下半部分应该不难选择。当推后的那部分中断程序需要睡眠时,工作队列毫无疑问是你的最佳选择;否则,还是用tasklet吧。

中断上下文


在了解中断上下文时,先来回顾另一个熟悉概念:进程上下文(这个中文翻译真的不是很好理解,用“环境”比它好很多)。一般的进程运行在用户态,如果这个进程进行了系统调用,那么此时用户空间中的程序就进入了内核空间,并且称内核代表该进程运行于内核空间中。由于用户空间和内核空间具有不同的地址映射,并且用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行。这样就产生了进程上下文。

所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容。当内核需要切换到另一个进程时(上下文切换),它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态继续执行。上述所说的工作队列所要做的工作都交给工作者线程来处理,因此它可以表现出进程的一些特性,比如说可以睡眠等。

对于中断而言,是硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。因此处于中断上下文的tasklet不会有睡眠这样的特性。

工作队列的使用


内核中通过下述结构体来表示一个具体的工作:

struct work_struct
{
	unsigned long pending;//这个工作是否正在等待处理
	struct list_head entry;//链接所有工作的链表,形成工作队列
	void (*func)(void *);//处理函数
	void *data;//传递给处理函数的参数
	void *wq_data;//内部使用数据
	struct timer_list timer;//延迟的工作队列所用到的定时器
};

而这些工作(结构体)链接成的链表就是所谓的工作队列。工作者线程会在被唤醒时执行链表上的所有工作,当一个工作被执行完毕后,相应的work_struct结构体也会被删除。当这个工作链表上没有工作时,工作线程就会休眠。

通过如下宏可以创建一个要推后的完成的工作:

DECLARE_WORK(name,void(*func)(void*),void *data);

也可以通过下述宏动态创建一个工作:

INIT_WORK(struct work_struct *work,void(*func)(void*),void *data);

与tasklet类似,每个工作都有具体的工作队列处理函数,原型如下:

void work_handler(void *data)

将工作队列机制对应到具体的中断程序中,即那些被推后的工作将会在func所指向的那个工作队列处理函数中被执行。

实现了工作队列处理函数后,就需要schedule_work函数对这个工作进行调度,就像这样:

schedule_work(&work);

这样work会马上就被调度,一旦工作线程被唤醒,这个工作就会被执行(因为其所在工作队列会被执行)。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key