驱动程序基石

前言

应用程序进入到驱动程序会经过系统调用,并且在系统调用中会做出相应的处理。根据调用的函数不同,做的处理也不同;但是都会生成驱动程序的struct file结构体,这个结构体对于应用程序来说应该就是fd(文件描述符),对于驱动程序来说就是struct file(或struct inode,注意struct file和struct inode的联系和区别)。

graph LR
APP/open --> sys_call/sys_open
sys_call/sys_open --> drv/drv_open

比如fs/select.c函数中的sys_poll函数。

/* 应用程序调用poll或select后,系统调用就会到这里来。在此函数中对超时时间做简单判断后,就调用do_sys_poll。
*/
SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds, int, timeout_msecs)

/* 此函数中又会调用其它函数 */
int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds, struct timespec *end_time)

/* 最终会调用驱动中的poll函数,并对返回值做相应处理 */

休眠与唤醒

正点原子和韦东山的代码很不相似,感觉完全不一样,正点原子更加繁琐。

当应用程序必须等待某个事件发生,比如必须等待按键被按下时,可以使用“休眠-唤醒”机制

  1. APP 调用 read 等函数试图读取数据,比如读取按键;
  2. APP 进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回;
  3. 如果 APP 在内核态,也就是在驱动程序中发现没有数据,则 APP 休眠;
  4. 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒 APP;
  5. APP 继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户空间并马上返回。

请记住:在中断处理函数中,不能休眠,也就不能调用会导致休眠的函数。

等待队列头和等待队列项

等待队列头

阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完 成唤醒工作。Linux 内核提供了**等待队列(wait queue)**来实现阻塞进程的唤醒工作,如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体 wait_queue_head_t 表示,wait_queue_head_t 结构体定义在文件 include/linux/wait.h 中,结构体内容如下所示:

休眠函数

参看:include/linux/wait.h

#define wait_event(wq, condition)					\
do {									\
	might_sleep();							\
	if (condition)							\
		break;							\
	__wait_event(wq, condition);					\
} while (0)
#define wait_event_timeout(wq, condition, timeout)			\
({									\
	long __ret = timeout;						\
	might_sleep();							\
	if (!___wait_cond_timeout(condition))				\
		__ret = __wait_event_timeout(wq, condition, timeout);	\
	__ret;								\
})
#define wait_event_interruptible(wq, condition)				\
({									\
	int __ret = 0;							\
	might_sleep();							\
	if (!(condition))						\
		__ret = __wait_event_interruptible(wq, condition);	\
	__ret;								\
})

唤醒函数

void __wake_up(
    wait_queue_head_t *q, 
    unsigned int mode, 
    int nr, 
    void *key);

void __wake_up_locked(
    wait_queue_head_t *q, 
    unsigned int mode, 
    int nr);

#define wake_up(x)		__wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_nr(x, nr)	__wake_up(x, TASK_NORMAL, nr, NULL)
#define wake_up_all(x)	__wake_up(x, TASK_NORMAL, 0, NULL)

#define wake_up_interruptible(x)	__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr)	__wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x)	__wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)

一般框架

要休眠的线程,放在 wq 队列里,中断处理函数从 wq 队列里把它取出来唤醒。

所以,我们要做这几件事:

  1. 初始化 wq 队列

  2. 在驱动的 read 函数中,调用 wait_event_interruptible

    它本身会判断 event 是否为 FALSE,如果为 FASLE 表示无数据,则休眠。 当从 wait_event_interruptible 返回后,把数据复制回用户空间。

  3. 中断服务程序里设置 event 为 TRUE,并调用 wake_up_interruptible 唤醒线程。

poll机制

对于休眠唤醒机制,必须要有人唤醒才能返回;但是poll可以设置超时时间,时间到后还是没有等到相应的时间也会返回。

驱动编写要点

  1. 在驱动中把当前线程放入某个等待队列wq(但不会导致休眠),并且根据实际情况(比如当前有无按键值可以读)返回驱动程序的状态(如POLLIN)。
  2. 在系统调用sys_poll中会对驱动中的poll的返回值做相应处理,比如如果poll的返回值不为0而是POLLIN,那么sys_poll就会记录下这个状态并且返回到应用程序中(时间超时后就算没有得到相关事件也会返回)。
  3. 进入中断表明某件事情发生,比如按键按下了,那么驱动中的poll就可以根据这个状态返回POLLIN,表明现在有按键可以读。
