Linux内核架构
进程抢占
进程是可抢占的,在新的进程进入TASK_RUNNING状态,表示可以运行的时候,内核会检查他们的动态优先级。如果比当前的进程优先级高就调用调度器进行调度。
调度
调度算法和调度类
SCHED_FIFO
一个先入先出的实时过程。当调度器将CPU分配给进程时,它会将进程描述符保留在运行队列列表中的当前位置。如果没有其他更高优先级的实时进程可以运行,那么该进程将继续使用CPU,即使其他具有相同优先级的实时进程也可以运行。
SCHED_RR
一个实时的时间片轮过程。当调度器将CPU分配给进程时,它会将进程描述符放在运行队列列表的末尾。此策略可确保将CPU时间公平地分配给所有具有相同优先级的SCHED_RR实时进程。
SCHED_NORMAL
普通的分时进程。
普通进程的调度
基础时间片的计算
时间片不是每个进程都一样的。是根据静态优先级算出来的。优先级越高时间片越长。
动态优先级和平均睡眠时间
动态优先级会在静态优先级的基础上根据前一段时间统计的平均睡眠时间进行小幅度调整。平均睡眠时间长,内核认为这个进程可能是交互式进程。会抬高它的动态优先级。实际的调度是按照动态优先级调度的。
实时进程调度
每个实时进程都与实时优先级相关联,其值范围从1(最高优先级)到99(最低优先级)。调度程序总是倾向于高优先级的可运行进程,而不是低优先级的进程;换句话说,是实时的进程在保持可运行时抑制每个低优先级进程的执行。
同优先级FIFO RR并存
Linux 提供了两种实时调度策略,SCHED_FIFO 和 SCHED_RR。正常的非实时调度策略是 SCHED_NORMAL。 SCHED_FIFO 实现了一个简单的没有时间片的先进先出调度算法。一个可运行的 SCHED_FIFO 任务总是调度在任何 SCHED_NORMAL 任务之上。当 SCHED_FIFO 任务变为可运行时,它会继续运行,直到它阻塞或显式让出处理器;它没有时间片,可以无限期地运行。只有更高优先级的 SCHED_FIFO 或 SCHED_RR 任务可以抢占 SCHED_FIFO 任务。具有相同优先级的两个或多个 SCHED_FIFO 任务循环运行,但同样仅在它们明确选择这样做时才让出处理器。如果 SCHED_FIFO 任务是可运行的,则所有优先级较低的任务在完成之前都无法运行。 SCHED_RR 与 SCHED_FIFO 相同,只是每个进程只能运行到它用完预定的时间片。也就是说,SCHED_RR 是带有时间片的 SCHED_FIFO,它是一种实时循环调度算法。当 SCHED_RR 任务耗尽其时间片时,任何其他处于其优先级的实时进程都将被调度轮询。时间片仅用于允许重新安排相同优先级的进程。与 SCHED_FIFO 一样,高优先级的进程总是立即抢占低优先级的进程,而低优先级的进程永远不能抢占 SCHED_RR 任务,即使它的时间片已用尽。
只有当发生以下事件之一时,实时进程才会被另一个进程取代:
该进程被具有更高实时优先级的另一个进程抢占。
- 该进程执行阻塞操作,并将其置于休眠状态(状态为TASK_可中断或TASK_UNINTERRUPTIBLE)。
2. 进程停止(状态为TASK_STOPPED或TASK_TRACED),或被终止(状态为EXIT_ZOMBIE或EXIT_DEAD)。
3. 该进程通过调用sched_yield()系统调用来自愿放弃CPU。
4. 这个过程是实时循环(SCHED_RR),它已经耗尽了它的时间片。
内存管理
内存定义
- 内存又称主存,是 CPU 能直接寻址的存储空间,由半导体器件制成
- 内存的特点是存取速率快
内存作用
- 暂时存放 cpu 的运算数据
- 硬盘等外部存储器交换的数据
- 保障 cpu 计算的稳定性和高性能
linux 内存地址空间
- linux 内存地址空间 Linux 内存管理全貌
- 内存地址——用户态&内核态
用户态:Ring3 运行于用户态的代码则要受到处理器的诸多。
内核态:Ring0 在处理器的存储保护中,核心态。
用户态切换到内核态的 3 种方式:系统调用、异常、外设中断。
区别:每个进程都有完全属于自己的,独立的,不被干扰的内存空间;用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用;内核态线程共享内核地址空间;
- 内存地址——MMU 地址转换
- 内存地址——分段机制
段选择符
为了方便快速检索段选择符,处理器提供了 6 个分段寄存器来缓存段选择符,它们是: cs,ss,ds,es,fs 和 gs。
段的基地址(Base Address):在线性地址空间中段的起始地址。
段的界限(Limit):在虚拟地址空间中,段内可以使用的最大偏移量。
分段实现
逻辑地址的段寄存器中的值提供段描述符,然后从段描述符中得到段基址和段界限,然后加上逻辑地址的偏移量,就得到了线性地址
- 内存地址——分页机制(32 位)
分页机制是在分段机制之后进行的,它进一步将线性地址转换为物理地址。
10 位页目录,10 位页表项, 12 位页偏移地址。
单页的大小为 4KB。
- 用户态地址空间
TEXT:代码段可执行代码、字符串字面值、只读变量
DATA:数据段,映射程序中已经初始化的全局变量
BSS 段:存放程序中未初始化的全局变量
HEAP:运行时的堆,在程序运行中使用 malloc 申请的内存区域
MMAP:共享库及匿名文件的映射区域
STACK:用户进程栈
- 内核态地址空间
直接映射区:线性空间中从 3G 开始最大 896M 的区间,为直接内存映射区。
动态内存映射区:该区域由内核函数 vmalloc 来分配。
永久内存映射区:该区域可访问高端内存。
固定映射区:该区域和 4G 的顶端只有 4k 的隔离带,其每个地址项都服务于特定的用途,如: ACPI_BASE 等。
- 进程内存空间
用户进程通常情况只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。
内核空间是由内核负责映射,不会跟着进程变化;内核空间地址有自己对应的页表,用户进程各自有不同额页表。
Linux内核基础知识
并发与竞争
并发就是多个“用户”同时访问同一个共享资源。并发访问带来的问题就是竞争,所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的。
原子整形操作 API 函数
如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,如下所示:
atomic_t a; //定义 a
也可以在定义原子变量的时候给原子变量赋初值,如下所示:
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0
可以通过宏 ATOMIC_INIT 向原子变量赋初值。
原子变量有了,接下来就是对原子变量进行操作,比如读、写、增加、减少等等,Linux 内
核提供了大量的原子操作 API 函数,如表所示:
函数 | 描述 |
ATOMIC_INIT(int i) | 定义原子变量的时候对其初始化。 |
int atomic_read(atomic_t *v) | 读取 v 的值,并且返回。 |
void atomic_set(atomic_t *v, int i) | 向 v 写入 i 值。 |
void atomic_add(int i, atomic_t *v) | 给 v 加上 i 值。 |
void atomic_sub(int i, atomic_t *v) | 从 v 减去 i 值。 |
如果使用 64 位的 SOC 的话,就要用到 64 位的原子变量,Linux 内核也定义了 64 位原子 结构体,如下所示:
示例代码 47.2.2.2 atomic64_t 结构体
typedef struct {
long long counter;
} atomic64_t;
相应的也提供了 64 位原子变量的操作 API 函数,这里我们就不详细讲解了,和表 47.2.1.1
中的 API 函数有用法一样,只是将“atomic_”前缀换为“atomic64_”,将 int 换为 long long。如 果使用的是 64 位的 SOC,那么就要使用 64 位的原子操作函数。Cortex-A7 是 32 位的架构,所以本书中只使用表 47.2.2.1 中的 32 位原子操作函数。原子变量和相应的 API 函数使用起来很简单,参考如下示例:
示例代码 47.2.2.2 原子变量和 API 函数使用
atomic_t v = ATOMIC_INIT(0); /* 定义并初始化原子变零 v=0 */
atomic_set(10); /* 设置 v=10 */
atomic_read(&v); /* 读取 v 的值,肯定是 10 */
atomic_inc(&v); /* v 的值加 1,v=11 */
自旋锁
当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有, 只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁 正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。
自旋锁的缺点:那就等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长。所以自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景那就需要换其他的方法了。
自旋锁 API 函数
函数 | 描述 |
DEFINE_SPINLOCK(spinlock_t lock) | 定义并初始化一个自选变量。 |
int spin_lock_init(spinlock_t *lock) | 初始化自旋锁。 |
void spin_lock(spinlock_t *lock) | 获取指定的自旋锁,也叫做加锁。 |
void spin_unlock(spinlock_t *lock) | 释放指定的自旋锁。 |
int spin_trylock(spinlock_t *lock) | 尝试获取指定的自旋锁,如果没有获取到就返回 0 |
int spin_is_locked(spinlock_t *lock) | 检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0。 |
的自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生。自旋锁会自动禁止抢占,也就说当线程A得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自 动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了!线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,好了,死锁发生了!
自旋锁API 函数用于线程之间的并发访问,如果此时中断也要插一脚,中断也想访问共享资源,那该怎么办呢?首先可以肯定的是,中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC
来说会有多个 CPU 核),否则可能导致锁死现象的发生。
最好的解决方法就是获取锁之前关闭本地中断,Linux 内核提供了相应的 API 函数,如表
47.3.2.2 所示:
函数 | 描述 |
void spin_lock_irq(spinlock_t *lock) | 禁止本地中断,并获取自旋锁。 |
void spin_unlock_irq(spinlock_t *lock) | 激活本地中断,并释放自旋锁。 |
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) | 保存中断状态,禁止本地中断,并获取自旋锁。 |
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。 |
表 47.3.2.2 线程与中断并发访问处理 API 函数
使用 spin_lock_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际
上内核很庞大,运行也是“千变万化”,我们是很难确定某个时刻的中断状态,因此不推荐使用spin_lock_irq/spin_unlock_irq。建议使用 spin_lock_irqsave/spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。一般在线程中使用 spin_lock_irqsave/ spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock。
下半部(BH)也会竞争共享资源,有些资料也会将下半部叫做底半部。关于下半部后面的
章节会讲解,如果要在下半部里面使用自旋锁,可以使用表 47.3.2.3 中的 API 函数:
函数 | 描述 |
void spin_lock_bh(spinlock_t *lock) | 关闭下半部,并获取自旋锁。 |
void spin_unlock_bh(spinlock_t *lock) | 打开下半部,并释放自旋锁。 |
自旋锁注意点:
- 因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要
短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如稍后要讲的信号量和互斥体。
- 自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能
导致死锁。
- 能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就
必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
- 在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还
是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。
信号量
相比于自旋锁,信号量可以使线程进入休眠状态,使用信号量会提高处理器的使用效率。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。总结一下信号量的特点:
- 因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
- 因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
- 如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
通过信号量可以控制访问资源的线程数,在初始化的时候将信号量值设置的大于 1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于 1,此时的信号量就是一个二值信号量。
信号量 API 函数
要想使用信号量就得先定义,然后初始化信号量。有关信号量的 API 函数如表 47.4.2.1 所
示:
函数 | 描述 |
DEFINE_SEAMPHORE(name) | 定义一个信号量,并且设置信号量的值为 1。 |
void sema_init(struct semaphore *sem, int val) | 初始化信号量 sem,设置信号量值为 val。 |
void down(struct semaphore *sem) | 获取信号量,因为会导致休眠,因此不能在中断中使用。 |
int down_trylock(struct semaphore *sem); | 尝试获取信号量,如果能获取到信号量就获 取,并且返回 0。如果不能就返回非 0,并且不会进入休眠。 |
int down_interruptible(struct semaphore *sem) | 获取信号量,和 down 类似,只是使用 down 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。 |
void up(struct semaphore *sem) | 释放信号量 |
表 47.4.2.1 信号量 API 函数
互斥体
将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。
在使用 mutex 之前要先定义一个 mutex 变量。在使用 mutex 的时候要注意如下几点:
①、mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样,mutex 保护的临界区不可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
互斥体 API 函数
有关互斥体的 API 函数如表 47.5.2.1 所示:
函数 | 描述 |
DEFINE_MUTEX(name) | 定义并初始化一个 mutex 变量。 |
void mutex_init(mutex *lock) | 初始化 mutex。 |
void mutex_lock(struct mutex *lock) | 获取 mutex,也就是给 mutex 上锁。如果获 取不到就进休眠。 |
void mutex_unlock(struct mutex *lock) | 释放 mutex,也就给 mutex 解锁。 |
int mutex_trylock(struct mutex *lock) | 尝试获取 mutex,如果成功就返回 1,如果失败就返回 0。 |
int mutex_is_locked(struct mutex *lock) | 判断 mutex 是否被获取,如果是的话就返回 1,否则返回 0。 |
int mutex_lock_interruptible(struct mutex *lock) | 使用此函数获取mutex失败进入休眠以后可 以被信号打断。 |
内核定时器
定义好定时器以后还需要通过一系列的 API 函数来初始化此定时器,这些函数如下:
1、init_timer 函数
init_timer 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定要先用 init_timer 初始化一下。init_timer 函数原型如下:
void init_timer(struct timer_list *timer)
函数参数和返回值含义如下:
timer:要初始化定时器。
返回值:没有返回值。
2、add_timer 函数
add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:
void add_timer(struct timer_list *timer)
函数参数和返回值含义如下:
timer:要注册的定时器。
返回值:没有返回值。
3、del_timer 函数
del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。
在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。del_timer 函数原型如下:
int del_timer(struct timer_list * timer)
函数参数和返回值含义如下:
timer:要删除的定时器。
返回值:0,定时器还没被激活;1,定时器已经激活。
4、del_timer_sync 函数
del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,
del_timer_sync 不能使用在中断上下文中。del_timer_sync 函数原型如下所示:
int del_timer_sync(struct timer_list *timer)
函数参数和返回值含义如下:
timer:要删除的定时器。
返回值:0,定时器还没被激活;1,定时器已经激活。
5、mod_timer 函数
mod_timer 函数用于修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时器!函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)
函数参数和返回值含义如下:
timer:要修改超时时间(定时值)的定时器。
expires:修改后的超时时间。
返回值:0,调用 mod_timer 函数前定时器未被激活;1,调用 mod_timer 函数前定时器已被激活。
中断
中断 API 函数
1、中断号
每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中
断线。在 Linux 内核中使用一个 int 变量表示中断号。
2、request_irq 函数
在 Linux 内核中要想使用某个中断是需要申请的,request_irq 函数用于申请中断,request_irq 函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断,request_irq 函数原型如下:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev)
函数参数和返回值含义如下:
irq:要申请中断的中断号。
handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志,这里我们
介绍几个常用的中断标志,如表 51.1.1.1 所示:
函数 | 描述 |
IRQF_SHARED | 多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话,request_irq 函数的 dev 参数就是唯一区分他们的标志。 |
IRQF_ONESHOT | 单次中断,中断执行一次就结束。 |
IRQF_TRIGGER_NONE | 无触发。 |
IRQF_TRIGGER_RISING | 上升沿触发。 |
IRQF_TRIGGER_FALLING | 下降沿触发 |
IRQF_TRIGGER_HIGH | 高电平触发。 |
IRQF_TRIGGER_LOW | 低电平触发 |
表 51.1.1.1 常用的中断标志
name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将
dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经
被申请了。
3、free_irq 函数
使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放
掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。free_irq 函数原型如下所示:
void free_irq(unsigned int irq, void *dev)
函数参数和返回值含义如下:
irq:要释放的中断。
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断
只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。
4、中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就
是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构。中断处理函数的返回值为 irqreturn_t 类型,irqreturn_t 类型定义如下所示:
示例代码 51.1.1.1 irqreturn_t 结构
10 enum irqreturn {
11
IRQ_NONE = (0 << 0),
12
IRQ_HANDLED = (1 << 0),
13
IRQ_WAKE_THREAD = (1 << 1),
14 };
15
16 typedef enum irqreturn irqreturn_t;
可以看出 irqreturn_t 是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如
下形式:
return IRQ_RETVAL(IRQ_HANDLED)
5、中断使能与禁止函数
常用的中断使用和禁止函数如下所示:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
enable_irq 和 disable_irq 用于使能和禁止指定的中断,irq 就是要禁止的中断号。disable_irq
函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数:
void disable_irq_nosync(unsigned int irq)
disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三
个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,这个时候可以使用如下两个函数:
local_irq_enable()
local_irq_disable()
local_irq_enable 用于使能当前处理器中断系统,local_irq_disable 用于禁止当前处理器中断
系统。假如 A 任务调用 local_irq_disable 关闭全局中断 10S,当关闭了 2S 的时候 B 任务开始运行,B 任务也调用 local_irq_disable 关闭全局中断 3S,3 秒以后 B 任务调用 local_irq_enable 函数将全局中断打开了。此时才过去 2+3=5 秒的时间,然后全局中断就被打开了,此时 A 任务要关闭 10S 全局中断的愿望就破灭了,然后 A 任务就“生气了”,结果很严重,可能系统都要被A 任务整崩溃。为了解决这个问题,B 任务不能直接简单粗暴的通过 local_irq_enable 函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数:
local_irq_save(flags)
local_irq_restore(flags)
这两个函数是一对,local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。
local_irq_restore 用于恢复中断,将中断到 flags 状态。
上半部与下半部
工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的
工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
使用 INIT_WORK 宏来初始化工作,INIT_WORK 宏定义如下:
#define INIT_WORK(_work, _func)
_work 表示要初始化的工作,_func 是工作对应的处理函数。
也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
n 表示定义的工作(work_struct),f 表示工作对应的处理函数。
和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原型如下所示:
bool schedule_work(struct work_struct *work)
函数参数和返回值含义如下:
work:要调度的工作。
返回值:0 成功,其他值 失败。
阻塞和非租塞IO
等待队列
- 等待队列头
使用 init_waitqueue_head 函数初始化等待队列头,函数原型如下:
void init_waitqueue_head(wait_queue_head_t *q)
参数 q 就是要初始化的等待队列头。
也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化。
- 等待队列项
使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:
DECLARE_WAITQUEUE(name, tsk)
name 就是等待队列项的名字,tsk 表示这个等待队列项属于哪个任务(进程),一般设置为
current , 在 Linux 内核中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。 因 此 宏 DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。
- 将队列项添加/移除等待队列头
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,
只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,等待队列项添加 API 函数如下:
void add_wait_queue(wait_queue_head_t *q,
wait_queue_t *wait)
函数参数和返回值含义如下:
q:等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
返回值:无。
等待队列项移除 API 函数如下:
void remove_wait_queue(wait_queue_head_t *q,
wait_queue_t *wait)
函数参数和返回值含义如下:
q:要删除的等待队列项所处的等待队列头。
wait:要删除的等待队列项。
返回值:无。
- 等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数:
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。
wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进 程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。
- 等待事件
除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒
等待队列中的进程,和等待事件有关的 API 函数如表 52.1.2.1 所示:
函数 | 描述 |
wait_event(wq, condition) | 等待以 wq 为等待队列头的等待队列被唤醒,前提是 condition 条件必须满足(为真),否则一直阻塞 。 此 函 数 会 将 进 程 设 置 为 TASK_UNINTERRUPTIBLE 状态 |
wait_event_timeout(wq, condition, timeout) | 功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位。此函数有返回值,如果返回 0 的话表示超时时间到,而且 condition 为假。为 1 的话表示 condition 为真,也就是条件满足了。 |
wait_event_interruptible(wq, condition) | 与 wait_event 函数类似,但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打断。 |
wait_event_interruptible_timeout(wq, condition, timeout) | 与 wait_event_timeout 函数类似,此函数也将进程设置为 TASK_INTERRUPTIBLE,可以被信号打断。 |
表 52.1.2.1 等待事件 API 函数
异步通知
通过信号的方式由底层通知应用层获取数据。
驱动中的信号处理
fasync 函数
如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,此
函数格式如下所示:
int (*fasync) (int fd, struct file *filp, int on)
fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针,fasync_helper 函数原型如下:
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
fasync_helper 函数的前三个参数就是 fasync 函数的那三个参数,第四个参数就是要初始化
的 fasync_struct 结构体指针变量。当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”改变
fasync 标记的时候,驱动程序 file_operations 操作集中的 fasync 函数就会执行。
kill_fasync 函数
当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。kill_fasync
函数负责发送指定的信号,kill_fasync 函数原型如下所示:
void kill_fasync(struct fasync_struct **fp, int sig, int band)
函数参数和返回值含义如下:
fp:要操作的 fasync_struct。
sig:要发送的信号。
band:可读时设置为 POLL_IN,可写时设置为 POLL_OUT。
返回值:无。