前言
本文介绍进程的休眠与唤醒。
嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!
行文目录
- 前言
- 1. 阻塞和非阻塞
- 2. 进程的几种状态
- 3. 等待队列
- 3.1 等待队列头
- 3.2 等待队列项
- 参考资料
1. 阻塞和非阻塞
当应用程序对设备驱动进行操作时,如果不能获取到设备资源,那么阻塞IO就会将应用程序挂起,直到设备资源可以获取为止。其模式如下所示:
应用程序调用read
函数从设备中读取数据,当设备不可用或数据未准备好的时候进入到休眠态,等设备可用就会从休眠态唤醒,然后从设备中读取数据返回到应用程序。
阻塞访问最大的好处就是当设备文件不可操作时进程进入休眠态,可以把CPU资源让出来。
应用程序用阻塞IO访问的代码如下。默认读取方式就是阻塞式访问。
int fd;
int data = 0;
fd = open("dev/xxx_dev", O_RDWR); // 阻塞方式打开
ret = read(fd, &data, sizeof(data)); // 读取数据
非阻塞式IO中,应用程序对应的线程不会挂起,要么一直轮询等待,直到设备资源可用,要么直接放弃,其模式如下:
应用程序调用read
函数从设备中获取数据,当设备不可用或数据未准备好时会立即向内核返回一个错误码,表示数据读取失败,应用程序会再次重新读取数据,这样循环往复,直到数据读取成功。
应用程序用阻塞IO访问的代码如下,主要在open
函数的第二个参数上有变化:
int fd;
int data = 0;
fd = open("dev/xxx_dev", O_RDWR | O_NONBLOCK); // 阻塞方式打开
ret = read(fd, &data, sizeof(data)); // 读取数据
2. 进程的几种状态
TASK_RUNNING: 正在运行或处于就绪状态:就绪状态是指进程申请到了CPU以外的其他所有资源,提醒:一般的操作系统教科书将正在CPU上执 行的进程定义为RUNNING状态、而将可执行但是尚未被调度执行的进程定义为READY状态,这两种状态在Linux下统一为 TASK_RUNNING状态.
TASK_INTERRUPTIBLE: 处于等待队伍中,等待资源有效时唤醒(比如等待键盘输入、socket连接、信号等等),但可以被中断唤醒.一般情况下,进程列表中的绝大多数进程都处于 TASK_INTERRUPTIBLE状态.毕竟皇帝只有一个(单个CPU时),后宫佳丽几千;如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来.
TASK_UNINTERRUPTIBLE:处于等待队伍中,等待资源有效时唤醒(比如等待键盘输入、socket连接、信号等等),但不可以被中断唤醒.
TASK_ZOMBIE:僵死状态,进程资源用户空间被释放,但内核中的进程PCB并没有释放,等待父进程回收.
TASK_STOPPED:进程被外部程序暂停(如收到SIGSTOP信号,进程会进入到TASK_STOPPED状态),当再次允许时继续执行(进程收到SIGCONT信号,进入TASK_RUNNING状态),因此处于这一状态的进程可以被唤醒.
3. 等待队列
阻塞访问中,如果设备不可操作的时候,进程进入休眠态,但当设备文件可以操作时就必须唤醒进程,一般在终端中完成唤醒工作。Linux内核提供等待队列来实现阻塞进程的唤醒工作。
3.1 等待队列头
如果要使用等待队列,必须先新建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t
:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
定义好等待队列头后就使用函数init_waitqueue_head
进行初始化。
/*
* @description: 初始化等待队列头
* @param-q : 要初始化的等待队列头
* @return : 无
*/
void init_waitqueue_head(wait_queue_head_t *q)
3.2 等待队列项
等待队列头是一个等待队列的头部,每个访问设备的进程为一个队列项,当设备不可用时将这些进程对应的等待队列项添加到等待队列中,用结构体wait_queue_t
表示等待队列项
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
DECLARE_WAITQUEUE
定义并初始化一个等待队列项
// 定义并初始化一个等待队列,name是等待队列项的名字,tsk是该等待队列项属于哪个任务(进程),一般设置为current
DECLARE_WAITQUEUE(name, tsk)
以下是添加等待队列项的函数:
/*
* @description: 添加队列项
* @param-q : 等待队列项要加入的等待对猎头
* @param-wait : 要加入的等待队列项
* @return : 无
*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
以下是删除等待队列项的函数:
/*
* @description: 删除队列项
* @param-q : 要删除等待队列项的等待对猎头
* @param-wait : 要删除的等待队列项
* @return : 无
*/
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
唤醒休眠态进程有两个函数wake_up()
和wake_up_interruptible()
。
wake_up 可以唤醒处于
TASK_INTERRUPTIBLE
和TASK_UNINTERRUPTIBLE
状态的进程
wake_up_interruptible只可以唤醒处于TASK_INTERRUPTIBLE
的进程
/*
* @description: 环境休眠态的进程
* @param-q : 要唤醒的等待队列头,其中的所有线程都会唤醒
* @return : 无
*/
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
除了主动唤醒,也可以设置等待队列等待某个事件来唤醒等待队列的线程。
/*
* @description : 当condition为真时,唤醒以wq为等待对猎头的等待队列,condition为假时,会一直阻塞。
* 此函数会将进程设置为TASK_UNINTERRUPTIBLE 状态
* @param-wq : 要唤醒的等待队列头,其中的所有线程都会唤醒
* @param-condition: 唤醒条件
* @return : 无
*/
wait_event(wq, condition)
/*
* @description : 功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位
* @param-wq : 要唤醒的等待队列头,其中的所有线程都会唤醒
* @param-condition: 唤醒条件
* @param-timeout : 超时时间
* @return : 0,表示超时时间到,而且 condition为假。1,表示 condition 为真,也就是条件满足了
*/
wait_event_timeout(wq, condition, timeout)
/*
* @description : 与 wait_event 函数类似,但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打断
*/
wait_event_interruptible(wq, condition)
/*
* @description : 与 wait_event_timeout 函数类似,但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打断
*/
wait_event_interruptible_timeout(wq, condition, timeout)
总结:使用等待队列实现阻塞访问重点注意两点:
①、将任务或者进程加入到等待队列头
②、在合适的点唤醒等待队列,一般都是中断处理函数里面。
参考资料
[1] 【正点原子】I.MX6U嵌入式Linux驱区动开发指南 第五十二章
[2] linux内核任务调度-- wait_event