static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    /* 把当前线程fp放入等待队列中,wait是系统调用传进来的参数 */
	poll_wait(fp, &gpio_key_wait, wait);
    /* 判断是否有按键值,有就返回POLLIN */
	return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}


static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
	struct gpio_key *gpio_key = dev_id;
	int val;
	int key;
	
	val = gpiod_get_value(gpio_key->gpiod);
	printk("key %d %d\n", gpio_key->gpio, val);
	key = (gpio_key->gpio << 8) | val;
    /* 现在有按键可读了 */
	put_key(key);
    /* 并唤醒等待队列中的所有线程 */
	wake_up_interruptible(&gpio_key_wait);
	
	return IRQ_HANDLED;
}

应用程序编写要点

struct pollfd fds[1];
int timeout_ms = 5000;

fds[0].fd = fd;  /* 要监控的fd */
fds[0].events = POLLIN; /* 监控事件的类型-有数据可读 */

while (1)
{
    /* 3. 读文件 */
    ret = poll(fds, 1, timeout_ms);
    /* 返回事件revents的标志是否为POLLIN,这个标志是sys_poll根据drv_poll的返回值进行设定的 */
    if ((ret == 1) && (fds[0].revents & POLLIN))
    {
        read(fd, &val, 4);
        printf("get button : 0x%x\n", val);
    }
    else
    {
        printf("timeout\n");
    }
}

close(fd);

异步通知

信号的种类:include/uapi/asm-generic/signal.h

核心就是驱动程序在合适的时刻给应用程序发信号

  1. 谁发:驱动程序发
  2. 发什么:信号
  3. 发什么信号:SIGIO
  4. 怎么发:内核里提供有函数
  5. 发给谁:APP,APP 要把自己(pid)告诉驱动
  6. APP 收到后做什么:执行信号处理函数
  7. 信号处理函数和信号,之间怎么挂钩:APP 注册信号处理函数

APP 打开驱动程序时,内核(系统调用)会创建对应的 struct file 结构体,file 中有 f_flags;f_flags 中有一个 FASYNC 位,它被设置为 1 时表示使能异步通知功能。当 f_flags 中的 FASYNC 位发生变化时,驱动程序的 fasync 函数被调用

struct fasync_struct *button_fasync;

驱动编写要点

  • 编写fasync函数提供给file_operation结构体,在此函数中调用fasync_helper(fd,file,on,&fasync_struct),此函数的目的是将驱动与app相联系起来,以便以后驱动知道把信号发给谁(file中含有app的pid,然后此函数把pid赋给fasync_struct)。
  • 在中断中使用函数kill_fasync(fasync_struct,SIGIO,POLL_IN)向app发信号。
    • 第 1 个参数:fasync->fa_file 非空时,可以从中得到 PID,表示发给哪一个 APP;
    • 第 2 个参数表示发什么信号:SIGIO;
    • 第 3 个参数表示为什么发信号:POLL_IN,有数据可以读了(APP 用不到这个参数)。

应用编写要点

  • 要把自己的pid告诉驱动(通过系统调用告诉驱动)

    /* 告诉fd(驱动程序)我(应用程序)的pid */
    fcntl(fd, F_SETOWN, getpid());
    
  • 使能驱动的异步通知功能

    /* get flags,获得标志 */
    flags = fcntl(fd, F_GETFL);	
    /* set flags,设置标志 ,即使能异步通知功能 */
    fcntl(fd, F_SETFL, flags | FASYNC);	
    
  • 绑定信号函数

    signal(SIGIO, sig_func);
    static void sig_func(int sig)
    {
    	int val;
    	read(fd, &val, 4);
    	printf("get button : 0x%x\n", val);
    }
    

阻塞与非阻塞

所谓阻塞,就是等待某件事情发生。比如调用 read 读取按键时,如果没有按键数据则 read 函数不会返回,它会让线程休眠等待。

  • 使用 poll 时,如果传入的超时时间不为 0,这种访问方法也是阻塞的
  • 使用 poll 时,可以设置超时时间为 0,这样即使没有数据它也会立刻返回,这就是非阻塞方式

驱动编写要点

static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	//printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	int err;
	int key;

	if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
		return -EAGAIN;
	
	wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
	key = get_key();
	err = copy_to_user(buf, &key, 4);
	
	return 4;
}

从驱动代码也可以看出来,当 APP 打开某个驱动时,在内核中会有一个 struct file 结构体对应这个驱动,这个结构体中有 f_flags,就是打开文件时的标记位;可以设置 f_flasgs 的 O_NONBLOCK 位,表示非阻塞;也可以清除这个位表示阻塞。

