这本书并不厚,惭愧的是结合其他教程,读了好几遍才算基本弄懂。个人觉得适合作为学习Linux操作系统的一本进阶一些的教材。
本章介绍进程相关的知识,思维导图如图所示。

1.1 进程的基本概念
进程不仅仅局限于可执行程序代码,通常还包含其他资源,如打开的文件,挂起的信号,一个或多个具有内存映射的内存地址空间,一个或多个执行线程,存放全局变量的数据段等。进程是处于执行期的程序以及相关资源的总称。
执行线程是进程中活动的对象,每个线程都拥有一个独立的程序计数器,进程栈和一组进程寄存器。内核调度的对象是线程。对Linux而言,线程与进程并无特别区分,线程相当于一种特殊的进程。
1.2 进程描述符及任务结构
1.2.1 进程描述符
内核将进程列表存放在称为任务队列的双向循环链表中。链表中的项类型为task_struct,称为进程描述符,进程描述符包含一个具体进程的所有信息,可以完整描述一个正在执行的程序。
对于x86硬件体系,进程在进程栈的栈底(向下增长的栈)创建结构struct thread_info。struct thread_info在文件<asm/thread_info.h>中定义如下,其中的task域包含了指向该进程的task_struct指针。如下图所示:

struct thread_info
{
struct task_struct *task;
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int preempt_count;
mm_segment_t addr_limit;
struct reastart_block restart_block;
void *sysenter_return;
int uaccess_err;
};
因此在x86系统中,可以通过current_thread_info函数在栈底找到thread_info结构,并从中找到task_struct地址:
current_thread_info()->task;
内核通过PID来标识每个进程,每个进程的PID存放在进程各自的描述符中。
1.2.2 进程状态
进程共有以下几种状态:
- TASK_RUNNING(运行) 进程是可执行的,或正在执行,或在运行队列中等待执行。进程在用户空间中执行的唯一可能状态
- TASK_INTERRUPTIBLE(可中断) 进程正在睡眠,等待条件达成。条件达成后,内核会将进程状态设置为运行;或接收到信号被唤醒。
- TASK_UNINTERRUPTIBLE(不可中断) 接收到信号也不会被唤醒,使用较少
- __TASK_TRACED 被其他进程跟踪的进程
- __TASK_STOPPED(停止) 进程没有投入运行也不能投入运行,发生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。
进程状态的转化如图所示:

内核可以通过set_task_state(task, state)函数设置进程状态:
set_task_state(task, state); //将任务task状态设置为state
1.2.3 进程上下文
一般程序在用户空间执行,当一个程序执行了系统调用或触发了某个异常,就会陷入内核空间。
进程只有通过系统调用和异常处理程序才能陷入内核。
1.2.4 进程家族树
所有进程都是PID为1的init进程的后代。
系统中每个进程都有一个父进程,有零个或多个子进程。父进程相同的子进程称为兄弟进程。每个task_struct包含一个指向其父进程task_struct称为parent的指针,以及一个称为children的子进程链表。
可以通过如下代码获取当前进程的父进程的进程描述符:
struct task_struct *my_parent = current->parent;
init进程的进程描述符是作为init_task静态分配的。可以通过如下代码找到init进程指针:
struct task_struct *task;
for(task=current; task!=&init_task; task=task->parent)
//经过for循环后task指向init
1.3 进程创建
Linux的进程不是直接产生的,而是通过复制已有进程产生的,进程的创建主要用到fork()与exec()两个函数。
首先fork()通过复制当前进程创建一个子进程,随后exec()读取可执行文件并将其载入地址空间开始运行。
1.3.1 fork()
fork()函数调用clone(),clone()调用do_fork(),do_fork()函数完成创建中的主要工作。do_fork()调用copy_process()函数完成以下工作:
- 调用dup_task_struct()为新进程创建内核栈、thread_info结构与task_struct,此时这些值与当前进程相同
- 子进程开始于父进程区别开来,初始化进程描述符中的许多成员
- 子进程状态设置为TASK_UNINTERRUPTIBLE,确保不会投入运行
- 调用copy_flags()函数更新task_struct的flags成员
- 调用alloc_pid()为新进程分配一个有效的PID
- 根据传递给clone()的参数标识,copy_process()复制或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。
- copy_process()返回一个指向子进程的指针
返回到do_fork()函数,如copy_process()函数成功返回,新创建的子进程被唤醒并投入运行。
1.3.2 线程创建
在Linux中,线程被视为一个与其他进程共享某些资源的进程。
线程的创建于普通进程创建类似,只是在调用clone()时需传递一些参数标志来指明需共享的资源:
clone(CLONE_VM | CLONE_FS |CLONE_FILES | CLONE_CIGHAND, 0);
对比一个普通fork()的实现:
clone(SIGCHLD, 0);
内核需要在后台完成的操作可以通过内核线程来完成。内核线程只能在内核空间中运行。可以通过kthread_run()来创建并运行一个内核进程:
struct task_struct *kthread_run(int (*threadfn)(void* data),
void *data,
const char namefmt[],
...)
内核线程启动后就一直运行,直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出。
1.4 进程终结
一个进程终结时,内核必须释放它所占有的资源并通知其父进程。
进程终结发生在调用exit()系统调用时,既可能是显式调用,也可能是隐式地从某个主函数返回。当进程接受到不能处理也不能忽略地信号或异常时,也可能被动地终结。无论如何终结,大部分工作需要由do_exit()完成。
该函数完成资源释放,调用exit_notify向其父进程发送信号,并给其子进程重新寻找养父。养父为线程组中的其他线程或init进程。
此时进程处于EXIT_ZOMBIE状态,用于向父进程提供信息。父进程确认为无关信息后,该进程占用的内存被释放,归还给系统,进程描述符也被删除。

1098

被折叠的 条评论
为什么被折叠?



