《Linux内核设计与实现》课本第三章自学笔记——20135203齐岳

时间:2023-12-29 12:27:56

《Linux内核设计与实现》课本第三章自学笔记

进程管理

By20135203齐岳

进程

  • 进程:处于执行期的程序。包括代码段和打开的文件、挂起的信号、内核内部数据、处理器状态一个或多个具有内存映射的内存地址空间或执行线程等其他资源。

  • 线程:是在进程中活动的对象。每个线程都有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程而非进程。操作系统中进程提供两种虚拟机制:虚拟存储器和虚拟内存。

  • 程序:本身并不是进程,进程是处于执行期的程序以及相关的资源的总称。不同的进程可以执行同一个程序。

  • 在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。包含在同一个进程中的线程可以共享虚拟内存,但是每个都拥有各自的虚拟处理器。

进程描述符及任务结构

内核把进程的列表存放在叫做任务队列的双向循环链表中。链表中的每一项都是进程描述符。类型为task_struct,里面包含的数据有:它打开的文件、进程的地址空间、挂起的信号、进程的状态以及其他相关信息。

《Linux内核设计与实现》课本第三章自学笔记——20135203齐岳

分配进程描述符

Linux通过slab分配器分配task_struct结构——能达到对象复用和缓存着色的目的。

由于通过slab分配器动态生成,只需在栈底或者栈顶创建一个新的结构struct thread_info。

每个任务的thread_info结构在它的内核栈的尾端分配。

结构中task域中存放的是指向该任务实际task_struct的指针。

《Linux内核设计与实现》课本第三章自学笔记——20135203齐岳

进程描述符的存放

内核通过一个唯一的进程标识值PID来标识每个进程。

pid类型为pid_t,实际上就是一个int类型。内核把每个进程的pid存放在各自的进程描述符中。

pid最大值默认设置为32768,通过修改/proc/sys/kernel/pid_max可以提高上限。

获得指向task_struct指针的方法:

通过current宏查找到当前正在运行进程的进程描述符。
在内核栈的尾端创建thread_info结构
current通过current_thread_info()把栈指针的后13个有效位屏蔽用来计算出thread_info的偏移。
从thread_info的task域中提取并返回task_struct的地址。

进程状态

进程描述符中的state域描述了进程的当前状态。系统中的每个进程都必然属于下列的五种状态之一:

  • TASK_RUNNING(运行):进程是可执行的,或者正在执行,或者在运行队列中等待执行。是进程在用户空间中执行的唯一可能的状态。

  • TASK_INTERRUPTIBLE(可中断):进程正在睡眠/被阻塞。处于此状态的进程会因为收到信号而被唤醒从而准备投入运行。

  • TASK_UNINTERRUPTIBLE(不可中断):睡眠/被阻塞进程不被信号唤醒。这个状态通常在进程必须等待时不受干扰或等待时间很快就会发生时出现。

  • TASK_TRACED:被其他进程跟踪的进程。

  • TASK_STOPPED(停止):进程停止执行;进程没有投入运行也不能投入运行。接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号时,或者调试时收到任何信号,都可以进入这种状态。

《Linux内核设计与实现》课本第三章自学笔记——20135203齐岳

设置当前进程状态

内核需要使用set_task_state(task,state)函数来调整某个进程的状态。

set_task_state(task,state); //将任务task的状态设置为state
一般情况下等价于task->state=state; set_current_state(state)//等价于set_task_state(current,state)

进程上下文

程序执行系统调用或者触发异常后,会陷入内核空间,这时候内核代表进程执行,并且处于进程上下文中。

进程对内核的访问必须通过接口:系统调用和异常处理程序。

进程家族树

所有的进程都是pid为1的init进程的后代。

内核在系统启动的最后阶段启动init进程。

系统中的每一个进程必有一个父进程,可以拥有0个或多个子进程,拥有同一个父进程的进程叫做兄弟。

进程间的关系存放在进程描述符中,parent指针指向父进程task_struct,children是子进程链表。

获得父进程的进程描述符:

struct task_struct *my_parent = current->parent;

访问子进程:

struct task_struct *task;
struct list_head *list; list_for_each(list, &current->children){
task = list_entry(list, struct task_struct, sibling);
/* task现在指向当前的某个子进程 */
}

init进程的进程描述符是作为init_task静态分配的。所有进程之间的关系:

struct task_struct *task;
for(task=current;task1=&init_task;task=task->parent)
;
/*task现在指向init*/

获取链表中的下一个进程:

list_entry(task->tasks.next, struct task_struct, tasks);

获取链表中的上一个进程:

list_entry(task->tasks.prev, struct task_struct, tasks);

以上依赖于next_task(task)和prev_task(task)这两个宏实现。for_each_process(task)宏,依次访问整个任务队列,每次访问任务指针都指向链表中的下一个元素。

