进程和线程的概念
什么是进程?
进程就是处于执行时期的程序和相关资源的总称(程序本身并不是进程)。
进程就是处于执行时期的程序,但是并不仅仅局限于一段可执行代码,还包括其他资源,比如:打开的文件,挂起的信号,内核内部数据,处理器的状态,一个或者多个具有内存映射的内存地址空间和多个执行线程等。
什么是线程?
线程就是执行线程,是在进程中活动等对象。每个线程有有一个独立的程序计数器,进程栈以及一组进程寄存器。内核调度的对象是线程,不是进程。Linux中对线程的实现比较特别,它对进程和线程并不特别区分,对Linux而言,线程就是一种特殊的进程。
现代操作系统中,进程提供两种虚拟机制。虚拟处理器和虚拟内存。虽然实际可能是许多进程正在分享一个处理器,但是虚拟处理器给进程一种假象,让进程感觉自己在独享处理器,而虚拟内存让进程在分配和管理内存事觉得自己拥有整个系统的所有内存资源。
进程的描述符和任务结构
进程描述符
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
pid_t pid;
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
/*
* children/sibling forms the list of my natural children
*/
struct list_head children; /* list of my children */
int prio, static_prio, normal_prio;
struct list_head tasks;
...
}
在内核中使用一个task_struct的数据结构来描述进程,它包含的数据能够完整的描述一个正在执行的程序。如:进程的地址空间,它打开的文件,挂起的信号,进程的状态等等。
内核把所有的进程存放在一个task list列表中,这个列表称为任务队列,它是一个双向循环链表,链表中每一项都是一个task_struct。
进程PID
内核通过一个唯一的进程标识值PID来标识每个进程。PID是一个数,表示为pid_t的隐含类型,实际上就是一个int类型,内核把每个进程的PID存放在他们自己的进程描述符的pid_t pid变量中。
进程状态
进程描述符中的state变量描述了进程的当前状态,系统的每个进程都必然处于5种进程状态的一种。
- TASK_RUNNING (运行) : 进程是可执行的;它可能正在执行,在运行队列中等待执行。
- TASK_INTERRUPTIBLE(可中断) : 进程正在阻塞,等待某些条件达成。 一旦这些条件达成,内核就会把进程设置为运行状态。
- TASK_UNINTERRUPTIBLE(不可中断) :这个状态通常在进程必须在等待时不受干扰或等待事件很快发生时出现。
- TASK_TRACED:被其他进程跟踪的进程,例如用ptrace对进程进行调试跟踪。
- TASK_STOPPED(停止) : 进程停止执行。

进程家族树
Linux进程之间有明显的继承关系,所有的进程都是PID为1的init进程的后代。 内核在系统启动的最后阶段启动init进程,该进程对去系统的初始化脚本并执行其他相关程序,最终完成系统的整个启动过程。
系统中每个进程都一个父进程,相应的,每个进程也可以拥有0个或者多个字进程。拥有同一个父进程的所有进程称为兄弟进程。进程间的关系也是存放在进程描述符task_struct数据结构中。每个task_struct都包含一个指向其父进程task_struct的指针,叫做parent指针。还包含一个children的子进程链表。
进程创建
Linux使用两个函数来创建线程:fork()和exec()。
fork : 通过拷贝当前进程创建一个子进程。子进程和父进程的区别在于PID和PPID(父进程号)不一致, 子进程PPID被设置为被拷贝进程的PID。还有某些资源的统计量也不一致。
exec : 它负责读取可执行未见并将其载入地址空间开始运行。
fork()
传统的fork系统调用时直接把 所有的资源复制给新创建的进程。这种实现效率低下,因为新的进程如果立刻执行一个新的映像,所有的拷贝就会前功尽弃。Linux的fork使用写时拷贝实现。只有在写入的时候,数据才会被复制,在此之前只是以读的方式共享。
fork实际开销就是复制父进程的页表给子进程以及创建一个唯一的进程描述符。Linux通过clone系统调用实现fork, 而clone又会去调用do_fork,do_fork完成了创建过程的大部分工作。
fork的过程:
- 调用dup_task_struct为新进程创建一个内核栈和task_struct,这些值与当前进程的值相同。此时,子进程和父进程时完全相同的。
- 检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源限制。
- 子进程着手时自己和父进程区别开来。进程描述符中的许多变量被设置成初始值。但是大部分数据依然和父进程保持一致。
- 子进程的状态设置为TASK_UNINTERRUPTIBLE,确保它不会运行。
- 表明进程还没有调用exec函数的标志PF_FORKNOEXEC被设置。
- 调用alloc_pid为新进程分配一个有效的PID
- copy_process()拷贝或者共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。
- 扫尾工作,并返回一个指向子进程的指针。
- 新建的进程并让其投入运行。内核有意让子进程首先执行,这样子进程会马上调用exec函数,可以避免写时拷贝的额外开销。
线程创建
Linux中线程创建和普通进程创建类似,只是在调用clone的时候需要传递一些参数来指明哪些资源需要共享。
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
上述代码和fork()调用差不过,fork调用也是通过调用clone函数来实现的。通过该clone函数可知,父子进程共享地址空间,文件系统资源,文件描述符和信号处理程序。 新建的进程也就是所谓的线程。
终结进程
当一个进程终结时,内核必须释放它所占的资源,并将这个消息告知其父进程。无论进程时怎么终结的,大部分都是依靠内核的do_exit函数来完成。
- 将task_struct中的标志设置为PF_EXITING。
- 调用del_timer_sync删除任一内核定时器
- 调用exit_mm()方法释放进程占用的mm_struct,如果没有别的的进程使用他们,就彻底释放
- 接下来调用sem_exit()函数,如果进程排队等待IPC信号,则离开队列。
- 调用exit_files和exit_fs递减文件描述符和文件系统数据的引用计数
- 调用exit_notify向父进程发送信号,然后重新给自己的子进程寻找新的父亲。
- 调用schedule将调度切换到新的进程。
do_exit执行完成之后,尽管该进程已经不在执行,但是系统还是保留了它的进程描述符。只有当父进程已经获得了已经终结的子进程的信息后,子进程的的task_struct才会被释放。
进程调度
调度程序负责决定那个程序投入运行,何时运行以及运行多长时间。进程调度可以看作在可运行态的进程之间分配有限的处理器资源的内核子系统。
网友评论