💡Yupureki:个人主页

✨个人专栏:《C++》 《算法》《Linux系统编程》


🌸Yupureki🌸的简介:


目录

1. 进程创建

1.1 为什么能返回两次?

1.2 写时拷贝

2. 进程终止

2.1 进程退出的方法

2.2 进程退出码

2.3 exit和_exit

3. 进程等待

3.1 wait和waitpid

3.2 获取子进程status

4. 进程替换

4.1 exec 族函数列表

4.2 测试用例

execl-列表参数,需带完整路径

execv-数组参数,需带完整路径

execlp-利用 PATH 搜索

execle-传递自定义环境变量

典型使用模式:fork() + exec()


1. 进程创建

在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,
而原进程为父进程。

#include <unistd.h>
pid_t fork(void);
返回值:⼦进程中返回0,⽗进程返回⼦进程id,出错返回-1

1.1 为什么能返回两次?

关键在于 fork 在内核中的实现机制

调用过程

  1. 用户态调用:父进程调用 fork,触发软中断进入内核态。

  2. 内核处理

    • 内核创建一个新的进程描述符task_struct),并为其分配内核栈等资源。

    • 复制父进程的内存管理结构(页表等),但暂时不复制物理页面,而是标记为“写时拷贝”(COW),以提高效率。

    • 复制父进程的寄存器上下文(包括程序计数器、栈指针等)到子进程的内核栈中。

    • 设置子进程的返回值:在子进程的内核栈或寄存器中,将 fork 的返回值设为 0。

    • 将子进程添加到调度队列中。

返回用户态

  • 当内核完成创建后,它会安排父进程和子进程先后(或同时)返回用户态。

  • 返回时,两个进程都从 fork 调用的下一条指令开始执行,但它们各自持有的返回值寄存器(如 x86 的 eax)内容不同:

    • 父进程:内核将子进程的 PID 写入父进程的返回值寄存器。

    • 子进程:内核将 0 写入子进程的返回值寄存器。

因此,虽然代码相同,但返回值不同,程序可以根据返回值判断自己是在父进程还是子进程中。

1.2 写时拷贝

一开始子进程创建出来时,其页表和父进程页表相同,此时虚拟地址相同,对应映射的物理地址也相同,指向物理内存中相同的地址

但若其中一方对某变量进行修改时,则会发生写时拷贝,即修改的那一方虚拟地址不变,但物理地址发生变化,指向一个新的地址,对应的值就为修改的值

因为有写时拷贝技术的存在,所以父子进程得以彻底分离离!完成了进程独立性的技术保证!
写时拷贝,是一种延时申请技术,可以提高整机内存的使用率

2. 进程终止

进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2.1 进程退出的方法

正常终止(可以通过echo$?查看进程退出码):

  1. 从main返回。我们所见的进程都从main函数退出,也一般从main函数退出
  2. 调用exit。调用exit函数强制进程退出
  3. _exit

异常终止

  • ctrl + c强制进程退出(信号驱使)

2.2 进程退出码

退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码0时表示执行成功,没有问题。代码1或0以外的任何代码都被视为不成功。

当进程退出时,输入echo $?查看上一个进程退出的退出码

int main()
{
	return 30;
}

再次输入查看退出码时发现是0,这是因为命令本身也是程序,再次查看退出码是命令的退出码,即0

常见退出码:

