一、休眠与唤醒


要休眠的线程,放在 wq 队列里,中断处理函数从 wq 队列里把它取出来唤醒。所以,我们要做这几件事:
① 初始化 wq 队列
② 在驱动的 read 函数中,调用 wait_event_interruptible:
它本身会判断 event 是否为 FALSE,如果为 FASLE 表示无数据,则休眠。
当从 wait_event_interruptible 返回后,把数据复制回用户空间。
③ 在中断服务程序里:
设置 event 为 TRUE,并调用 wake_up_interruptible 唤醒线程。

1、驱动关键代码
初始化等待队列

 static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);...//在驱动的读函数里调用 wait_event_interruptible
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{int err;wait_event_interruptible(gpio_key_wait, g_key);  //不一定会进入休眠,它会先判断 g_key 是否为 TRUEerr = copy_to_user(buf, &g_key, 4);g_key = 0;return 4;
}...static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{struct gpio_key *gpio_key = dev_id;int val;val = gpiod_get_value(gpio_key->gpiod);printk("key %d %d\n", gpio_key->gpio, val);g_key = (gpio_key->gpio << 8) | val;      //确定按键值 g_key,g_key 也就变为 TRUE 了wake_up_interruptible(&gpio_key_wait);    //唤醒 gpio_key_wait 中的第 1 个线程return IRQ_HANDLED;
}

注意这 2 个函数,一个没有使用“&”,另一个使用了“&”:

wait_event_interruptible(gpio_key_wait, g_key);
wake_up_interruptible(&gpio_key_wait);

2、应用程序关键代码

/* 2. 打开文件 */
fd = open(argv[1], O_RDWR);if (fd == -1)
{printf("can not open file %s\n", argv[1]);return -1;
}while (1)   //循环读,app基本处于休眠
{/* 3. 读文件 */read(fd, &val, 4);printf("get button : 0x%x\n", val);
}

3、使用环形缓冲区改进驱动程序


使用环形缓冲区之后,休眠函数可以这样写:

wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);

唤醒函数可以这样写:

key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);

二、poll机制


函数执行流程如上图①~⑧所示,重点从③开始看。假设一开始无按键数据:
③ APP 调用 poll 之后,进入内核态;
④ 导致驱动程序的 drv_poll 被调用:
注意,drv_poll 要把自己这个线程挂入等待队列 wq 中;假设不放入队列里,那以后发生中断时,中断服务程序去哪里找到你嘛?
⑤ 假设当前没有数据,则休眠一会;
⑥ 在休眠过程中,按下了按键,发生了中断:
在中断服务程序里记录了按键值,并且从 wq 中把线程唤醒了。
⑦ 线程从休眠中被唤醒,继续执行 for 循环,再次调用 drv_poll:drv_poll 返回数据状态
⑧ 哦,你有数据,那从内核态返回到应用态吧
⑨ APP 调用 read 函数读数据
如果一直没有数据,调用流程也是类似的,重点从③开始看,如下:
③ APP 调用 poll 之后,进入内核态;
④ 导致驱动程序的 drv_poll 被调用:
⑤ 假设当前没有数据,则休眠一会;
⑥ 在休眠过程中,一直没有按下了按键,超时时间到:内核把这个线程唤醒;
⑦ 线程从休眠中被唤醒,继续执行 for 循环,再次调用 drv_poll:drv_poll 返回数据状态
⑧ 哦,你还是没有数据,但是超时时间到了,那从内核态返回到应用态吧
⑨ APP 不能调用 read 函数读数据
注意几点
① drv_poll 要把线程挂入队列 wq,但是并不是在 drv_poll 中进入休眠,而是在调用 drv_poll 之后休眠
② drv_poll 要返回数据状态
③ APP 调用一次 poll,有可能会导致 drv_poll 被调用 2 次
④ 线程被唤醒的原因有 2:中断发生了去队列 wq 中把它唤醒,超时时间到了内核把它唤醒
⑤ APP 要判断 poll 返回的原因:有数据,还是超时。有数据时再去调用 read 函数。