驱动程序要根据这个标记位决定事件未就绪时是休眠和还是立刻返回。

如果没有按键可以读取并且是非阻塞,那么就立刻返回;否则就休眠。

应用程序编写要点

/* 2. 非阻塞模式打开文件 */
fd = open(argv[1], O_RDWR | O_NONBLOCK);
if (fd == -1)
{
    printf("can not open file %s\n", argv[1]);
    return -1;
}
/* 会立刻被执行 */
for (i = 0; i < 10; i++) 
{
    if (read(fd, &val, 4) == 4)
        printf("get button: 0x%x\n", val);
    else
        printf("get button: -1\n");
}

flags = fcntl(fd, F_GETFL);
/* 阻塞模式打开 */
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);

/* 有按键按下才会打印 */
while (1)
{
    if (read(fd, &val, 4) == 4)
        printf("get button: 0x%x\n", val);
    else
        printf("while get button: -1\n");
}
close(fd);

实验结果

非阻塞立刻执行

阻塞方式(按下一次按键才打印一次)

定时器

expire的意思是到期、期满。

内部机制

怎么实现定时器,逻辑上很简单:每发生一次硬件中断时,硬件中断处理完后就会看看有没有软件中断要处理。

定时器就是通过软件中断来实现的,它属于 TIMER_SOFTIRQ 软中断

对于 TIMER_SOFTIRQ 软中断,初始化代码如下:

void __init init_timers(void)
{
    init_timer_cpus();
    init_timer_stats();
    open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}

当发生硬件中断时,硬件中断处理完后,内核会调用软件中断的处理函数。对于 TIMER_SOFTIRQ,会调用 run_timer_softirq,它的函数如下:

run_timer_softirq
	__run_timers(base);
 		while (time_after_eq(jiffies, base->clk)) {
			......
			expire_timers(base, heads + levels);
 				fn = timer->function;
 				data = timer->data;
 				call_timer_fn(timer, fn, data);
 					fn(data);

简单地说,add_timer 函数会把 timer 放入内核里某个链表;

  • 在 TIMER_SOFTIRQ 的处理函数中,会从链表中把这些超时的 timer 取出来,执行其中的函数。

  • 怎么判断是否超时?jiffies 大于或等于 timer->expires 时,timer 就超时。

内核中有很多 timer,如果高效地找到超时的 timer?这是比较复杂的,可以看看这文章

使用步骤

  • setup_timer(定时器,对应函数,传给对应函数的数据)
  • 配置定时器成员,如gpio_keys_100ask[i].key_timer.expires = ~0; /* 先设置超时时间为最大值 */
  • 向内核添加定时器add_timer(&gpio_keys_100ask[i].key_timer)
  • 中断发生时,说明可能有按键被按下,此时在中断中设置定时器的到期时间用于消抖,即mod_timer(&gpio_key->key_timer, jiffies + HZ/50)
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
	struct gpio_key *gpio_key = dev_id;
	printk("gpio_key_isr key %d irq happened\n", gpio_key->gpio);
    /* 消抖时间设为20ms */
	mod_timer(&gpio_key->key_timer, jiffies + HZ/50);
	return IRQ_HANDLED;
}

/* 定时器对应的超时函数 */
static void key_timer_expire(unsigned long data)
{
	/* data ==> gpio */
	struct gpio_key *gpio_key = data;
	int val;
	int key;

	val = gpiod_get_value(gpio_key->gpiod);

	printk("key_timer_expire key %d %d\n", gpio_key->gpio, val);
	key = (gpio_key->gpio << 8) | val;
	put_key(key);
	wake_up_interruptible(&gpio_key_wait);
	kill_fasync(&button_fasync, SIGIO, POLL_IN);
}

中断下半部-tasklet

中断处理的两个原则:

  • 不能嵌套
  • 越快越好

在处理当前中断时,即使发生了其他中断,其他中断也不会得到处理,所以中断的处理要越快越好。但是某些中断要做的事情稍微耗时,这时可以把中断拆分为上半部、下半部。

  • 在上半部处理紧急的事情,在上半部的处理过程中,中断是被禁止的
  • 在下半部处理耗时的事情,在下半部的处理过程中,中断是使能的

