04_进程管理

发布时间 2024-01-10 13:16:23作者: Qing-Huan

0. 写在前面

  1. 进程与线程的关系
  2. Linux如何存放和表示进程(tast_struct、thread_info)
  3. 如何创建进程(fork()、最终调用clone())
  4. 如何把新的执行映像装入到地址空间(exec()族)
  5. 如何表示进程的层次关系,父进程如何收集后代信息(wait()系统调用族)
  6. 进程是如何消亡的(exit())

一、进程描述符

每个进程都是内核维护的一个结构体:task_struct,众多进程也就是众多结构体组成双向循环链表。

二、进程描述符 —— slab分配

Linux通过slab分配器分配task_struct结构。

三、进程描述符 —— 存放

  1. pid 是每个进程的唯一标识符,最大值默认为32768,通过修改/proc/sys/kernel/pid_max 提高上限。
  2. 有的硬件体系结构有专门寄存器来存放指向当前进程 task_struct 的指针,用于加速访问。x86这种体系结构,只能在内核栈的尾端创建 thread_info 结构,通过计算偏移间接查找 task_struct 结构。

四、进程的五态(三态是进程调度)

  • 当fork一个子进程时,子进程进入就绪态,当进程调度该子进程时,进入运行态。此时分为四种情况:
  1. CPU时间片耗尽,回到就绪态。
  2. 进程结束后,进入僵尸态,需要父进程调用 wait waitpid 释放进程描述符。
  3. 进程接收到 SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU 信号,进入停止态。若接收到 SIGCONT 信号,会重新进入就绪态
  4. 进程未申请到所需资源,进入等待态,等待态分为两种情况
    4.1 可中断等待态,所需资源被满足或被信号、中断唤醒,重新进入就绪态
    4.2 不可中断等待态,只有资源被满足才会重新进入就绪态。
  • 设置进程状态的函数:set_task_state(task, state)

五、进程上下文 —— 系统调用

系统调用中断是应用层陷入内核层的方法。
当一个程序执行了系统调用,或触发了某个异常,它就陷入了内核空间。此时称内核“代表进程执行”并处于进程上下文中。使用current宏定位task_struct指针。

六、进程家族树

  1. 获得父进程描述符
    struct task_struct *my_parent = current->parent;
  2. 访问子进程
struct task_struct *task;
struct list_head *list;
list_for_each(list, &current->children) {
	task = list_entry(list, struct task+struct, sibling);
	// task指向当前的某个子进程
}
  1. init进程的进程描述符是作为init_task静态分配的。
struct task_struct *task;
for(task = current; task != &init_task; task = task->parent);
// task指向init
  1. 获取链表中的下一个进程
    list_entry(task->task.next, struct task_struct, tasks)
  2. 获取链表中的上一个进程
    list_entry(task->task.prev, struct task_struct, tasks)
  3. 循环访问整个任务队列
struct task_struct *task;
for_each_process(task) {
	printk("%s[%d]\n", task->comm, task->pid);
}
  1. 通过摘链方法隐藏进程

七、读时共享/写时复制

  • 在调用 fork() 时先复制数据,这种复制是通过页面映射机制实现的,此时地址空间数据是只读的。
    1. 当父/子对数据进行写操作时,会触发页错误,此时内核会真正复制整个进程地址空间。
    2. 若此时调用exec(),它就无需复制了。
  • fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

八、Linux通过clone()系统调用实现fork(),然后由clone()调用do_fork()

  1. fork ---> sys_fork ---> ... ---> do_fork
    读时共享,写时复制:内核对父进程的每个内存页都为子进程创建一个相同的副本
  2. vfork ---> sys_vfork ---> ... ---> do_fork
    父子进程间共享数据,节省大量CPU时间(如果一个进程操纵共享数据,另一个会被影响)
    子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()
  3. clone ---> sys_clone ---> ... ---> do_fork
    clone产生线程,可以对父子进程间的共享、复制进行精准控制

九、线程是特殊的进程

线程的创建和普通进程的创建类似,之色在调用clone()的时候需要传一些参数标志指明需要共享的资源:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
共享地址空间、文件系统资源、文件描述符、信号处理程序
fork()的实现:
clone(SIGCHLD, 0);
vfork()的实现:
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
调用vfork(),所以父进程准备睡眠等待子进程将其唤醒,共享地址空间

十、内核线程

内核线程和普通进程间的区别在于内核线程没有独立的地址空间(指向地址空间的mm指针被置为NULL)

  • 内核线程创建并运行
#define kthread_run(threadfn, data, namefmt, ...)			   \
({									   \
	struct task_struct *__k						   \
		= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
	if (!IS_ERR(__k))						   \
		wake_up_process(__k);					   \
	__k;								   \
})
  • 内核线程退出
int kthread_stop(struct task_struct *k);