2.1、驱动编程
使用 poll 机制时,驱动程序的核心就是提供对应的 drv_poll 函数。
在 drv_poll 函数中要做 2 件事:
① 把当前线程挂入队列 wq:poll_wait
APP 调用一次 poll,可能导致 drv_poll 被调用 2 次,但是我们并不需要把当前线程挂入队列 2 次。
可以使用内核的函数 poll_wait 把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。
② 返回设备状态:
APP 调用 poll 函数时,有可能是查询“有没有数据可以读”:POLLIN,也有可能是查询“你有没有空间给我写数据”:POLLOUT。
所以 drv_poll 要返回自己的当前状态:(POLLIN | POLLRDNORM)(POLLOUT | POLLWRNORM)
POLLRDNORM等同于 POLLIN,为了兼容某些 APP 把它们一起返回。
POLLWRNORM等同于 POLLOUT ,为了兼容某些 APP 把它们一起返回。

APP 调用 poll 后,很有可能会休眠。对应的,在按键驱动的中断服务程序中,也要有唤醒操作。
驱动程序中 poll 的代码如下:

static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);poll_wait(fp, &gpio_key_wait, wait);return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}


在调用 poll 函数时,要指明:
① 你要监测哪一个文件:哪一个 fd
② 你想监测这个文件的哪种事件:是 POLLIN、还是 POLLOUT
最后,在 poll 函数返回时,要判断状态。

应用程序代码如下:

struct pollfd fds[1];
int timeout_ms = 5000;
int ret;fds[0].fd = fd;
fds[0].events = POLLIN;        //想等待什么事件ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN))  //得到了什么事件
{read(fd, &val, 4);printf("get button : 0x%x\n", val);
}

三、异步通知

使用休眠-唤醒、POLL 机制时,都需要休眠等待某个事件发生时,它们的差别在于后者可以指定休眠的时长。

Linux下的信号

给某个信号注册处理函数,用法如下:

APP 还要做什么事?想想这几个问题:
① 内核里有那么多驱动,你想让哪一个驱动给你发 SIGIO 信号?
APP 要打开驱动程序的设备节点。
② 驱动程序怎么知道要发信号给你而不是别人?
APP 要把自己的进程 ID 告诉驱动程序。
③ APP 有时候想收到信号,有时候又不想收到信号:
应该可以把 APP 的意愿告诉驱动。

驱动程序要做什么?发信号。
① APP 设置进程 ID 时,驱动程序要记录下进程 ID;
② APP 还要使能驱动程序的异步通知功能,驱动中有对应的函数:
APP 打开驱动程序时,内核会创建对应的 file 结构体,file 中有 f_flags;
f_flags 中有一个 FASYNC 位,它被设置为 1 时表示使能异步通知功能。
当 f_flags 中的 FASYNC 位发生变化时,驱动程序的 fasync 函数被调用。
③ 发生中断时,有数据时,驱动程序调用内核辅助函数发信号。
这个辅助函数名为 kill_fasync。


重点从②开始:
② APP 给 SIGIO 这个信号注册信号处理函数 func,以后 APP 收到 SIGIO 信号时,这个函数会被自动调用;
③ 把 APP 的 PID(进程 ID)告诉驱动程序,这个调用不涉及驱动程序,在内核的文件系统层次记录 PID;
④ 读取驱动程序文件 Flag;
⑤ 设置 Flag 里面的 FASYNC 位为 1:当 FASYNC 位发生变化时,会导致驱动程序的 fasync 被调用;
⑥⑦ 调用 faync_helper,它会根据 FAYSNC 的值决定是否设置 button_async->fa_file=驱动文件 filp:驱动文件 filp 结构体里面含有之前设置的 PID。
⑧ APP 可以做其他事;
⑨⑩ 按下按键,发生中断,驱动程序的中断服务程序被调用,里面调用 kill_fasync 发信号;
⑪⑫⑬ APP 收到信号后,它的信号处理函数被自动调用,可以在里面调用 read 函数读取按键。

驱动编程
① 提供对应的 drv_fasync 函数;
② 并在合适的时机发信号。

drv_fasync 函数很简单,调用 fasync_helper 函数就可以,如下:

static struct fasync_struct *button_async;
static int drv_fasync (int fd, struct file *filp, int on)
{return fasync_helper (fd, filp, on, &button_async);
}