理解中断上下部的要点

  • 正在中断函数中时,是不能相应其它中断的,即中断是被禁止的
  • 中断上半部(也就是isr)处理完后,会进入中断下半部(也可能不会进入),此时是可以响应其它中断的。
  • 在中断下半部发生中断后又会进入中断isr中,执行完毕后不会执行新的下半部,而是接着上一次的下半部

中断下半部使用结构体 tasklet_struct 来表示,它在内核include\linux\interrupt.h 中定义:

struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

其中的 state 有 2 位:

  • bit0 表示 TASKLET_STATE_SCHED,即调度状态位

    等于 1 时表示已经执行了函数tasklet_schedule 把该 tasklet 放入队列了;函数tasklet_schedule 会判断该位,如果已经等于 1 那么它就不会再次把 tasklet 放入队列。

  • bit1 表示 TASKLET_STATE_RUN,即运行状态位

    等于 1 时,表示正在运行 tasklet 中的 func 函数;函数执行完后内核会把该位清 0。

其中的 count 表示该 tasklet 是否使能:

  • 等于 0 表示使能了。
  • 非 0 表示被禁止了。对于 count 非 0 的tasklet,里面的 func 函数不会被执行。

相关函数

/* 相关宏定义 */
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
/* 与上面不同的是count成员被初始化为1, 即默认失能;使用前需要用函数tasklet_enable一下 */
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

extern void tasklet_init(
    struct tasklet_struct *t,
	void (*func)(unsigned long), 
    unsigned long data);

/* 其实就是把count减1 */
static inline void tasklet_enable(struct tasklet_struct *t);
/* 把count加1 */
static inline void tasklet_disable(struct tasklet_struct *t);
/* 把t放入某个链表,并且设置它的TASKLET_STATE_SCHED状态为1 */
static inline void tasklet_schedule(struct tasklet_struct *t);

extern void tasklet_kill(struct tasklet_struct *t);

tasklet内部机制

tasklet 属于TASKLET_SOFTIRQ 软件中断,入口函数为 tasklet_action,这在内核 kernel\softirq.c中设置:

void __init softirq_init(void)
{
	int cpu;

	for_each_possible_cpu(cpu) {
		per_cpu(tasklet_vec, cpu).tail =
			&per_cpu(tasklet_vec, cpu).head;
		per_cpu(tasklet_hi_vec, cpu).tail =
			&per_cpu(tasklet_hi_vec, cpu).head;
	}

	open_softirq(TASKLET_SOFTIRQ, tasklet_action);
	open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

当发生硬件中断时,内核处理完硬件中断后,会处理软件中断。对于TASKLET_SOFTIRQ 软件中断,会调用tasklet_action 函数。执行过程还是挺简单的:从队列中找到 tasklet,进行状态判断后执行 func 函数,从队列中删除 tasklet。 从这里可以看出

  • tasklet_schedule 调度 tasklet 时,其中的函数并不会立刻执行,而只是把 tasklet 放入队列;
  • 调用一次 tasklet_schedule,只会导致 tasklet 的函数被执行一次
  • 如果 tasklet 的函数尚未执行,多次调用 tasklet_schedule 也是无效的,只会放入队列一次。

驱动编写要点

  • 在probe中使用tasklet_init(t, func, data)初始化tasklet;
  • 在中断服务中调用tasklet_schedule(tasklet)使其加入到队列中去
  • 在func中做相应的事
tasklet_init(
    &gpio_keys_100ask[i].tasklet, 
    key_tasklet_func, 
    &gpio_keys_100ask[i]);

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
	struct gpio_key *gpio_key = dev_id;
	//printk("gpio_key_isr key %d irq happened\n", gpio_key->gpio);
	tasklet_schedule(&gpio_key->tasklet);
	mod_timer(&gpio_key->key_timer, jiffies + HZ/50);
	return IRQ_HANDLED;
}

static void key_tasklet_func(unsigned long data)
{
	/* data ==> gpio */
	struct gpio_key *gpio_key = data;
	int val;
	int key;
	val = gpiod_get_value(gpio_key->gpiod);
	printk("key_tasklet_func key %d %d\n", gpio_key->gpio, val);
}

应用编写要点

tasklet只是实现中断下半部的一种方法,和阻塞、非阻塞这些没有矛盾关系,是可以在一起用的,具体场景具体分析。

工作队列

前面讲的定时器、下半部 tasklet,它们都是在中断上下文中执行,它们无法休眠。当要处理更复杂的事情时,往往更耗时。这些更耗时的工作放在定时器或是下半部中,会使得系统很卡;并且循环等待某件事情完成也太浪费 CPU 资源了。