struct task_struct *task;

for_each_process(task){
/* 它打印出每一个任务的名称和pid */
printk("%s[%d]\n",task->comm, task->pid);
}

进程创建

Unix的进程创建机制:

  • fork():通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅在于PID,PPID和某些资源和统计量

  • exec():读取可执行文件并将其载入地址空间开始运行。

写时拷贝

写时拷贝是一种可以推迟甚至免除拷贝数据的技术,内核不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝。

资源的复制只有在需要写入时才会进行,在此之前以只读方式读取。

fork的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符,进程创建后都会马上运行一个可执行的文件,避免拷贝大量不会被使用的数据,从而实现优化。

fork()

Linux通过clone()系统调用实现fork()。实现过程如下:

  • fork()、vfork()、 _ clone()都根据各需要的参数标志调用clone()。由clone()去调用do _ fork()。

  • do _ fork()调用copy _ process()函数,然后让进程开始运行。

  • 返回do _ fork()函数,如果copy _ process()函数成功返回,新创建的子进程被唤醒并让其投入运行。

一般内核会选择子进程首先执行。因为子进程会马上调用exec()函数,避免写时拷贝的额外开销。

vfork()

除了不拷贝父进程的页表项之外,vfork()系统调用和fork()的功能相同。理想情况下不要调用vfork()。

子进程作为父进程的一个单独的线程在它的地址空间里运行 ,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址空间写入。

vfork()系统调用的实现是通过向clone()传递一个特殊标志来进行的。

  • 调用copy _ process()是,task _ struct的vfor _ done成员被设置为NULL。

  • 执行do _ fork()时,如果给定特定标志,则vfor _ done会指向一个特定地址。

  • 子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,知道子进程通过vfor _ done指针向它发送信号。

    在调用mm _ release()时,该函数用于进程退出内存地址空间,并且检查vfor _ done是否为空,如果不为空,则会向父进程发送信号。

  • 回到do_fork(),父进程醒来并返回。

线程在Linux中的实现

线程机制是现代编程技术中常用的一种抽象概念,该机制提供了在同一程序内共享内存地址空间运行的一组线程,可以共享打开的文件和其他资源,支持并发程序设计,在多处理器系统上可以保证真正的并行处理。

Linux内核的角度来看并没有线程这个概念,它把所有线程都当做进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。

对于Linux来说,线程只是一种进程间共享资源的手段。

创建线程

线程的创建和普通进程的创建类似,只不过在调用clone()时需要传递一些参数标志来指明需要共享的资源:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
/*共享地址空间、文件系统资源、文件描述符和信号处理程序。*/

普通的fork:

clone(SIGCHLD, 0);

vfork():

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类。参数标志如下:

《Linux内核设计与实现》课本第三章自学笔记——20135203齐岳

内核线程

内核线程:独立运行在内核空间的标准进程。

内核线程没有独立的地址空间,只在内核空间运行,从来不切换到用户空间,可以被调度和抢占。

内核线程只能由其他内核线程创建:

struct task_struct *kthread_creat(int (*threadfn)(void *data),
void *data,const char namefmt[],...)

kthread _ create()通过clone()系统调用创建内核线程后,处于不可运行状态,如果不通过wake _ up _ process()明确唤醒它,不会主动运行。创建一个进程并让它运行起来 ,可以调用kthread _ run()。

struct task_struct *kthread_run(int (*threadfn)(void *data),
void *data,const char namefmt[],...)

内核线程启动后就一直运行直到调用do _ exit()退出,或者内核的其他部分调用kthread _ stop()退出,传递给kthread _ stop()的参数为kthread _ create()函数返回的task _ struct结构的地址。

int kthread_stop(struct task_struct *k)

进程终结

进程终结时,内核必须释放它所占有的资源并告知父进程。

进程终结的原因:一般是来自自身,发生在调用exit()系统调用时。

  • 显式的调用
  • 隐式的从某个程序的主函数返回

大部分依赖于do_exit()来完成。

删除进程描述符

释放task_struct结构发生在父进程获得已终结的子进程信息并且通知内核不关注后,需要的系统调用是wait4():

挂起调用它的进程,直到其中的一个子进程退出,此时函数返回该子进程的PID。

释放进程描述符时,需要调用release_task()。

孤儿进程

如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态。

解决方法有:

  1. 在当前进程组找一个线程作为养父
  2. 让init成为它们的父进程。

具体实现的过程:

  • do _ exit()调用exit _ notify()
  • exit _ notify()会调用forget _ original _ parent()
  • forget _ original _ parent()会调用find _ new _ reaper()
  • 遍历所有子进程并为它们设置新的父进程。
  • 调用ptrace _ exit _ finish()同样进行新的寻父过程,是给ptraced的子进程寻找父亲。
  • init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。