Archive for the ‘Linux’ category

具有阻塞操作的字符设备驱动

October 13th, 2014

http://www.lizhaozhong.info/archives/1062

在上面这篇文章里面,我们做的是一个简单的非阻塞操作的字符设备,也就是说对于这个设备的操作要么放弃,要么不停的轮询,直到操作可以进行下去。

而支持阻塞设备的字符操作,我们可以实现读操作或者写操作的睡眠。这就是我们要完成的操作。

关于程序的框架,与之前的其实是类似的,比如我们在全局设定一个等待队列头。

#define MYCDEV_SIZE 100
#define DEVICE_NAME "lzz_block_cdev"

static char globalmem[MYCDEV_SIZE];
static wait_queue_head_t rdwait;
static wait_queue_head_t wrwait;
static struct semaphore mutex;

static int len;
ssize_t myblock_read(struct file*,char*,size_t count,loff_t*);
ssize_t myblock_write(struct file*,char*,size_t count,loff_t*);
ssize_t mycdev_open(struct inode *inode, struct file *fp);
ssize_t mycdev_release(struct inode *inode, struct file *fp);

在模块初始化中,首先要初始化mutex,rdwait,wrwait。

static int __init mycdev_init(void)
{
	int ret;

	printk("myblock module is working..\n");

	ret=register_chrdev(MYCDEV_MAJOR,DEVICE_NAME,&fops);
	if(ret<0)
	{
		printk("register failed..\n");
		return 0;
	}
	else
	{
		printk("register success..\n");
	}
	sema_init(&mutex,1);
	init_waitqueue_head(&rdwait);
	init_waitqueue_head(&wrwait);

	return 0;
}

先创建一个代表当前进程的等待队列结点wait,并把它加入到读等待队列当中。

当共享数据区的数据长度为0时,就阻塞该进程。因此,在循环中,首先将当前进程的状态设置TASK_INTERRUPTIBLE。

然后利用schedule函数进行重新调度,此时,读进程才会真正的睡眠,直至被写进程唤醒。在睡眠途中,如果用户给读进程发送了信号,那么也会唤醒睡眠的进程。

当共享数据区有数据时,会将count字节的数据拷贝到用户空间,并且唤醒正在睡眠的写进程。当上述工作完成后,会将当前进程从读等待队列中移除,并且将当前进程的状态设置为TASK_RUNNING。

关于从全局缓冲区移出已读数据,这里要特别说明一下。这里利用了memcpy函数将以(globalmem+count)开始的(len-count)字节的数据移动到缓冲区最开始的地方。

在调用schedule函数退出CPU后,下次唤醒后进入运行时将从schedule语句的下一条语句开始,即if (signal_pending(current)) 语句。

signal_pending(current)检查当前进程是否有信号处理,若要处理就返回非0!然后这个函数退出,插入到等待队列,等待下次重新开始执行!

ssize_t myblock_read(struct file*fp,char*buf,size_t count,loff_t*offp)
{
	int ret;
	DECLARE_WAITQUEUE(wait,current);

	down(&mutex);
	add_wait_queue(&rdwait,&wait);

	while(len==0)
	{
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&mutex);
		schedule();
		if(signal_pending(current))
		{
			ret=-1;
			goto signal_out;
		}

		down(&mutex);
	}

	if(count>len)
	{
		count=len;
	}

	if(copy_to_user(buf,globalmem,count)==0)
	{
		memcpy(globalmem,globalmem+count,len-count);
		len-=count;
		printk("read %d bytes\n",count);
		wake_up_interruptible(&wrwait);
		ret=count;
	}
	else
	{
		ret=-1;
		goto copy_err_out;
	}

copy_err_out:up(&mutex);
signal_out:remove_wait_queue(&rdwait,&wait);

	set_current_state(TASK_RUNNING);
	return ret;
}

写函数的控制流程大致与读函数相同,只不过对应的等待队列是写等待队列。
唤醒后如何执行。无论因哪种方式而睡眠,当读进程被唤醒后,均顺序执行接下来的代码。

