关于进程的定义,可以参照wiki,这里就不多说了。
进程标识
每个进程都有一个非负整数表示的唯一进程ID(PID)。和文件描述符一样,进程ID是可以复用的。当一个进程终止后,其进程ID就成为了复用的候选者。
大多数Unix使用延迟复用算法,使得赋予新建进程的PID不同于最近终止进程所使用的PID。这防止了将新近进程误认为是使用同一PID的某个先前终止的进程。这一点上同文件描述符是有所不同的,前者总是返回可重用的候选者之中最小的。
PID为0的进程通常是调度进程,常被称为交换进程。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。PID为1的通常是init进程,在自举过程结束时由内核调用。init进程决不会终止。它是一个普通的用户进程,但它却以超级用户的特权进行。而且它会成为所有孤儿进程的父进程。
下面的这些函数可以返回进程的一些标识符
#include <unistd.h>
pid_t getpid(void); /* 返回调用进程的PID */
pid_t getppid(void); /* 返回调用进程的父进程的PID */
uid_t getuid(void); /* 返回调用进程的实际用户ID */
uid_t geteuid(void); /* 返回调用进程的有效用户ID */
gid_t getgid(void); /* 返回调用进程的实际组ID */
gid_t getegid(void); /* 返回调用进程的有效组ID */
创建进程
一个现有进程可以调用fork函数创建一个新进程。
#include <unistd.h>
pid_t fork(void); /* 子进程返回0,父进程返回子进程PID;若出错,则返回-1 */
由fork创建的进程被称为子进程。fork被调用一次,但返回两次。
之所以将子进程ID返回给父进程,是因为一个进程可以有多个子进程,并且没有一个函数可以获得其所有子进程的PID。而使子进程得到返回值0的理由是:一个进程只会有一个父进程,它总可以调用getppid函数得到其父进程的PID。
子进程会拷贝父进程的数据空间以及堆和栈,即子进程是其父进程的一个副本。父子进程共享程序的正文段。
由于fork之后经常跟随着exec,所以现在很多实现并不拷贝一个父进程数据段、堆和栈的完全副本。作为替代,使用了写时复制技术。
所谓写时复制,即这些区域由父子进程共享,而且内核将它们的访问权限更改为只读。如果父子进程中的一个企图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟内存中的一页。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int globvar = 5;
char buf[] = "hello, world\n";
int main(void)
{
int var = 12;
pid_t pid;
if (write(1, buf, sizeof(buf) - 1) != sizeof(buf) - 1)
fprintf(stderr, "write error");
printf("there is a buf\n");
if ((pid = fork()) < 0)
fprintf(stderr, "fork error");
else if (pid == 0) { /* 子进程 */
globvar++;
var++;
} else
sleep(2); /* 父进程休眠2秒,以等待子进程 */
printf("pid = %d, glob = %d, var = %d\n", getpid(), globvar, var);
exit(0);
}
运行结果如下:
$ ./a.out
hello, world
there is a buf
pid = 95382, glob = 6, var = 13
pid = 95381, glob = 5, var = 12
很明显子进程中变量值的改变并没有影响到父进程中变量的值。但当我们将结果重定向到一个文件中去时,结果又有所不同了。
$ ./a.out > tmp
$ cat tmp
hello, world
there is a buf
pid = 95405, glob = 6, var = 13
there is a buf
pid = 95404, glob = 5, var = 12
可以注意到,there is a buf 输出了两次,而非一次。我们知道,write是不带缓冲的。因为在fork之前调用write,所以其数据写到标准输出一次。
但是标准I/O库是带缓冲的,如果输出与终端设备相关连,则是行缓冲的。所以当以交互式方式运行该程序时,只得到printf输出的行一次,原因就是标准输出缓冲区由换行符冲洗。
而将标准输出重定向到一个文件中时,却得到printf输出的行两次,原因就是此时标准输出已不再是行缓冲的了,而是全缓冲,由于子进程拥有父进程的一份拷贝,自然也就有了该缓冲区中的内容,在exit之前的第二个printf将其数据追加到已有的缓冲区中。当每个进程终止时,其缓冲区中的内容被写到相应的文件中去。
关于上面的程序还应注意的一点是,当父进程的标准输出被重定向时,子进程的标准输出同样会被重定向。fork的一个特性是父进程的所有打开文件描述符都被复制到了子进程中,而且父子进程每个相同的打开文件描述符共享一个文件表项。当然除此之外,父进程的许多其他属性也为子进程所继承。
更重要的一点是,父进程与子进程共享同一个文件偏移量。这样,当父子进程同时向一个文件中写入时才不会发生混乱。在本例中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写到标准输出,并且知道其输出会追加到子进程所写的数据之后。
但如果父进程没有等待子进程结束,则上述描述就是不成立的。它们的输出很可能会混合在一起。
在fork之后处理文件描述符通常有以下这两种情况。
父进程等待子进程结束。当子进程终止后,它曾经读写过的文件描述符的文件偏移量已经作了相应更新。
父子进程各自执行不同的程序段。fork之后各自关闭它们不需使用的文件描述符,防止互相干扰。
因为fork之后经常会立即exec,所以某些操作系统将这两个操作合并成一个操作。但在Unix中,将它们分开是有其重要原因的,因为在很多场合需要单独使用fork,并且这样可以使得子进程在exec之前去更改自己的属性,如I/O重定向、用户ID、信号安排等。
char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if(fork() == 0) {
/* 在exec之前对子进程进行修改,将input.txt重定向到标准输入 */
close(0);
open("input.txt", O_RDONLY); /* 返回的fd保证是0 */
exec("cat", argv);
}
终止进程
进程有5种正常终止及3种异常终止。
正常终止:
- 在main函数中执行return语句,这等效于调用exit
- 调用exit
- 调用_Exit(ISOC)或_exit(系统调用)。两者对标准I/O流并不进行冲洗。
- 进程的最后一个线程在其启动例程中执行return语句
- 进程的最后一个线程调用pthread_exit函数
异常终止:
- 调用abort
- 当进程接收到某些信号时
- 最后一个线程对”取消”请求作出响应
wait和waitpid函数
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
/* 若成功,则返回进程ID;若出错,返回0或-1 */
调用这两个函数,可能会出现下列三种情况:
- 如果其所有子进程都还在运行,则阻塞。
- 如果一个子进程已终止,正等待父进程获取其终止状态(如果父进程此时没有获取该子进程的终止状态,那么该子进程就被称为僵死进程),则取得该子进程的终止状态之后立即返回。
- 如果它没有任何子进程,则立即出错返回。
两个函数的区别如下:
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞
- waitpid并不等待在其调用之后的第一个终止子进程,它有若干选项,可以控制它所等待的进程
如果子进程已终止,并且是一个僵死进程,则wait立即返回并取得该子进程的状态;否则wait使其调用者阻塞,直到一个子进程终止。
参数statloc中存储着进程的终止状态,如果不关心终止状态,可将它置空。
如果想要等待某个特定的子进程终止,则可以使用waitpid函数,对于waitpid中的pid参数的作用解释如下:
参数 | 作用 |
---|---|
pid > 0 | 等待进程ID与pid相等的子进程 |
pid == 0 | 等待组ID等于调用进程组ID的任一子进程 |
pid == -1 | 等待任一子进程,此时等价于wait |
pid < 1 | 等待组ID等于pid绝对值的任一子进程 |
options参数使我们进一步控制waitpid的操作,此参数或是0,或是下表所示常量按位或运算的结果
常量 | 说明 |
---|---|
WCONTINUED | 若实现支持作业控制,那么由pid指定的任一子进程在终止后仍在继续,但其状态尚未报告,则返回其状态 |
WNOHANG | 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0 |
WUNTRACED | 若实现支持作业控制,而由pid指定的任一子进程已处于终止状态,并且其状态自终止以来还从未报告过,则返回其状态 |
exec族函数
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
/* 若出错,返回-1;若成功,不返回 */