如果使用线程来处理这些耗时的工作,那就可以解决系统卡顿的问题:因为线程可以休眠

在内核中,我们并不需要自己去创建线程,可以使用**“工作队列”(workqueue)**。内核初始化工作队列时,就为它创建了内核线程。以后我们要使用“工作队列”,只需要把“工作”放入“工作队列中”,对应的内核线程就会取出“工作”,执行里面的函数。

/* 某个工作 */
struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

/* 工作队列(我没找到在哪定义的) */
struct workqueue_struct;

注意

  • 工作是work_struct,而工作队列是workqueue_struct

  • 工作队列是一个内核线程,而我们要做的就是定义一个工作任务work_struct,然后使用schedule_work把它加入到工作队列中去。

  • 工作队列中有多个工作时,工作队列线程是一个一个取出来执行

  • 我们也可以提供自己的工作队列,使用schedule_work是把工作放入Linux系统提供的工作队列中,使用queue_work可以把工作放到指定的工作队列中去。

  • container_of(结构体的成员a1的位置,结构体类型,a1是结构体的那个成员)可以获取结构体S的首地址。

    struct stu {
    	int a;
    	char b;
    	int arr[5];
    }
    struct stu s1;
    /* 我们知道s1的b成员位置是add,那么怎么获得s1的位置呢?*/
    s1=contianer_of(add,struct stu,b);
    

相关函数

/* 定义自己的工作队列,Linux提供默认的system_wq工作队列 */
#define create_workqueue(name)						\
	alloc_workqueue("%s", WQ_MEM_RECLAIM, 1, (name))

#define INIT_WORK(_work, _func)						\
	__INIT_WORK((_work), (_func), 0)

#define __INIT_WORK(_work, _func, _onstack)				\
	do {								\
		__init_work((_work), _onstack);				\
		(_work)->data = (atomic_long_t) WORK_DATA_INIT();	\
		INIT_LIST_HEAD(&(_work)->entry);			\
		(_work)->func = (_func);				\
	} while (0)
#endif

内部工作机制

比较复杂,暂且不分析

使用步骤

  • 构造一个work_struct结构体,里面有对应的函数

    INIT_WORK(
        &gpio_keys_100ask[i].work,
        key_work_func);
    
    /* 通过container_of可以得到结构体的首地址 */
    static void key_work_func(struct work_struct *work)
    {
        /* 获得gpio_key的首地址 */
    	struct gpio_key *gpio_key = container_of(
            work, struct gpio_key, work);
    	int val;
    
    	val = gpiod_get_value(gpio_key->gpiod);
    
    	printk("key_work_func: the process is %s pid %d\n",current->comm, current->pid);	
    	printk("key_work_func key %d %d\n", gpio_key->gpio, val);
    }
    
  • 使用schedule_work(work)将工作放入工作队列,内核线程会运行work中的函数。(在中断中调用此函数进入中断下半部)

    static irqreturn_t gpio_key_isr(int irq, void *dev_id)
    {
    	struct gpio_key *gpio_key = dev_id;
    	//printk("gpio_key_isr key %d irq happened\n", gpio_key->gpio);
    	schedule_work(&gpio_key->work);
    	return IRQ_HANDLED;
    }
    

中断的线程化处理

工作队列有一个缺点:工作队列中有多个 work,前一个 work 没处理完会影响后面的 work。解决方法有很多种,比如干脆自己创建一个内核线程,不跟别的 work 凑在一块了。

对于中断处理,还有另一种方法:threaded irq,线程化的中断处理。中断的处理仍然可以认为分为上半部、下半部。上半部用来处理紧急的事情,下半部用一个内核线程来处理,这个内核线程专用于这个中断。

内部机制

暂不分析

相关函数

/* 参考:kernel/irq/manage.c */
int request_threaded_irq(
		unsigned int irq, 		/* 那个中断 */
		irq_handler_t handler,	/* 中断上半部,可以为空 */
		irq_handler_t thread_fn, 	/* 中断下半部 */
		unsigned long irqflags,	/* 中断类型/方式 */
		const char *devname, 	/* 应该是中断名字 */
		void *dev_id)			/* 传给上/下半部的参数 */

    
void free_irq(unsigned int irq, void *dev_id)

注意

