操作系统主要目的是能够运行用户程序,进程管理是操作系统的核心。
(1)进程概念
通常所指进程就是执行期的程序,但进程并不仅仅局限于一段可执行代码,通常还要包含其他资源:打开的文件、进程通信IPC、内核内部数据、处理器状态、内存地址空间及执行线程、数据段、堆栈等。实际上,进程就是正在执行的程序代码的实时结果。
执行线程,是进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程而不是进程。linux系统中线程和进程不特别区分,对linux而言,线程是一种特殊的进程。
linux系统通常调用fork函数来创建一个新的进程,该系统调用通过复制一个现有进程来创建一个全新的进程,fork调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用exec一组函数可以创建新的地址空间,并把新的程序载入其中,现代内核中,fork实际是由clone系统调用实现的。
最后,程序通过exit系统调用退出进程。这个函数会终结进程并将其占用的资源释放掉。进程退出执行后被设置为僵死状态,直到它的父进程调用wait或waitpid为止。
(2)进程描述符及任务结构
内核把进程的列表存放在task_list的双向循环链表中,链表中每一项都是类型为task_struct、被称为进程描述符的结构。task_struct包含了内核管理一个进程所需的所有信息:打开的文件、进程地址空间、挂起的信号、进程的状态等;
分配进程描述符:
linux通过slab分配器分配task_struct,只需要在栈底或栈顶创建一个新的结构体struct thread_info。
进程描述符:
内核通过一个唯一的进程标识符(PID),PID是一个整型数,默认最大值设置为32768,这个最大值实际上就是系统中允许同时存在的进程的最大数目。一些僵尸进程则会占用这些PID导致系统无法重现分配。系统管理员可以通过修改/proc/sys/kernel/pid_max来提高上限。
进程状态:
TASK_RUNNING(运行)——进程可执行或正在执行;
TASK_INTERRUPTIBLE(可中断)——进程正睡眠,等待某些条件达成;
TASK_UNINTERRUPTIBLE(不可中断)——
__TASK_STOPPED(停止)——进程停止执行;
进程上下文:
可执行程序代码是进程的重要组成部分,这些代码从一个可执行文件载入到进程的地址空间执行,一般程序在用户空间执行,当一个程序执行了系统调用或者出发了某个异常,它就陷入了内核空间。此时,称内核“代表进程执行”并处于进程上下文中。
(3)进程创建
Unix采用两个单独的函数创建进程:fork和exec,首先fork通过拷贝当前进程创建一个子进程,此时,子进程与父进程的区别仅仅是PID、PPID和某些资源和统计量。exec函数负责读取可执行文件并将其载入地址空间开始运行。
写时拷贝(copy-on-write)
fork使用写时拷贝页实现,内核并不复制整个地址空间,而是让父进程和子进程共享同一个拷贝,只有在需要写入的时候,数据才会被复制,从而使整个进程拥有各自的拷贝。资源的复制只有在需要写入时才进行。fork的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化避免了大量拷贝。
fork函数过程:
linux通过clone系统调用实现fork,clone调用do_fork()函数,do_fork函数调用copy_process函数完成:
1. 调用dup_task_struct为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程相同;
2. 检查进程数目是否超过系统允许的个数;
3. 设置进程描述符的成员清0或设置为初始值;
4. 设置子进程状态为TASK_UNINTERRUPTIBEL;
5. copy_process调用copy_flags以更新task_struct的flags成员;
6. 调用alloc_pid()为新进程分配一个有效的PID;
7. 根据clone传递的参数,拷贝或共享打开的文件、文件系统信息、IPC、进程地址空间和命名空间等;
8. 返回一个指向子进程的指针;
回到do_fork函数,新创建的子进程被唤醒并让其运行,内核有意选择子进程首先执行。
(4)linux线程
Linux实现线程的机制非常独特,从内核的角度来说,并没有线程这个概念,linux将所有线程都当做进程来实现,内核没有特别的调度算法和数据结构来表征线程,线程仅仅被视为一个与其他进程共享某些资源的进程。
创建线程:
线程的创建与普通进程的创建类似,只不过在调用clone时需要传递一些参数来指明共享的资源:
进程终结:
一般来说,进程的析构由自身引起,进程调用exit或者隐式返回,不管进程是怎么终结,大部分都要靠do_exit来完成:
1. 将task_struct中的标志成员设置为PF_EXITING;
2. 调用del_timer_sync删除内核定时器;
3. 调用exit_mm释放进程占用的mm_struct(地址空间);
4. 调用sem__exit函数,如果进程排队等待IPC信号,则离开队列;
5. 调用exit_files和exit_fs,分别递减文件描述符、文件系统数据的引用计数;
6. 调用exit_notify向父进程发送信号,给子进程重新找父进程,父进程为线程组中的其他线程或者init进程,并把进程设成EXIT_ZOMBIE;
7. do_exit调用schedule()切换到新进程;
8. 进程处于EXIT_ZOMBIE后不会再被调度,它占用的所有内存就是内核栈、thread_info结构和task_struct结构,此时进程存在的唯一目的就是向它的父进程提供信息,父进程查到信息后通知内核将进程所持有的剩余内存释放。
释放过程:
wait一组函数挂起调用它的进程,直到其中一个子进程退出,最终释放进程描述符时,release_task会被调用:
1. 调用__exit_signal,接着调用_unhash_process,后者调用detach_pid从pidhash上删除该进程,同时从任务列表中删除该进程;
2. _exit_signal释放僵死进程所使用的所有剩余资源;
3. 如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task就要通知僵死进程的领头进程的父进程;
4. release_task调用put_task_struct释放进程内核栈和thread_info结构,并释放task_struct所占有的slab高速缓存。
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。