fasync_helper 函数会分配、构造一个 fasync_struct 结构体 button_async:
① 驱动文件的 flag 被设置为 FAYNC 时:

button_async->fa_file = filp; // filp 表示驱动程序文件,里面含有之前设置的 PID

② 驱动文件被设置为非 FASYNC 时:

button_async->fa_file = NULL;

以后想发送信号时,使用 button_async 作为参数就可以,它里面“可能”含有 PID。

怎么发信号呢?代码如下:

kill_fasync (&button_async, SIGIO, POLL_IN);

第 1 个参数:button_async->fa_file 非空时,可以从中得到 PID,表示发给哪一个 APP;
第 2 个参数表示发什么信号:SIGIO;
第 3 个参数表示为什么发信号:POLL_IN,有数据可以读了。(APP 用不到这个参数)

应用编程

① 编写信号处理函数:

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

② 注册信号处理函数:

signal(SIGIO, sig_func);

③ 打开驱动:

fd = open(argv[1], O_RDWR);

④ 把进程 ID 告诉驱动:

fcntl(fd, F_SETOWN, getpid());

⑤ 使能驱动的 FASYNC 功能:

flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);

四、阻塞与非阻塞

所谓阻塞,就是等待某件事情发生。比如调用 read 读取按键时,如果没有按键数据则 read 函数不会返回,它会让线程休眠等待。
使用 poll 时,如果传入的超时时间不为 0,这种访问方法也是阻塞的。

注意:对于普通文件、块设备文件,O_NONBLOCK 不起作用。
注意:对于字符设备文件,O_NONBLOCK 起作用的前提是驱动程序针对 O_NONBLOCK 做了处理。

只能在 open 时表明 O_NONBLOCK 吗?在 open 之后,也可以通过 fcntl 修改为阻塞或非阻塞。

应用编程
open 时设置:

int fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK); /* 非阻塞方式 */
int fd = open(“/dev/xxx”, O_RDWR ); /* 阻塞方式 */

open 之后设置:

int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* 非阻塞方式 */
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); /* 阻塞方式 */

驱动编程

static ssize_t drv_read(struct file *fp, char __user *buf, size_t count, loff_t *ppos)
{if (queue_empty(&as->queue) && fp->f_flags & O_NONBLOCK)return -EAGAIN;wait_event_interruptible(apm_waitqueue, !queue_empty(&as->queue));……
}

驱动开发原则

驱动程序程序“只提供功能,不提供策略”。就是说驱动程序可以提供休眠唤醒、查询等等各种方式,驱动程序只提供这些能力,怎么用由 APP 决定。

五、定时器

5.1、内核函数

在内核中使用定时器很简单,涉及这些函数(参考内核源码 include\linux\timer.h):
① setup_timer(timer, fn, data):
设置定时器,主要是初始化 timer_list 结构体,设置其中的函数、参数。
② void add_timer(struct timer_list *timer):
向内核添加定时器。timer->expires 表示超时时间。
当超时时间到达,内核就会调用这个函数:timer->function(timer->data)。
③ int mod_timer(struct timer_list *timer, unsigned long expires):
修改定时器的超时时间,
它等同于:del_timer(timer); timer->expires = expires; add_timer(timer);
但是更加高效。
④ int del_timer(struct timer_list *timer):
删除定时器。

5.2、定时器时间单位

定时器的时间就是基于 jiffies 的,我们修改超时时间时,一般使用这 2 种方法:
① 在 add_timer 之前,直接修改:

timer.expires = jiffies + xxx; // xxx 表示多少个滴答后超时,也就是 xxx*10ms
timer.expires = jiffies + 2*HZ; // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒

② 在 add_timer 之后,使用 mod_timer 修改:

mod_timer(&timer, jiffies + xxx); // xxx 表示多少个滴答后超时,也就是 xxx*10ms
mod_timer(&timer, jiffies + 2*HZ); // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒

5.3、使用定时器处理按键抖动
按下或松开一个按键,它的 GPIO 电平会反复变化,最后才稳定。一般是几十毫秒才会稳定。如果不处理抖动的话,用户只操作一次按键,中断程序可能会上报多个数据。怎么处理?
① 在按键中断程序中,可以循环判断几十亳秒,发现电平稳定之后再上报
② 使用定时器
显然第 1 种方法太耗时,违背“中断要尽快处理”的原则,你的系统会很卡。
怎么使用定时器?看下图:
核心在于:在 GPIO 中断中并不立刻记录按键值,而是修改定时器超时时间,10ms 后再处理。
如果 10ms 内又发生了 GPIO 中断,那就认为是抖动,这时再次修改超时时间为 10ms。
只有 10ms 之内再无 GPIO 中断发生,那么定时器的函数才会被调用。
在定时器函数中记录按键值。

六、中断下半部tasklet

在前面我们介绍过中断上半部、下半部。中断的处理有几个原则:
① 不能嵌套;
② 越快越好。
在处理当前中断时,即使发生了其他中断,其他中断也不会得到处理,所以中断的处理要越快越好。但是某些中断要做的事情稍微耗时,这时可以把中断拆分为上半部、下半部。
在上半部处理紧急的事情,在上半部的处理过程中,中断是被禁止的;
在下半部处理耗时的事情,在下半部的处理过程中,中断是使能的。

6.1、内核函数
中断下半部使用结构体 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 函数不会被执行。

使用中断下半部之前,要先实现一个 tasklet_struct 结构体,这可以用这 2 个宏来定义结构体:

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

使用 DECLARE_TASKLET 定义的 tasklet 结构体,它是使能的;
使 用 DECLARE_TASKLET_DISABLED 定义 的 tasklet 结构体,它是禁止的;使用之前要先调用tasklet_enable 使能它。

也可以使用函数来初始化 tasklet 结构体:

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

使能/禁止 tasklet

static inline void tasklet_enable(struct tasklet_struct *t);
static inline void tasklet_disable(struct tasklet_struct *t);

调度 tasklet

static inline void tasklet_schedule(struct tasklet_struct *t);

kill tasklet

extern void tasklet_kill(struct tasklet_struct *t);

6.2、tasklet 使用方法

先定义 tasklet,需要使用时调用 tasklet_schedule,驱动卸载前调用 tasklet_kill。
tasklet_schedule 只是把 tasklet 放入内核队列,它的 func 函数会在软件中断的执行过程中被调用。

七、工作队列

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

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

工作队列的应用场合:要做的事情比较耗时,甚至可能需要休眠,那么可以使用工作队列。
缺点:多个工作(函数)是在某个内核线程中依序执行的,前面函数执行很慢,就会影响到后面的函数。
在多 CPU 的系统下,一个工作队列可以有多个内核线程,可以在一定程度上缓解这个问题。

7.1、内核函数

内核线程、工作队列(workqueue)都由内核创建了,我们只是使用。使用的核心是一个 work_struct 结构体,定义如下:

使用工作队列时,步骤如下:
① 构造一个 work_struct 结构体,里面有函数;
② 把这个 work_struct 结构体放入工作队列,内核线程就会运行 work 中的函数。

定义 work

参考内核头文件:include\linux\workqueue.h

#define DECLARE_WORK(n, f) \
struct work_struct n = __WORK_INITIALIZER(n, f)
#define DECLARE_DELAYED_WORK(n, f) \
struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)

如果要在代码中初始化 work_struct 结构体,可以使用下面的宏:

#define INIT_WORK(_work, _func)

使用 work:schedule_work

调用 schedule_work 时,就会把 work_struct 结构体放入队列中,并唤醒对应的内核线程。内核线程就会从队列里把 work_struct 结构体取出来,执行里面的函数。

其他函数

