【Linux 12】进程控制

🌈 Ⅰ 进程创建

01. fork 函数介绍

fork 函数介绍

  • 在 Linux 中可以使用 fork 函数从已经存在的进程中创建新进程
  • 新的进程为子进程,而原进程为父进程。
#include <unistd.h>

pid_t fork(void);

fork 的返回值

  • fork 函数有三种返回值
返回值状态说明
返回值 < 0表示子进程创建失败
返回值 = 0表示当前进程为子进程
返回值 > 0该返回值是子进程的 pid,当前进程为父进程,其持有子进程 pid

02. 写时拷贝

1. 什么是写时拷贝

  • 通常情况下,父子进程代码共享,父子进程在不进行写入操作时,其数据也是共享的,当任意一方试图写入时,便会以写时拷贝的方式各自留存一份副本。

在这里插入图片描述

2. 为什么写时拷贝

  1. 创建子进程时,子进程不一定要用到父进程的全部数据,因此不需要直接将父进程的所有数据全部拷贝一份给子进程,而是在要进行修改时再从父进程那拷贝这部分数据即可即可。
  2. 子进程正在尝试对这部分数据进行修改,但是父进程不打算修改这部分共享的数据,因此子进程就必须将旧数据拷贝一份进行修改。

03. fork 常规用法

父子进程执行不同的代码段

  • 判断 fork 的返回值,从而让不同的进程去执行不同的代码段。
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
 
using std::cout;
using std::endl;
 
int main()
{
   	pid_t id = fork();	// 创建进程
 
	if (0 == id)    	// 执行子进程部分代码  
    	cout << "I am child process" << endl;  
    else if(id < 0) 	// 子进程的创建失败了
  	    cout << "process creation failure" << endl;
 	else            	// 执行父进程部分代码
 		cout << "I am parent process" << endl;
 		
 	return 0;                               
}

04. fork 调用失败的原因

  1. 系统中进程太多,再 fork 时就会内存不足从而导致进程创建失败。
  2. 实际用户的进程数超过了限制,系统对每个用户能创建的进程数量是有上限的。

🌈 Ⅱ 进程终止

01. 进程退出场景

  1. 代码运行完毕,结果正确。
  2. 代码运行完毕,结果错误。
  3. 代码没执行完,异常终止。

02. 常见退出方法

1. 使用 _exit 函数退出进程

  • 函数原型
    • 该函数是个系统调用。
    • _exit 函数 不支持 刷新缓冲区。
#include <unistd.h>

// 用于终止进程,status 表示进程退出时的退出码
void _exit(int status);
  • 函数用例
int main()
{
    while (true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        _exit(3);
    }

    return 0;
}

在这里插入图片描述

2. 使用 exit 函数退出进程

  • 函数原型
    • 该函数本质上是执行了系统调用的 _exit 函数。
    • exit 函数 支持 刷新缓冲区。
#include <stdlib.h>

// 用于终止进程,status 表示进程退出时的退出码
void exit(int status);
  • 函数用例
    • 在代码的任何地方调用 exit 函数都表示退出进程。
int main()
{
    while (true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        exit(2);
    }

    return 0;
}

在这里插入图片描述

3. 使用 return 退出进程

  • 执行 return n 等同于执行 exit(n),因为调用 main 的运行时函数会将 main 函数的返回值当做 exit 函数的参数。
  • return 返回的值为 0 则表示进程执行成功,反之则表示进程执行失败。且 0 之外的不同数字能够表示进程执行失败的不同原因。
#include <cstdio>
#include <cstdlib>
#include <unistd.h>

int main()
{
    return 3;
}
  • 使用 echo $? 能够查看最近一次执行的进程的退出码。

在这里插入图片描述

🌈 Ⅲ 进程等待

01. 进程等待必要性

为什么要进程等待

  • 子进程在退出时,父进程如果对其不管不问,就会造成僵尸问题,从而造成内存泄漏。
  • 进程如果进入了僵尸状态,就没人能够将其干掉,无法杀死一个已经死去的进程。
  • 父进程需要知道派发给子进程的任务完成得如何,结果是否正确,是否正常退出等。
  • 因此父进程需要通过进程等待得方式,去回收子进程所占用得资源,并且获取子进程退出的相关信息。

