读书目的:了解内核编程基础,为学习《Linux设备驱动程序》和《深入理解Linux内核》做铺垫
读书收获:
心得
进程
1 进程管理
1.1进程
进程:处于执行期的程序以及相关资源的总称
1.2进程描述符及任务结构
进程描述符包含的数据能完整地描述一个正在执行的程序:打开的文件、进程的地址空间、挂起的信号、进程状态…;
内核把进程的列表存放在任务队列的双向循环链表中。
- 分配进程描述符
linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的
- 进程描述符的存放
有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程task_struct的指针,用于加快访问速度。由于x86这样的体系结构寄存器并不富余,只能在内核栈的尾端创建thread_info结构,通过计算偏移间接查找task_struct结构 - 进程状态
struct task_struct {
volatile long state;
...
}
TASK_RUNNING(运行)
TASK_INTERRUPTIBLE(信号可中断)
TASK_UNINTERRUPTIBLE(信号不可中断)
__TASK_TRACED(被跟踪)
__TASK_STOPPED(停止)
- 设置当前进程状态
set_task_state(task, state);
- 进程上下文
当用户程序执行系统调用或者触发某个异常,它就陷入了内核空间,由内核代表进程执行,并处于进程上下文中。 - 进程家族树
所有进程都是PID为1的init进程的后代
/* 父进程和子进程链表 */
struct task_struct {
...
struct task_struct *parent;
struct list_head children;
...
}
/* 访问子进程 */
struct task_struct *task;
struct list_head *list;
list_for_each(list, ¤t->children) {
task = list_entry(list, struct task_struct, sibling);
}
1.3进程创建
Unix进程创建:fork()通过拷贝当前进程创建一个子进程;exec()读取可执行文件并将其载入地址空间开始运行
- 写时拷贝
linux的fork()使用写时拷贝,资源的复制只有在需要写入的时候才进行。fork()后立即调用exec()就不会拷贝数据了 - fork()
创建子进程,拷贝父进程资源到新的地址空间 - vfork()
父子进程共用父进程的资源,当子进程运行时父进程挂起
1.4线程在linux中的实现
linux把所有线程都当做进程来实现,线程仅仅被视为与其他进程共享某些资源的进程
- 创建线程
线程的创建和普通进程创建类似,只不过在调用clone()时需要传递一些参数标志指明共享资源
/* clone和fork差不多,只是父子进程共享地址空间、文件系统、文件描述符、信号处理 */
/* 新建的进程和它的父进程就是所谓的线程 */
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
- 内核线程
- 内核线程的地址空间指针mm被设置为NULL,并且只在内核空间运行
- 内核线程只能由其他内核线程创建
/* 从现有内核线程中创建一个新内核线程方法(需要wake_up_process唤醒) */
struct task_struct *kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...)
/* 从现有线程创建一个即刻运行的线程方法 */
struct task_struct *kthread_run(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...)
/* 线程结束 */
int kthread_stop(struct task_struct *k)
1.5进程终结(调用do_exit())
显式结束:调用exit()系统调用
隐式结束:主函数返回
被动结束:接收到不能处理的信号或异常
- 删除进程描述符
进程调用do_exit()后,线程僵住不动,系统仍保留该进程的进程描述符,直到父进程获得已终结的子进程的信息后,子进程的tast_struct结构才被释放 - 孤儿进进程造成的进退维谷
父进程在子进程之间退出,其子进程处于僵死状态,必须为其在其进程组内找一个新的父进程,若找不到,则让init作为其父进程
2 进程调度
调度程序决定将那个进程投入运行,何时运行以及运行运行多长时间
2.1多任务
多任务系统分为两类:抢占式多任务(进程被动强制挂起)和非抢占式(进程主动挂起自己)
2.2linux的进程调度
O(1)–>CFS
2.3策略
- I/O消耗型和处理器消耗型进程
I/O消耗型进程大部分时间用来提交I/O请求或是等待I/O请求,调度策略提高调度频率,缩短调度时间;
处理器消耗型进程大部分时间在执行代码,调度策略降低调度频率,延长调度时间 - 进程优先级
优先级高的进程先运行 - 时间片
进程被抢占前所能持续运行的时间
2.4linux调度算法
- 调度器类
Linux调度器以模块方式提供,允许不同类型进程针对性选择调度算法,这种模块化结构称为调度器类,每个调度器类都有一个优先级。 - 公平调度
CFS理念:每个进程将能获得1/n的处理器时间,n为运行进程数;
CFS做法:允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程,CFS在所有可运行进程总数基础上计算出一个进程应该运行多久,nice的相对值被作为获得处理器运行比的权重(不再有时间片的概念)。
2.5linux调度的实现
时间记账
调度器实体结构
调度器实体结构struct sched_entity作为名为se的成员变量,嵌入在进程描述符struct task_struct内虚拟实时
struct sched_entity中的vruntime变量存放进程的虚拟运行时间,CFS使用vruntime变量来记录一个程序到底运行了多长时间以及它还应该再运行多长时间;
update_curr()函数实现了该记账功能,由系统定时器周期性调用,传递运行时间给vruntime。
- 进程选择
当CFS需要选择下一个运行进程时,它会挑一个具有最小vruntime的进程,这就是CFS调度算法的核心。
- 挑选下一个任务
__pick_next_entity():运行rbtree中最左边叶子节点所代表的那个进程 - 向树中加入进程
enqueue_entity():加入进程到rbtree并缓存最左叶子节点 - 从树种删除进程
dequeue_entity():删除动作发生在进程堵塞或终止时
- 挑选下一个任务
- 调度器入口
进程调度主要入口是schedule(),它会调用pick_next_task(),pick_next_task()以优先级为序,从高到低,依次检查每一个调度类,并且从最高优先级的调度类中,选择最高优先级的进程。 - 睡眠和唤醒
休眠:进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行其他进程
唤醒:进程被设置为可执行状态,然后再从等待队列中移到可执行红黑树中。
- 等待队列
等待队列是由等待某些事件发生的进程组成的简单链表,wake_queue_head_t表示,DECLARE_WAITQUEUE()静态创建,init_waitqueue_head()动态创建
- 等待队列
/* q是希望休眠的等待队列 */
DEFINE_WAIT(wait); //创建等待队列项
add_wait_queue(q, &wait); //将q进程加入到等待队列
while (!condition) { //等待的事件
prepare_to_wait(&q, &wait, TASK_INTERRUPTIBLE); //将进程状态更变为TASK_INTERRUPTIBLE
if (signal_pending(current)) //进程信号被唤醒
/* 处理信号 */
schedule(); //执行其他进程
}
finish_wait(&q, &wait); //把q进程移出等待队列
2. 唤醒
wake_up(),它会唤醒制定的等待队列上的说有进程,它调用try_to_wake_up()将进程设置为TASK_RUNING,调用enqueue_task()将进程放入红黑树中
2.6抢占和上下文切换
上下文切换:从一个可执行进程切换到另一个可执行进程,由context_switch()负责;
内核提供need_resched标志来表明是否需要重新执行一次调度,当某个进程应该被抢占时,scheduler_tick()就会设置这个标志。
- 用户抢占
根据need_resched标志判断是否有进程抢占
- 从系统调用返回用户空间时
- 从中断处理程序返回用户空间时
- 内核抢占
只要没有持有锁(preempt_count=0),内核就可以进行抢占
- 中断处理程序正在执行,且返回内核空间之前
- 内核代码再一次具有可抢占性的时候
- 内核任务显示调用schedule()
- 内核任务阻塞导致调用schedule()
2.7实时调度策略
内核提供两种实时调度策略:SCHED_FIFO和SCHED_RR;普通非实时调度策略为SCHED_NORMAL;SCHED_RR>SCHED_FIFO>SCHED_NORMAL
1. SCHED_FIFO:先入先出调度算法,不使用时间片,处于可执行状态的进程会一直执行,直到被阻塞、抢占或显示调用schedule()
2. SCHED_RR:带有时间片的SCHE_FIFO
2.8与调度相关的系统调用
- 与调度策略和优先级相关的系统调用
- sche_setscheduler():设置进程调度策略和实时优先级(改写进程task_struct的policy和rt_priority)
- sche_getscheduler():获取进程调度策略和实时优先级(读取进程task_struct的policy和rt_priority)
- sched_setparam():设置进程实时优先级
- sched_getparam():获取进程实时优先级
- sched_get_priority_max():返回给定调度策略的最大优先级
- sched_get_priority_min():返回给定调度策略的最小优先级
- nice():调用内核set_user_nice()设置task_struct的static_prio和prio
- sched_rr_get_interval():获取进程的时间片
- 与处理器绑定有关的系统调用
- sched_setaffinity():设置task_struct中的cpus_allowed,指定在特定cpu运行
- sched_getaffinity()
- 放弃处理器时间
- sched_yield():暂时让出处理器
系统调用&数据结构
3 系统调用
3.1与内核通信
Linux中,系统调用使用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口
3.2API、POSIX和C库
POSIX是提供API和系统调用对应关系的一套标准,API由C库实现
3.3系统调用
系统调用在出现错误时,C库会把错误码写入errno全局变量,通过调用perror()将变量翻译成用户可以理解的字符串;
通过asmlinkage限定词声明系统调用函数:asmlinkage long sys_getpid(void),asmlinkage通知编译器仅从栈中提取该函数的参数。
- 系统调用号
内核记录了系统调用表中所有已注册过的系统调用的列表,存储在sys_call_table中 - 系统调用的性能
Linux系统调用比其他系统执行要快:1上下文切换时间段,2系统调用处理程序简洁
3.4系统调用处理程序
用户程序通知内核执行系统调用是通过软中断实现:通过引发一个异常来促使系统切换到内核态去执行异常处理程序
- 指定恰当的系统调用
在x86上,系统调用号通过eax寄存器传递给内核,返回值通过eax寄存器传递给用户进程;
system_call()通过将给定的系统调用号与NR_syscalls作比较来检查其有效性,大于或等于NR_syscalls返回-ENOSYS,否则执行相应的系统调用。
- 参数传递
x86-32系统上,ebx、ecx、edx、esi、edi按照顺序存放前五个参数。
3.5系统调用的实现
- 参数验证
copy_to_user():内核向用户空间写入数据
copy_from_user():内核从用户空间读取数据
reboot()系统调用的实现:省略
3.6系统调用上下文
- 绑定一个系统调用的最后步骤
- 在系统调用表最后加入一个表项
- 对于所支持的各种体系结构,系统调用号必须定义在
4 内核数据结构
4.1链表
单向链表、双向链表、环形链表
- Linux内核中的实现
//链表数据结构
struct list_head {
struct list_head *next;
struct list_head *prev;
}
//定义链表:通过嵌入list_head结构体到自己的数据结构中
struct fox {
unsigned long tail_length;
struct list_head list;
}
//定义链表头
static LIST_HEAD(fox_list)
- 操作链表
//增加节点
list_add(struct list_head *new, struct list_head *head)//head->new
list_add_tail(struct list_head *new, struct list_head *head)//head-...->new
//删除节点
list_del(struct list_head *entry)
list_del_init(struct list_head *entry)
//移动节点
list_move(struct list_head *list, struct list_head *head)
list_move_tail(struct list_head *list, struct list_head *head)
//检查链表是否为空
list_empty(struct list_head *list)
//合并两个链表
list_splice(struct list_head *list, struct list_head *head)//head->list...
list_splice_init(struct list_head *list, struct list_head *head)
- 遍历链表
//基本方法
struct list_head *p;
list_for_each(p, list)
//基本方法+contain_of()
list_for_each_entry(pos, head, member)
list_for_each_entry_reverse(pos, head, member)
//遍历的同时删除
list_for_each_entry_safe(pos, next, head, member)
4.2队列
- 创建队列
//动态创建队列
struct kfifo fifo;
int ret;
ret = kfifo_alloc(&fifo, PAGE_SIZE, GFP_KERNEL);
if (ret)
return ret;
- 推入队列数据
unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len);
- 摘取队列数据
unsigned int kfifo_out(struct kfifo *fifo, void *to, unsigned int len);
- 获取队列长度
static inline unsigned int kfifo_size(struct kfifo *fifo);//返回字节数
static inline unsigned int kfifo_avail(struct kfifo *fifo);//查看队列可用空间
static inline unsigned int kfifo_is_empty(struct kfifo *fifo);//空则返回非0
static inline unsigned int kfifo_if_full(struct kfifo *fifo);//满则返回非0
- 重置和撤销队列
static inline void kfifo_reset(struct kfifo *fifo);//抛弃队列中的内容
void kfifo_free(struct kfifo *fifo);//撤销一个使用kfifo_alloc()分配的队列
- 队列使用举例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kfifo-new.h>
#include <asm/page.h>
MODULE_LICENSE("Dual BSD/GPL");
struct kfifo fifo;
static int myfifo_init(void)
{
int ret;
unsigned int val;
unsigned int i;
ret = kfifo_alloc(&fifo, PAGE_SIZE, GFP_KERNEL);
if (ret)
return ret;
for (i = 0; i < 32; i++)
kfifo_in(&fifo, &i, sizeof(i));
ret = kfifo_out_peek(&fifo, &val, sizeof(val));
if (ret != sizeof(val))
return -EINVAL;
printk(KERN_INFO "%u\n", val);
while (kfifo_avail(&fifo)) {
unsigned int val;
ret = kfifo_out(&fifo, &val, sizeof(val));
if (ret != sizeof(val))
return -EINVAL;
printk(KERN_INFO "%u\n", val);
}
return 0;
}
static void myfifo_exit(void)
{
kfifo_free(&fifo);
}
module_init(myfifo_init);
module_exit(myfifo_exit);
4.3映射
映射即关联数组,是一个由唯一键组成的集合,每个键必须关联一个特定的值。
- 初始化一个idr
struct idr id_huh; //静态定义idr结构
idr_init(&id_huh); //初始化idr结构
- 分配一个新的UID
int id;
do {
if (!idr_pre_get(&idr_huh, GFP_KERNEL)) //调整后备树大小,成功返回1
return -ENOSPC;
ret = idr_get_new(idr_huh, ptr, &id); //获取新UID,将其加到idr
} while (ret == -EAGAIN);
- 查找UID
struct my_struct *ptr = idr_find(&idr_huh, id); //调用成功,则返回关联的指针
if (!ptr)
return -EINVAL;
- 删除UID
void idr_remove(struct idr *idp, int id); //将id关联的指针一起从映射中删除
void idr_remove_all(struct idr *idp); //强制删除所有的UID
- 撤销idr
void idr_destroy(struct idr *idp); //释放idr中未使用的内存
4.4二叉树
- 二叉搜索树
- 根的左分支节点值都小于根节点值
- 右分支节点值都大于节点值
- 所有子树也都是二叉搜索树
- 自平衡二叉搜索树
平衡二叉树:所有叶子节点深度差不超过1的二叉搜索树
自平衡二叉树:任何操作都试图维持(半)平衡的二叉搜索树
红黑树
- 所有节点要么红色,要么黑色
- 叶子节点都是黑色
- 叶子节点不包含数据
- 所有非叶子节点都有两个子节点
- 如果一个节点是红色,则其子节点都是黑色
- 在一个节点到其叶子节点的路径中,如果总是包含同样数目的黑色节点,则还路径相对其他路径是最短的
struct rb_root root = RB_ROOT; //初始化根节点
struct rb_node node; //非根节点由rb_node描述
... //搜索、插入操作用户自己实现
4.5数据结构以及选择
对数据集合的主要操作是遍历数据就用链表
代码符合生产者/消费者模式,使用队列
需要映射一个UID到一个对象,使用映射
存储大量数据,要求检索迅速,使用红黑树
4.6算法复杂度
- 算法
一个算法就像一个函数y = f(x) - 大o符号
f(x) = O(g(x))
完成f(x)的时间总是短于或等于完成g(x)的时间和任意常量的乘积 - 时间复杂度
O(g(x)) | 名称 |
---|---|
1 | 恒量(理想的复杂度) |
log n | 对数的 |
n | 线性的 |
n^2 | 平方的 |
n^3 | 立方的 |
2^n | 指数的 |
n! | 阶乘 |
中断
5 中断和中断处理
让硬件在需要的时候向内核发电信号,使高速处理器和低速外设协同工作
中断
中断:异步中断,外部中断(cpu在执行某条指令时发生中断)
异常:同步中断,内部中断(cpu在执行完指令后异常才发生)
中断处理程序
即中断的上半部,每个中断号对应一个中断处理程序,收到相应中断后就开始执行中断处理程序
上半部与下半部
上半部:即中断处理程序,执行紧急的任务,不可休眠
- 如果一个任务对时间非常敏感,将其放在中断处理程序中执行
- 如果一个任务和硬件相关,将其放在中断处理程序中执行
- 如果一个任务要保证不被其他中断打断,将其放在中断处理程序中执行
- 其他所有任务放在下半部执行
下半部:执行相对上半部不紧急的任务
注册中断处理程序
request_irq()可能会休眠,不能再中断上下文中使用
int request_irq(unsigned int irq, //中断号
irq_handler_t handler, //中断处理函数指针
unsigned long flags, //中断处理程序标志
const char *name, //中断设备名
void *dev) //用于共享中断线
void free_irq(unsigned int irq, void *dev)
- 中断处理程序标志
IRQF_DISABLED:执行中断处理函数时,禁止其他所有中断
IRQF_SAMPLE_RANDOM:该设备产生的中断对内核熵池(随机数)有贡献
IRQF_TIMER:系统定时器的中断处理
IRQF_SHARED:一个中断线允许多个中断处理函数 - 中断例子
中断上下文
当执行一个中断处理程序时,内核处于中断上下文;中断处理程序拥有自己的中断栈,每个处理器一个,大小为1页。
中断处理机制的实现
/proc/interrupts
中断线 中断计数器 中断控制器 设备名
中断控制
中断控制是为了同步,通过禁止中断,确保某个中断处理程序不会抢占当前进程。获取锁既防止其他处理器对共享数据的并发访问,也禁止了中断对内核进程的抢占
- 禁止和激活中断
unsigned long flags; //存储中断状态的变量
local_irq_save(flags); //保存当前中断状态
local_irq_disabled(); //禁止中断
//local_irq_enabled(); //或者激活中断
local_irq_restore(flags); //恢复之前的中断状态
- 禁止指定中断线
用于新设备的驱动程序应该倾向于不使用这些接口
void disable_irq(unsigned int irq); //禁止中断控制器上的指定中断线
void disable_irq_nosync(unsigned int irq); //不会等待当前中断处理程序执行完毕
void enable_irq(unsigned int irq); //激活中断控制器上的制定中断线
void synchronize_irq(unsigned int irq); //等待一个特定的中断处理程序退出
- 中断系统的状态
irqs_disabled(); //本地中断传递被禁止,返回非0,否则返回0
in_interrupt(); //处于中断上下文中,返回非0,否则返回0
in_irq(); //当前正在执行中断处理程序,返回非0,否则返回0
6 下半部和推后执行的工作
下半部
下半部的任务就是执行中断处理密切相关但中断处理程序本身不执行的工作
- 为什么要用下半部
缩短中断被屏蔽的时间对系统响应能力至关重要;通常下半部在中断处理程序一返回就会马上运行,下半部执行期间允许所有中断
- 下半部的环境
性能:软中断 > tasklet > 工作队列
软中断
- 软中断的实现
- 编译期间静态分配,由softirq_action结构表示
- 单处理器的软中断之间不会相互抢占,但可以在多处理器上同时执行
- 注册的软中断必须被标记后才能执行,中断处理程序返回前会标志他的软中断,使其在稍后被执行
- 软中断在do_softirq()中执行,如果有待处理的软中断,do_softirq()会遍历每一个,调用他们的处理函数
/*软中断结构体*/
struct softirq_action {
void (*action)(struct softirq_action *);
};
/*软中断处理程序*/
my_softirq->action(my_softirq); //my_softirq指向softirq_vec数组的某项
- 使用软中断
- 分配索引
用枚举类型静态声明软中断,索引表示一种相对优先级 - 注册处理程序
调用open_softirq()注册软中断处理程序 - 触发软中断
调用raise_softirq()函数,调用之前需禁止中断
- 分配索引
tasklet
tasklet的实现
tasklet结构体
struct tasklet_struct { struct tasklet_struct *next; //链表的下一个tasklet unsigned long state; //tasklet的状态 atomic_t count; //引用计数器 void (*func)(unsigned long); //tasklet处理函数 unsigned long data; //tasklet处理函数的参数 }
调度tasklet
tasklet_schedule()和tasklet_hi_schedule()将tasklet调度到tasklet_vec和tasklet_hi_vec链表中,由do_softirq()执行
使用tasklet
声明自己的tasklet
DECLARE_TASKLET(name, func, data) //静态声明 tasklet_init(t, tasklet_handler, dev) //动态声明
编写自己的tasklet处理程序
tasklet处理程序中不能休眠void tasklet_handler(unsigned long data)
调度自己的tasklet
调用tasklet_schedule()函数并传递给他相应的tasklet_struct指针,该tasklet就会被调度tasklet_schedule(&my_tasklet);
ksoftirqd
每个处理器都有一组辅助处理软中断和tasklet的内核线程。当内核中出现大量软中断和tasklet时,这些内核进程就会辅助处理它们。
工作队列
工作队列运行在进程上下文,他通常可以用内核线程替换。但是由于内核开发者们非常反对创建新的内核线程,所以我们也推荐使用工作队列。
工作队列的实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列的任务。它创建的这些内核线程称为工作者线程。表示线程的数据结构
struct workqueue_struct { struct cpu_workqueue_struct cpu_wq[NR_CPUS]; struct list_head list; const char *name; int singlethread; int freezeable; int rt; } struct cpu_workqueue_struct { spinlock_t lock; struct list_head worklist; wait_queue_head_t more_work; struct work_struct *current_struct; struct workqueue_struct *wq task_t *thread; }
表示工作的数据结构
这些结构体被连接成链表,当一个工作者线程被唤醒时,它会执行它的链表上的所有工作,工作完成继续休眠。
struct work_struct { atomic_long_t data; struct list_head entry; work_func_t func; };
使用工作队列
创建推后的工作
DECLARE_WORK(name, void(*func)(void *), void *data); //编译时静态创建 INIT_WORK(struct work_struct *work, void(*func)(void *), void *data); //运行时动态初始化
工作队列处理函数
运行在进程上下文,只有用户进程通过系统调用陷入内核,才能访问用户空间void work_handler(void *data);
对工作进行调度
schedule_work(&work); schedule_delayed_work(&work, delay);
刷新操作
排入队列的工作会在工作者线程下一次被唤醒时执行,为了保证不再有待处理的工作,需要调用以下函数void flush_scheduled_work(void); //队列中所有工作执行完毕后返回 int cancel_delayed_work(struct work_struct *work); //取消任何与work_struct相关的挂起工作
创建新的工作队列
struct workqueue_struct *create_workqueue(const char *name); int queue_work(struct workqueue_struct *wq, struct work_struct *work) flush_workqueue(struct workqueue_struct *wq);
同步
7 内核同步介绍
同步:避免并发、防止竞争条件
临界区和竞争条件
临界区:访问和操作共享数据的代码段
竞争条件:两个执行线程可能处于同一个临界区同时执行
在临界区并发操作共享数据会造成数据不一致
加锁
锁机制可以防止并发执行,防止共享数据遭到破坏。
- 造成并发执行的原因
中断、软中断和tasklet、内核抢占、睡眠(唤醒调度程序重新调度)、对称多处理
- 了解要保护些什么
如果由其他执行线程可以访问这些数据,就给这些数据加上某种形式的锁。记住:要给数据而不是代码加锁
死锁
要有一个或多个执行线程和一个或多个资源(锁),每个线程都在等待其中的一个资源(锁),但所有的资源(锁)都被占用了。
避免死锁规则:
1. 按顺序加锁。使用嵌套的锁时必须保证以相同的顺序获取锁
2. 防止发生饥饿。这个代码的执行是否一定会结束
3. 不要重复请求同一个锁
4. 设计力求简单
争用和扩展性
争用:当锁正在被占用时,有其他线程试图获取该锁。被高度争用的锁会成为系统瓶颈,严重降低系统性能。
扩展性:对系统可扩展性的一个度量,理想情况下,处理器数量加倍应该会使系统处理性能翻倍。而实际上由于锁机制是不可能达到的,锁粒度的精细化能提高系统性能。
8 内核同步方法
原子操作
原子操作是其他同步方法的基石,他通过读取和增加变量的行为包含在一个单步中执行。
- 原子整数操作
让原子函数只接受atomic_t类型的操作数,可以确保原子操作只与这种特殊类型数据一起使用。
typedef struct {
volatile int counter;
} atomic_t; //atomic_t类型定义
atomic_t v; //定义原子整数v
atomic_set(&v, 4); //设置v数值为4
atomic_add(2, &v); //v数值增加2
atomic_inc(&v); //v数值增加1
atomic_read(&v); //将v转换为int型
- 64位原子操作
typedef struct {
volatile long counter;
} atomic64_t; //atomic64_t类型定义
- 原子位操作(操作原子整数和非原子整数)
void set_bit(int nr, void *addr)
void clear_bit(int nr, void *addr)
void change_bit(int nr, void *addr)
int test_bit(int nr, void *addr)
自旋锁
一个执行线程试图获得一个已经持有的自旋锁,那么该线程就会一直进行忙循环,检查自旋锁是否可用。自旋锁不应该被长时间持有。
- 自旋锁方法
DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* 临界区 */
spin_unlock(&mr_lock);
/* 用于中断处理程序 */
DEFINE_SPINLOCK(mr_lock);
unsigned long flags;
spin_lock_irqsave(&mr_lock, flags); //保存本地中断当前状态并获取锁
/* 临界区 */
spin_unlock_irqrestore(&mr_lock, flags);
- 自旋锁和下半部
临界区位置 | 注意事项 |
---|---|
中断处理程序&下半部 | 下半部获取锁并禁止中断 |
下半部&进程上下文 | 进程上下文加锁并禁止下半部 |
多处理器的软中断 | 获取自旋锁即可 |
同类tasklet之间 | 不需要保护 |
不同类tasklet之间 | 获取自旋锁即可 |
读-写自旋锁
一个或多个读任务可以并发地持有读锁,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。
DEFINE_RWLOCK(mr_rwlock);
read_lock(&mr_rwlock);
/* 临界区(只读) */
read_unlock(&mr_rwlock);
DEFINE_RWLOCK(mr_rwlock);
write_lock(&mr_rwlock);
/* 临界区(读写) */
write_unlock(&mr_rwlock);
/* 执行以下两个函数将会死锁 */
read_lock(&mr_rwlock);
write_lock(&mr_rwlock);
信号量
睡眠锁,争用信号量的进程会睡眠,被调度程序调度到等待队列。
- 计数信号量和二值信号量
二值信号量:计数等于1,也叫互斥信号量(内核常用)
计数信号量:计数大于1,允许多个进程持有(不常用) - 创建和初始化信号量
struct semaphore mr_sem;
sema_init(&mr_sem, count);
- 使用信号量
struct semaphor name;
sema_init(&name, 1);
/* 试图获取信号量 */
if (down_interruptible(&mr_sem))
/* 信号被接收,信号量还未获取 */
/* 临界区 */
/* 释放信号量 */
up(&mr_sem);
读-写信号量
static DECLARE_RWSEM(name); //静态声明读-写信号量
down_read(&mr_rwsem); //试图获取信号量用于读
/* 临界区(只读) */
up_read(&mr_rwsem); //释放信号量
down_write(&mr_rwsem); //试图获取信号量用于写
/* 临界区 */
up_write(&mr_rwsem); //释放信号量
互斥体
其行为和互斥信号量相似,但操作接口更简单,实现更高效,使用限制更强。
相比信号量,应优先考虑使用互斥体
DEFINE_MUTEX(name); //静态定义
mutex_init(&mutex); //动态定义
mutex_lock(&mutex);
/* 临界区 */
mutex_unlock(&mutex);
事件驱动&时间驱动
9 定时器和时间管理
系统定时器是一种可编程硬件芯片,它以固定频率产生中断。该中断就是定时器中断,对应的中断处理程序负责更新时间,执行需要周期性运行的任务
内核中的时间概念
通过频率之间的时间间隔计算时间的流逝
节拍率:HZ
系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定,称作节拍率(一般为100HZ)
jiffies
全局变量jiffies用来记录自系统启动以来产生的节拍总数。
硬时钟和定时器
体系结构提供了两种设备进行计时——实时时钟和系统定时器。
- 实时时钟(RTC)
持久存放系统时间的设备,系统启动时,内核通过读取RTC来初始化墙上时间,改时间存放在xtime变量中。 - 系统定时器
提供一种周期性触发中断的机制
定时器
定时器是管理内核流逝的时间的基础,用于推后执行某些代码。
- 使用定时器
//数据结构
struct timer_list {
struct list_head entry; //定时器链表入口
unsigned long expireds; //以jiffies为单位的定时值
void (*function)(unsigned long); //定时器处理函数
unsigned long data; //传给处理函数的长整型参数
struct tvec_t_base_s *base; //定时器内部值,用户不要使用
}
//定义定时器
struct timer_list my_timer;
//初始化定时器数据结构的内部值
init_timer(&my_timer);
//填充结构中需要的值
my_timer.expires = jiffies + delay; //定时器超时节拍数
my_timer.data = 0; //给定时器处理函数传入0值
my_timer.function = my_function; //定时器超时调用函数
//定义定时器处理函数
void my_timer_function(unsigned long data) {
...
}
//激活定时器
add_timer(&my_timer);
//更改已经激活的定时器超时时间
mod_timer(&my_timer, jiffies+new_delay);
//在超时前停止定时器
del_timer_sync(&my_timer);
延迟执行
- 忙等待
unsigned long delay = jiffies + 2 * HZ;
while (timer_before(jiffies, delay))
;
- 短延迟
void udelay(unsigned long usecs)
void ndelay(unsigned long usecs)
void mdelay(unsigned long usecs)
- schedule_timeout()
set_current_state(TASK_INTERRUPTIBLE); //将任务设置为可中断睡眠状态
schedule_timeout(s * HZ); //小睡一会儿,“s”秒后唤醒
内存
10 内存管理
处理器寻址单位:字节
MMU寻址单位:页
页
32位体系结构:4KB页
64位体系结构:8KB页
struct page {
unsigned long flags; //页状态
atomic_t _count; //引用计数器
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual; //页虚拟地址
}
该结构体描述物理内存本身,而非包含在其中的数据
分配内存页时创建struct page,释放时销毁struct page
系统中每个物理页都要分配该结构体
区
区(物理内存概念) | 物理内存<==>虚拟内存 | 描述 |
---|---|---|
ZONE_DMA | 0~16MB<==>3GB~3GB+16MB | 直接映射给DMA使用 |
ZONE_NORMAL | 16~896MB<==>3GB+16MB~3GB+896MB | 正常可寻址页,直接映射,内核直接访问 |
ZONE_HIGHMEM | 896MB~4GB<==>3GB+896MB~4GB or 0~3GB | 动态映射的页 |
获得页
//分配2^order个页,返回第一个页的page结构体
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
//返回给定页的逻辑地址
void *page_address(struct page *page)
//分配2^order个页,返回第一个页的逻辑地址
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
- 获得填充为0的页
//返回的页内容全为0
unsigned long get_zeroed_page(unsigned int gfp_mask)
- 释放页
void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr, unsigned int order)
void free_pages(unsigned long addr)
分配内存(字节为单位)
- kmalloc():分配物理地址上连续的内存
void *kmalloc(size_t size, gfp_t flags)
void kfree(const void *ptr)
- vmalloc():分配逻辑地址上连续的内存
void *vmalloc(unsigned long size)
void vfree(const void *addr)
slab层
slab相当于通用数据结构的缓存层,由一个或多个物理上连续的页组成,每个高速缓存可以有多个slab组成。
//创建高速缓存
struct kmem_cache *kmem_cache_create(const char *name,
size_t size,
size_t align,
unsigned long flags,
void (*ctor)(void*));
//撤销高速缓存
int kmem_cache_destroy(struct kmem_cache *cachep);
//从高速缓存中分配
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
//释放从高速缓存中分配的对象
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
在栈上的静态分配
每个进程都有两页的内核栈
- 单页内核栈
随着机器运行时间增加,物理内存逐渐变为碎片,给一个新进程分配虚拟内存的压力也在增加。
当我们使用只有一个页面的内核栈时,中断处理程序就不放在栈中了,而是使用中断栈。 - 在栈上光明正大的工作
高端内存的映射
- 永久映射
//映射一个给定的page结构到内核地址空间
void *kmap(struct page *page) //只能用在进程上下文,会睡眠
void kumap(struct page *page)
- 临时映射(原子映射)
//建立临时映射
void *kmap_atomic(struct page *page, enum km_type type) //可以用在不能睡眠的地方
void kunmap_atomic(void *kvaddr, enum km_type type)
每个CPU的分配
新的每个CPU接口
- 编译时的每个CPU数据
- 运行时的每个CPU数据