ssize_t myblock_write(struct file*fp,char*buf,size_t count,loff_t*offp)
{
	int ret;
	DECLARE_WAITQUEUE(wait,current);

	down(&mutex);
	add_wait_queue(&wrwait,&wait);

	while(len==MYCDEV_SIZE)
	{
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&mutex);
		schedule();
		if(signal_pending(current))
		{
			ret=-1;
			goto signal_out;
		}

         	down(&mutex);
	}
	if(count>(MYCDEV_SIZE-len))
	{
		count=MYCDEV_SIZE-len;
	}

	if(copy_from_user(globalmem+len,buf,count)==0)
	{
		len=len+count;
		printk("written %d bytes\n",count);
		wake_up_interruptible(&rdwait);
		ret=count;
	}
	else
	{
		ret=-1;
		goto COPY_ERR_OUT;
	}

signal_out:up(&mutex);
COPY_ERR_OUT:remove_wait_queue(&wrwait,&wait);
	set_current_state(TASK_RUNNING);

	return ret;
}

读写函数down操作和add_wait_queue操作交换,可能会造成死锁。

up操作和remove_wait_queue操作交换。如果读进程从内核空间向用户空间拷贝数据失败时,就会从up往后执行。

因为读进程是在获得信号量后才拷贝数据的,因此必须先释放信号量,再将读进程对应的等待队列项移出读等待队列。而当读进程因信号而被唤醒时,则直接跳转到remove_wait_queue操作,并往后执行(仅仅只是睡眠了,wake_up后继续向后执行)。

此时读进程并没有获得信号量,因此只需要移出队列操作即可。如果交换上述两个操作,读进程移出等待队列时还未释放互斥信号量,那么写进程就不能写。而当读进程因信号而唤醒时,读进程并没有获得信号量,却还要释放信号量。

设想一种情况:

while(len==0)
	{
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&mutex);
		schedule();
		if(signal_pending(current))
		{
			ret=-1;
			goto signal_out;
		}

		down(&mutex);
	}

当schedule()重新调度该程序,从if()开始执行遭遇用户的Ctrl+C ,就会跳入signal_out。如果没有用户信号,因为在schedule()前已经up()了,所以接下来要拷贝数据,使用down()

对于goto操作:就是从Label以后,一直往后执行,并不是执行一条语句而已。

使用模块的方式,上篇博文说的很清楚。
使用的时候:

  • 终端输入:cat /dev/blockcdev&;即从字符设备文件中读数据,并让这个读进程在后台执行,可通过ps命令查看到这个进程;

  • 中断继续输入:echo ‘I like eating..’ > /dev/blockcdev;即向字符设备文件中写入数据;

 

实践字符设备驱动

October 7th, 2014

我们都知道在linux里面存在块设备与字符设备。我们这里是设计的字符驱动,在不久,我会加入支持阻塞的功能。

我们可以通过查看cat /proc/device查看已注册的设备

[lzz@localhost device_character]$ cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 14 sound
 21 sg
 29 fb
 99 ppdev
116 alsa
128 ptm
136 pts
162 raw
180 usb
188 ttyUSB
189 usb_device
202 cpu/msr
203 cpu/cpuid
226 drm
250 hidraw
251 usbmon
252 bsg
253 watchdog
254 rtc

Block devices:
259 blkext
  8 sd
  9 md
 11 sr
 65 sd
 66 sd
 67 sd
 68 sd
 69 sd
 70 sd
 71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
253 device-mapper
254 mdp

我们选取一个231作为我们字符设备号,别的不废话,上代码.

字符设备驱动程序:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>
#include <asm/io.h>
#include <linux/mman.h>
#include <linux/uaccess.h>

MODULE_AUTHOR("lzz");
MODULE_LICENSE("GPL");

#define MYCDEV_MAJOR 231 /*the predefined mycdev's major devno*/
#define MYCDEV_SIZE 100

