1.进程的创建
在上文中讲过,当一个进程复制(fork)出一个子进程时,子进程会复制父进程的某些资源。子进程会复制父进程的内存映像,包括代码段、数据段和堆段,下面是函数原型:
#include <unistd.h>
/**
* @description: 创建子进程
* @return {
*>0 表示父进程,此时返回值中存放的是子进程的id号
==0 表示子进程
-1 表示失败
}
*/
pid_t fork();
pid_t vfork();
需要注意的是:
1、使用vfork会使得子进程与父子进程共享资源,而使用fork是额外拷贝一份资源,互不影响。
使用fork代码如下
int main()
{
int init_num = 0;
int statloc = 0;
pid_t child_pid = fork();
if (child_pid == -1)
{
printf("失败\n");
}
else if (child_pid == 0) // 子进程
{
init_num = 5;
// exit(0);
}
else if( child_pid > 0 )
{
// waitpid(child_pid,&statloc,0);
printf("父进程中Num = %d\n", init_num);
}
return 0;
}
代码输出结果为 父进程中的Num = 0;
原因是子进程的Num其实是一份拷贝,独立存在。
如果将上述代码中的fork直接替换成vfork,是否就能让init_num变为5了呢?答案是否定的,因为vfork创建的子进程共享父进程的变量,由于父进程在子进程修改 init_num
后才打印,因此在子进程执行完成之前,父进程可能会读取到未被完全赋值的 init_num
,这可能导致未定义的行为,包括段错误。通过在子进程的末尾调用 _exit(0)
或 exit(0)
,可以确保子进程及时终止,而不会继续执行父进程的代码,从而避免潜在的问题。
2、使用vfork创建子进程
int main()
{
int init_num = 0;
int statloc = 0;
pid_t child_pid = fork();
if (child_pid == -1)
{
printf("失败\n");
}
else if (child_pid == 0) // 子进程
{
init_num = 5;
exit(0);
}
else if( child_pid > 0 )
{
//waitpid(child_pid,&statloc,0);
printf("父进程中Num = %d\n", init_num);
}
return 0;
}
上述代码输出结果为:父进程中Num = 5;
2、进程的回收。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
2.1、wait函数介绍
主要功能:
1、阻塞当前进程。
2、等待其子进程退出并回收其资源。
接口解析:
1、如果当前进程没有子进程,则该函数立即返回。
2、如果当前进程有不止1个子进程,则该函数会回收第一个变成僵尸态的子进程的系统资源。
3、子进程的退出状态(包括退出值、终止信号等)将被放入wstatus所指示的内存中,若wstatus指针为NULL,则代表当前进程放弃其子进程的退出状态。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
if(fork() == 0)
{
printf("[%d]: 我将在3秒后正常退出,退出值是88\n", getpid());
for(int i=3; i>=0; i--)
{
fprintf(stderr, " ======= %d =======%c", i, i==0?'\n':'\r');
sleep(1);
}
exit(88);
}
else
{
printf("[%d]: 我正在试图回收子进程的资源...\n", getpid());
int status;
wait(&status);
if(WIFEXITED(status))
{
printf("[%d]: 子进程正常退出了,其退出值是:%d\n", getpid(), WEXITSTATUS(status));
}
}
}
执行结果是
gec@ubuntu:$ ./a.out
[3611]: 我正在试图回收子进程的资源...
[3612]: 我将在3秒后正常退出,退出值是88
======= 0 =======
[3611]: 子进程正常退出了,其退出值是:88
gec@ubuntu:$
注意,上述代码中,status 用来存放子进程的退出状态,注意status包含了子进程退出的诸多信息,而不仅仅是退出值,因此父进程如果要获取这些信息,需要用以下宏对status进程解析:
宏 | 功能 |
WIFEXITED(status) | 判断子进程是否正常退出 |
WEXITSTATUS(status) | 获取正常退出的子进程的退出值 |
WIFSIGNALED(status) | 判断子进程是否被信号杀死 |
WTERMSIG(status) | 获取杀死子进程的信号的值 |
2.2、waitpid函数介绍
waitpid函数中,pid和options参数的取值和作用详见下表:
pid | 作用 | options | 作用 |
---|---|---|---|
<-1 | 等待组ID等于pid绝对值的进程组中的任意一个子进程 | 0 | 阻塞等待子进程的退出 |
-1 | 等待任意一个子进程 | WNOHANG | 若没有僵尸子进程,则函数立即返回 |
0 | 等待本进程所在的进程组中的任意一个子进程 | WUNTRACED | 当子进程暂停时函数返回 |
>0 | 等待指定pid的子进程 | WCONTINUED | 当子进程收到信号SIGCONT继续运行时函数返回 |
3.加载并执行指定程序
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
主要功能:
给进程加载指定的程序,如果成功,进程的整个内存空间都被覆盖。
接口解析:
执行指定程序之后,会自动获取原来的进程的环境变量。
各个后缀字母的含义:
l : list 以列表的方式来组织指定程序的参数
v: vector 矢量、数组,以数组的方式来组织指定程序的参数
e: environment 环境变量,执行指定程序前顺便设置环境变量
p: 专指PATH环境变量,这意味着执行程序时可自动搜索环境变量PATH的路径
这组函数只是改变了进程的内存空间里面的代码和数据,但并未改变本进程的其他属性。
下面以运行 ls -l 命令以及由用户自己在/mnt/hgfs/share_file 下创建的cfb程序,探究这些函数的参数应该如何填写。
//带了e的是可以设置临时环境变量
//带了p的是可以搜寻系统环境变量
int main()
{
// 参数1是程序路径,参数2是命令名称,参数3是命令参数,参数4是结束标志
// execl("/mnt/hgfs/share_file/cfb", "cfb", "4", NULL); // 正确,成功执行4*4乘法表
// execl("cfb", "cfb", "4", NULL); // 错误,找不到乘法表路径
// execl("ls", "ls", "-l","./1.c", NULL); // 错误,即使你做了export PATH操作,也不会检索系统环境变量路径
// execl("/bin/ls", "ls", "-l","./1.c", NULL); // 正确,正确设置了环境路径
// execlp("ls", "ls", "-l","./1.c", NULL); // 正确,会自动搜索环境变量找到ls
// execlp("/bin/ls", "ls", "-l","./1.c", NULL); // 正确,也可以自己写出ls绝对路径
// execlp("/mnt/hgfs/share_file/cfb", "cfb", "4", NULL); // 正确,规定了cfb的路径
//如果你在此之前在终端输入了 export PATH="/mnt/hgfs/share_file:$PATH"规定了环境变量路径,那么下面也是正确的
// execlp("cfb", "cfb", "4", NULL);
//规定在执行 ls -l 的时候把环境变量设置成用户叫做John,语言设置成英语
//如果你是一个普通用户(不在root下)编译运行这个代码,会看见用户变成;John,日期一些相关会变成英语
// char *envp[] = {"USER=John","LANG=en_US", NULL};
// execle("/bin/ls", "ls", "-l", NULL, envp);
// 或使用可以寻找环境变量的版本
// execlp("ls", "ls", "-l", NULL, envp);
// 功能:在当前进程中执行指定程序文件名的程序,系统会根据 PATH 环境变量搜索程序文件,参数通过数组传递。
// 参数:file 是要执行的程序文件名,argv 是一个指向参数数组的指针,数组的最后一个元素必须为 NULL。
// 示例:
// char *args[] = {"ls", "-l", NULL};
// execvp("ls", args);
perror("error"); // 如果失败,打印错误信息
return 0;
}
下面是一个使用了execl 和 execlp的综合案例
编写一个程序,使之产生一个子进程c,并使用exec函数族中的任意一个版本,使子进程c执行系统命令ls -l去查看某个文件的信息,父进程判断子进程是否执行成功。
代码如下:
文件1: lscpy.c 用于执行最终的显示功能
int main(int argc, char *argv[])
{
char *path = argv[1];
printf("path = %s\n",path);
execlp("ls","ls","-l",path,NULL); //寻找环境变量的
return 0;
}
文件2: temp.c 用于创建子进程,调用lscpy程序
int main(int argc, char *argv[])
{
int status = 0;
char *path = argv[1];
pid_t child_pid = fork();
if(child_pid == -1)
{
printf("创建失败\n");
}
else if(child_pid == 0)
{
execl("./lscpy","./lscpy",path,NULL);
exit(0);
}
else
{
wait(&status);
if(WIFEXITED(status))
{
printf("子进程正常退出\n");
}
}
return 0;
}
编译结果如下
root@ubuntu:/mnt/hgfs/share_file/系统编程/1.进程有关接口# gcc lscpy.c -o lscpy
root@ubuntu:/mnt/hgfs/share_file/系统编程/1.进程有关接口# gcc temp.c -o temp
root@ubuntu:/mnt/hgfs/share_file/系统编程/1.进程有关接口# ./temp ./fork.c
path = ./fork.c
-rwxrwxrwx 1 root root 483 7月 10 17:54 ./fork.c
子进程正常退出
root@ubuntu:/mnt/hgfs/share_file/系统编程/1.进程有关接口#
2023/7/11补充:
孤儿进程和僵尸进程的区别:
孤儿进程和僵尸进程是两种不同的状态,它们在进程的生命周期中具有不同的含义和行为。
1. 孤儿进程(Orphan Process):
- 孤儿进程是指其父进程先于它自己终止或者不再负责它的执行和资源管理的子进程。
- 当一个进程的父进程终止时,操作系统会将孤儿进程的父进程设置为 init 进程(进程ID为1),这样 init 进程会接管孤儿进程并负责它的资源管理。
- 孤儿进程会继续在系统中执行,直到它自己终止。
2. 僵尸进程(Zombie Process):
- 僵尸进程是指子进程在终止后,父进程尚未对其进行资源回收(使用 `wait()` 或 `waitpid()`)的进程。
- 在子进程终止后,其进程描述符和部分资源会被保留在系统进程表中,但是它已经不再执行任何代码。
- 僵尸进程的存在是因为操作系统将子进程的退出状态信息保留下来,以便父进程在需要时获取。
- 父进程应该在合适的时机调用 `wait()` 或 `waitpid()` 来获取子进程的退出状态,并彻底清理僵尸进程。
区别:
- 孤儿进程是指父进程先于子进程终止,而僵尸进程是指子进程先于父进程终止。
- 孤儿进程会被 init 进程接管,继续执行,而僵尸进程已经停止执行,只保留了部分资源供父进程获取退出状态。
- 孤儿进程的父进程变为 init 进程,而僵尸进程的父进程仍然存在,但尚未对其进行资源回收。
- 孤儿进程是活跃的进程,僵尸进程是已经终止的进程。
为了避免产生僵尸进程,父进程应该及时调用 `wait()` 或 `waitpid()` 来回收子进程的资源,并释放相关的进程描述符和其他资源。
4.进程的退出
`exit`、`_exit` 和 `_Exit` 是三个不同的函数,它们用于终止进程并返回退出状态。它们之间的区别如下:
1. `exit` 函数: 会冲刷缓冲区
`exit` 函数用于正常终止进程,并返回一个退出状态给父进程。
它首先执行一些清理工作,比如关闭文件描述符、释放动态分配的内存等,然后调用内核函数来终止进程。
`exit` 函数的原型为:`void exit(int status);`,其中 `status` 是一个整数值,表示退出状态。
2. `_exit` 函数:不会冲刷缓冲区
`_exit` 函数用于快速终止进程,不执行任何清理操作,直接调用内核函数来终止进程。
它不会执行任何注册的终止处理程序(比如通过 `atexit` 注册的函数)。
`_exit` 函数的原型为:`void _exit(int status);`,其中 `status` 是一个整数值,表示退出状态。
3. `_Exit` 函数:不会冲刷缓冲区
`_Exit` 函数与 `_exit` 函数功能相同,用于快速终止进程,不执行任何清理操作。
`_Exit` 函数是 C 标准库函数,在 C11 标准中引入。
`_Exit` 函数的原型为:`void _Exit(int status);`,其中 `status` 是一个整数值,表示退出状态。
下面演示wait函数如何获取exit的退出状态值
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t child_pid, wait_pid;
int exit_status;
child_pid = fork();
if (child_pid == -1) {
perror("fork failed");
exit(1);
} else if (child_pid == 0) { // 子进程
printf("子进程正在执行\n");
// 子进程退出,状态值为 42
exit(42);
} else { // 父进程
// 等待子进程结束,获取退出状态值
wait_pid = wait(&exit_status);
if (WIFEXITED(exit_status)) {
printf("子进程 %d 正常终止,退出状态值为 %d\n", wait_pid, WEXITSTATUS(exit_status));
} else if (WIFSIGNALED(exit_status)) {
printf("子进程 %d 被信号终止,终止信号为 %d\n", wait_pid, WTERMSIG(exit_status));
}
}
return 0;
}
运行上述代码,父进程会等待子进程结束,并打印子进程的退出状态值。在示例中,子进程的退出状态值为 42。