进程
进程是处于执行期的程序及相关资源的总称,通常还包括其他资源,如打开的文件,挂起的信号,内核内部数据,处理器状态,具有内存映射的内存地址空间,执行线程,存放全局变量的数据段等。
线程 是在进程中活动的对象,拥有独立的程序计数器、进程栈和一组进程寄存器。内核调度对象是线程,而非进程。在Linux系统中,线程和进程没有特别区分。
现代操作系统中,进程提供两种虚拟机制:虚拟处理器(进程调度)和虚拟内存(内存管理)
在Linux系统中,通常进行 fork()系统调用复制现有进程来创建全新进程,调用 fork() 的进程称为父进程,新产生的进程称为子进程。fork() 系统调用从内核返回两次,分别到父进程和子进程。
通常创建新的进程都是为了执行新的程序,因此调用 exce() 这组函数创建新的地址空间,并载入新的程序。
程序通过 exit() 系统调用退出执行,此函数会将其占用的资源进行释放,父进程可以通过 wait4() 系统调用查询子系统是否终结。进程退出执行后被设置为僵死状态,直到其父进程调用 wait() 或 waitpid()。
进程的另一个名字是任务 (task), Linux 内核通常把进程也叫做任务
进程描述符及任务结构
任务列表 是用于存放进程的双向循环链表,其中每一项的类型都是文件描述符(task_struct),结构定义于 <linux/sched.h>中。
struct task_struct 包含了具体进程的所有信息,相对较大,包含了 打开的文件、进程的地址空间、挂起的信号、进程的状态等信息。
分配进程描述符
Linux 通过slab分配器(内存管理)分配 task_struct 结构,达到对象复用和缓存着色的目的。
在某些体系中,有专门的寄存器存放 task_struct 指针。在x86体系中,由于寄存器不富裕,在内核栈尾端使用 struct thread_info 用于存放 task_struct 的位置,因为 task_struct 目前由 slab 回收与分配。
内核通过唯一的进程标识值 (PID)标识每一个进程,存放于task_struct 中。PID的最大默认值是32768(short int,为了与老版本的Unix兼容,可以修改 /proc/sys/kernel/pid_max 来提高上限)
进程状态
进程描述符中的state域描述进程当前的状态,共有一下五种状态:
TASK_RUNNING(运行) :正在执行或者处于运行队列等待执行
TASK_INTERRUPITIBLE(可中断) : 正在睡眠(被阻塞),等待某些条件达成,转为运行
TASK_UNINTERRUPITIBLE(不可中断) : 与(可中断)类似,但是对信号不做响应,等待时不被干扰
__TASK_TRACED(追踪) :被其他进程跟踪的进程
__TASK_STOPPED(停止) : 进程停止执行,通常发生在接收到 SIGSTOP SIGTSTP SIGTTIN SIGTTOU 等信号
通常通过 set_task_state(task, state)设置指定进程的状态
进程上下文 :一般进程运行在用户空间,当一个程序执行了系统调或者触发异常,即陷入内核空间,此时称内核“代表进程执行”并处于进程上下文中。
进程家族树
Linux 所有的进程都是 PID 为1的 init 进程的后代,内核在系统启动的最后阶段启动 init 进程。
每个进程必有一个父进程已经零个或多个子进程,进程间关系存放在进程描述符中,每个 task_struct 都包含一个指向父进程 task_struct 的指针 parent,和一个包含子进程链表的指针 children
进程创建
Linux 的 fork() 使用写时拷贝,内核此时不会为子进程复制整个内核空间,而且父子进程共享同一拷贝。只有在写入时,数据才会被复制,因此 fork() 之后马上调用 exec() 就无须复制了。
fork() 、 vfork() 、__clone() 都根据自身需要的参数标志调用 clone(), clone() 调用 do_fork()
do_fork() 调用了创建中的大部分工作,定义于 kernel/fork.c 文件中,该函数调用 copy_process()函数,然后让进程开始运行
-1- 调用 dup_task_struct() 为进程创建内核栈、thread_info 和 task_struct
-2- 子进程初始化自己进程描述符的部分数据,task_struct的大部分数据没有被初始化
-3- 子进程的状态被设置为TASK_UNINTERRUPTIBLE,确保其不被投入运行
-4- copy_process() 调用 copy_flags() 以更新 task_struct 的 flags 成员,PF_SUPERPRIV(表明进程是否有超级用户权限),PF_FORKNOEXEC(标识进程是否调用 exec() 函数)
-5- 调用alloc_pid()为新进程分配有效的 PID
-6- copy_process() 拷贝或共享打开的文件、文件系统信息、信号处理函数 、进程地址空间和命名空间
-7- 最后,返回一个指向子进程的指针
线程在Linux中的实现
线程机制提供了在同一程序内共享内存地址空间的一组线程。在多处理器上保证真正的并行处理。从Linux内核看线程仅仅被视为一个与其他进程共享某些资源的进程。
创建线程时在调用 clone() 时需要传递一些参数标志来指明需要共享的资源,如
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHABD, 0);
clone 用到的参数标志以及他们的作用,在 <linux/sched.h> 中定义
内核线程是独立运行在内核空间的标准进程,内核线程和普通进程的主要区别在于内核线程没有独立的地址空间(实际指向地址空间的mm指针被设置为NULL)。
进程终结
一般而言,进程的析构由自身引起,可能是显示或者隐式的进行 exit() 系统调用,该任务大部分要靠 do_exit() 来完成
在调用了 do_exit() 之后,线程僵死不再运行,但是系统还是保留了其进程描述符(用于让系统能获得其信息,进程终结时所需的清理工作和进程描述符的删除被分开执行)。在父进程获得以终结的子进程的信息后,或者通知内核并不关心相关信息之后,子进程的 task_struct 结构才被释放。
如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则孤儿进程会在退出时永远处于僵死状态。系统会给子进程在当前线程组内找到一个线程作为父亲,实在不行就让 init 做其父亲。 在 do_exit 中调用 exit_notify, 该函数调用 forget_original_parent, 最后调用 find_new_reaper 执行寻父过程。
在2.6内核前,其兄弟线程需要遍历系统的所有进程来寻找,现在通过 ptrace 保存被跟踪的进程的兄弟进程链表