static char kernel_buf[MYCDEV_SIZE];

static int mycdev_open(struct inode *inode, struct file *fp)
{
 return 0;
}

static int mycdev_release(struct inode *inode, struct file *fp)
{
 return 0;
}

static ssize_t mycdev_read(struct file *fp, char __user *buf, size_t size, loff_t *pos)
{

 unsigned long p = *pos;
 unsigned int count = size;
// int i;

 if(p >= MYCDEV_SIZE)
 return -1;
 if(count > MYCDEV_SIZE)
 count = MYCDEV_SIZE - p;

 if (copy_to_user(buf, kernel_buf, count) != 0) 
 {
 printk("read error!\n");
 return -1;
 }

 printk("lzz's reader: %d bytes was read...\n", count);
 return count;

}

static ssize_t mycdev_write(struct file *fp, const char __user *buf, size_t size, loff_t *pos)
{
 unsigned long p = *pos;
 unsigned int count = size;
 int ret =0;

 if(copy_from_user(kernel_buf+p,buf,count))
 ret = -EFAULT;
 else
 {
 *pos+=count;
 ret = count;
 printk("lzz's write: %d bytes was wroten...\n", count);
 }
 return ret;
}

/*filling the mycdev's file operation interface in the struct file_operations*/
static const struct file_operations mycdev_fops =
{
 .owner = THIS_MODULE,
 .read = mycdev_read,
 .write = mycdev_write,
 .open = mycdev_open,
 .release = mycdev_release,
};

/*module loading function*/
static int __init mycdev_init(void)
{
 int ret;

 printk("mycdev module is staring..\n");

 ret=register_chrdev(MYCDEV_MAJOR,"lzz_cdev",&mycdev_fops);
 if(ret<0)
 {
 printk("register failed..\n");
 return 0;
 }
 else
 {
 printk("register success..\n");
 } 

 return 0;
}

/*module unloading function*/
static void __exit mycdev_exit(void)
{
 printk("mycdev module is leaving..\n");
 unregister_chrdev(MYCDEV_MAJOR,"lzz_cdev");
}

module_init(mycdev_init);
module_exit(mycdev_exit);

Makefile文件:

obj-m:= map_driver.o

CURRENT_PATH:=$(shell pwd)

LINUX_KERNEL:=$(shell uname -r)

LINUX_KERNEL_PATH:=/lib/modules/$(LINUX_KERNEL)/build

all:
 make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules

clean:
 make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

用户态测试程序:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
 int testdev;
 int i, ret;
 char buf[100]="test lzz's cdev ok!";
 char buff[100];

 testdev = open("/dev/mycdev", O_RDWR);

 if (-1 == testdev) {
 printf("cannot open file.\n");
 exit(1);
 }

 write(testdev,buf,sizeof(buf)-1);

 if ((ret = read(testdev, buff, sizeof(buff)-1)) < 0) {
 printf("read error!\n");
 exit(1);
 }

 printf("%s\n", buff);

 close(testdev);

 return 0;
}

使用方法:
1.make编译map_driver.c文件,并插入到内核;
2.通过cat /proc/devices 查看系统中未使用的字符设备主设备号,比如当前231未使用;
3.创建设备文件结点:sudo mknod /dev/mycdev c 231 0;具体使用方法通过man mknod命令查看;
4.修改设备文件权限:sudo chmod 777 /dev/mycdev;(可选)
5.以上成功完成后,编译本用户态测试程序;运行该程序查看结果;
6.通过dmesg查看日志信息;

Screenshot from 2014-10-07 22:10:11

内核锁的使用

October 6th, 2014

内核锁有三种形态:

  • 原子操作
  • spinlock
  • semaphore

1.原子结构

typedef struct {
    int counter;
} atomic_t;

不能直接赋值,需要使用函数比如ATOMIC_INIT()等初始化等函数(arch/alpha/include/asm/atomic.h)
初始化宏的源码很明显的说明了如何初始化一个原子变量。我们在定义一个原子变量时可以这样的使用它:

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指令格式。

 

