1.2、Linux进程编程
1、进程编程基础
1.1、fork()
在Linux中创建一个新进程的方法是使用fork()函数。fork()函数是Linux中一个非常重要的函数,和读者以往遇到的函数有一些区别,因为它看起来执行一次却返回两个值。一个函数真的能返回两个值吗?
(1)fork()函数说明
fork()函数用于从已存在的进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。使用fork()函数得到的子进程是父进程的一个复制品,它从父进程外继承了整个进程的地址空间,包括进程上下文、代码段、进程号、当前工作目录、根目录、资源限制和控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。
由于fork()完整的拷贝了父进程的整个地址空间,因此执行速度是比较慢的。为了提高效率,UNIX系统设计者创建了vfork()。vfork()也创建新进程,但不产生父进程的副本,他通过允许父子进程可访问相同物理内存从而伪装了对进程地址空间的真实拷贝,当子进程需要改变内存中数据时才拷贝父进程。这就是注明的“写操作时才拷贝”技术。
因为子进程几乎是父进程的完全复制,所以父子两个进程会运行同一个程序。因此需要一种方式来区分它们,并使它们照此运行,否则,这两个进程只能做相同的事。
父子进程一个很重要的区别是:fork()的返回值不同。父进程中的返回值是子进程的进程号,而子进程中返回0.可以通过返回值来判定该进程是父进程还是子进程。
子进程没有执行fork()函数,而是从fork()函数调用的下一条语句开始执行。
(2)fork()函数语法
|
头文件 |
#include <unistd.h> #include <sys/types.h> |
|
|
函数原型 |
pid_t fork(void); |
|
|
作用 |
创建子进程 |
|
|
参数 |
无 |
|
|
返回值 |
成功 |
执行子进程时返回0 执行父进程时返回创建的子进程的进程号 |
|
失败 |
-1 |
|
|
头文件 |
#include <unistd.h> #include <sys/types.h> |
|
|
函数原型 |
pid_t getpid(void); //当前进程进程号 pid_t getppid(void); //当前进程的父进程的进程号 |
|
|
作用 |
获取进程号 |
|
|
参数 |
无 |
|
|
返回值 |
成功 |
进程号 |
|
失败 |
-1 |
|
2、exec函数族
(1)exec函数族说明
fork()函数用于创建一个子进程,该子进程几乎复制了父进程的全部内容。我们能否让子进程执行一个新的程序呢?
exec函数族就提供了一个在进程中执行另一个程序的方法,它可以根据指定的文件名或目录名找到可执行文件,并用它来取代当前进程的数据段、代码段和堆栈段。在执行完之后,当前进程除了进程号外,其它内容都被替换了。这里的可执行文件既可以是一个二进制文件,也可以是Linux下任何可执行的脚本文件。
在Linux中使用exec函数族主要有两种情况:
A、当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用exec函数族中的任意一个函数让自己重生。
B、如果一个进程想执行另一个程序,那么它就可以调用fork()函数新建一个进程,然后调用exec函数族中的任意一个函数,这样看起来就想通过执行应用程序而产生了一个新进程(这种情况非常普遍)。
(2)exec函数族语法
实际上,在Linux中并没有exec()函数,而是有6个以exec开头的函数,它们之间在语法上有细微差别,如下所示:
|
头文件 |
#include <unistd.h> |
|
|
函数原型 |
int execl(const char *path, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); 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[]); |
|
|
返回值 |
成功 |
|
|
失败 |
-1 |
|
这6个函数在函数名和使用语法的规则上都有细微的区别,下面就从可执行文件查找方式、参数传递方式及环境变量这几个方面进行比较。
A、查找方式:读者可以注意到,上表中的前4个函数的查找方式都是完整的目录路径,而最后两个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动按照环境变量“$PATH”所指定的路径进行查找。
B、参数传递方式:exec函数族的参数传递方式有两种:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。在这里是以函数名的第5位字母来区分的,字母为“l”(list)的表示逐个列举参数的方式,其语法为const char *arg;字母为“v”(vertor)的表示将所有参数整体构造指针数组传递,其语法为char *const argv[]。读者可以观察execl()、execle()、execlp()的语法与execv()、execve()、execvp()的区别,它们的具体用法在后面的实例讲解中会具体说明。
这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的是,这些参数必须以NULL结尾。
C、环境变量:exec函数族可以使用默认的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数execle()和execve()就可以在envp[]中指定当前进程所使用的环境变量。
下表对这6个函数中函数名和对应语法做了一个小结,主要指出了函数名中每一位所表明的含义,希望读者结合此表加以记忆。
exec函数名对应含义如下表:
|
前4位 |
统一为exec |
|
|
第5位 |
l:参数传递为逐个列举方式 |
execl、execle、execlp |
|
v:参数传递为构造指针数组方式 |
execv、execve、execvp |
|
|
第6位 |
e:可传递新进程环境变量 |
execle、execve |
|
P:可执行文件查找方式为文件名 |
execlp、execvp |
|
事实上,这6个函数中真正的系统调用只有execve()函数,其它5个都是库函数,它们最终都会调用execve()这个系统调用。
在使用exec函数族时,一定要加上错误判断语句。exec很容易执行失败,其中最常见的原因如下:
A、找不到文件或路径,此时errno被设置为ENOENT。
B、数组argv和envp忘记用NULL结束,此时error被设置为EFAULT。
C、没有对应可执行文件的运行权限,此时error被设置为EACCES。
(3)exec使用实例
下面的第一个示例说明了如何使用文件名的方式来查找可执行文件,同时使用参数列表的方式。这里用的函数是execlp()。
//execlp.c #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { if (fork() == 0) { /* 调用execlp()函数,这里相当于调用了“ps –ef”命令 */ if ((ret = execlp("ps", "ps", "-ef", NULL)) < 0) { printf("Execlp error\n"); } } }
在该程序中,首先使用fork()函数创建一个子进程,然后在子进程中使用execlp()函数。读者可以看到,这里的参数列表列出了在shell中使用的命令名和选项,并且当使用文件名进行查找时,系统会在默认的环境变量PATH中寻找该可执行文件。读者可将编译后的结果下载到目标板上,运行结果如下:
$ ./execlp
PID TTY Uid Size State Command
1 root 1832 S init
2 root 0 S [keventd]
3 root 0 S [ksoftirqd_CPU0]
4 root 0 S [kswapd]
5 root 0 S [bdflush]
6 root 0 S [kupdated]
7 root 0 S [mtdblockd]
8 root 0 S [khubd]
35 root 2104 S /bin/bash /usr/etc/rc.local
36 root 2324 S /bin/bash
41 root 1364 S /sbin/inetd
53 root 14260 S /Qtopia/qtopia-free-1.7.0/bin/qpe -qws
54 root 11672 S quicklauncher
65 root 0 S [usb-storage-0]
66 root 0 S [scsi_eh_0]
83 root 2020 R ps -ef
$ env
…
PATH=/Qtopia/qtopia-free-1.7.0/bin:/usr/bin:/bin:/usr/sbin:/sbin
…
此程序的运行结果与在shell中直接输入命令“ps -ef”是一样的,当然,在不同系统的不同时刻可能会有不同的结果。
接下来的示例使用完整的文件目录来查找对应的可执行文件。注意,目录必须以“/”开头,否则将其视为文件名。
//execl.c #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { if (fork() == 0) { /* 调用execl()函数,注意这里要给出ps程序所在的完整路径 */ if (execl("/bin/ps","ps","-ef",NULL) < 0) { printf("Execl error\n"); } } }
同样将代码下载到目标板上运行,运行结果同上例。
下面的示例利用execle()函数将环境变量添加到新建的子进程中,这里的“env”是查看当前进程环境变量的命令,代码如下:
//execle.c #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { /* 命令参数列表,必须以NULL结尾 */ char *envp[]={"PATH=/tmp","USER=david", NULL}; if (fork() == 0) { /* 调用execle()函数,注意这里也要指出env的完整路径 */ if (execle("/usr/bin/env", "env", NULL, envp) < 0) { printf("Execle error\n"); } } }
下载到目标板后的运行结果如下:
$ ./execle
PATH=/tmp
USER=sunq
后一个示例使用execve()函数,通过构造指针数组的方式来传递参数,注意参数列表一定要以NULL作为结尾标识符。其代码如下:
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { /* 命令参数列表,必须以NULL结尾 */ char *arg[] = {"env", NULL}; char *envp[] = {"PATH=/tmp", "USER=david", NULL}; if (fork() == 0) { if (execve("/usr/bin/env", arg, envp) < 0) { printf("Execve error\n"); } } }
下载到目标板后的运行结果如下:
$ ./execve
PATH=/tmp
USER=david
3、exit()和_exit()
(1)exit()和_exit()函数说明
exit()和_exit()函数都是用来终止进程的。当程序执行到exit()或_exit()时,进程会无条件的停止剩下的所有操作,清除各种数据结构,并终止本进程的运行。但是,这两个函数还是有区别的,这两个函数的调用过程如下图所示:

