每一个进程都有一个非负整数的唯一进程ID。虽然是唯一的,但是进程ID是可复用的。当一个进程终止后,其进程ID就成为复用的候选者。大多数UNIX系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID。这防止了将新进程误认为是使用同一ID的某个已经终止的先前进程。
ID=0的进程为调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID=1的通常是init进程,在自举过程结束时由内核调用。此进程负责在自举内核后启动一个UNIX系统。init通常读取与系统有关的初始化文件,并将系统引导到一个状态(如多用户)。init进程决不会终止。他是一个普通的用户进程(与交换进程不同,它不是内核种的系统进程),但是它以超级用户特权运行。系统种的所有进程构成了一棵以调度进程为根的进程树。
父进程创建子进程后,子进程在操作系统的调度下与其父进程同时运行。如果父进程先于子进程终止,子进程就称为孤儿进程,同时被init进程收养,成为init进程的子进程,因此init进程也叫孤儿院进程。 一个进程成为孤儿进程是很正常的,系统中的很多守护进程其实都是孤儿进程。
父进程在创建子进程以后。子进程在操作系统的调度下与父进程同时运行。如果子进程先于父进程终止,但由于某种原因,父进程并没有回收该子进程的终止状态,这时候子进程处于僵尸状态,被称为僵尸进程。僵尸进程虽然已经不再活动,即不会继续消耗处理机资源,但其所携带的进程终止状态会消耗内存资源。因此,作为程序的设计者,无论对子进程的终止状态是否感兴趣,都应该尽可能及时地回收子进程的僵尸。
父进程标识:除了根进程意外,每一个进程都有父进程标识,PPID。
实际用户标识和实际组标识:由启动该进程的用户及其隶属的组决定。
有效用户标识和有效组标识:一般情况下与进程的实际用户ID和实际组ID一致,除非该进程的可执行文件带有设置用户ID位和设置组ID位,这时该进程的有效用户ID和有效组ID就取自可执行文件的属主和属组。进程的有效ID决定了它的权限。
#include<unistd.h>
pid_t getpid(void) ;
pid_t getppid(void) ;
pid_t getuid(void) ;
pid_t getgid(void) ;
pid_t getepid(void) ;
pid_t getegid(void) ;
fork函数
一个现有的进程可以调用fork函数创建一个子进程
#include<unistd.h>
pid_t fork(void) ; 子进程返回0,父进程返回子进程的pid,除错返回-1。
将子进程的ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有的子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid获得其父进程的进程ID(进程ID 0 总是由内核交换进程所使用,所以一个子进程的进程ID不可能位0)。
由fork产生的子进程是其父进程的不完全副本,子进程在内存中的映像除了代码段与父进程共享同一块物理内存,其他各个区映射到独立的物理内存,但其内容从父进程拷贝。
fork函数成功返回以后,父子进程各自独立的运行,其被调度的先后顺序并不确定,某些实现可以保证子进程先被调度。
fork函数成功返回以后,系统内核为父进程维护的文件描述符表也被复制到了子进程的进程表项中,但并不复制文件表项。也就是说父子进程每个相同的文件描述符共享一个文件表项
由于在fork之后经常跟随这exec,所以现在的很多实现并不执行一个父进程数据段,栈和堆的完全副本。作为替代,使用了写时复制(Copy-On-Write , COW)技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。
在fork之后处理文件描述符有两种情况: 1 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读,写操作的任一共享描述符的文件偏移量已做到了相应更新。 2 父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭它们不需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务器经常使用的。
子进程不继承父进程设置的文件锁。 子进程的未处理闹钟被清除。 子进程的未处理信号集设置为NULL。
fork失败的主要原因: 系统中已经有了太多的进程(/proc/sys/kernel/thread-max)。 该实际用户ID的进程总数超过了系统限制(ulimit -u)
vfork函数
#include<unistd.h>
pid_t vfork(void)
vfork函数创建的子进程不复制父进程的任何物理内存,也不拥有自己的独立内存映射,而是与父进程共享全部地址空间。 vfork函数会在创建子进程的同时挂起其父进程,直到子进程终止或者调用的exec函数创建了另一个新进程,父进程被唤醒并继续运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁)。
终止进程
1)正常终止进程:同步性,根据业务流程的需要有进程主动选择终止的时机。
进程是内存中的代码和数据,而线程则是执行代码的过程。每一个进程可以包含一个或多个线程,但至少要有一个主线程。每个线程都可以被看做是在一个独立的执行过程中调用了一个特殊的函数,谓之线程过程函数。线程开始,线程过程函数即被调用,线程过程函数一旦返回,线程即告终止。因此main函数也可以被看做是进程的主线程的线程过程函数。main函数一旦返回,主线程即终止,进程即终止,进程一旦终止,进程中的所有线程通通终止。这就是main函数的返回与其他函数的返回在本质上的区别。main函数的返回值即进程的退出码,父进程可以在回收子进程的同时得到该退出码,以了解导致其终止的具体原因。
借鉴errno的策略,用0标识成功,用一个大于0的整数表示不同的错误。 借鉴函数返回的策略,用0表示成功,-1表示失败。
调用exit函数令进程终止:
#include<stdlib.h>
void exit(int status); 不返回,status 进程的退出码,相当于main函数的返回值。
虽然exit函数的参数和main函数的参数的返回值都是int类型,但只有其中最低数位的字节可被父进程回收,高三位字节会被忽略,因此在设计进程的退出码时最好不要超过[-128,127] 或 [0.255]值域
通过return语句终止的进程只能在main函数中实现,但是调用exit函数终止的进程可以在包括main函数在内的任何函数中使用。
void exit(int status){
调用实现通过atexit或on_exit函数注册的退出处理函数;
冲刷并关闭所有仍处于打开状态的标准I/O流
删除所有通过tmpfile函数创建的临时文件
_exit(status);
}
调用_exit/_Exit函数令进程终止
void _exit(int status){
冲刷并关闭所有仍处于打开状态的文件描述符,
将调用进程的所有子进程托付给init进程收养,
向调用进程的父进程发送SIGCHLD(17)信号,
令调用进程终止运行,将status的低八位作为退出码保存在其终止状态中,
}
2)异常终止进程
当进程执行了某些在系统看来具有危险性的操作,或系统本身发生了某种故障或意外,内核会向相关进程发送特定的信号。如果进程无意针对收到的信号采取补救措施,那么内核将按照缺省方式将进程杀死,并视情形生成核心转储文件(core)以备事后分析,俗称吐核。
人为触发信号 SIGINT(2):Ctrl + C 。 SIGQUIT(3):Ctrl + \ 。 SIGKILL(9)不能被捕获或忽略 。 SIGTERM(15) 可以被捕获或忽略。
向进程自己发送信号: abort()
回收子进程
为何要回收?
父进程需要等待子进程的终止,以继续后续的工作。
父进程需要知道子进程终止的原因。 正常:退出码? 异常:终止信号?
清除僵尸进程,避免消耗系统资源。
#include<sys/wait.h>
pid_t wait(int* status);
pid_t waitpid(pid_t pid , int *status , int options); 成功返回进程ID,出错返回0或-1.
父进程在创建若干子进程以后调用wait函数:
1 若所有子进程都在运行,则阻塞,直到有子进程终止才返回。
2 若有一个子进程已经终止,则返回该子进程的PID并通过status参数输出其终止状态
3 若没有任何可被等待并回收的子进程,则返回-1,置errno为ECHILD。
WIFEXITED(status)
真:正常终止 可以调用 WEXITSTATUS(status) 得到进程退出码
假:异常终止 可以调用WTERMSIG(status) 获得终止进程的信号
WIFSIGNALED(status)
真:异常终止 可以调用WTERMSIG(status) 终止进程的信号
假:正常终止 可以调用WEXITSTATUS(status) 获得进程退出码
waitpid函数: pid = -1 , 等待并回收任意子进程。 pid > 0 等待并回收特定子进程。
status: 输出子进程的终止状态,可置NULL。
options: 0 ,阻塞模式,若所有子进程都在运行,则阻塞,知道有满足条件子进程终止才返回。 | WNOHANG 非阻塞模式 ,若所有子进程都在运行,则不阻塞,立即返回0
waitpid(-1 , &status , 0) == wait(&status)
static void handle_child(int sig){ pid_t pid; int stat; while( (pid = waitpid(-1 , &stat , WNOHANG) ) > 0){ //.. } }