跳转至

进程管理

约 721 个字 57 行代码 3 张图片 预计阅读时间 3 分钟

内核态和用户态

现代处理器架构一般允许 CPU 至少在两种不同状态下运行,即:用户态和核心态(有时也称之为监管态 supervisor mode)。执行硬件指令可使 CPU 在两种状态间来回切换。与之对应,可将虚拟内存区域划分(标记)为用户空间部分或内核空间部分。在用户态下运行时,CPU 只能访问被标记为用户空间的内存,试图访问属于内核空间的内存会引发硬件异常。当运行于核心态时,CPU 既能访问用户空间内存,也能访问内核空间内存。

仅当处理器在核心态运行时,才能执行某些特定操作。这样的例子包括:执行宕机(halt)指令去关闭系统,访问内存管理硬件,以及设备 I/O 操作的初始化等。实现者们利用这一硬件设计,将操作系统置于内核空间。这确保了用户进程既不能访问内核指令和数据结构,也无法执行不利于系统运行的操作。

Linux将task_struct中的thread_info线程信息和对应的内核栈紧凑的放在一起,例如占用连续的2个页共8K。从用户态刚切换到内核态后,进程的内核栈总是空的。内核栈位于内核直接映射区(虚拟地址=物理地址+PAGE_OFFSET,不需要页表管理,不需要缺页异常,直接线性映射物理内存),类似的每个CPU还有独立的中断栈用于中断处理程序使用。

struct task_struct {
    /**
     * 进程状态。
     */
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    /**
     * 进程的基本信息。
     */
    struct thread_info *thread_info;
    ....
}
struct thread_info {
    unsigned long       flags;      /* low level flags */
    mm_segment_t        addr_limit; /* address limit */
    struct task_struct  *task;      /* main task structure */
    struct exec_domain  *exec_domain;   /* execution domain */
    __u32           cpu;        /* cpu */
    __u32           cpu_domain; /* cpu domain */
    struct cpu_context_save cpu_context;    /* cpu context */
    __u8            used_cp[16];    /* thread used copro */
    unsigned long       tp_value;
    union fp_state      fpstate;
    union vfp_state     vfpstate;
    struct restart_block    restart_block;
};

image.png

进程状态

/**
 * 进程要么在CPU上执行,要么准备执行。
 */
#define TASK_RUNNING        0
/**
 * 可中断的等待状态。
 */
#define TASK_INTERRUPTIBLE  1
/**
 * 不可中断的等待状态。
 * 这种情况很少,但是有时也有用:比如进程打开一个设备文件,其相应的驱动程序在探测硬件设备时,就是这种状态。
 * 在探测完成前,设备驱动程序如果被中断,那么硬件设备的状态可能会处于不可预知状态。
 */
#define TASK_UNINTERRUPTIBLE    2
/**
 * 暂停状态。当收到SIGSTOP,SIGTSTP,SIGTTIN或者SIGTTOU信号后,会进入此状态。
 */
#define TASK_STOPPED        4
/**
 * 被跟踪状态。当进程被另外一个进程监控时,任何信号都可以把这个置于该
 */
#define TASK_TRACED     8
/**
 * 僵死状态。进程的执行被终止,但是,父进程还没有调用完wait4和waitpid来返回有关
 * 死亡进程的信息。在此时,内核不能释放相关数据结构,因为父进程可能还需要它。
 */
#define EXIT_ZOMBIE     16
/**
 * 在父进程调用wait4后,删除前,为避免其他进程在同一进程上也执行wait4调用
 * 将其状态由EXIT_ZOMBIE转为EXIT_DEAD,即僵死撤销状态。
 */
#define EXIT_DEAD       32

sleep (1) vs 挂起(Stopped/T 状态):核心区别

特性 sleep (1) (S 状态) 挂起(T 状态,kill -STOP)
进程状态 TASK_INTERRUPTIBLE(可中断睡眠) TASK_STOPPED(暂停)
唤醒方式 超时自动唤醒 / 信号提前唤醒 只能通过 SIGCONT 信号唤醒
核心目的 主动等待指定时间 被动暂停,无时间限制
返回值 0(超时)/ 剩余时间(被打断) 无返回,直到收到 SIGCONT
典型场景 延时执行、降低 CPU 占用 调试暂停、Ctrl+Z 暂停进程

即sleep是通过将进程切成TASK_INTERRUPTIBLE态,指定时间内进程不参与调度,超时中断后切回TASK_RUNNING态。

而挂起是切成TASK_STOPPED态,无法参与调度且只能通过SIGGONT信号重新切换TASK_RUNNING态;

gdb/lldb 调试程序时,打断点本质就是让进程进入 TASK_STOPPED 状态:

  • 调试器向进程发送 SIGSTOP,进程进入 T 状态,停止调度;
  • 你执行 next/step 时,调试器发送 SIGCONT 让进程短暂运行,再立即发 SIGSTOP 暂停;
  • 整个调试过程,进程大部分时间处于 T 状态,完全不被调度。 image.png

image.png

进程调度