2.自旋锁spinlock(include/linux/spinlock_types.h)

typedef struct spinlock {
    union {
        struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
        struct {
            u8 __padding[LOCK_PADSIZE];
            struct lockdep_map dep_map;
        };
#endif
    };
} spinlock_t;

自旋锁的使用:

1.定义初始化自旋锁

使用下面的语句就可以先定一个自旋锁变量,再对其进行初始化:

spinlock_t lock;
spin_lock_init(&lock);

2.获得自旋锁

void spin_lock(spinlock_t*);
int spin_trylock(spinlock_t*);

使用spin_lock(&lock)这样的语句来获得自旋锁。如果一个线程可以获得自旋锁,它将马上返回;否则,它将自旋至自旋锁的保持者释放锁。

另外可以使用spin_trylock(&lock)来试图获得自旋锁。如果一个线程可以立即获得自旋锁,则返回真;否则,返回假,此时它并不自旋。

3.释放自旋锁

void spin_unlock(spinlock_t*);

使用spin_unlock(&lock)来释放一个已持有的自旋锁。注意这个函数必须和spin_lock或spin_trylock函数配套使用。

3.信号量semaphore(include/linux/semaphore.h)

struct semaphore {
    raw_spinlock_t      lock;
    unsigned int        count;
    struct list_head    wait_list;
};

#define __SEMAPHORE_INITIALIZER(name, n)                \
{                                   \
    .lock       = __RAW_SPIN_LOCK_UNLOCKED((name).lock),    \
    .count      = n,                        \
    .wait_list  = LIST_HEAD_INIT((name).wait_list),     \
}

#define DEFINE_SEMAPHORE(name)  \
    struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)

