13.进程控制_2

一、非阻塞式等待

1.1 首先介绍status值的简单获取

printf("success:%d, sig number:%d, child exit code:%d\n",
               ret, 
               (status & 0x7F),       // &上信号位低七位数据,来查看是否是正常终止 
               (status >> 8) & 0xFF); // 在正常终止的情况下,再去查看退出状态
               我想说的是这里的>>并不会改变status,并没有赋值
               右移获取次低八位数据

  • 查看进程是否正常退出(status & 0x7F) —> WIFEXITED(status)
  • 查看正常退出情况下进程退出码(status >> 8) & 0xFF) —> WEXITSTATUS(status)

1.2 非阻塞式

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
    pid_t id = fork();
    if (id == 0) {
        // 子进程
        int cnt = 5;
        while (cnt--) {
            printf("我是子进程: %d, 父进程: %d, 计数: %d\n", getpid(), getppid(), cnt + 1);
            sleep(1);
        }
        exit(10); // 子进程退出码
    }

    // 父进程:非阻塞等待子进程
    int status = 0;
    while (1) {
        pid_t ret = waitpid(id, &status, WNOHANG);
        if (ret == 0) {
            // 子进程还在运行
            printf("父进程轮询中...\n");
            sleep(1);
        } else if (ret > 0) {
            // 子进程退出
            if (WIFEXITED(status)) {
                printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
            } else if (WIFSIGNALED(status)) {
                printf("子进程被信号终止,信号编号:%d\n", WTERMSIG(status));
            }
            break;
        } else {
            perror("waitpid 出错");
            break;
        }
    }
    return 0;
}

这里对于前面的阻塞式等待做了哪些修改呢?

  1. 阻塞式
waitpid(id, &status, 0); //其中 0 表示父进程会阻塞等待子进程结束。
  1. 非阻塞式
waitpid(id, &status, WNOHANG); // 使用WNOHANG这个宏,这样父进程就会立即返回,不会等待子进程结束

1.3 非阻塞等待+函数指针调度

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

#define Num 10

// 定义函数指针类型
typedef void (*func_t)();

// 函数指针数组
func_t handlerTask[Num];

// 示例任务函数
void task1() {
    printf("执行任务1\n");
}

void task2() {
    printf("执行任务2\n");
}

// 加载任务到函数指针数组
void loadTask() {
    memset(handlerTask, 0, sizeof(handlerTask));
    handlerTask[0] = task1;
    handlerTask[1] = task2;
}

int main() {
    loadTask(); // 加载任务

    pid_t id = fork();
    if (id == 0) {
        // 子进程
        int cnt = 5;
        while (cnt--) {
            printf("我是子进程: %d, 父进程: %d, 计数: %d\n", getpid(), getppid(), cnt + 1);
            sleep(1);
        }
        exit(10); // 子进程退出码
    }

    // 父进程:非阻塞等待子进程
    int status = 0;
    while (1) {
        pid_t ret = waitpid(id, &status, WNOHANG);
        if (ret == 0) {
            // 子进程还在运行
            printf("父进程轮询中...\n");
            sleep(1);
        } else if (ret > 0) {
            // 子进程退出
            if (WIFEXITED(status)) {
                printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
            } else if (WIFSIGNALED(status)) {
                printf("子进程被信号终止,信号编号:%d\n", WTERMSIG(status));
            }
            break;
        } else {
            perror("waitpid 出错");
            break;
        }
    }

    // 父进程执行任务
    printf("父进程开始执行任务...\n");
    for (int i = 0; i < Num; ++i) {
        if (handlerTask[i]) {
            handlerTask[i](); // 执行任务函数
        }
    }

    return 0;
}


1.4 非阻塞等待的意义

1. 保持父进程活跃

父进程不会因为等待子进程而停滞,可以继续执行其他任务,比如:

  • 处理用户输入
  • 执行后台逻辑
  • 维护网络连接

2. 支持并发任务调度

可以同时管理多个子进程,轮询它们的状态,而不是一个个阻塞等待。

for (int i = 0; i < N; ++i) {
    waitpid(child[i], &status[i], WNOHANG);
}

3. 提高程序响应性

在图形界面、服务器、嵌入式系统中,阻塞会导致界面卡顿或服务中断。非阻塞等待能让程序持续响应外部事件。

4. 更灵活的控制流程

你可以根据子进程状态决定是否继续等待、重启任务、或执行清理操作,而不是被动等待。


二、进程程序替换

2.1 程序替换如何执行

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

int main()
{
    printf("process is running\n");

    // 使用 execlp 自动查找路径并执行 ls 命令
    execlp("ls", "ls", "-a", "-l", NULL);

    // 如果 execlp 执行失败,打印错误信息
    perror("execlp");

    // 这行代码不会执行,除非 execlp 失败
    printf("process done\n");

    return 0;
}

程序输出结果
在这里插入图片描述
在这之前我们再来介绍一下创建子进程的两个目的

  1. 子进程执行父进程代码的一部分,也就是执行对应的磁盘代码的一部分
  2. 想让子进程执行一个全新的程序,子进程加载磁盘上指定的程序,执行新的程序代码和数据,这也就是今天这一节的主题也就是进程程序替换

这里我们观察输出结果,发现在进程程序替换之后,printf("process done");这一句代码并没有执行,也就是说明,进程程序替换,是做了一个覆盖。

替换原理: 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

2.2 exec 系列函数一览(共 6 个)

