开发者可以查看创建新进程的系统调用,这个模块会讨论与进程相关的Unix系统调用,下一个模块会讨论Win32 APIs相关的进程。
在经典的Unix系统,所有的进程都是用fork()创建的,这个系统调用创建一个新的进程,这个新的进程是调用fork的进程的完美副本,这个调用进程被称为父进程,而fork创建的新进程被称为子进程。父子进程都是可运行的,并且在fork系统调用后立即开始运行。
这是函数原型:
#include
#include
pid_t fork(void);
数据类型pid_t是进程id的类型,在所有系统上它都是一个无符号整型。fork()的返回值很重要,在父进程中,这个从fork()中返回的值是子进程的进程id。在子进程中,fork()的返回值是0。在有错误的情况下,fork()会返回一个负值。请看示例:
#include
#include
#include
extern int errno;
int main()
{
pid_t pid;
pid = fork();
if(pid == 0)
printf("I'm the child\n");
else if(pid > 0){
printf("I'm the parent\n");
printf("child pid is %d\n",pid);
}
else{/* pid < 0 */
perror("Error forking");
fprintf(stderr,"errno is %d\n",errno);
}
return 0;
}
当正在运行的程序执行第8行的fork系统调用时,就会创建一个新进程,该子进程与父进程具有完全相同的代码。这个例子中没有其他变量,但是如果父进程中碰巧有一个名为 x 的变量,其值为 17,那么子进程中也会有一个名为 x 的变量,其值为 17。 父子进程都会在fork这行以后开始运行,开发者区分父子进程的唯一方式,是通过fork的返回值。
下面这个图展示了在进程1234调用fork创建子进程1235之前和之后的进程图。
在正常情况下,调用 fork 不太可能失败。 但是,所有 Unix 系统对单个用户可以运行的进程总数以及进程表中同时存在的进程总数都有限制,因此如果创建新进程会导致超过两者中的任意一个限制,就会失败并返回负值,也不会创建子进程。
以下几条父子进程是一样的:
要知道,尽管这些变量的值是一样的,所有不同的数据段,包括运行时的堆栈都已被拷贝,所以每个变量有两个实例,从而允许每个进程各自独立地更新这些数据。
父进程和子进程的区别如下:
要知道每个进程除了init进程(init进程pid为0,并且是在启动时创建的第一个进程,在系统shut down之前一直运行)都有一个父进程,因此存在一个以 init 为根的进程树。
这是 fork 的作用:
这是一个简短的程序。 假设本机上没有其他进程在运行,那么当创建一个新进程时,它的进程id比当前进程大1。
系统调用getpid()返回了调用它的进程id,系统调用getppid()返回父进程的进程id。
Note: 系统调用getpid和getppid不能失败。
该进程的父进程是 shell。 如果这个程序被编译并运行,并且在一次特定的运行过程中,它的进程id是1000,而shell的进程id是500,那么这个程序会打印什么?
#include
#include
#include
int main()
{
pid_t p,x,y;
x = getpid();
printf("%d\n",x); /* prints 1000 */
y = getppid();
printf("%d\n",y); /* prints 500 */
p = fork();
if(p > 0) {
sleep(1); /* sleep for on second */
printf("%d\n",p);
x = getpid();
printf("%d\n",x);
y = getppid();
printf("%d\n",y);
exit(0);
}
else if(p == 0){
printf("%d\n",p);
x = getpid();
printf("%d\n",x);
y = getppid();
printf("%d\n",y);
exit(0);
}
return 0;
}
父进程和子进程谁先运行是不确定的,这个术语叫竟态条件。因此这个程序有2个可能的输出:
I'm the child
I'm the parent, child pid is 22970
是其中一种,另一种是:
I'm the parent, child pid is 22970
I'm the child
父进程可以通过wait()系统调用来控制这一点,这个调用会导致在子进程消亡之前,父进程都是被阻塞的。如果父进程没有子进程,那么wait会立即返回。
这里是函数原型:
#include
#include
pid_t wait(int *stat_loc);
wait 的返回值是死亡子进程的进程 ID,这个简短的程序用来演示这一点。
#include
#include
#include
#include
extern int errno;
int main()
{
pid_t pid,retval;
int status;
pid = fork();
if(pid == 0)
printf("I'm the child\n");
else if(pid > 0){
retval = wait(&status);
printf("I'm the parent,");
printf("the child %d has died\n",retval);
}
else{ /* pid < 0 */
perror("Error forking");
fprintf(stderr,"errno is %d\n",errno);
}
return 0;
}
如果父进程碰巧先运行,它将执行 wait 系统调用,这会导致进程阻塞,它保持阻塞状态,直到子进程终止,此时一个信号被发送到父进程,唤醒它并返回到可运行状态。 如果子进程恰好先运行并在父进程运行之前终止,则对 wait 的调用将立即返回。 无论哪种情况,返回值都是子进程的进程 ID。
垂死孩子的父母可能想知道孩子是如何死亡的,所以子进程可能想向父进程发送消息,这两者都是使用传递给 wait 的参数来完成的。 因为这是一个引用参数,所以它的值是由系统调用设置的。 最低有效字节指示子进程如何死亡。 如果子进程正常终止(即进程到达 main() 的末尾或调用了 exit() 系统调用),则状态的最低字节将为零。 如果子进程异常终止(例如,由于内存异常错误(分段错误)或用户发送终止信号(cntl-c)而终止),则最低字节将设置为终止它的信号的数值 。
如果子进程通过调用 exit() 正常终止,则子进程可以将参数传递给 exit(),并且该值将位于status的第二个字节中。 例如,如果子进程调用 exit(5),则状态的二进制值将是:
00000000 00000000 00000101 00000000
十六进制表示为 00 00 05 00
科普一下C语言的运算符:
这里, >>是右移运算符,<< 是左移运算符,& 是按位与运算符,| 是按位或运算符。
开发者可以使用它们来检查状态的每个字节的值。 例如,要检查最低位字节是否为零,请使用按位与运算符和 0xFF(C 中数字常量前面的 0x 表示该值是十六进制的)。
if (status & 0xFF != 0)
printf("The child died abnormally");
要检查第三个字节的值,请将值右移 8 位,然后使用 0xFF 执行逻辑与。
int temp;
....
temp = status >> 8; /* right shift */
temp = temp & 0xFF;
printf("exit status was %d\n",temp);
如果一个进程在其所有子进程终止之前终止,则子进程将成为“孤儿”。 由于除 init 之外的所有进程都有父进程,因此“孤儿”进程会被 init 进程回收。
#include /* standard unix header file */
int execl(const char *path, const char *arg0, ..., const
char *argn, NULL);
第一个参数 path 应该是可执行程序的路径名。 其余参数是要作为 argv 传递给该程序的参数。 参数列表以 NULL 结束。
这是一个简短的示例程序。
#include
#include
#include
#include
extern int errno;
int main()
{
pid_t p;
p=fork();
if (p == 0) { /* child */
execl("/bin/ls", "ls", "-l", NULL);
perror("Exec failed");
}
else if (p > 0) {
wait(NULL);
printf("Child is done\n");
}
else {
perror("Could not fork");
}
return 0;
}
该程序会创建一个新进程。 子进程的映像被命令 /bin/ls 的映像覆盖,并且使用两个参数 ls 和 -l 来调用它(回想一下,按照约定,argv[0] 是命令的名称),然后子进程运行 ls,当它终止时,父进程被唤醒,显示其消息,并且也终止。
任何 exec 调用都可能失败,比较明显的失败原因是路径不是可执行文件的路径名。
exec 系列中还有其他五个系统调用,都是用新的镜像覆盖当前进程,而他们的不同之处仅在于他们所接受的参数以及其他一些细微的方面。
int execv(const char *path, char *const argv[])
此调用与 execl 相同,只是它只接受两个参数,第二个参数是参数向量。
示例程序:
#include
#include
#include
#include
extern int errno;
int main()
{
pid_t p;
char *args[100];
args[0]="ls";
args[1]="-l";
args[2]=NULL;
p=fork();
if (p == 0) { /* child */
execv("/bin/ls", args);
perror("Exec failed");
}
else if (p > 0) {
wait(NULL);
printf("Child is done\n");
}
else {
perror("Could not fork");
}
return 0;
}
int execle(const char *path, const char *arg0, …, const char *argn, char * /NULL/, char *const envp[])
与 execl 一样,此调用采用可变数量的参数,但其最终参数是表示新环境的向量。
默认情况下,执行的进程的环境与父进程的环境相同,但这允许用户更改环境。
#include
#include
#include
#include
extern int errno;
int main()
{
pid_t p;
char *envp[100];
envp[0]="USER=ingallsr";
envp[1]="HOME=/cs/ingallsr";
envp[2]="PWD=/cs/ingallsr/public.html/OS/c4";
envp[3]=NULL;
p=fork();
if (p == 0) { /* child */
execle("/bin/ls", "ls", "-l", NULL, envp);
perror("Exec failed");
}
else if (p > 0) {
wait(NULL);
printf("Child is done\n");
}
else {
perror("Could not fork");
}
return 0;
}
int execve(const char *path, char *const argv[], char *const envp[])
与 execv 相同,只是它传递环境向量作为第三个参数。int execlp(const char *file, const char *arg0, …, const char *argn, char * /NULL/)
这与上面的调用不同,它的第一个参数只是文件名而不是路径,并且调用在 PATH 环境变量中搜索可执行文件。
#include
#include
#include
#include
extern int errno;
int main()
{
pid_t p;
p=fork();
if (p == 0) { /* child */
execlp("ls", "ls", "-l", NULL);
perror("Exec failed");
}
else if (p > 0) {
wait(NULL);
printf("Child is done\n");
}
else {
perror("Could not fork");
}
return 0;
}
int execvp(const char *file, char *const argv[])
与 execlp 相同,只是参数作为单个参数传递。