进程等待能做什么

  1. 父进程能够通过 wait 方法,回收子进程的资源 (必然)。
  2. 父进程能够通过 wait 方法,获取子进程的退出信息 (退出码、退出信号) (可选)。

02. 进程等待的方法

2.1 wait 方法

wait 函数原型

#include <sys/wait.h>
#include <sys/types.h>

pid_t wait (
	int *status);	// 输出型参数,能通过该参数获取子进程退出结果,默认置为空即可

wait 函数功能

  • 父进程阻塞等待任意一个子进程,子进程不退则父进程不退。
  • 该函数能够回收子进程资源,以及获取子进程的 pid。

wait 函数返回值

  • 返回值 > 0:返回值是所等待的子进程的 pid。
  • 返回值 < 0:等待失败。

函数用例

  • 演示父进程回收子进程资源
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>

using std::cout;
using std::endl;

int main()
{
    // 创建子进程
    pid_t id = fork();

    // 子进程执行自己的代码
    if (0 == id) 
    {
        for (size_t i = 0; i < 5; i++)
        {
            cout << "child process is running, pid: " 
                    << getpid() << ", ppid: " << getppid() << endl;
            sleep(1);
        }

        cout << "子进程准备退出,马上变僵尸" << endl;
        exit(0);
    }

    cout << "父进程休眠" << endl;
    sleep(8);
    cout << "父进程开始回收僵尸进程" << endl;

    // 父进程阻塞等待任意子进程
    pid_t rid = wait(nullptr);

    // 等待成功,rid 是子进程的 pid
    if (rid > 0)
        cout << "wait success, rid: " << rid << endl;

    cout << "父进程回收僵尸进程成功" << endl;
    sleep(1);

    return 0;
}

在这里插入图片描述

2.2 waitpid 方法

waitpid 函数原型

#include <sys/wait.h>
#include <sys/types.h>

pid_t waitpid (pid_t pid, int *status, int options);

waitpid 函数参数

  • pid_t pid
    • pid 为 -1 时,等待任意一个子进程,功能等同 wait。
    • pid > 0 时,指定具体想要等待的那个进程。
  • int *status:输出型参数,能通过该参数获取子进程退出结果,默认置为空即可
  • int options:指定父进程的等待方式,为 0 则让父进程进行 阻塞 等待,非 0 则进行 非阻塞 等待。

waitpid 函数功能

  • 回收子进程资源,解决僵尸问题的同时,还能够获取子进程退出信息

waitpid 函数返回值

  • 返回值 > 0:返回值是所等待的子进程的 pid。
  • 返回值 < 0:等待失败。

函数用例

#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>

using std::cout;
using std::endl;

int main()
{
    // 创建子进程
    pid_t id = fork();

    // 子进程
    if (0 == id)
    {
        for (size_t i = 0; i < 5; i++)
        {
            cout << "child process is running, pid: "
                 << getpid() << ", ppid: " << getppid() << endl;
            sleep(1);
        }

        cout << "子进程准备退出,马上变僵尸" << endl;
        exit(1);
    }

    cout << "父进程休眠" << endl;
    sleep(8);
    cout << "父进程开始回收僵尸进程" << endl;

    // 用来获取子进程的退出信息 (退出码 + 退出信号)
    int status;

    // 父进程阻塞等待由 id 指定的子进程
    pid_t rid = waitpid(id, &status, 0);

    // 等待成功,rid 是子进程的 pid
    if (rid > 0)
    {
        cout << "等待成功, 子进程 pid: " << rid
             << " 子进程退出码: " << status << endl;
    }

    sleep(1);

    return 0;
}

在这里插入图片描述

  • 这里子进程的退出码之所以不是 1 而是 256 的原因是变量 status 装着退出码和退出信号两部分信息。
  • 在 status 中,只有低 16 位用于存储 退出码 + 退出信号。
    • 正常退出时,这 16 位的高 8 位存储退出码,低 8 位默认全部存 0。
    • 异常退出时,这 16 位的高 8 位不使用,低 7 位 存储终止信号,还有 1 位是 core dump 标志位。
  • 当前子进程没有出异常,因此为 1 的退出码在内存中是这样存储的 0000 0001 0000 0000,因此按照整形的方式打印出的结果就是 256 了。