退出码 解释
0 命令成功执行
1 通用错误代码
2 命令(或参数)使用不当
126 权限被拒绝(或)无法执行
127 未找到命令,或 PATH 错误
128+n 命令被信号从外部终止,或遇到致命错误
130 通过 Ctrl+C 或 SIGINT 终止(终止代码 2 或键盘中断)
143 通过 SIGTERM 终止(默认终止)
255/* 退出码超过了 0-255 的范围,因此重新计算(LCTT 译注:超过 255 后,用退出码对 256 取模)

2.3 exit和_exit

exit和_exit函数格式相同

#include <unistd.h>
void _exit(int status);
void exit(int status);
//status是整型变量,传入退出码即可

区别在于exit是C语言的库函数,而_exit是系统调用

  • 进程如果exit()退出的时候,会进行缓冲区的刷新
  • 进程如果_exit()退出的时候,不会进行缓冲区的刷新

3. 进程等待

之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存
泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill-9也无能为力,因为谁也
没有办法杀死一个已经死去的进程。

最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是
不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.1 wait和waitpid

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
    成功返回被等待进程pid,失败返回-1。
参数:
    输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL


pid_t waitpid(pid_t pid,int *status,int options);
返回值:
    当正常返回的时候waitpid返回收集到的子进程的进程ID;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回o;
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
    pid:
        Pid=-1,等待任一个子进程。与wait等效。
        Pid>0,等待其进程ID与pid相等的子进程。
    status:输出型参数
        WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程
        是否是正常退出)
        WEXITSTATUS(statuS):若WIFEXITED非零,提取子进程退出码。(查看进程
        的退出码)
    options:默认为o,表示阻塞等待
    WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回o,不予以等
    待。若正常结束,则返回该子进程的ID。
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

3.2 获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16
比特位)

当正常终止时,前8位全部为0,退出状态在后8位

当异常终止时,你进程的退出状态就不重要了,重要的是为什么终止,因此看前8位

如同考试时,一般都是看成绩的。但当你作弊了,什么成绩之类的就已经不重要了,重要的是你作弊了

测试代码

#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(void)
{
	pid_t pid;
	if ((pid = fork()) == -1)
		perror("fork"), exit(1);
	if (pid == 0)
	{
		sleep(2);
		exit(10);
	}
	else
	{
		int st;
		int ret = wait(&st);
		if (ret > 0 && (st & 0X7F) == 0)
		{ // 正常退出
			printf("child exit code:%d\n", (st >> 8) & 0XFF);
		}
		else if (ret > 0)
		{ // 异常退出
			printf("sig code : %d\n", st & 0X7F);
		}
	}
}

4. 进程替换

在 Linux 系统中,进程替换(Process Replacement)是指用一个新的程序替换当前进程的代码、数据和堆栈,新程序从 main 函数开始执行,而进程 ID 保持不变。这种替换通过 exec 族函数 实现。当进程调用 exec 函数成功后,该进程中原有的内容会被完全替换(除进程 ID 外),执行流跳转到新程序的入口点,且 exec 函数调用成功后不会返回;如果调用失败,则返回 -1 并设置 errno。

4.1 exec 族函数列表

函数名 原型 特点说明
execl int execl(const char *path, const char *arg, ... /* (char *) NULL */); 参数以列表形式(l)传递,需指定程序路径(path)。
execv int execv(const char *path, char *const argv[]); 参数以数组(向量)形式(v)传递,需指定程序路径。
execle int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */); 参数以列表形式传递,并可以指定自定义环境变量(e)。
execve int execve(const char *filename, char *const argv[], char *const envp[]); 系统调用底层实现,参数以数组形式传递,可指定自定义环境变量。
execlp int execlp(const char *file, const char *arg, ... /* (char *) NULL */); 参数以列表形式传递,通过环境变量 PATH 搜索可执行文件(p)。
execvp int execvp(const char *file, char *const argv[]); 参数以数组形式传递,通过 PATH 搜索可执行文件。
execvpe int execvpe(const char *file, char *const argv[], char *const envp[]); 参数以数组形式传递,通过 PATH 搜索可执行文件,并可指定环境变量。

命名约定

  • l:表示参数以列表形式(arg0, arg1, ..., NULL)给出。

  • v:表示参数以向量(数组)形式(char *argv[])给出。

  • p:表示可执行文件通过环境变量 PATH 搜索,不需提供完整路径。

  • e:表示用户可以传递自定义环境变量envp[]),新程序不再继承原环境变量。

参数形式

  • 路径与文件名

    • path:包含路径的可执行文件(如 "/bin/ls")。

    • file:仅指定文件名(如 "ls"),使用 p 后缀的函数会在 PATH 中搜索。

  • 参数列表

    • 以 arg0 开始,通常是程序名,之后依次是选项参数,最后必须以 NULL 结尾。

  • 环境变量

    • 默认情况下,新程序继承调用进程的环境变量。

    • 使用带 e 后缀的函数时,需传递一个 char *envp[] 数组,该数组以 NULL 结尾,用于指定新程序的环境变量。

4.2 测试用例

由简入繁:

execl-列表参数,需带完整路径

#include <unistd.h>

int main()
{
	execl("/usr/bin/ls","ls","-l",NULL);
    //带l表示以列表的形式传入,同时最前面需要指定替换程序的位置(带路径)
    //这样就相当于是在命令行输入ls -l的命令
	return 0;
}

execl函数将此进程替换为了"ls -l"的命令,不再执行后面的代码,转而执行"ls -l"的代码

最后面的NULL表示已全部传入参数,到了末尾

execv-数组参数,需带完整路径

#include <unistd.h>
#include <stdio.h>

int main()
{
	char* argc[] = {"ls","-l",NULL};
    //v表示以数组的形式传入
	execv("/usr/bin/ls",argc);
	return 0;
}

execlp-利用 PATH 搜索

#include <unistd.h>
#include <stdio.h>

int main()
{
	execlp("ls","ls","-l",NULL);
    //带p自动往环境变量中设置的默认路径内查找程序
	return 0;
}

execle-传递自定义环境变量

#include <unistd.h>
#include <stdio.h>

int main() 
{
    char *env[] = {"MY_VAR=hello", "PATH=/usr/bin", NULL};
    execle("/bin/bash", "bash", "-c", "echo $MY_VAR", NULL, env);
    //带e表示传入自定义环境变量
    return 1;
}

典型使用模式:fork() + exec()

由于 exec 函数会替换当前进程,如果希望在原进程继续执行其他任务,通常先调用 fork() 创建一个子进程,然后在子进程中调用 exec 执行新程序,父进程可以继续原工作或等待子进程结束。

#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        execlp("ls", "ls", "-l", NULL);
        perror("execlp");  // 仅当 exec 失败时执行
        return 1;
    } else {
        // 父进程等待子进程结束
        wait(NULL);
        printf("Child finished\n");
    }
    return 0;
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