Process Management

process 是运行中的程序以及其相关资源,它是程序代码运行的结果

thread 是 objects 在进程内的活动,是内核调度的基本单位。每个 thread 包含独自地程序计数器、栈、寄存器等资源,thread 共享进程的资源,比如虚拟内存。对 Linux 来说,并不区分线程与进程,线程只是一种特殊的进程,可以与其他进程共享资源

对其它系统来说,线程相当于轻量级的进程,相比于进程消耗更小,并且可以快速执行

进程提供了两种虚拟化:

  • virtualized processor,让进程认为自己独占系统
  • virtual memory,让进程认为自己独占整个内存

进程 API

  • exit,退出进程并释放资源
  • wait,父进程等待子进程退出,可以利用 wait 获取子进程的退出状态。子进程退出后,父进程调用 wait 前处于 zombie state

Process Descriptor

进程相关信息被封装到 task_struct 中,并通过进程描述符(process descriptor)。进程描述符通过引用双向链表进行连接,被称为 task list(linux/sched.h)。

task_struct 是通过 slab allocator 来分配的, thread_info 存储这其引用。为了快速获取当前运行中的进程,定义了 current macro。一些系统使用寄存器保存其位置,但是 x86 为了节省寄存器,将 thread_info 存储在栈底部(取 sp 寄存器的低 13 位),并通过 thread_info 获取 task_struct

1
2
movl $-8192, %eax
andl %esp, %eax

进程通常在用户空间运行,当触发 exception 或 system call 是会进入内核空间,此时 kernel 处于 process context 中,可以通过 current macro 获取当前进程

进程描述符的最大数量可以通过修改 /proc/sys/kernel/pid_max 来进行修改,默认为 32771。进程描述符的最大数量限制了系统能够同时存在的进程数量。

Process State

系统中每个进程都处于以下 5 中状态中的一种

  • TASK_RUNNING:The Process is runnable。要么正在运行,要么在运行队列中等待运行。在用户空间执行的进程的唯一状态
  • TASK_INTERRUPTIBLE:The Process is sleeping(blocked), 等待某个条件。一旦条件成立,转变为 TASK_RUNNING
  • TASK_UNINTERRUPTIBLE:与 TASK_INTERRUPTIBLE 等价,除了并不会被唤醒。用于必须等待并且不能被中断或者 event 很快就会完成的场景
  • __TASK_TRACED:进程正在被另一个进程 traced,比如 debug ptrace
  • __TASK_STOPPED:进程停止执行。接收到 SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 时会进入该状态

可以通过 set_task_state(task, state) 修改进程的状态

Process Creation

大多数 OS 采用 spawn 机制来创建进程并执行进程。Unix 将这两个行为拆分为两个函数 fork(), exec()

  • fork,创建一个进程,其内容是父进程的复制,仅有 PID, PPID 和某些资源和统计信息(比如 pending signals 不继承)不同。fork 返回两次
  • exec family,创建一个新的地址空间,并加载可执行程序开始执行

copy-on-write:fork 时延迟或或组织数据的复制,其数据对读操作来说是共享的。对于写操作,会为父子进程分别复制。如果 fork 立马 exec,那么就不需要复制。因此采用 COW 机制的 fork 只需要复制父进程的 page table 并为子进程创建进程描述符

  1. 调用 dup_task_struct()创建内核栈、thread_infotask_struct,这些值与当前进程一样
  2. 检查子进程是否会超过资源的限制
  3. 将进程描述符设置为初始值,使之与父进程区分开来
  4. 将子进程的状态设置为 TASK_UNINTERRUPTIBLE
  5. copy_process 调用 copy_flags 更新 task_struct 的 flags 成员
  6. 调用 alloc_pid() 分配新的 PID
  7. 拷贝或共享(根据传递给 clone 的 flags)相关资源
  8. copy_process 清理并返回子进程的指针

子进程创建完成后会被唤醒,通常会让子进程先运行(因为通常子进程会调用 exec,这样就不需要复制 data 了。如果先让父进程写数据,那么会产生复制开销)

vfokefork 类似,但是不会复制 page table entries,直到子进程调用 exec 或退出前,父进程会一直阻塞

线程创建与进程相同,只不过调用 clone 时传递的 flags 不同(clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);),其地址空间、文件系统资源、文件描述符和 信号 handlers 都是共享的

kernel 通过 kernel thread 完成某些后台工作。kernel thread 存在于内核空间中,他们没有地址空间(指向 NULL),他们只在内核空间中进行操作,不会切换回用户空间。可以通过 ps -ef 显示 kernel thread

Process Termination

当进程终止时,会释放所拥有的资源,并向其父进程发送通知

  1. 设置 task_structPF_EXITING flags 成员
  2. 调用 del_timer_sync() 移除 kernel timer。
  3. 如果 BSD Process accounting is enable,调用 acct_update_integrals()
  4. 调用 exit_mm() 释放进程所持有的 mm_struct,如果没有其他进程在使用这个地址空间,kernel 会将其销毁
  5. 调用 exit_sem(),如果进程重在排队等待 IPC 信号,将其出队
  6. 调用 exit_files()exit_fs 减少对文件描述符和文件系统数据的引用,如果引用数减为 0 就销毁
  7. 设置 exit code(存储在 task_struct 中),这个 exit code 会被父进程获取
  8. 调用 exit_notify() 给父进程发送信号,并将其下的 children 的 parent 定义为线程组的另一个线程或 init 进程。然后将自己的exit_state 设置为 EXIT_ZOMBIE
  9. do_exit() 调用 schedule() 切换到新的进程

总结一下就是移除 kernel timer、释放内存、文件描述符、IPC 信号,并设置退出 code,并传递给父进程。进程终止后会变为僵尸进程,但是还保留着 内核栈、thread_infotask_struct,这是为了向其父进程提供信息。当父进程接收到信号后,会将这些信息也释放掉

如果父进程在子进程之前退出,这些子进程就会变为孤儿进程(Parentless),因此需要为这些孤儿进程重新定义父级,否则这些孤儿进程就会一直被保留,占用资源。有两种解决方法

  • 让当前线程组中的一个线程称为其父级
  • 让 init 进程成为其父级