读书目的:了解内核编程基础,为学习《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系统调用处理程序
用户程序通知内核执行系统调用是通过软中断实现:通过引发一个异常来促使系统切换到内核态去执行异常处理程序
-