在这里插入图片描述

03. 获取子进程状态

3.1 使用位运算获取退出信息

  1. 获取子进程退出码:将获取的状态码右移 8 位再和 0xFF 相与即可,(status >> 8) & 0xFF
  2. 获取子进程退出信号:将获取的状态码和 0x7F 相与即可,status & 0x7F
// 1. 使用位运算获取退出信息
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>

using std::cout;
using std::endl;

int main()
{
    // 创建子进程
    pid_t id = fork();

    // 子进程
    if (0 == id)
    {
        for (size_t i = 0; i < 5; i++)
        {
            cout << "child process is running, pid: "
                 << getpid() << ", ppid: " << getppid() << endl;
            sleep(1);
        }

        cout << "子进程准备退出,马上变僵尸" << endl;
        exit(1);
    }

    cout << "父进程休眠" << endl;
    sleep(8);
    cout << "父进程开始回收僵尸进程" << endl;

    // 用来获取子进程的退出信息 (退出码 + 退出信号)
    int status;

    // 父进程阻塞等待由 id 指定的子进程
    pid_t rid = waitpid(id, &status, 0);

    // 等待成功,rid 是子进程的 pid
    if (rid > 0)
    {
        cout << "等待成功, 子进程 pid: " << rid
             << " 子进程退出信号: " << (status & 0x7F)
             << " 子进程退出码: " << ((status >> 8) & 0xFF) << endl;
    }

    sleep(1);

    return 0;
}

在这里插入图片描述

3.2 使用宏获取退出信息

  1. WIFEXITED(status):如果子进程是正常退出,则该宏的值位真。(用以查看进程是否是正常退出)
  2. WEXITSTATUS(status):如果 WIFEXITED 的值为真,则提取子进程退出码。(用以查看进程的退出码)
// 2. 使用宏获取退出信息
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>

using std::cout;
using std::endl;

int main()
{
    // 创建子进程
    pid_t id = fork();

    // 子进程
    if (0 == id)
    {
        for (size_t i = 0; i < 5; i++)
        {
            cout << "child process is running, pid: "
                 << getpid() << ", ppid: " << getppid() << endl;
            sleep(1);
        }

        cout << "子进程准备退出,马上变僵尸" << endl;
        exit(1);
    }

    cout << "父进程休眠" << endl;
    sleep(8);
    cout << "父进程开始回收僵尸进程" << endl;

    // 用来获取子进程的退出信息 (退出码 + 退出信号)
    int status;

    // 父进程阻塞等待由 id 指定的子进程
    pid_t rid = waitpid(id, &status, 0);

    // 等待成功,rid 是子进程的 pid
    if (rid > 0)
    {
    	// 子进程是正常退出的, 用户只需要关心退出码即可
        if (WIFEXITED(status)) 
        {
            cout << "等待成功, 子进程 pid: " << rid
                 << " 子进程退出码: " << WEXITSTATUS(status) << endl;
        }
    }

    sleep(1);

    return 0;
}

在这里插入图片描述

04. 非阻塞轮询访问

  • 父进程在阻塞等待子进程时,父进程这时候什么事的做不了,只能等待子进程退出才能去做自己的事,效率太低,非阻塞轮询访问就因此出现。

1. 非阻塞轮询访问

  • 父进程每隔一段时间就执行一次系统调用,判断子进程是否退出。
  • 如果子进程没有退出,则父进程子继续执行自己的任务。
  • 如果子进程已经退出,则父进程回收子进程的资源及退出信息。

2. 如何使用非阻塞等待

  • 将 waitpid 函数的第三个参数改成非 0 值即可,一般是用 WNOHANG 宏作为参数。
pid_t rid = waitpid(id, &status, WNOHANG);

3. 非阻塞轮询访问实例

// 非阻塞轮询访问
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>

using std::cout;
using std::endl;