从上图中可以看出,_exit()函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构;exit()函数则在这些基础上做了一些包装,在执行退出之前加了若干道工序。exit()函数与_exit()函数大的区别就在于exit()函数在终止当前进程之前要检查该进程打开过哪些文件,把文件缓冲区中的内容写回文件,也就是上图中的“清理I/O缓冲”一项。
由于在Linux的标准函数库中,有一种被称做“缓冲I/O(buffered I/O)”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。
每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件时,也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。
这种技术大大增加了文件读写的速度,但也为编程带来了一些麻烦。比如有些数据认为已经被写入到文件中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内,这时用_exit()函数直接将进程关闭掉,缓冲区中的数据就会丢失。因此,若想保证数据的完整性,最好使用exit()函数。
(2)exit()和_exit()函数语法
|
头文件 |
#include <stdlib.h> |
|
|
函数原型 |
void exit(int status); |
|
|
作用 |
终止进程 参数status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其他的数值表示出现了错误,进程非正常结束。 在实际编程时,可以用wait()系统调用接收子进程的返回值,针对不同的情况进行不同的处理 |
|
|
参数 |
status |
0:程序正常退出 |
|
非0:程序异常退出 |
||
|
返回值 |
无 |
|
|
头文件 |
#include <unistd.h> |
|
|
函数原型 |
void _exit(int status); |
|
|
作用 |
终止进程 |
|
|
参数 |
status |
0:程序正常退出 |
|
非0:程序异常退出 |
||
|
返回值 |
无 |
|
(3)exit()和_exit()使用实例
以下两个示例比较了exit()和_exit()函数的区别。由于printf()函数使用的是缓冲I/O方式,该函数在遇到“\n”换行符时自动从缓冲区中将记录读出。以下示例中就是利用这个性质来进行比较的。
#include <stdio.h> #include <stdlib.h> int main() { printf("Using exit...\n"); printf("This is the content in buffer"); exit(0); } $ ./exit Using exit... This is the content in buffer $
从输出的结果中可以看到,调用exit()函数时,缓冲区中的记录也能正常输出。
#include <stdio.h> #include <unistd.h> int main() { printf("Using _exit...\n"); printf("This is the content in buffer"); /* 加上回车符之后结果又如何 */ _exit(0); } $ ./_exit Using _exit... $
从输出的结果中可以看到,调用_exit()函数无法输出缓冲区中的记录。
4、wait()和waitpid()
(1)wait()和waitpid()函数说明
wait()函数用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或者该进程接收到了一个指定的信号为止。如果该父进程没有子进程或者它的子进程已经结束,则wait()就会立即返回。
waitpid()的作用和wait()一样,但它并不一定要等待第一个终止的子进程,它还有若干选项,如可提供一个非阻塞版本的wait()功能,也能支持作业控制。实际上,wait()函数只是waitpid()函数的一个特例,在Linux内部实现wait()函数时直接调用的就是waitpid()函数。
(2)wait()和waitpid()函数格式说明
wait()函数的语法要点如下:
|
头文件 |
#include <sys/types.h> #include <sys/wait.h> |
|
|
函数原型 |
void wait(int *status); |
|
|
作用 |
阻塞父进程 |
|
|
参数 |
status |
这里的status是一个整型指针,是该子进程退出时的状态。若status不为空,则通过它可以获得子进程的结束状态。另外,子进程的结束状态可由Linux中一些特定的宏来测定 |
|
返回值 |
成功 |
已结束运行的子进程的进程号 |
|
失败 |
-1 |
|
|
头文件 |
#include <sys/types.h> #include <sys/wait.h> |
|
|
函数原型 |
pid_t waitpid(pid_t pid, int *status, int options) |
|
|
作用 |
阻塞父进程 |
|
|
参数 |
pid |
pid > 0:只等待进程ID等于pid的子进程,不管是否已经有其他子进程运行结束退出,只要指定的子进程还没有结束,waitpid()就会一直等下去 |
|
pid = -1:等待任何一个子进程退出,此时和wait()作用一样 |
||
|
pid = 0:等待其组ID等于调用进程的组ID的任一子进程 |
||
|
pid < -1:等待其组ID等于pid的绝对值的任一子进程 |
||
|
status |
这里的status是一个整型指针,是该子进程退出时的状态。若status不为空,则通过它可以获得子进程的结束状态。另外,子进程的结束状态可由Linux中一些特定的宏来测定 |
|
|
options |
WNOHANG:若由pid指定的子进程没有结束,则waitpid()不阻塞而立即返回,此时返回值为0 |
|
|
WUNTRACED:为了实现某种操作,由pid指定的任一子进程已被暂停,且其状态自暂停以来还未报告过,则返回其状态 |
||
|
返回值 |
成功 |
已结束运行的子进程的进程号 使用选项WNOHANG且没有子进程退出:0 |
|
失败 |
-1 |
|
(3)waitpid()使用实例
由于wait()函数的使用较为简单,在此仅以waitpid()为例进行讲解。本例中首先使用fork()创建一个子进程,然后让其子进程暂停5s(使用了sleep()函数)。接下来对原有的父进程使用waitpid()函数,并使用参数WNOHANG使该父进程不会阻塞。若有子进程退出,则waitpid()返回子进程号;若没有子进程退出,则waitpid()返回0,并且父进程每隔1s循环判断一次。该程序的流程图如下图所示:

