1、进程是什么
- 进程就是处于执行期的程序,同时还包括其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,以及用来存放全局变量的数据段等。所以,进程是处于执行期的程序以及相关资源的总称。
- 线程是在进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。在Linux中,线程和进程并不特别区分,线程只不过是一种特殊的进程。
- 进程提供两种虚拟机制:虚拟处理器和虚拟内存。注意同一进程中的线程之间可以共享虚拟内存,但每个都拥有自己的虚拟处理器。
2、进程描述符
(1)进程描述符的结构
内核把进程的列表存放在任务队列(task list)中,任务队列是一个双向循环链表结构,链表中的每一项都是类型为task_struct的结构,称为进程描述符,该结构定义在<linux/sched.h>文件中。进程描述符包含一个进程的所有信息。task_struct相对较大,在32位机器上大约有1.7KB,进程描述符中包含的信息有:打开的文件、进程地址空间、挂起的信号、进程的状态、进程ID以及其他更多信息。
(2)进程描述符的分配与存放
Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的。各个进程的task_struct存放在它们内核栈的尾端。每个进程的thread_info结构在它的内核栈的尾端分配,该结构的task指针指向该进程实际task_struct。内核通过唯一的进程ID(pid)来标识每个进程。pid的最大默认设置为32768(short int的最大值)。struct thread_info在文件<asm/thread_info.h>中定义如下:
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 restart_block restart_block;
void *sysenter_return;
int uaccess_err;
};
(3)进程的状态
进程描述符task_struct中的state描述了进程的当前状态,每个进程必然处于以下五个状态其中之一:
- TASK_RUNNING(运行):进程是可执行的;正在执行或者在运行队列中等待执行。
- TASK_INTERRUPTIBLE(可中断):进程正在睡眠(也就是被阻塞),等待某些条件的达成,然后进入运行态。
- TASK_UNINTERRUPTIBLE(不可中断):进程正在睡眠(也就是被阻塞),但是准备投入运行了以及此时接受到信号也不会被唤醒,相当于进程在等待时不受干扰。
- __TASK_TRACED:被其他进程跟踪的进程。
- __TASK_STOPPED:进程还没有运行停止执行。通常这种状态发生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。
设置进程状态:set_task_state(task,state); /*将任务task的状态设置为state*/
进程上下文:当一个程序执行了系统调用,他就陷入了内核空间,此时我们称内核代表进程执行并处于进程上下文中。在内核退出的时候,程序会恢复在用户空间会继续执行。
进程家族树:所有的进程都是PID为1的init进程的后代,进程间的关系存放在进程描述符中,每个task_struct都包含一个指向其父进程task_struct,叫做parent的指针,还包含一个称为children的子进程链表。
3、进程如何创建
(1)fork()和exec()
Unix进程创建通过fork()和exec()两个独立的函数共同完成,首先,fork()通过拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅在于PID和PPID和某些没有继承的资源和统计量。exec()函数负责读取可执行文件并将其载入地址空间开始运行。
(2)fork()的写时拷贝技术
fork()使用写时拷贝页实现:写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。资源赋值只有在需要写入的时候才进行,在此之前只是以制度方式共享。所以fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
(3)fork()具体实现
linux通过clone()系统调用实现fork()。fork()根据自己需要的参数标志去调用clone(SIGCHLD, 0),然后由clone()去调用do_fork(),do_fork()完成了创建中的大部分工作,然后调用copy_process()函数让进程开始运行。
copy_process()函数具体工作:
- 调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct
- 检查并确保新创建的子进程小于等于32768(最大进程数目)
- 子进程使自己与父进程区别开来,那些没有继承的资源和统计量清零或设为初始化
- 子进程的状态设置为TASK_UNINTERRUPTIABLE,使他不会投入运行
- copy_process()调用copy_flags()更新task_struct的flags成员
- 调用alloc_pid()为新进程分配一个pid
- 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
- copy_process()做扫尾工作并返回一个指向子进程的指针
再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行,因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销。
(3)vfork()具体实现
vfork()系统调用和fork()的区别在于不拷贝父进程的页表项。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞直到子进程退出或执行exec(),子进程不能向地址空间写入。
现在fork()引入了写时拷贝并且明确了子进程先执行,vfork()的好处就仅限于不执行父进程的页表项了。
4、线程在Linux中的实现
(1)线程是什么
在Linux中,线程和进程并不特别区分,线程只不过是一种特殊的进程。Linux把线程当做进程来实现,线程被视为一个与其他进程共享某些资源的进程,也就是说线程机制只是一种进程间共享资源的手段。共享的资源包括内存地址空间、文件系统资源、文件描述符和信号处理程序。
(2)线程的创建
线程的创建和进程的创建类似,只不过调用clone()的时候需要传递额外参数标识使子进程和父进程共享地址空间、文件系统资源、文件描述符和信号处理程序:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONES_SIGHAND,0)。
(3)内核线程
内核线程是独立运行在内核空间的标准进程。内核线程和普通进程的区别在于内核线程没有独立的地址空间,只在内核空间运行。内核线程可以被调度或抢占。内核线程只能由其他内核线程创建,创建方法:struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char namefmt[], ...)。
5、进程的终结
(1)第一步调用do_exit()释放占用的资源
正常情况,进程的析构是自身引起的,发生在进程调用exit()时。特殊情况,当进程接受到它既不能处理也不能忽略的信号或异常时,进程被动地终结。当一个进程终结时,内核必须释放它所占有的资源并告知父进程,终结任务依靠do_exit()来完成:
- 将task_struct中的标志flags成员设置为PE_EXITING
- 调用del_timer_sync()删除任一内核定时器
- 如果BSD的记账功能是开启的,do_exit()调用acct_update_integrals()来输出记账信息
- 调用exit_mm()函数释放进程占用的mm_struct
- 调用sem__exit(),使进程离开等待IPC信号的队列
- 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源
- 把存放在task_struct的exit_code成员中的任务退出代码置为exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作,退出代码存放在此供父进程随时检索
- 调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init进程,并把task_struct中的exit_state进程状态设成EXIT_ZOMBIE
- do_exit()调用schedule()切换到新的进程
此时与进程相关联的所有资源都被释放掉了,进程不可运行并处于EXIT_ZOMBIE退出状态。它占用的所有内存只剩下内核栈、thread_info结构和tast_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放。
(2)第二步调用release_task()删除task_struct
调用do_exit()后,系统还保留着僵死进程的的进程描述符,在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。
wait()函数都是通过系统调用wait4()来实现的,父进程调用wait()挂起父进程,直到子进程退出,父进程获得已终结的子进程的信息,此时父进程调用release_task()释放子进程描述符task_struct,最终wait()函数返回该子进程的PID。
- 调用__exit_signal(),该函数调用_unhash_process(),后者又调用detach_pid()从pidhash上删除该进程,同时也要从任务列表中删除该进程
- _exit_signal()释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录
- 如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task()就要通知僵死的领头进程的父进程
- release_task()调用put_task_struct()释放进程内核栈和thread_info结构所占的页,并释放task_struct所站的slab告诉缓存
孤儿进程问题:如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态,耗费内存。解决方法是给子进程在当前进程组内找一个线程做父亲,不行的话就让init做他们的父进程。具体实现是在do_exit()调用exit_notify()向父进程发送信号,并给子进程重新找养父的时候。