函数名参数形式是否查找 PATH说明
execl列表❌ 不查找 PATH参数一个个列出来
execlp列表✅ 查找 PATH自动查找命令路径
execle列表 + 环境❌ 不查找 PATH可指定环境变量
execv数组❌ 不查找 PATH参数用数组传递
execvp数组✅ 查找 PATH自动查找命令路径
execve数组 + 环境❌ 不查找 PATH最底层,需指定环境变量

不过相比于上面的表格,下面这张我认为更加方便大家记忆

字母含义说明
llist(列表)参数一个个列出来,如 "ls", "-l", NULL
vvector(数组)参数用数组传递,如 char* args[] = {"ls", "-l", NULL}
ppath(自动查找路径)会根据环境变量 PATH 自动查找程序位置
eenv(自定义环境变量)允许你传入自己的环境变量数组 envp[]

2.3 示例:执行 ls -a -l 命令

1. execl

execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

2. execlp

execlp("ls", "ls", "-a", "-l", NULL);

3. execle

/* 对于自己构建的环境变量 */
char*const envp_[] = {
							 (char*)"MYENV=111111", NULL // 这里不进行强转的话他又不认
							 };
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, envp_);

/* 对于系统环境变量的传入 */
extern char** environ; //  声明这个二级指针,否则头文件又不知道了
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);

/* 那如果我这两种都想要呢?怎么办? 这个时候就要用到 putenv */
// putenv:向当前进程添加或修改环境变量
putenv("MYENV=22222");
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);
// 这个时候咱们自己的环境变量就被添加到这张系统环境变量表当中了

4. execv

char* args_[] = { "ls", "-a", "-l", NULL };
execv("/usr/bin/ls", args_);

5. execvp

char* args[] = { "ls", "-a", "-l", NULL };
execvp("ls", args);

6. execve

char* args[] = { "ls", "-a", "-l", NULL };
char* envp[] = { "PATH=/bin", NULL };
execve("/usr/bin/ls", args, envp);

2.4 程序替换调用别的语言(C 调用 Python 脚本)


示例代码:C 调用 Python 脚本 hello.py

🔹 Python 脚本(hello.py)

# hello.py
print("Hello from Python!")

🔹 C 程序(call_python.c)

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

int main() {
    printf("Calling Python script...\n");

    // 使用 execlp 调用 python3 并执行 hello.py
    execlp("python3", "python3", "hello.py", NULL);

    // 如果 exec 调用失败
    perror("execlp failed");
    return 1;
}

🧪 编译并运行

gcc call_python.c -o call_python
./call_python

输出:

Calling Python script...
Hello from Python!

2.5 execve真正的系统调用

使用man指令查看手册会发现这六个函数
在这里插入图片描述
我们继续深入理解 execve ——真正的系统调用。


为什么说 execve 是“真正的系统调用”?

因为它是 Linux 内核提供的原始接口,其他所有 exec 系列函数(如 execl, execvp, execle 等)都是对它的封装和简化。它直接与内核交互,完成进程替换的核心操作。


示例:使用 execve 执行 ls -l

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

int main() {
    char *args[] = { "ls", "-l", NULL };
    char *env[] = { "PATH=/bin", NULL };

    execve("/bin/ls", args, env);

    // 如果 execve 失败,打印错误信息
    perror("execve failed");
    return 1;
}

exec 系列函数调用关系图(简化)

execl   ┐
execle  ├───► 封装参数 + 环境变量 → 调用 execve
execlp  ┘

execv   ┐
execvp  ├───► 封装参数数组 → 调用 execve
execve  ◄─── 最终调用的系统接口

这个底层调用在2号手册之中,而这一章就是系统调用的手册页!

在这里插入图片描述

2.5 关于内建命令

就拿cd命令进行举例
当然可以!下面我用一个简单的例子来说明如何在你自己的 shell 程序中处理内建命令 cd(切换目录)。


用户输入命令

假设用户在你的 shell 中输入:

cd /home/user/Documents

你会读取这一行并解析成一个字符串数组:

myargv[0] = "cd";
myargv[1] = "/home/user/Documents";

do_exec() 中处理 cd

void do_exec(char** myargv)
{
    if (myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
    {
        if (myargv[1] != NULL)
        {
            if (chdir(myargv[1]) != 0)
            {
                perror("cd failed");
            }
        }
        else
        {
            fprintf(stderr, "cd: missing argument\n");
        }
        return; // 处理完内建命令后直接返回
    }

    // 非内建命令,使用 fork 和 execvp 执行
    pid_t id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        execvp(myargv[0], myargv);
        perror("execvp failed");
        exit(1);
    }
    int status;
    waitpid(id, &status, 0);
}

所以我们发现,有些程序是不需要调用子进程来完成的,或者说是必须要使用父进程来进行操作,这样的命令我们就称之为内建命令。


三、Makefile

.PHONY: all           # 声明伪目标,避免与文件名冲突
all: mybin processsub # 默认目标,构建 mybin 和 processsub 两个可执行文件

mybin: mybin.c        # 构建 mybin,依赖 mybin.c
	gcc -o $@ $^ -std=c99  # 编译命令:$@ 表示目标名,$^ 表示所有依赖

processsub: processsub.c # 构建 processsub,依赖 processsub.c
	gcc -o $@ $^ -std=c99  # 编译命令,同上

.PHONY: clean         # 声明 clean 为伪目标
clean:                # 清理目标
	rm -f processsub mybin # 删除生成的可执行文件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值