这篇匹配《第三版》的第三章,进程,上半部分,主要是进程相关的一些关键数据结构等
目录
相关源文件
kernel/sched.c 和 include/linux/sched.h
这两个文件是主干文件,其中会依赖
include/asm-i386/thread_info.h
arch/i386/kernel/init_task.c
include/linux/list.h
kernel/pid.c
include/linux/pid.h
include/linux/wait.h
等文件
进程结构
毫无疑问,在进程调度中,进程结构体是最关键的数据结构。
// include/linux/sched.h/line:528
struct task_struct {
...
}
// // include/linux/sched.h/line:167
typedef struct task_struct task_t;
另外一个比较重要的结构体是 thread_info
// include/asm-i386/thread_info.h/line:28
struct thread_info {
...
}
// 从文件路径可以看出,这个结构体是和硬件相关联的,需要具备硬件抽象的功能
// 为啥这个也重要呢,因为它常驻内核,是 task 在内核中的代表
// include/asm-i386/thread_info.h/line:110
#define alloc_thread_info(tsk) kmalloc(THREAD_SIZE, GFP_KERNEL)
// include/asm-i386/thread_info.h/line:113
#define free_thread_info(info) kfree(info)
// include/asm-i386/thread_info.h/line:88,屏蔽低13位
static inline struct thread_info *current_thread_info(void)
Linux 中的进程管理,是通过双向链表实现的,核心结构体:
// include/linux/list.h/line: 28
struct list_head {
struct list_head *next, *prev;
};
// 链表相关的其他操作也在这个文件里
// 这个 list_head 的设计还是比较精妙的
// struct task_struct 中的 tasks 元素,把前后的进程描述符串联起来。
// 下面这个宏定义,可以通过 list 找到包含它的结构体;这个比较厉害
/**
* list_entry - get the struct for this entry
* @ptr: the &struct list_head pointer.
* @type: the type of the struct this is embedded in.
* @member: the name of the list_struct within the struct.
*/
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
/**
* container_of - cast a member of a structure out to the containing structure
*
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
// 把上面的宏展开下:
/*
* 假设:
* ptr: cur_ptr
* type: struct task_struct
* member: tasks
*/
{
const typeof(((struct task_struct *)0)->tasks) *__mptr = (cur_ptr);
(struct task_struct *)((char *)__mptr - ((size_t)&((struct task_struct *)0)->tasks));
}
// 上面用到了 typeof,这是 GNU C 的一个扩展,没有被包括在 32 个 C 语言关键字里面
// 和宏搭配使用能取得一个比较好的结果
// 编译器可以根据一个已知变量,推测出要定义的变量的类型
// 对上面展开的解释:
// 定义了一个 struct list_head 指针,并将当前的 list_head 指针对其进行赋值
// 得到当前的指针了,通过 char * 类型做差,便得到了这个指针元素所在的结构体,最后进行强制转换一下
// 当然,最后要说的一件事情,是这个是一个 {} 包起来的一个代码块,调用者将这个代码块直接赋给某个变量
// 比如:
struct task_struct* task_curr = list_entry(ptr, type, member)
// 也许会有疑问,为什么要做这么复杂的操作?能想到的两个优势:
// 1,C 语言级别的模板编程
// 2,可以解除复杂的依赖关系
这时候,引入了一个很关键的变量:init_task
// arch/i386/kernel/init_task.c/line: 32
/*
* Initial task structure.
*
* All other task structs will be allocated on slabs in fork.c
*/
struct task_struct init_task = INIT_TASK(init_task);
// 这就是传说中的 0 号进程的进程描述符
// 显然,这是个静态分配的进程描述符。所有的故事,都是从这里开始的
进程相关操作
解析下几个比较重要的宏。
// include/linux/sched.h/line: 998
#define SET_LINKS(p) do { \
if (thread_group_leader(p)) \
list_add_tail(&(p)->tasks,&init_task.tasks); \
add_parent(p, (p)->parent); \
} while (0)
// 相关调用
// include/linux/sched.h/line: 1022
#define thread_group_leader(p) (p->pid == p->tgid)
// include/linux/list.h/line: 78
static inline void list_add_tail(struct list_head *new, struct list_head *head)
// include/linux/sched.h/line: 990
#define add_parent(p, parent) list_add_tail(&(p)->sibling,&(parent)->children)
// 假设
struct task_struct *curr;
p = curr;
/* 对上面这个宏进行展开 */
// 也就是说,只有线程组的 leader 才是真正意义的进程,才会被插到 init_task 引领的进程链表中
// 这就有一个疑问,需要后面确认,get_pid 得到的是线程 pid,还是它的 tgid?
if (curr->pid == curr->tgid) { // thread_group_leader 的两者相等
// 双向链表,init_task 的前一个就是整个链表的尾部
// 注意 -> 的优先级比 & 高;tasks 元素是结构体,有内存分配,不是指针
list_add_tail(&(curr)->tasks, &init_task.tasks);
}
// 这行代码,把 curr 的 sibling 实体,插到了 parent 的 children 中
list_add_tail(&(curr)->sibling, &((curr)->parent)->children)
// 所以说,每个线程都被当做一个独立的进程来看待,都有它自己的父母兄弟
// sibing 元素是分配了内存的结构体实体
// parent 是指针,它指向的实体的内存,是在他的父 task 的进程描述符里分配的
// children 是实体,但是这里是 curr 的 parent 的实体,不是 curr 本身的
// parent 的 children 将是这个双向链表的头,表头后面跟的是子 task 的 sibling
// 同时也可以通过 curr 的 sibling,对这个双向链表进行访问
// 想法:
// linux 的 task 使用这种双向链表的方式进行组织,就好像把一个 task 实体分饰了多个角色
// 每个 list_head 的实体都在一定的维度上代表了这个 task
// 这里有一个疑惑,为什么要保持父子进程的这种关系?
// 暂时先不研究了,猜测和多用户有关
// include/linux/sched.h/line: 992
#define REMOVE_LINKS(p) do { \
if (thread_group_leader(p)) \
list_del_init(&(p)->tasks); \
remove_parent(p); \
} while (0)
// 相关调用
// include/linux/list.h/line: 217
static inline void list_del_init(struct list_head *entry)
// include/linux/sched.h/line: 989
#define remove_parent(p) list_del_init(&(p)->sibling)
// 展开。假设:
struct task_struct *curr;
p = curr;
// 则
if (curr->pid == curr->tgid) {
list_del_init(&(curr)->tasks); // 此时,进程描述符还在,但是 head_list 已经不在 init_task 链表里了
}
list_del_init(&(curr)->sibling); // 同样,进程描述符还在,但是它的 parent 的 children 链表里就没有这个元素了
// 下面这两个宏,是获得当前进程描述符的前面或者后面的进程描述符
// 是的,通过 head_list 链表元素,反向找到包含该元素的进程描述符;list_entry 上面已经分析过了
#define next_task(p) list_entry((p)->tasks.next, struct task_struct, tasks)
#define prev_task(p) list_entry((p)->tasks.prev, struct task_struct, tasks)
#define for_each_process(p) \
for (p = &init_task ; (p = next_task(p)) != &init_task ; )
// 具体使用时后面要加语句块
for (p = &init_task; (p = next_task(p)) != &init_task; ) {
; // do something
}
// 所有的判断运算符,都是从左到右的顺序;所以先运算 != 左边的括号里的内容
// 先取下个指针,再判断是否相等
关于 TASK_RUNNING 状态的链表,Linux 还是做了优先级的处理的,关联平时我们说的 nice 值。
关键结构体 struct prio_array:
// kernel/sched.c/line: 185
struct prio_array {
unsigned int nr_active;
unsigned long bitmap[BITMAP_SIZE];
struct list_head queue[MAX_PRIO];
};
// 展开
// 具体运算式的意义暂时不细究了
#define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long))
/*
* Priority of a process goes from 0..MAX_PRIO-1, // 0..139
* valid RT * priority is 0..MAX_RT_PRIO-1, // 0..99
* and SCHED_NORMAL tasks are in the range MAX_RT_PRIO..MAX_PRIO-1. // 100..139
* Priority values are inverted: lower p->prio value means higher priority.
*
* The MAX_USER_RT_PRIO value allows the actual maximum
* RT priority to be separate from the value exported to user-space.
* This allows kernel threads to set their priority to a value higher than any user task.
* Note: MAX_RT_PRIO must not be smaller than MAX_USER_RT_PRIO.
*/
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
#define MAX_PRIO (MAX_RT_PRIO + 40) // 140
// 从其实现的功能来看,这个结构体应该以单例的形式来实现
// 找了一下,没有直接找到这个结构体的实例化
// 猜测是在 struct runqueue 中被实例化的
// kernel/sched.c/line: 198
struct runqueue {
...
prio_array_t *active, *expired, arrays[2];
...
};
// struct runqueue 的实例化:
// kernel/sched.c/line: 281
static DEFINE_PER_CPU(struct runqueue, runqueues);
// include/asm-generic/percpu.h/line: 29
#define DEFINE_PER_CPU(type, name) \
__typeof__(type) per_cpu__##name
// "##"是片段连接符
// 展开
static __typeof__(struct runqueue) per_cpu__runqueues;
// 为每个 cpu 都分配一个数组吗?不应该啊,进程队列应该是整个软件内核的资源
// 当然了,估计还要往后看,也许可运行 task 队列就是针对每个 cpu 讲的呢;每个 cpu 维护自己的可运行 task
// kernel/sched.c/line: 566
static void dequeue_task(struct task_struct *p, prio_array_t *array)
// kernel/sched.c/line: 574
static void enqueue_task(struct task_struct *p, prio_array_t *array)
pidhash 散列表
pidhash.1 —— pidhash的组织形式和生成
要解决的问题:给定一个 PID 数值后,找到其对应的进程描述符指针。
这是一个比较不太容易实现的事情;kernel 通过散列表来对其进行实现。
// 这是一个非常关键且核心的数据
// kernel/pid.c/line: 31
static struct hlist_head *pid_hash[PIDTYPE_MAX];
// 四个 PID 哈希表的起始地址
// 这定义了一个结构体指针的数组;数组的大小是个很棒的设计
enum pid_type
{
PIDTYPE_PID, // 轻量进程
PIDTYPE_TGID, // 轻量进程组
PIDTYPE_PGID, // 真正的进程
PIDTYPE_SID, // 会话组
PIDTYPE_MAX
};
// 分析一下:
// line31,是一个结构体指针的数组,数组里面的元素是一个指针
// 可以假设,指针指向的实体是一个结构体(这个结构体里只有一个元素,是一个指针)
// 但是,实际上&更复杂的是,这个数组里的元素(一个指针),是另一个数组的起始地址!!!
// 实际上,有四个数组,这四个数组就是传说中的四个 PID 的 hash 表;
// 四个 PID 哈希表的初始化:
// kernel/pid.c/line: 32
static int pidhash_shift; // 注意这是一个静态变量啊
// kernel/pid.c/line: 255
void __init pidhash_init(void)
{
int i, j, pidhash_size;
unsigned long megabytes = nr_kernel_pages >> (20 - PAGE_SHIFT); // 猜测 nr_kernel_pages 是和硬件相关的
pidhash_shift = max(4, fls(megabytes * 4));
pidhash_shift = min(12, pidhash_shift);
pidhash_size = 1 << pidhash_shift; // 所以,每个 hash 表的大小总是 2^x
...
for (i = 0; i < PIDTYPE_MAX; i++) {
// 申请的大小 = 数组数 * (struct hlist_head)的大小
pid_hash[i] = alloc_bootmem(pidhash_size * sizeof(*(pid_hash[i])));
if (!pid_hash[i])
panic("Could not alloc pidhash!\n");
// 申请得到了一个预期大小的数组空间,空间的基地址存放在了 pid_hash 中
for (j = 0; j < pidhash_size; j++) {
// 对数组中的每一个元素都进行初始化(赋值 0)
// 这个宏的参数是 struct hlist_head 类型的指针
// 括号运算符的优先级是高于 & 的;所以,是对一个二维数组的元素取地址
INIT_HLIST_HEAD(&pid_hash[i][j]);
}
}
}
// kernel/pid.c/line: 30
#define pid_hashfn(nr) hash_long((unsigned long)nr, pidhash_shift)
// fn 应该是 function 的意思
// 给定一个数,返回在哈希表中的位置
// 不管是向哈希表新插入元素,还是访问哈希表中的数据,都使用同样的操作,所以同样的输入会得到同样的输出
// 注意,这是 h 的 list_node;下面对这两个双向链表做一下对比
// list_head vs hlist_head:
struct list_head { // Simple doubly linked list implementation.
struct list_head *next, *prev;
};
/*
* Double linked lists with a single pointer list head.
* Mostly useful for hash tables where the two pointer list head is too wasteful.
* You lose the ability to access the tail in O(1).
* lose: 因为,头节点指向的只有一个指针,指向一个 node;尾指针的next 指向空
*/
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
// 注意,hlist_node 的第二个元素是 **pprev,是个双重指针
// 为什么呢?因为链表的第一个 hlist_node 也要指向它的前一个节点
// 但是它的前一个节点不是 hlist_node 而是 hlist_head,所以不能直接使用节点指针
// 所以直接指向 hlist_head 里的元素 first,而 first 是一个指针,所以 pprev 就是一个双重指针了
// 为了理解 hlist_node,下面分析几个相关的函数
// include/linux/list.h/line:563
static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) // 这是个头插操作啊
{
struct hlist_node *first = h->first; // 存放的是一个地址,hlist_node 的地址,对于一个空头,first 应该是空
n->next = first; // 因为是头插,所以把链表的第一个节点放到新节点的 next 上
if (first) // 现在 first 指向的是链表的旧的第一个节点,如果不是空
first->pprev = &n->next; // 用新插入节点的next的地址(指针的地址,双重指针)赋给链表的旧的第一个节点的 pprev
h->first = n; // 把新节点放到链表头上
// 这时候,h->first 已经指向新节点了
// 但是,现在关心的不是 first 指向了哪里而是first这个指针变量的实体的地址
n->pprev = &h->first;
}
pidhash.2 —— pidhash 和进程描述符的关联
// 进程描述符中的另一个关键元素:
// include/linux/sched.h/line:528
struct task_struct {
...
/* PID/PID hash table linkage. */
struct pid pids[PIDTYPE_MAX]; // line: 589
...
}
// include/linux/pid.h/line: 13
struct pid
{
/* Try to keep pid_chain in the same cacheline as nr for find_pid */
int nr;
struct hlist_node pid_chain;
/* list of pids with the same nr, only one of them is in the hash */
struct list_head pid_list;
};
以举例的方式来说明两者的关联:
假设一个进程的 PID 号(或者 TGID 。。。)为 nr,对 nr 进行 pid_hashfn 得到它在 pid_hash[x] 指向的数组的偏移。
基于 PID 的类型,对进程描述符的 pids 这个数组中的第 x 个元素:
- 将 pid.nr 赋值;猜测,这个值就等于它本身的 pid 或者 tgid 或者。。寻找的时候可以用
- 将 pid.pid_chain 插入到 pid_hash[x][n].first 中;这样就可以通过 pid_hash[x] 找到这个 pid 结构体了
- pid.pid_list 是用来维持 tgid,pgrp,session 等关系的;要知道,线程组、进程组什么的,和父子进程、兄弟进程是不同的概念
// 解析下,知道一个 PID 的数值以后,怎么找到它对应进程描述符的 pid 元素
struct pid * fastcall find_pid(enum pid_type type, int nr)
{
struct hlist_node *elem;
struct pid *pid;
hlist_for_each_entry(pid, elem,
&pid_hash[type][pid_hashfn(nr)], pid_chain) {
if (pid->nr == nr)
return pid;
}
return NULL;
}
#define hlist_for_each_entry(tpos, pos, head, member) \
for (pos = (head)->first; \
pos && ({ prefetch(pos->next); 1;}) && \
({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \
pos = pos->next)
{
for (elem = head->first; pos; pos = pos->next) {
// 下面这两行,是在宏定义里带来的,所以每次都会被运行,但是不需要用户手动去写
prefetch(pos->next); // 这个先不看
// 通过 pid 的元素 pid_chain(也就是pos),获得 pid 结构体的地址
pid = hlist_entry(pos, typeof(*pid), pid_chain);
if (pid->nr == nr) // 如果遍历到的这个结构体的 nr 和目标 nr 相等,那么就对了
return pid;
}
}
所以,当知道一个 PID 以后:
- 可以定位到 hash 表中的位置
- 这个位置里放的是一个链表头,因为也许会有不同的 PID 被映射到 hash 表的同一个位置;这就需要用链表来组织
- 找到这个链表头以后,需要遍历这个链表,以确认和这个 PID 真正相等的
这里会有两层链表:
1)hash 表的头指针指向的是一个链表:被映射到同一个位置,但是 PID 不同
2)基于 nr 找到想要的 pid_chain 了,而 pid 的 pid_list 元素代表的是一个双向链表,代表线程组、会话组什么的
pidhash.3 —— 几个常用的宏
// 这两个宏要搭配用的
#define do_each_task_pid(who, type, task) \
if ((task = find_task_by_pid_type(type, who))) { \
prefetch((task)->pids[type].pid_list.next); \
do {
#define while_each_task_pid(who, type, task) \
} while (task = pid_task((task)->pids[type].pid_list.next,\
type), \
prefetch((task)->pids[type].pid_list.next), \
hlist_unhashed(&(task)->pids[type].pid_chain)); \
}
/* 展开 */
if ((task = find_task_by_pid_type(type, who))) { // 如果至少存在一个的话
prefetch((task)->pids[type].pid_list.next);
do {
;
} while (task = pid_task((task)->pids[type].pid_list.next,type),
prefetch((task)->pids[type].pid_list.next),
hlist_unhashed(&(task)->pids[type].pid_chain));
}
// 相关引用
// kernel/pid.c/line:207
// 给定 PID 的数组和类型,返回指向的 task;但是,符合条件的 task 有可能是一个链表,但这个返回的是链表的头
task_t *find_task_by_pid_type(int type, int nr)
{
struct pid *pid;
pid = find_pid(type, nr); // 上面分析过了,这里返回的是 pid 的结构体
if (!pid)
return NULL;
return pid_task(&pid->pid_list, type); // 通过 pids[type].pid_list 找到它所在的 task_t 结构体
}
// include/linux/pid.h/line:22
#define pid_task(elem, type) list_entry(elem, struct task_struct, pids[type].pid_list)
// 再重点看下这个语句
while (task = pid_task((task)->pids[type].pid_list.next,type),
prefetch((task)->pids[type].pid_list.next),
hlist_unhashed(&(task)->pids[type].pid_chain))
// 这几行代码还是很厉害的
// 这个 while 语句用的是逗号运算符,最终的值,是最后那个语句
// 被链接到 hash 表里的节点,pprev 都不是零,所以函数会返回 0
// 未被链接到 hash 表里的节点,pprev 都是是零,所以函数会返回 1
// include/linux/list.h/line:504
static inline int hlist_unhashed(const struct hlist_node *h)
{
return !h->pprev;
}
// 还是那个点,被映射到 hash 表同一个位置上,有可能会有很多个元素,这些元素被链接成一个链表
// 而这个链表还带着一个结构体,这个结构体的里的另一个同伴,pid_list 是一个正规意义的双向链表,循环的
// 这个双向链表中,直接挂到 hash 位置链表上的那个节点,是头
// while 里的第一个语句会不停的取下一个 pid_list
// 当取到某个 pid_list 所对应的 pid_chain 的 pprev 不为空时,便是遍历了这个 pid_list
下面这几个函数和上面的操作类似,就不拆解了,直接读代码就好。
// kernel/pid.c/line:147
int fastcall attach_pid(task_t *task, enum pid_type type, int nr)
// kernel/pid.c/line:192
void fastcall detach_pid(task_t *task, enum pid_type type)
组织进程
// include/linux/wait.h/line:51
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
// include/linux/wait.h/line:33
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
struct task_struct * task;
wait_queue_func_t func;
struct list_head task_list;
};
// include/linux/wait.h/line:30
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int sync, void *key);
// 以下两个函数分别静态、动态的初始化一个 wait_queue_head_t
// include/linux/wait.h/line:74
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
// include/linux/wait.h/line:80
static inline void init_waitqueue_head(wait_queue_head_t *q)
// include/linux/wait.h/line:325
#define DEFINE_WAIT(name) \
wait_queue_t name = { \
.task = current, \
.func = autoremove_wake_function, \
.task_list = { .next = &(name).task_list, \
.prev = &(name).task_list, \
}, \
}
// kernel/wait.c/line:123
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
// include/linux/wait.h/line:93
static inline void init_waitqueue_func_entry(wait_queue_t *q,
wait_queue_func_t func)
// 可以自定义一个唤醒函数
// kernel/wait.c/line:14
void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue(q, wait);
spin_unlock_irqrestore(&q->lock, flags);
}
// include/linux/wait.h/line:119
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
list_add(&new->task_list, &head->task_list);
}
// 上面这个函数可以理解,就是把 queue 插到 queue_head 中了
// 但是现在的疑问是,queue_head 应该有很多个,这若干个 queue_head 是怎么串联起来的呢
// kernel/wait.c/line:25
void fastcall add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
// kernel/wait.c/line:36
void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
// include/linux/wait.h/line:101
static inline int waitqueue_active(wait_queue_head_t *q)
进入等待
进程可以在若干种情况下,进入等待状态。
// kernel/sched.c/line:3199
void fastcall __sched sleep_on(wait_queue_head_t *q)
{
SLEEP_ON_VAR
current->state = TASK_UNINTERRUPTIBLE;
SLEEP_ON_HEAD
schedule();
SLEEP_ON_TAIL
}
// 函数里的几个宏,展开以后还是比较好理解的
// 复杂的地方在 schedule(),这个无疑是进程中最复杂的函数了;暂时先不看了,后面章节会再讲
// 随便找个 sleep_on 的调用场景:
// drivers/scsi/atari_scsi.c/line:498
static DECLARE_WAIT_QUEUE_HEAD(falcon_fairness_wait);
// drivers/scsi/atari_scsi.c/line:554
static void falcon_get_lock( void )
{
...
while( !in_interrupt() && falcon_got_lock && stdma_others_waiting() )
sleep_on( &falcon_fairness_wait );
...
}
// 这说明,wait_queue_head_t 是一个比较上层的数据结构了,可以在应用层直接实例化变量
// 回到上面 __add_wait_queue() 这个函数下面的疑问,应用模块是知道自己的等待队列的头在哪里的
// 下面这几个函数或者宏略微有些超纲,简单看看先,暂时不深入分析了
// kernel/sched.c/line:3171
void fastcall __sched interruptible_sleep_on(wait_queue_head_t *q)
// kernel/sched.c/line:3212
long fastcall __sched sleep_on_timeout(wait_queue_head_t *q, long timeout)
// kernel/sched.c/line:3184
long fastcall __sched interruptible_sleep_on_timeout(wait_queue_head_t *q, long timeout)
// kernel/wait.c/line:59
void fastcall prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
// kernel/wait.c/line:78
void fastcall prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)
// kernel/wait.c/line:97
void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
// include/linux/wait.h/line:172
#define wait_event(wq, condition) \
do { \
if (condition) \
break; \
__wait_event(wq, condition); \
} while (0)