static inline void sema_init(struct semaphore *sem, int val)
{
    static struct lock_class_key __key;
    *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
    lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

extern void down(struct semaphore *sem);
extern int __must_check down_interruptible(struct semaphore *sem);
extern int __must_check down_killable(struct semaphore *sem);
extern int __must_check down_trylock(struct semaphore *sem);
extern int __must_check down_timeout(struct semaphore *sem, long jiffies);
extern void up(struct semaphore *sem);

信号量(semaphore)是保护临界区的一种常用方法。它的功能与自旋锁相同,只能在得到信号量的进程才能执行临界区的代码。但是和自旋锁不同的是,一个进程不能获得信号量时,它会进入睡眠状态而不是自旋。

semaphore的使用

1.定义初始化信号量

使用下面的代码可以定义并初始化信号量sem:

struct semaphore sem;
sema_init(&sem,val);

其中val即为信号量的初始值。

除上面的方法,可以使用下面的两个宏定义并初始化信号量:

DECLARE_MUTEX(name);

DECLARE_MUTEX_LOCKED(name);

其中name为变量名。

2.获得信号量

down(&sem);

进程使用该函数时,如果信号量值此时为0,则该进车会进入睡眠状态,因此该函数不能用于中断上下文中。

down_interruptibale(&sem);

该函数与down函数功能类似,只不过使用down而睡眠的进程不能被信号所打断,而使用down_interruptibale的函数则可以被信号打断。

如果想要在中断上下文使用信号量,则可以使用下面的函数:

dwon_try(&sem);

使用该函数时,如果进程可以获得信号量,则返回0;否则返回非0值,不会导致睡眠。

3.释放信号量

up(&sem);

该函数会释放信号量,如果此时等待队列中有进程,则唤醒一个。

三种比较常用的互斥方式

需求 建议加锁的方法
低开销加锁 优先使用自旋锁
短期加锁 优先使用自旋锁
长期加锁 优先使用信号量
中断上下文加锁 使用自旋锁
持有锁时需要睡眠 使用信号量

自旋锁是忙等锁。当其他线程持有自旋锁时,如果另有线程想获得该锁,那么就只能循环的等待。这样忙的功能对CPU来说是极大的浪费。因此只有当由自旋锁保护的临界区执行时间很短时,使用自旋锁才比较合理。

spinlock.c
sharelist.c

在Linux3.X 添加 systemcall

September 25th, 2014

从代码级看syscall的问题

1.寄存器eax,传进来带的是中调用号,ret_from_sys_call()传出的是错误值。
2.由于内核函数是在内核中实现的,因此它必须符合内核编程的规则,比如函数名以sys_开始,函数定义时候需加asmlinkage标识符等。这个修饰符使得GCC编译器从堆栈中取该函数的参数而不是寄存器中。另外,系统调用函数的命名规则都是sys_XXX的形式。
3.不同的syscall,普通用户可用的命令和管理可用的命令分别被存放于/bin和/sbin目录下。

另外比较重要的一个特性就是在Linux 3.X系统中添加新的syscall 这相比于Linux 2.X添加syscall简单不少。Linux 2.X内核网上很多添加调用分析,就不赘述了。

1.定义系统调用服务例程。

#include &lt;linux/syscall.h&gt;

SYSCALL_DEFINE0(mycall)
{
        struct task_struct *p;
        printk(&quot;********************************************\n&quot;);
        printk(&quot;------------the output of mysyscall------------\n&quot;);
        printk(&quot;********************************************\n\n&quot;);
        printk(&quot;%-20s %-6s %-6s %-20s\n&quot;,&quot;Name&quot;,&quot;pid&quot;,&quot;state&quot;,&quot;ParentName&quot;);
        for(p = &amp;init_task; (p = next_task(p)) != &amp;init_task;)
                printk(&quot;%-20s %-6d %-6d %-20s\n&quot;,p-&gt;comm , p-&gt;pid, p-&gt;state, p-&gt;parent-&gt;comm);
        return 0;
}

以上定义宏SYSCALL_DEFINE0实际上会扩展为:

asmlinkage int sys_mycall()
{
               struct task_struct *p;
        printk(&quot;********************************************\n&quot;);
        printk(&quot;------------the output of mysyscall------------\n&quot;);
        printk(&quot;********************************************\n\n&quot;);
        printk(&quot;%-20s %-6s %-6s %-20s\n&quot;,&quot;Name&quot;,&quot;pid&quot;,&quot;state&quot;,&quot;ParentName&quot;);
        for(p = &amp;init_task; (p = next_task(p)) != &amp;init_task;)
                printk(&quot;%-20s %-6d %-6d %-20s\n&quot;,p-&gt;comm , p-&gt;pid, p-&gt;state, p-&gt;parent-&gt;comm);
        return 0;
}

内核在kernel/sys.c文件中定义了SYSCALL_DEFINE0~SYSCALL_DEFINE6等七个提供便利的宏。

2. 为系统调用服务例程在系统调用表中添加一个表项。

编译文件arch/x86/syscalls/syscall_64.tbl,在调用号小于512的部分追加一行:

2014-09-25 19:29:55的屏幕截图

注意:
① 上面的316是紧接着目前已定义系统调用315之后。
② 若为32系统添加系统调用号,在syscall_32.tbl中相应位置追加即可。系统调用在两个文件中的调用号没有必要一样。
③ 不需要像Linux 2.6的内核一样,在<asm/unistd.h>中添加类似于#define __NR_getjiffies 314之类的宏定义了,3.x的内核会自动根据系统调用表的定义生成。

3. 在内核头文件中,添加服务例程的声明。

在include/linux/syscalls.h文件的最后,#endif之前加入系统调用服务例程sys_mysyscall()的声明:

asmlinkage long sys_mycall(void);
#endif

剩下就是按照编译内核的方法进行编译好了  http://www.lizhaozhong.info/archives/496

4.系统调用表产生的过程

之所以Linux3.X与Linux2.X变化这么大。

主要是为了简化添加调用表的过程。

1. 系统调用表的产生过程。

内核开发者是在syscall_64.tbl中声明系统调用号与服务例程的对应关系,以及其ABI,但系统调用表的真正定义是在arch/x86/kernel/syscall_64.c中。

1)arch/x86/kernel/syscall_64.c, arch/x86/kernel/syscall_32.c文件中存放了实际的系统调用表定义,以64位系统为例,其中有如下内容:

