1、创建子进程 fork() 和 vfork()
fork()
#include <unistd.h>
pid_t fork(void);
//失败返回-1;成功:父进程返回子进程的ID(非负) 子进程返回 0
//pid_t 类型表示进程 ID,但为了表示-1,它是有符号整型。(0 不是有效进程 ID,init 最小,为 1)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int var = 34;
int main(void)
{
pid_t pid;
pid = fork();
if (pid == -1 ) {
perror("fork");
exit(1);
} else if (pid > 0) {
sleep(2);
var = 55;
printf("I'm parent pid = %d, parentID = %d, var = %d\n", getpid(),
getppid(), var);
} else if (pid == 0) {
var = 100;
printf("child pid = %d, parentID=%d, var = %d\n", getpid(), getppid(),
var);
}
printf("var = %d\n", var);
return 0;
}
fork创建的新进程成为子进程。fork函数被调用一次,但返回两次。
子进程是父进程的副本。父进程和子进程共享正文段。
fork之后经常跟exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆得完全副本。作为代替使用写时复制(Copy-On-Write,COW)技术。这些区域由父进程和子进程共享,而且内核将他们的访问权限改变为只读。
fork两种用法:
(1)父进程希望复制自己。使父进程和子进程执行不同的代码段。
(2)一个进程执行不同的程序。子进程从fork后立即调用exec。
另外,父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap 建立的映射区 (进程间通信详解)
特别的,fork 之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法
注意:在循环创建n个子进程时避免子进程在创建自己的子进程要使用break
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int n = 5, i; //默认创建5个子进程
if (argc == 2) {
n = atoi(argv[1]);
}
for (i = 0; i < n; i++) //出口1,父进程专用出口
if (fork() == 0)
break; //出口2,子进程出口,i不自增
if (n == i) {
sleep(n);
printf("I am parent, pid = %d\n", getpid());
} else {
sleep(i);
printf("I'm %dth child, pid = %d\n",
i+1, getpid());
}
return 0;
}
vfork()
vfork()函数 vfork跟fork的返回值意义相同,两者之间的差别是,vfork的创建的子进程和父进程内存数据是共享的,vfork的用处主要是在创建子进程后,子进程优先执行,并且调用exec。这样不会引用父进程的地址空间。相比fork不用复制一个副本,很大的提高了效率。
也就是主要区别就是:vfork保证子进程先运行,在调用exec或exit之后父进程才可能被调度运行,当子进程调用这个函数其中一个时,父进程会恢复运行。
vfork使用不当会出现很多问题,所以也有优化的技术,就是写时复制,就是fork之后不复制父进程的地址空间,而是在需要的时候复制,这样成本小很多,但是部分复制和完全不复制,显然还是vfork()效率高,这也是vfork()还存在的原因。
2、进程ID相关函数
getpid 函数 获取当前进程 ID
pid_t getpid(void);
getppid 函数 获取当前进程的父进程 ID
pid_t getppid(void);
getuid 函数 获取当前进程实际用户 ID
uid_t getuid(void);
geteuid函数 获取当前进程有效用户 ID
uid_t geteuid(void);
getgid 函数 获取当前进程使用用户组 ID
gid_t getgid(void);
getegid 函数 获取当前进程有效用户组 ID
gid_t getegid(void);
gdb 调试进程
gdb 调试的时候,gdb 只能跟踪一个进程。可以在 fork 函数调用之前,通过指令设置 gdb 调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。
set follow-fork-mode child 命令设置 gdb 在 fork 之后跟踪子进程。
set follow-fork-mode parent 设置跟踪父进程。
注意,一定要在 fork 函数调用之前设置才有效。
exit函数
进程五种正常终止方式
1、main函数中执行return。等效于exit
2、调用exit函数,exit函数由ISO C定义,其操作包括调用各终止处理程序,然后关闭所有标准I/O流等
3、_exit或_Eixt函数 ISO C定义_Exit,目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法
4、进程的最后一个线程在其启动例程中执行return语句。当最后一个线程从其启动例程返回时,该进程以终止状态0返回
5、进程的最后一个线程调用pthread_exit函数。
3种异常终止方式:
1、调用abort。产生SIGABRT信号
2、当进程接收到某些信号
3、最后一个线程对"取消"请求作出响应
wait函数和waitpid函数
#include<sys/wait.h>
pid_t wait(int *status); //成功:清理掉的子进程 ID;失败:-1 (没有子进程)
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在 Shell 中用特殊变量$?查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程。
父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:
1 阻塞等待子进程退出
2 回收子进程残留资源
3 获取子进程结束状态(退出原因)。如果没有子进程,则立即出错返回
当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2. 释放用户空间分配的内存。内核的 PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
可使用 wait 函数传出参数 status 来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
1. WIFEXITED(status) 为非 0 → 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit 的参数)
2. WIFSIGNALED(status) 为非 0 → 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
*3. WIFSTOPPED(status) 为非 0 → 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid; pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
} else if(pid == 0){ //son
printf("I'm process child, pid = %d\n", getpid());
sleep(7); //困了...
} else {
lable:
wpid = wait(NULL); //死等!!!
if(wpid == -1){
perror("wait error");
goto lable;
}
printf("I'm parent, I catched child process,"
"pid = %d\n", wpid);
}
return 0;
}
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid;
int status;
pid = fork();
if(pid == -1){
perror("fork error");
exit(1);
} else if(pid == 0){ //son
printf("I'm process child, pid = %d\n", getpid());
#if 0
execl("./abnor", "abnor", NULL);
perror("execl error");
exit(1);
#endif
sleep(1);
exit(10);
} else {
//wpid = wait(NULL); //传出参数
wpid = wait(&status); //传出参数
if(WIFEXITED(status)){ //正常退出
printf("I'm parent, The child process "
"%d exit normally\n", wpid);
printf("return value:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) { //异常退出
printf("The child process exit abnormally, "
"killed by signal %d\n", WTERMSIG(status));
//获取信号编号
} else {
printf("other...\n");
}
}
return 0;
}
waitpid 函数
pid_t waitpid(pid_t pid, int *status, in options); //成功:返回清理掉的子进程 ID;失败:-1(无子进程)
参数 pid:
> 0 回收指定 ID 的子进程
-1 回收任意子进程(相当于 wait)
0 回收和当前调用 waitpid 一个组的所有子进程
< -1 回收指定进程组内ID等于pid绝对值的任意子进程
返回 0:参 3 为 WNOHANG,且子进程正在运行。
注意:一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。
waitpid函数具有wait函数没有的三个功能:
(1)waitpid可等待一个特定的进程,而外套则返回任一终止子进程的状态
(2)waitpid提供一个wait的非阻塞版本。有时希望获取一个子进程的状态,但不想阻塞,
(3)waitpid通过WUNTRACED和WCONTINUED选项支持作业控制
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, pid2, wpid;
int flg = 0;
pid = fork();
pid2 = fork();
if(pid == -1){
perror("fork error");
exit(1);
} else if(pid == 0){ //son
printf("I'm process child, pid = %d\n", getpid());
sleep(5);
exit(4);
} else { //parent
do {
wpid = waitpid(pid, NULL, WNOHANG);
//wpid = wait(NULL);
printf("---wpid = %d--------%d\n", wpid, flg++);
if(wpid == 0){
printf("NO child exited\n");
sleep(1);
}
} while (wpid == 0); //子进程不可回收
if(wpid == pid){ //回收了指定子进程
printf("I'm parent, I catched child process,"
"pid = %d\n", wpid);
} else {
printf("other...\n");
}
}
return 0;
}
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
}
if (pid == 0) {
int i;
for (i = 3; i > 0; i--) {
printf("This is the child\n");
sleep(1);
}
exit(34);
} else {
int stat_val;
waitpid(pid, &stat_val, 0); //阻塞
if (WIFEXITED(stat_val))
printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
else if (WIFSIGNALED(stat_val))
printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
}
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int n = 5, i;
pid_t p, q;
if(argc == 2){
n = atoi(argv[1]);
}
q = getpid();
for(i = 0; i < n; i++) {
p = fork();
if(p == 0) {
break;
}
}
if(n == i){ // parent
sleep(n);
printf("I am parent, pid = %d\n", getpid());
for (i = 0; i < n; i++) {
p = waitpid(0, NULL, WNOHANG);
printf("wait pid = %d\n", p);
}
} else {
sleep(i);
printf("I'm %dth child, pid = %d\n",
i+1, getpid());
}
return 0;
}
exec函数族
fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的main函数开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。
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[]);
int fexecve(int fd,char *const argv[],char *const envp[]);
//返回值:出错返回-1;成功,不返回
前四个函数取路径名作为参数,后两个取文件名作为参数,最后一个取文件描述符作为参数。如果file参数包含/,就将其视为路径名,否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。
execlp 函数
加载一个进程,借助 PATH 环境变量
int execlp(const char *file, const char *arg, ...);//成功:无返回;失败:-1
参数 1:要加载的程序的名字。该函数需要配合 PATH 环境变量来使用,当 PATH 中所有目录搜索后没有参数 1 则出错返回。
该函数通常用来调用系统程序。如:ls、date、cp、cat 等命令。
execl 函数
加载一个进程, 通过 路径+程序名 来加载。
int execl(const char *path, const char *arg, ...);//成功:无返回;失败:-1
对比 execlp,如加载"ls"命令带有-l,-F 参数
execlp("ls", "ls", "-l", "-F", NULL); 使用程序名在 PATH 中搜索。
execl("/bin/ls", "ls", "-l", "-F", NULL); 使用参数 1 给出的绝对路径搜索。
execvp 函数
加载一个进程,使用自定义环境变量 env
int execvp(const char *file, const char *argv[]);
变参形式: 1... 2 argv[] (main 函数也是变参函数,形式上等同于 int main(int argc, char *argv0, ...))
变参终止条件:1 NULL 结尾 2 固参指定
execvp 与 execlp 参数形式不同,原理一致。
exec 函数族一般规律
exec 函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在 exec 函数调用后直接调用 perror()和 exit(),无需 if 判断。
l (list) 命令行参数列表
p (path) 搜素 file 时使用 path 变量
v (vector) 使用命令行参数数组
e (environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量
事实上,只有 execve 是真正的系统调用,其它五个函数最终都调用 execve,所以 execve 在 man 手册第 2 节,其它函数在 man 手册第 3 节。这些函数之间的关系如下图所示。
system函数
#include <stdlib.h>
int system(const char *cmdstring);
system函数类似对fork、exec和waitpid的一种封装,下面是apue给的一个例子不是system的源码
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
int system(const char *cmdstring) /* version without signal handling */
{
pid_t pid;
int status;
if (cmdstring == NULL)
return(1); /* always a command processor with UNIX */
if ((pid = fork()) < 0) {
status = -1; /* probably out of processes */
} else if (pid == 0) { /* child */
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
_exit(127); /* execl error */
} else { /* parent */
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
status = -1; /* error other than EINTR from waitpid() */
break;
}
}
}
return(status);
}
system相对于直接使用fork和exec的优点是:system进行了所需的各种出错处理以及各种信号处理