Linux驱动编程(驱动程序基石)(上)相关推荐

  1. Linux驱动编程 step-by-step

    第三次看了LDD3了(虽然现在已经是kernel3.0但从这本书商还是能学到很多) 每次都有一些收获 现在终于能够写一写代码了 驱动程序的作用: 简单来说 驱动程序就是使计算机与设备通信的特殊的代码, ...

  2. linux驱动编程——ch340x驱动移植

    Linux驱动编程--ch340x驱动移植 主要概念: ​ ch340x驱动移植 ​ 作为通用器件,厂商都有提供适配各种平台的驱动.linux一般会提供驱动源码. ​ 一般所谓驱动移植,就是将厂商的驱 ...

  3. Linux驱动编程 step-by-step (二) 简单字符设备驱动

    简单字符设备驱动 1.主次设备号 主设备号标识设备连接的的驱动,此设备好由内核使用,标识在相应驱动下得对应的设备 在linux中设备号是一个32位的dev_t类型 typedef __u32    _ ...

  4. Linux驱动编程 step-by-step (二)

    简单字符设备驱动 1.主次设备号 主设备号标识设备连接的的驱动,此设备好由内核使用,标识在相应驱动下得对应的设备 在linux中设备号是一个32位的dev_t类型 typedef __u32    _ ...

  5. Linux驱动编程视频教程

    本视频教程主要介绍字符驱动.杂项设备.中断.调试驱动的基本方法以及驱动的移植等. Linux驱动编程视频教程: 链接:https://pan.baidu.com/s/1Yn5d4w9uudb4tGDT ...

  6. Linux驱动编程(驱动程序基石)(下)

    一.中断的线程化处理 复杂.耗时的事情,尽量使用内核线程来处理.上节视频介绍的工作队列用起来挺简单,但是它有一个缺点:工作队列中有多个 work,前一个 work 没处理完会影响后面的 work.解决 ...

  7. Linux驱动编程 step-by-step (五)主要的文件操作方法实现

    主要的文件操作方法实现 文件操作函数有很多的操作接口,驱动编程需要实现这些接口,在用户编程时候系统调用时候会调用到这些操作 [cpp] view plaincopy struct file_opera ...

  8. linux驱动read函数 copytouser,Linux驱动编程 step-by-step (五)主要的文件操作方法实现...

    主要的文件操作方法实现 文件操作函数有很多的操作接口,驱动编程需要实现这些接口,在用户编程时候系统调用时候会调用到这些操作 structfile_operations { ... loff_t (*l ...

  9. 【Linux系统编程】信号 (上)

    00. 目录 文章目录 00. 目录 01. 信号概述 02. 信号编号 03. 信号产生方式 04. kill发送信号 05. pause等待信号 06. 信号处理方式 07. 信号处理函数 08. ...

最新文章

  1. 【FFmpeg】警告:[mpegts] H.264 bitstream error, startcode missing, size 0
  2. html5游戏 糖果派对,糖果派对, 第三层的游戏攻略, 怎玩才能合理的获得比较高的得分?...
  3. Android: AndroidStudio使用OpenCV-Java
  4. rodbc 连接oracle,R語言 使用RODBC連接oracle數據庫
  5. java代码中的缓存类怎么找,JAVA缓存的实现 - dreamcloudz的个人空间 - OSCHINA - 中文开源技术交流社区...
  6. linux系统下,traceroute路由跟踪指令详解
  7. SpringDataJpa 概述
  8. Win7以上系统通过Dbgview打印驱动日志
  9. C#调用默认浏览器打开网页的几种方法
  10. 垃圾分类数据集-8w张图片245个类附赠tensorflow代码
  11. c++写一个函数验证哥德巴赫猜想
  12. 反向延长线段什么意思_《反向延长线》
  13. 如何发送工资条通知短信
  14. python用于财务数据分析_财务数据分析进阶之路
  15. android设备获取 关于手机-》手机名称
  16. windows的gitbash使用jq
  17. h5端登录是什么意思_关于app、小程序和h5之间的区别
  18. python中callable什么意思_python中callable对象有什么用
  19. 速学堂 Java300 第五章 作业
  20. 异常检测 and GAN网络(1)

热门文章

  1. Java练习:求圆、正方形面积,显示学生考试的总分和平均分,之后显示大于考试平均分的成绩信息。
  2. Java Swing弹出对话框之消息提示对话框MessageDialog
  3. 贪心入门案例(三)---------乘船问题
  4. 计时器、延时器的学习
  5. uicolor swift_Swift中的UIColor
  6. navicat添加外键_navicat怎么建立外键
  7. android系统开发中log的使用方法
  8. 从零开始导入(imp),导出(dmp)Oracle数据
  9. 第四章 确定女人的感觉
  10. ofstream换行