#include &lt;asm/asm-offsets.h&gt;

#define __SYSCALL_COMMON(nr, sym, compat) __SYSCALL_64(nr, sym, compat)

#ifdef CONFIG_X86_X32_ABI
# define __SYSCALL_X32(nr, sym, compat) __SYSCALL_64(nr, sym, compat)
#else
# define __SYSCALL_X32(nr, sym, compat) /* nothing */
#endif

#define __SYSCALL_64(nr, sym, compat) extern asmlinkage void sym(void) ; // 注意,是分号结尾
#include &lt;asm/syscalls_64.h&gt; // 引入系统调用服务例程的声明
#undef __SYSCALL_64

#define __SYSCALL_64(nr, sym, compat) [nr] = sym, //注意,是逗号结尾

typedef void (*sys_call_ptr_t)(void);

extern void sys_ni_syscall(void);

const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { //系统调用表定义
        /*
         * Smells like a compiler bug -- it doesn't work
         * when the &amp; below is removed.
         */
        [0 ... __NR_syscall_max] = &amp;sys_ni_syscall,
&lt;strong&gt;#include &lt;asm/syscalls_64.h&gt; // 系统调用服务例程地址,对应arch/x86/include/generated/asm/syscalls_64.h文件&lt;/strong&gt;
};

2) arch/x86/syscalls目录中的syscall_64.tbl、syscall_32.tbl文件是系统调用表声明。syscalltbl.sh脚本负责生产syscalls_XX.h文件,由Makefile负责驱动
3) arch/x86/include/generated目录,其中存放根据arch/x86/syscalls目录生成的文件。主要有generated/asm/syscalls_64.h、generated/asm/syscalls_32.h文件,用于生成系统调用表数组。生成的syscalls_64.h内容部分如下:

截图 - 2014年09月25日 - 19时36分57秒

2. 系统调用号声明头文件的生成(#define0 __NR_xxx之类的信息)。

类似于系统调用表的产生,arch/x86/syscalls/syscallhdr.sh脚本负责generated/uapi/asm/unistd_32.h, generated/uapi/asm/unistd_64.h文件,unistd_XX.h文件又被间接include到asm/unistd.h中,后者最终被include用户空间使用的<sys/syscall.h>文件中(安装之后)。生成的generated/uapi/asm/unistd_64.h部分内容如下

... ...
#define __NR_sched_getattr 315
#define __NR_mycall 316

#endif /* _ASM_X86_UNISTD_64_H */

注意,这里的unistd.h文件与用户空间使用的文件没有任何关系,后者声明了系统调用包装函数,包括syscall函数等

下面我们来调用这个mycall()

 

截图 - 2014年09月25日 - 19时38分27秒

还有一种方式使用内嵌汇编的方式,具体文件在arch/x86/um/shared/sysdep/stub_64.h下面,模拟_syscall0宏调用。

#define __syscall_clobber "r11","rcx","memory"
#define __syscall "syscall"

static inline long stub_syscall0(long syscall)
{
    long ret;

    __asm__ volatile (__syscall
        : "=a" (ret)
        : "0" (syscall) : __syscall_clobber );

    return ret;
}

然后编译运行,打印在dmesg里面

kernel.dmesg

参考:

http://lwn.net/Articles/604287/

http://lwn.net/Articles/604515/

 

中断下半部的两种实现方式

September 22nd, 2014

这篇文章的前提是我们已经知道了中断上下半部的划分。

中断的下半部有两种实现方式:tasklet与工作队列,软中断。不管是那种机制,它们均为下半部提供了一种执行机制,比上半部灵活多了。至于何时执行,则由内核负责。 » Read more: 中断下半部的两种实现方式