Blog icon indicating copy to clipboard operation
Blog copied to clipboard

Chapter 3 Process Management

Open jason--liu opened this issue 3 years ago • 0 comments

进程

进程是程序的运行态;包含各种资源,如文件,信号,内存等。线程(Threads)是内核的调度单位。

进程创建

进程通过fork系统调用复制一份(除了pending signals)现有进程创建出来。fork会返回两次,一次在父进程中,一次在新生进程中。 在fork后,通常需要运行新的程序,这通过exec族函数来实现;exec会创建新的address space来加载新程序。

Copy-On-Write

传统行为上,一旦fork,父进程的资源都会拷贝一份给子进程,这太低效了。在Linux中,fork使得父子进程可以以只读方式共享资源,当写的时候会触发异常然后将对应数据拷贝一份。fork的唯一开销是父进程的页表也会复制一份。

Fork流程

fork实际上是调用clone系统调用传入不同的Flag,最终调用到do_fork,再调用copy_process:

  1. 调用 dup_task_struct(),给新进程创建新的内核栈
  2. 然后检查新进程是否超过当前用户的进程数资源限制
  3. task_struct中部分资源清理或设置为初始值,以此和父进程区分开
  4. 子进程状态被设置为TASK_UNINTERRUPTIBLE
  5. copy_process()调用copy_flags()更新task_structflags,代表超级权限的PF_SUPERPRIV被清理,表示没有调用exec的PF_FORKNOEXEC被设置
  6. 调用alloc_pid()分配新的PID
  7. 根据clone()传入的参数不同,来拷贝不同的资源
  8. 最后,copy_process()返回新的进程的指针

如果copy_process成功返回,新进程会立即唤醒并执行。内核故意让子进程优先运行。这是考虑到一个简单的场景。在子进程简单地立即调用 exec() 的常见情况下,这消除了如果父进程首先运行并开始写入地址空间时会发生的任何写时复制开销”

进程退出

进程退出时会调用exit系统调用,这个函数会终止进程并进行资源回收,父进程可以用wait来获取子进程的终止状态。

进程描述符

内核将进程存储在称为task list的双向循环链表中,struct task_struct描述了进程包含的所有信息,task_struct通过slab allocator分配。 image 系统通过PID来标识每一个进程,系统支持的最大进程数可通过/proc/sys/kernel/pid_max调整。系统通过current来获取当前进程。

进程状态

task_structstate域描述了当前进程的状态,由下面五个Flag标识。

  • TASK_RUNNING 进程处于running态,或者在run-queue上waiting to run
  • TASK_INTERRUPTIBLE 进程处于睡眠态,可以接收信号
  • TASK_UNINTERRUPTIBLE 进程也是睡眠态,但不能接收信号
  • __TASK_TRACED 进程正在被调试追踪,比如ptrace
  • __TASK_STOPPED 进程停止执行,也不能继续执行,当进程收到如SIGSTOPSIGTSTPSIGTTIN, SIGTTOU,或者在调试模式下收到任何信号。

进程状态转换关系如下 image 可以使用如下接口函数设置进程状态:

set_task_state(task, state)   /* set task ‘task’ to state ‘state’ */
set_current_state(state)    /* set current task  to state ‘state’ */

相比于直接task->state = **state;set_current_state提供了内存屏障机制来保证在SMP情况下同步

进程上下文

什么是进程上下文?

When a program executes a system call or triggers an exception, it enters kernel-space.At this point, the kernel is said to be “executing on behalf of the process” and is in process context

系统调用异常处理是用户态程序进入内核态的唯一接口。

进程树

所有进程都是init进程的后代,其PID为1。task_structparent域指向父进程,children域是指向子进程的链表,如下代码片遍历进程所有的子进程。

struct task_struct *task;
struct list_head *list;
list_for_each(list, &current->children) {
    task = list_entry(list, struct task_struct, sibling);
    /* task now points to one of current’s children */
}

遍历到init进程

struct task_struct *task;
for (task = current; task != &init_task; task = task->parent)
;
/* task now points to init */

遍历所有进程

struct task_struct *task;
for_each_process(task) {
/* this pointlessly prints the name and PID of each task */
printk(“%s[%d]\n”, task->comm, task->pid);
}

线程

创建线程

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)

Clone标志位 image

内核线程

内核线程没有address space,也就是mm= NULL,内核线程的创建都是通过fork kthreadd来实现的。

// <linux/kthread.h>
struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char namefmt[],...)

通过这个接口创建的内核线程是unrunnable state,除非调用了wake_up_process。内核线程可以如下接口

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

来实现创建后runnable。其实现如下

#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *k; \
\
k = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(k)) \
wake_up_process(k); \
k; \
})

kernel线程会一直运行直到调用了do_exit()或者kthread_stop()

进程终止

进程终止分为自愿(如调用exit)和非自愿(如发生异常,或收到信号),但最终都会调用到do_exit

  1. 设置task_structPF_EXITING flag
  2. 调用del_timer_sync()删除定时器,确保没有定时器处理函数在运行
  3. 如果BSD记账被打开,调用acct_update_integrals()更新记账信息
  4. 调用exit_mm释放mm_struct,如果没有其他进程(即不是share)在用、它,内核销毁它
  5. 调用exit_sem,删除相关IPC 信号量
  6. It then calls exit_files() and exit_fs() to decrement the usage count of objects related to file descriptors and filesystem data, respectively.
  7. 设置exit_code
  8. 调用 exit_notify()给父进程发生信号,reparents当前进程的其他子进程,设置exit_stateEXIT_ZOMBIE
  9. 调用schedule()切换进程,do_exit不会返回

当父进程手动子进程退出信息后,子进程的内核栈,task_struct所占的内存才会被释放,此时release_task()被调用

  1. 调用__exit_signal(),然后调用 __unhash_process(),再调用detach_pid()将进程从pidhash和task list中移除
  2. __exit_signal()释放剩余资源
  3. If the task was the last member of a thread group, and the leader is a zombie, then release_task() notifies the zombie leader’s parent
  4. release_task() calls put_task_struct() to free the pages containing the process’s kernel stack and thread_info structure and deallocate the slab cache containing the task_struct.

jason--liu avatar Jul 02 '21 07:07 jason--liu