//waitpid.c #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { pid_t pc, pr; pc = fork(); if (pc < 0) { printf("Error fork\n"); } else if (pc == 0) /* 子进程 */ { /* 子进程暂停5s */ sleep(5); /* 子进程正常退出 */ exit(0); } else /* 父进程 */ { /* 循环测试子进程是否退出 */ do { /* 调用waitpid(),且父进程不阻塞 */ pr = waitpid(pc, NULL, WNOHANG); /* 若子进程还未退出,则父进程暂停1s */ if (pr == 0) { printf("The child process has not exited\n"); sleep(1); } } while (pr == 0); /* 若发现子进程退出,打印出相应情况 */ if (pr == pc) { printf("Get child exit code: %d\n",pr); } else { printf("Some error occured.\n"); } } } 将该程序交叉编译,下载到目标板后的运行结果如下: $ ./waitpid The child process has not exited The child process has not exited The child process has not exited The child process has not exited The child process has not exited Get child exit code: 75
可见,该程序在经过5次循环后,捕获到了子进程的退出信号,具体的子进程号在不同的系统上会有所区别。
读者还可以尝试把“pr = waitpid(pc, NULL, WNOHANG);”改为“pr = waitpid(pc, NULL, 0);”或者“pr = wait(NULL);”,运行的结果为:
$ ./waitpid
Get child exit code: 76
可见,在上述两种情况下,父进程在调用waitpid()或wait()之后就将自己阻塞,直到有子进程退出为止。

浙公网安备 33010602011771号