进程的控制大概包括进程创建、进程执行和进程终止,附加有进程的属性。
进程标识符
每个进程都有一个唯一的ID,它是一个非负整数。进程ID可以重用,当进程终止后,UNIX一般通过延迟重用算法,使得赋予新进程的ID不同于最近终止进程的ID。
系统中有些专用ID。ID为0的通常是调度进程,也被称为交换进程(swapper),它是内核的一部分,不执行磁盘上的任何程序,也被称为系统进程。ID为1的通常是init进程,在自举过程结束时由内核调用,它负责读写系统有关的初始化文件,并将系统引导到一个状态,但它不会终止;它是普通进程,但是以超级用户特权运行,它负责收留孤儿进程。ID为2的进程是页守护进程,负责支持虚拟系统的分页操作。
fork函数
fork函数可以创建新进程,新创建的为子进程。fork函数调用一次,返回两次。两次返回的唯一区别就是其返回值,在子进程中,其返回值为0,而在父进程中,返回值为子进程的ID。这样做的目的是一个进程子进程可以有多个,但是父进程无法获取子进程ID,但是子进程父进程只有一个,可以调用函数getppid获得父进程ID。
子进程创建后,复制了父进程的数据空间、堆、栈,但是共享代码段。现代操作系统通常并不是马上复制,而是使用了写时复制(copy-on-write,COW)。但是不是父进程所有资源都被继承,父进程的文件锁不会被继承,子进程的未处理的闹钟被清除,子进程未处理的信号集设置为空。
在调用fork后,父进程和子进程谁先执行是不确定的,取决于内核使用的调度算法。如果要求父进程、子进程同步,则要使用某种形式的进程间通信了。
在使用fork时通常有2中用法
1、父进程希望复制自己,使父子进程执行不同的代码。比如在网络服务中,父进程等待客户端请求,当请求到达时,创建子进程执行此请求,而父进程继续等待。
2、一个进程要执行一个不同的程序。比如在shell中,子进程创建后立即调用exec函数。
vfork函数
它与fork的不同在于vfork不复制进程的地址空间,它创建子进程的目的是为了执行新的程序,如果复制了也会被覆盖,浪费资源。vfork调用后子进程先运行,直到子进程调用exec函数或exit函数后,父进程才运行。
exit函数
之前已经讲过,进程正常终止有5中方式
1、在main函数返回,相当于调用exit
2、调用exit
3、调用_exit或_Exit
4、进程的最后一个线程在其启动的历程中执行返回语句。
5、进程的最后一个线程调用pthread_exit
进程异常终止有3种方式
1、调用abort,它产生SIGABRT信号,这是下一种异常终止的特例
2、进程接收到某些信号时
3、最后一个线程对“取消”(cancellation)请求做出响应
不过进程如何终止,都会执行同一段代码:为相应的进程关闭所有打开的文件描述符,释放所使用的存储器等。
不管进程如何终止,我们都希望进程可以通知其父进程它是如何终止的。exit、_exit、_Exit将其退出状态作为参数传送给函数。异常终止情况下,内核产生一个指示其异常终止原因的终止状态,其父进程可以调用wait或waitpid取得终止状态。
进程异常终止后,其终止状态信息包括进程ID、进程终止状态、进程使用CPU时间总量。如果其父进程还没处理终止状态信息,那么这个进程就叫做僵死进程(zombie)。如果父进程先于子进程终止,那么init进程就会领养它。
wait和waitpid函数
当一个进程终止时(正常或异常)内核会向其父进程发送SIGCHLD信号。父进程可以选择忽略,也可以提供一个信号处理程序来处理子进程。这里有一个博客参考。
如果编写信号处理程序的话,一般用到wait和waitpid函数。如果调用这两个函数会发生:
如果子进程还在运行,则阻塞
如果一个子进程已经终止,正等待父进程获得其终止状态,则取得子进程的终止状态立即返回。
如果没有子进程,则立即出错返回。
两个函数原型如下:
#include<sys/wait.h>
pid_t wait(int *statloc);
pid_t wait(pid_t pid, int *statloc, int options);
后者选项比较多,更加灵活。
exec函数
子进程可以调用exec函数执行另一个程序。当调用exec函数时,进程执行的程序完全被替换为新程序,但其ID并不变,只是用新程序替换了当前进程的正文、数据、堆和栈段。
exec函数共有六种:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
这些函数看起来相似。第一个参数都是在寻找可执行文件,第二个参数是传递的参数,其中l表示list,v表示vector。这些函数其实都是调用一个函数execve