进程描述符
进程描述符(process descriptor)包含了一个具体进程的所有信息。他的结构体定义在<linux/sched.h>
中,代码见linux/sched.h,类型为task_struct
。进城描述符几乎包含了所有进程相关的上下文,下面简单列举了一些:
- state: 描述了进程的当前状态信息(runable=0, unrunable=-1, stopped>0)
- pid: 唯一的进程标志
- cpu / recent_used_cpu等cpu相关信息
- mm等内存相关信息
- exitcode / exitstate等进程退出相关信息,通常由
wait4()
接收 - 调度相关信息
- parent / sibling: 父进程和兄弟进程等信息
- fs / files / nsproxy: 文件系统信息,打开的文件描述符以及命名空间等信息
- signal / signalhand: 信号量以及处理函数等信息
- irq: 软/硬中断请求信息
- bio_list / journal_info: 块设备信息,文件系统日志信息等IO相关的上下文
- cgroups: 控制组相关信息
- thread: 线程相关上下文
进程描述符的存放
在kernel<2.6中,进程的task_struct存放在各自的内核栈的尾部,不过在后来的内核中,由于使用了slab分配器来动态生成task_struct,所以还需要在栈尾部的thread_info中存放task_struct的指针。
在寄存器富裕的硬件体系架构,可能会有一个专门的寄存器用来存放当前进程task_struct的指针,因为在内核的进程处理相关逻辑中,这个结构体经常被用到。不过在x86这种寄存器不富裕的架构,当通过current宏返回进程描述符时,就需要通过内核栈尾部的thread_info寻找task_struct的位置了。
state 进程状态
当内核需要调整某个进程的状态时,可以使用set_task_state(task, state)
,可以调整到如下五种状态:
- runable=0
- TASK_RUNNING: 进程处于可执行状态,它要么在执行队列中等待被执行,要么正在被执行
- unrunable>0
- TASK_INTERRUPTIBLE: 进程处于阻塞状态,等待某些条件达成。当收到信号的时候,进程会被提前唤醒。
- TASK_UNINTERRUPTIBLE: 进程处于阻塞状态,等待某些条件达成。这个状态下会忽略信号,不可中断。
- __TASK_TRACED: 进程正在被其他进程跟踪,通常用于ptrace等调试工具调试
- stopped=-1
- __TASK_STOPPED: 进程停止运行
进程创建
当调用fork
创建子进程时,Linux会通过拷贝当前进程来创建子进程。由于使用了写时拷贝页技术(CopyOnWrite),所以进程的创建只需要复制页表和创建进程描述符,速度是比较快的。
进程拷贝调用链为fork -> clone -> do_fork(kernel/fork.c) -> copy_process,其中copy_process
- 首先创建一个内核栈,并拷贝父进程的thread_info以及进程描述符到内核栈中
- 将进程描述符中非继承的统计量设初始值,并将状态置为TASK_UNINTERRUPTIABLE,保证进程不会运行
- 分配一个有效的PID
- 拷贝或共享打开的文件句柄,文件系统信息,信号处理函数,进城地址空间和命名空间等。
- 扫尾工作并返回子进程指针
线程仅仅被视为与其他进程共享某些资源的进程
进程终结
当进程显式调用exit()或者main()函数返回隐式调用exit,或者接收到无法处理/忽略的信号量或异常,就得调用do_exit来终结这个进程:
- 状态转换,将task_struct中的flag设置为PF_EXITING
- 清理/释放IPC信号、地址空间、定时器、文件引用计数等系统资源
- 在进程描述符中设置exitcode,并设置exit_state为EXIT_ZOMBIE
- 最后调用schedule()切换到执行新的进程,该进程结束
不过还剩下了task_struct以及内核栈中的数据没有被释放,他们的作用就是通知父进程,子进程的结束状态。父进程将会收到一个信号量,此时可以通过wait来获取exitcode以及删除子进程的参与信息(内核栈,进程描述符,线程信息)
孤儿进程
当一个父进程退出时,就需要为他的子进程重新设置父进程,来接收未来的exitcode。首先会尝试在当前进程组中寻找一个进程作为父亲,如果找不到,就让init进程作为父进程。