int main()
{
    // 创建子进程
    pid_t id = fork();

    // 子进程
    if (0 == id)
    {
        for (size_t i = 0; i < 5; i++)
        {
            cout << "子进程正在运行, pid: "
                 << getpid() << ", ppid: " << getppid() << endl;
            sleep(1);
        }

        exit(1);
    }

    // 用来获取子进程的退出信息 (退出码 + 退出信号)
    int status = 0;

    while (true)
    {
        // 父进程以非阻塞状态等待子进程退出
        pid_t rid = waitpid(id, &status, WNOHANG);

        if (rid > 0)        // 等待成功, 子进程已经退出
        {
            cout << "等待成功, 子进程 pid: " << rid
                 << " 子进程退出码: " << WEXITSTATUS(status) << endl;
            break;          // 不需要再执行循环等待了
        }
        else if (0 == rid)  // 等待成功,子进程还没退出, 父进程可以执行其他任务
        {
            cout << "子进程还未退出, 父进程执行其他任务" << endl;
            // ... 父进程在等待子进程退出期间要执行的任务
        } 
        else                // 等待失败
        {
            perror("waitpid");
            break;
        }

        sleep(1);           // 父进程每隔 1 秒查询一次子进程是否退出
    }

    sleep(1);

    return 0;
}

在这里插入图片描述

🌈 Ⅳ 进程程序替换

01. 替换原理

1. 什么是程序替换

  • 在用 fork 创建子进程之后,子进程执行的是和父进程相同的程序 (但是有可能执行的是不同的代码分支),这样创建子进程就没多大意义了。
  • 如果创建的子进程想执行其他程序的代码,需要调用 exec 系列函数执行其他程序,这种操作被称之为程序替换
  • 当进程调用 exec 系列函数时,该进程的用户空间代码和数据会完成被新的程序所替换,从一个新的程序启动例程看i是执行。
  • 调用 exec 系列函数不会创建新进程,因此在调用 exec 系列函数的前后该进程的 pid 不变。

02. 替换函数

  • exec 系列函数总共有 6 种,都是以 exec 开头的,这些函数统称为 exec 函数。
#include <unistd.h>

int execl  (const char *path, const char *arg, ...);
int execlp (const char *file, const char *arg, ...);
int execle (const char *path, const char *arg, ..., char *const envp[]);
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[]);

03. 函数解释

1. execl 函数

int execl (					// 替换失败时返回 -1,成功时没有返回值
	const char *path, 		// 进程要执行的程序的所在路径
	const char *arg, ...);	// 参数列表,表示如何执行该程序 (命令行怎么写参数就怎么传), 以 NULL 结尾

2. execlp 函数

int execlp (
	const char *file, 		// 要执行的程序名,无需提供程序路径,会自动去 PATH 环境变量中查找
	const char *arg, ...);	// 参数列表,表示如何执行该程序 (命令行怎么写参数就怎么传)

3. execle 函数

int execle (
	const char *path, 		// 进程要执行的程序的所在路径
	const char *arg, ..., 	// 参数列表,表示如何执行该程序 (命令行怎么写参数就怎么传)
	char *const envp[]);	// 自己提供环境变量给子进程

4. execv 函数

int execv (
	const char *path, 		// 进程要执行的程序的所在路径
	char *const argv[]);	// 该参数是个指针数组,用以存储参数,表示如何执行该程序

5. execvp 函数

int execvp (
	const char *file, 		// 要执行的程序的程序名,无需提供程序路径
	char *const argv[]);	// 参数数组,表示如何执行该程序

6. execvpe 函数

int execvpe(
	const char *file, 		// 要执行的程序的程序名,无需提供程序路径
	char *const argv[], 	// 参数数组,表示如何执行该程序
	char *const envp[]);	// 自己提供环境变量给子进程

04. 命名理解

解释 exec 之外的每个字母所表示的含义

  • l (list) : 表示参数采用列表。
  • v (vector) : 表示参数用数组。
  • p (path) : 有 p 表示会自动搜索环境变量 PATH。
  • e (env) : 表示需要用户自己维护环境变量。