从前面可知,我们可以提供上半部函数,也可以不提供:

  • 不提供

    内核会提供默认的上半部处理函数:irq_default_primary_handler,它是直接返回 IRQ_WAKE_THREAD。

  • 提供

    返回值必须是:IRQ_WAKE_THREAD。

使用步骤

request_threaded_irq(
	gpio_keys_100ask[i].irq, 
	gpio_key_isr, 
	gpio_key_thread_func, I
	RQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
	"100ask_gpio_key", 
	&gpio_keys_100ask[i]);

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
	struct gpio_key *gpio_key = dev_id;
	//printk("gpio_key_isr key %d irq happened\n", gpio_key->gpio);
	tasklet_schedule(&gpio_key->tasklet);
	mod_timer(&gpio_key->key_timer, jiffies + HZ/50);
	schedule_work(&gpio_key->work);
	return IRQ_WAKE_THREAD;	/* 只有返回这个才能唤醒中断线程 */
}

static irqreturn_t gpio_key_thread_func(int irq, void *data)
{
	struct gpio_key *gpio_key = data;
	int val;

	val = gpiod_get_value(gpio_key->gpiod);

	printk("gpio_key_thread_func: the process is %s pid %d\n",current->comm, current->pid);	
	printk("gpio_key_thread_func key %d %d\n", gpio_key->gpio, val);
	
	return IRQ_HANDLED;
}

/* 在remove中freeirq */
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);

mmap

应用程序不能直接读写驱动程序中的 buffer,需要在用户态 buffer 和内核态 buffer 之间进行一次数据拷贝。这种方式在数据量比较小时没什么问题;但是数据量比较大时效率就太低了。比如更新 LCD 显示时,如果每次都让 APP 传递一帧数据给内核,假设 LCD 采用 102460032bpp 的格式,一帧数据就有102460032/8=2.3MB 左右,这无法忍受。改进的方法就是让程序可以直接读写驱动程序中的 buffer,这可以通过 mmap 实现(memory map),把内核的 buffer 映射到用户态,让 APP 在用户态直接读写。

大概过程

应用程序调用mmap,然后会进入到系统调用,在系统调用中会构造一个虚拟地址区域struct vm_area_struct vma变量,并传入到驱动中编写的对应的mmap函数。

在驱动程序中的mmap中应该做的事情:

  • 获得内核中虚拟地址[buf_kernel]的物理地址phy,通过virt_to_phys
  • 设置vma的一些属性,比如是否使用cache、buffer
  • 然后映射
int main(int argc, char **argv)
{
	int fd;
	char *buf;
	int len;
	char str[1024];
	
	
	/* 1. 打开文件 */
	fd = open("/dev/hello", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/hello\n");
		return -1;
	}

	/* 2. mmap 
	 * MAP_SHARED  : 多个APP都调用mmap映射同一块内存时, 对内存的修改大家都可以看到。就是说多个APP、驱动程序实际上访问的都是同一块内存
	 * MAP_PRIVATE : 创建一个copy on write的私有映射。当APP对该内存进行修改时,其他程序是看不到这些修改的。就是当APP写内存时, 内核会先创建一个拷贝给这个APP, 这个拷贝是这个APP私有的, 其他APP、驱动无法访问。
	 */
	buf =  mmap(NULL, 1024*8, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (buf == MAP_FAILED)
	{
		printf("can not mmap file /dev/hello\n");
		return -1;
	}
	printf("mmap address = 0x%x\n", buf);
	printf("buf origin data = %s\n", buf); /* old */

	/* 3. write */
	strcpy(buf, "new");

	/* 4. read & compare */
	/* 对于MAP_SHARED映射:  str = "new" 
	 * 对于MAP_PRIVATE映射: str = "old" 
	 */
	read(fd, str, 1024);  
	if (strcmp(buf, str) == 0)
	{
		/* 对于MAP_SHARED映射,APP写的数据驱动可见
		 * APP和驱动访问的是同一个内存块
		 */
		printf("compare ok!\n");
	}
	else
	{
		/* 对于MAP_PRIVATE映射,APP写数据时, 是写入原来内存块的"拷贝"
		 */
		printf("compare err!\n");
		printf("str = %s!\n", str);  /* old */
		printf("buf = %s!\n", buf);  /* new */
	}
	while (1)
	{
		sleep(10);  /* cat /proc/pid/maps */
	}
	
	munmap(buf, 1024*8);
	close(fd);
	
	return 0;
}

说明

应用程序调用mmap后,会通过驱动中的drv_mmap把buf_user映射到物理内存phy

赞赏