进程管理

进程数据结构

进程的数据结构是task struct

  1. cpu资源:
    1. 调度优先级
  2. 内存地址空间资源:
    1. mm_struct
  3. 打开的文件资源:
    1. file_struct files (一个数组, 存的就是打开的文件的地址, 索引即是文件描符 fd)

进程自己的信息与状态

  1. 进程状态: 存储在task->state, task->exit_state两个字段中
    1. 如TASK_RUNNING, TASK_INTERRUPTIBLE, TASK_UNINTERRUPTIBLE, __TASK_STOPED…
  2. 唯一ID
    1. pid: 线程级别的id
    2. gtid: 进程级别的id
  3. 文件系统信息 struct fs_struct *fs
  4. namespace
  5. 进程树关系: 该进程在整个进程树里面的位置

进程的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Used in tsk->state: */
#define TASK_RUNNING 0x00000000
#define TASK_INTERRUPTIBLE 0x00000001
#define TASK_UNINTERRUPTIBLE 0x00000002
#define __TASK_STOPPED 0x00000004
#define __TASK_TRACED 0x00000008
/* Used in tsk->exit_state: */
#define EXIT_DEAD 0x00000010
#define EXIT_ZOMBIE 0x00000020
/* Used in tsk->state again: */
#define TASK_DEAD 0x00000080

/* 组合状态(便利宏): */
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED)
#define TASK_IDLE (TASK_UNINTERRUPTIBLE | TASK_NOLOAD)
#define TASK_NORMAL (TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE)
// ...

常见状态语义说明:

存储在task->state的

  • TASK_RUNNING: 进程正在运行队列中, 准备运行/正在运行
  • TASK_NORAML/TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE:
    • TASK_NORMAL = (TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE) 是这两个状态的综合, 表明进程正在睡眠
    • TASK_INTERRUPTIBLE: 进程处于可打断的睡眠状态, 正在等待某个条件满足, 被wake_up唤醒, 或者被信号唤醒. 不会被计入load average
    • TASK_UNINTERRUPTIBLE: 进程处于不可中断的睡眠状态, 只能在条件满足时, 被wake_up唤醒, 不能被信号唤醒, 常用于(如磁盘 I/O)等不能被中途打断的SLEEP, 这也代表, 在这个状态的进程的数量能一定程度反应当前计算机的物理负载, 这个状态会被计入load average
    • TASK_KILLABLE = (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) 可以被致命信号杀死(SIGKILL)的不可中断睡眠
    • __TASK_STOPPED = 进程被暂停执行了, 不会被调度. 1. 收到SIGSTOP / SIGSTP / SIGTTIN / SIGTTOU 信号 2. ptrace attach 后发送 SIGSTOP. 常见的进入情景: 1. Ctrl + z 2. debug. 退出的时机: 收到 SIGCONT 信号
    • __TASK_TRACED = 进程正在被调试器跟踪
    • TASK_IDLE (TASK_UNINTERRUPTIBLE | TASK_NOLOAD):
      • 语义: TASK_UNINTERRUPTIBLE类似的不可中断睡眠, 但不计入 load average
      • 内核中空闲时会运行的 idle线程, 内核中某些无关紧要的等待
      • 如果 idle线程用TASK_UNINTERRUPTIBLE而包含TASK_NOLOAD会导致load average空长, 不能反应真实物理资源负载

存储在task->exit_code的

  • EXIT_ZOMBIE: 僵尸状态
    • 语义: 代码已经执行完毕
    • 进入的时机: 进程调用exit() -> do_exit() -> exit_notify() 中设置 tsk->exit_state = EXIT_ZOMBIE
    • 退出的时机: 父进程调用 wait / waitpid() 读取退出状态 -> wait_task_zombie() -> exit_state中改为 EXIT_DEAD -> task_struct 被释放
  • EXIT_DEAD (0x10) : 彻底死亡
    • 语义: 进程的task_struct正在被回收或者已经被回收
    • 进入的时机: 父进程 wait() 成功后, 状态从 EXIT_ZOMBIE 变成 EXIT_DEAD
    • 退出的时机: 这是一个瞬态, 存在的时间极短, 之后 task_struct 被回收

常见的进程类型

  • 僵尸进程: 进程(task_struct)已经被释放, 但是还保留PID和exit_state为EXIT_ZOMBIE

    • 为什么需要这个状态: 父进程可能需要在进程被关闭以后, 获取进程的PID并检查它的退出码来衡量进程是不是正常退出的
    • 资源泄漏: 状态为EXIT_ZOMBIE的进程占用一个PID和少量的内核内存. 如果大量堆积, 会导致内核内存泄漏, 或PID耗尽
    • 怎么产生的: 父进程没有调用wait()或者waitpid()
    • 解决方式:
      • 父进程调用 wait() / waitpid()
      • 设置 signal(SIGCHLD, SIG_IGN) 让内核自动回收 (do_notigy_parent返回true的时候直接设置状态为EXIT_DEAD)
      • 杀死父进程 -> 僵尸被init(PID=1)收养 -> init会自动 wait()清理
  • 孤儿进程: 进程的父进程先于子进程被杀死了, 子进程就成为了孤儿进程

    • 内核会将孤儿进程的父进程指向init, 或者当前 PID namespace 中的 subreaper 进程, init进程会定期调用 wait()来清理
    • 本身是正常的, 不会造成问题
  • 守护进程: 在后台运行的, 不再与任何终端关联的长期运行的进程. 如 sshd, nginx

    • 不是一个内核概念, 而是一个用户空间的编程的概念
    • 经典的创建过程: double-fork
      • fork() - 创建子进程
      • 父进程退出 - 子进程被托孤给init
      • setsid - 创建新的session (和原终端解绑)
      • 再次 fork() - 确保不会重新获得中断控制能力
      • chdir(“/) - 避免占用可卸载的文件系统
      • 关闭/重定向 stdin/stdout/stderr

进程的唯一ID

tgid和pid, 为什么要有两个ID
经典的用途是区分进程和线程, 本身是进程组概念, 父进程和他fork出来的子进程组成一个进程组, 共享一个tgid.
他们的id (pid, tgid)会是 (1, 1), (2, 1), (3, 1). 对于操作系统来说, 只有进程这个概念, 线程实际上是一个轻量级进程, 对于操作系统的调度来说, 进程和线程是同等地位的实体, 唯一通过PID来作为唯一标识, 而tgid是标识”进程”而非”线程”的唯一标识.

进程树