函数名参数格式是否带路径是否使用当前环境变量
execl列表否,需要自己提供程序路径
execlp列表
execle列表否,需要自己提供程序路径否,需要自己配置环境变量
execv数组否,需要自己提供程序路径
execvp数组
execve数组否,需要自己提供程序路径否,需要自己配置环境变量

05. 函数用例

5.1 execl 函数使用示例

  • 让当前进程以 ls -a -l 的方式执行 /usr/bin/ls 路径所指定的程序。
int main()
{
    cout << "程序替换开始" << endl;
    
    // 当前进程以 ls -a -l 的方式跑去执行 /usr/bin 目录下的 ls 程序
    execl("/usr/bin/ls", "ls", "-a", "-l", nullptr);
    
    cout << "程序替换结束" << endl;

    return 0;
}

在这里插入图片描述

5.2 execlp 函数使用示例

  • 用第一个参数作为要执行的程序的程序名,无需自己补全程序路径。
int main()
{
    pid_t id = fork();

    // 由子进程去进程程序替换
    if (0 == id)
    {
        cout << "程序替换开始" << endl;
        // 以 ls -a -l 的方式执行 ls 程序,无需自己补全程序路径
        execlp("ls", "ls", "-a", "-l", nullptr);
        cout << "程序替换结束" << endl;
        exit(1);
    }
	
	// 父进程阻塞等待子进程
    pid_t rid = waitpid(id, nullptr, 0);

    if (rid > 0)
        cout << "等待成功" << endl;

    return 0;
}

在这里插入图片描述

5.3 execle 函数使用示例

// 演示 execle 函数
int main()
{
    // 定义环境变量
    char *const env[] = { (char*)"hello=world", (char*)"PAHT=/" };
    pid_t id = fork();

    // 由子进程去进程程序替换
    if (0 == id)
    {   
        cout << "程序替换开始" << endl;
        // 以 mytest 的方式执行 当前目录下的 mytest.exe 文件
        execle("./mytest.exe", "mytest", NULL, env);
    }

    pid_t rid = waitpid(id, nullptr, 0);

    if (rid > 0)
        cout << "等待成功" << endl;

    return 0;
}

5.4 execv 函数使用示例

// 演示 execv 函数
int main()
{
    pid_t id = fork();

    // 由子进程去进程程序替换
    if (0 == id)
    {   
        // 存储参数的指针数组
        char *argv[] = { (char*)"ls", (char*)"-a", (char*)"-l" };

        cout << "程序替换开始" << endl;
        // 以 ls -a -l 的方式执行指定路径下的程序,
        execv("/usr/bin/ls", argv);
    }

    pid_t rid = waitpid(id, nullptr, 0);

    if (rid > 0)
        cout << "等待成功" << endl;

    return 0;
}

在这里插入图片描述

5.5 execvp 函数使用示例

// 演示 execvp 函数
int main()
{
	cout << "演示 execvp 函数" << endl;
    pid_t id = fork();

    // 由子进程去进程程序替换
    if (0 == id)
    {   
        // 存储参数的指针数组
        char *argv[] = { (char*)"ls", (char*)"-a", (char*)"-l" };

        cout << "程序替换开始" << endl;
        // 以 ls -a -l 的方式执行 ls 程序,无需指定程序所在路径
        execvp("ls", argv);
    }

    pid_t rid = waitpid(id, nullptr, 0);

    if (rid > 0)
        cout << "等待成功" << endl;

    return 0;
}

在这里插入图片描述

5.6 execvpe 函数使用示例

// 演示 execve 函数
int main()
{
    // 定义环境变量
    char *const env[] = { (char*)"hello=world", (char*)"PAHT=/" };
    pid_t id = fork();

    // 由子进程去进程程序替换
    if (0 == id)
    {   
        // 存储参数的指针数组
        char *argv[] = { "mytest" };

        cout << "程序替换开始" << endl;
        // 以 mytest 的方式执行 当前目录下的 mytest.exe 文件
        execve("./mytest.exe", argv, env);
    }

    pid_t rid = waitpid(id, nullptr, 0);

    if (rid > 0)
        cout << "等待成功" << endl;

    return 0;
}
  • 33
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值