Linux 进程控制

Linux 进程控制

fork

在 C 语言中,fork 函数是一个创建新进程的系统调用。它通过复制当前进程创建一个新的子进程,使得父进程和子进程在不同的执行路径上同时运行。

fork 函数的原型如下:

#include <unistd.h>

pid_t fork(void);

调用 fork 函数会返回两次。在父进程中,fork 函数返回新创建的子进程的进程 ID(PID),而在子进程中,fork 函数返回 0。

调用 fork,实际上做了:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork 返回,开始调度器调度

当一个进程调用 fork 之后,就有两个二进制代码相同的进程,而且它们都运行到 相同 的地方。

例如:

int main(void)
{
    printf("Before fork, pid = %d\n", getpid());

    pid_t forkPid = fork();
    if(forkPid < 0)
    {
        perror("fork error!");
        return 1;
    }
    printf("After fork, pid = %d, forkPid = %d\n", getpid(), forkPid);
}

输出:

Before fork, pid = 2157
After fork, pid = 2157, forkPid = 2158
After fork, pid = 2158, forkPid = 0

可以看到,Before 只打印了一次,而 After 打印了两次,这就说明了 fork 后,父子两个执行流分别执行。

注意: fork 后,谁先执行完全 由调度器决定

写时拷贝原则

父子进程不仅代码共享,在父子进程均没有写入新数据时,数据也是共享的

为什么不在创建子进程的时候,就为数据开辟新的空间?

  • 子进程不一定要使用(修改)父进程的所有数据
  • 实现按需分配

在这里插入图片描述

fork 使用场景

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从 fork 返回后,调用 exec 函数(后面会讲)。

fork 失败场景

  • 系统进程太多
  • 用户进程数超过限制

进程终止

一般来说,进程终止分为两种情况:

  • 正常退出,如:从 main 返回,_exit() 系统调用,exit() 系统调用
  • 异常退出,如 control + ckill -p <PID>

_exit 系统调用

函数声明:

#include <unistd.h>
void _exit(int status);

参数 status 定义了进程的终止状态,父进程通过 wait 来获取该值

exit 系统调用

函数声明:

#include <unistd.h>
void exit(int status);

参数 status 定义了进程的终止状态,父进程通过 wait 来获取该值

_exit 与 exit 的区别

_exitexit 的最大区别就是:exit 在进程退出前执行:

  • 用户通过 atexit 或 on_exit 定义的清理函数。
  • 关闭所有打开的流,所有的缓存数据均被写入
  • 调用 _exit

例如:

int main(void)
{
    printf("hello");
    exit(0);
}

输出:

Sky_Lee@SkyLeeMacBook-Pro test % ./cfile                 
hello

可以看到,字符串 “hello” 是被输出了的,执行 _exit 呢?

int main(void)
{
    printf("hello");
    _exit(0);
}

输出:

Sky_Lee@SkyLeeMacBook-Pro test % ./cfile  

可以看到,“hello” 并没有输出,这是因为 _exit不会 刷新输出缓冲区的,我们可以手动刷新缓冲区来解决这个问题

int main(void)
{
    printf("hello");
    fflush(NULL);
    _exit(0);
}

输出:

Sky_Lee@SkyLeeMacBook-Pro test % ./cfile                 
hello

return

return statusexit(status) 是等价的

return statusexit(status) 在 main 函数中是等价的

注意: 在其它函数中使用 return 不会使进程退出,而 exit 会(虽然这句话看起来很简单,但不注意的话…

进程等待

进程等待(Process waiting)是一种同步机制,用于父进程等待子进程的完成或状态改变。当父进程创建子进程后,通常需要等待子进程的完成或获取子进程的状态信息。进程等待可以通过系统调用(如 waitwaitpid )来实现。

为什么要有进程等待

进程等待的主要目的是:

  1. 同步:父进程可以等待子进程完成某个任务后再继续执行,以确保协调和顺序执行。这对于需要在父进程中依赖子进程结果的情况很有用。

  2. 回收子进程资源:当子进程终止时,它会进入一种称为"僵尸进程"的状态,此时它占用系统资源但不再执行任何任务。父进程通过等待子进程,可以及时回收子进程的资源,避免产生大量僵尸进程导致系统资源耗尽。

  3. 获取子进程状态:父进程可以通过进程等待来获取子进程的退出状态、终止原因、信号信息等。这样父进程就可以根据子进程的状态做出相应的处理,例如记录日志、重新启动子进程或采取其他措施。

进程等待的方式

有两种进程等待的方式:waitwaitpid 系统调用

wait 系统调用

在操作系统中,wait 是一个系统调用,用于父进程等待其子进程的结束并回收其资源。

wait 的原型如下:

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

pid_t wait(int *status);

wait 函数接受一个指向整型的指针 status 作为参数,用于存储子进程的退出状态。它会挂起当前进程的执行,直到任一子进程结束为止。当子进程结束后,wait 函数会返回子进程的进程 ID(PID),并将子进程的退出状态存储在 status 指针指向的位置上。

如果我们不关心子进程的退出状态,可以向 wait 传入一个空指针(NULL)

waitpid 系统调用

waitpid 是一个用于等待子进程状态改变的系统调用,它允许父进程阻塞自己,直到指定的子进程发生状态改变,或者指定的子进程终止。

waitpid 的函数原型如下:

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

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

参数说明:

  • pid:要等待的子进程的进程ID,可以有以下取值:
    • > 0:等待指定进程ID的子进程。
    • -1:等待任意子进程,类似于 wait
    • 0:等待与当前进程的进程组ID相同的任意子进程。
    • < -1:等待进程组ID等于 pid 绝对值的任意子进程。
  • status:用于保存子进程的终止状态信息的指针。如果不关心子进程的终止状态,可以传入 NULL
  • options:附加选项,用于控制 waitpid 的行为。常见的选项有:
    • WCONTINUED:等待一个已经停止的子进程继续运行。
    • WNOHANG:如果没有子进程改变状态,则 立即返回 ,而不进入阻塞状态。
    • WUNTRACED:等待一个已经停止的子进程。
返回值

waitpid 函数的返回值表示等待的子进程ID:

  • 如果指定的子进程终止,则返回该子进程的进程ID。
  • 如果指定的子进程没有终止,且使用了 WNOHANG 选项,则返回 0。
  • 如果发生错误,则返回 -1,并设置 errno 来指示错误的原因。
二者区别

waitwaitpid 的主要区别在于参数形式和阻塞行为。wait 等待任意子进程的状态改变并进入阻塞状态,而 waitpid 则允许指定要等待的具体子进程,并可以通过选项来控制阻塞行为。

  1. 参数形式:

    • wait:没有指定具体的子进程ID,它会等待任意子进程的状态改变。
    • waitpid:需要指定要等待的具体子进程ID。
  2. 阻塞行为:

    • wait:如果当前没有已终止的子进程,调用 wait 会使父进程进入阻塞状态,直到有子进程终止并返回。
    • waitpid:可以通过传递 WNOHANG 选项来控制其阻塞行为。如果指定了 WNOHANG,即使没有已终止的子进程,waitpid 也会立即返回。
  3. 等待条件:

    • wait:等待任意子进程的状态改变,包括终止、停止或继续运行。
    • waitpid:可以通过传递不同的 options 参数来控制等待的条件,如等待终止子进程、等待停止子进程或等待继续运行的子进程。
获取子进程状态

waitwaitpid 系统调用中,均包含了一个整形参数 status,然而,这个参数并不能简单的看成一个整形变量

这里只关心 status 的低 16 位

在这里插入图片描述

示例
int main(void)
{
    pid_t pid = fork();
    if (pid < 0)
    {
        throw std::runtime_error("fork error!\n");
        return 1;
    }
    if (pid == 0)
    {
        printf("child[%d]\n", getpid());
        sleep(30); // 留点时间杀掉子进程
        exit(10);  // 没被杀,返回 10
    }
    else
    {
        int status;
        pid_t childPID = wait(&status);
        // childPID > 0 => wait 没有发生错误
        // (status & 0x7f) => 如果子进程正常退出,status 低 7 位为 0
        if (childPID > 0 && (status & 0x7f) == 0)
        {
            std::cout << "child exit successfully with exit code: "
                      << ((status >> 8) & 0xff) << std::endl; // status 高 8 位是退出状态
        }
        else
        {
            std::cout << "child exit failed with SIG code: "
                      << (status & 0x7f) << std::endl; // status 低 7 位为 SIG 码(终止信号)
        }
    }
}

输出(正常退出):

child[3080]
child exit successfully with exit code: 10

输出(另一个终端执行 kill -9 <PID> 指令):

child[2940]
child exit failed with SIG code: 9

理解了 status 的细节后,我们可以使用宏定义来简化代码:

  • (status & 0x7f) == 0 等价于 WIFEXITED(status)
  • ((status >> 8) & 0xff) 等价于 WEXITSTATUS(status)
  • (status & 0x7f) 等价于 _WSTATUS(status)

下面再具体演示一下两种等待方式的实现:

阻塞式等待
int main(void)
{
    pid_t pid = fork();
    if (pid < 0)
    {
        throw std::runtime_error("fork error!\n");
        return 1;
    }
    if (pid == 0)
    {
        printf("child[%d]\n", getpid());
        sleep(5);
        exit(10);
    }
    else
    {
        int status;
        pid_t ret = waitpid(pid, &status, 0); // opinion = 0,阻塞式等待,等待 5 s
        if (ret == pid && WIFEXITED(status))
        {
            std::cout << "child exit successfully with exit code: "
                    << WEXITSTATUS(status) << std::endl;;
        }
        else
        {
            std::cout << "child exit failed with SIG code: "
                    << _WSTATUS(status) << std::endl;
        }
    }
}
非阻塞式等待
int main(void)
{
    pid_t pid = fork();
    if (pid < 0)
    {
        throw std::runtime_error("fork error!\n");
        return 1;
    }
    if (pid == 0)
    {
        printf("child[%d]\n", getpid());
        sleep(10);
        exit(10);
    }
    else
    {
        int status;
        pid_t ret = 0;
        while (1)
        {
            ret = waitpid(pid, &status, WNOHANG);
            if(ret == 0)
            {
                std::cout << "child is running..." << std::endl;
                sleep(1);
            }
            else break; // 子进程退出(可能正常,也可能不正常)
        }
        
        if (ret == pid && WIFEXITED(status))
        {
            std::cout << "child exit successfully with exit code: "
                      << WEXITSTATUS(status) << std::endl;
        }
        else
        {
            std::cout << "child exit failed with SIG code: "
                      << _WSTATUS(status) << std::endl;
        }
    }
}

进程程序替换

fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。

exec 函数

exec 函数是一个系列的系统调用函数,用于在 Linux 中执行其他程序。它用于将当前进程替换为新的可执行程序,从而使新程序取代原来的程序继续执行。

调用 exec 函数 不会 创建新进程,只是 进行了程序的替换,原来的 PCB,mm_struct 并没有改变

exec 函数的常见形式有以下几种:

#include <unistd.h>
int execl(const char *path, const char *arg0, ...);
int execlp(const char *file, const char *arg0, ...);
int execle(const char *path, const char *arg0, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

感觉很难记?

观察上述 6 种形式,可以发现:

  • l(list),表示参数用列表
  • v(vector),表示参数用一个数组
  • p(path),表示会自动搜索环境变量
  • e(env),表示自己维护环境变量

只要不自己维护环境变量,exec 就会使用当前的环境变量

理解这四个字母对应的含义,就好理解上面的六种形式了

此外,exec 如果调用成功,那就加载新的程序,原来程序 exec 后面的部分都 不执行

如果调用失败,返回 -1

示例

假设当前目录为 /Users/Sky_Lee/Documents/Linux/Test/,并且有以下文件:

  • cfile.c
  • test.cpp
  • makefile

其中,test.cpp 的内容如下:

#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    printf("Now in test[PID = %d]...\n", getpid());
    if(getenv("PATH"))
        std::cout << "OS PATH = " << getenv("PATH") << std::endl;
    else std::cout << "OS PATH = (null)" << std::endl;
    if(getenv("MYENV"))
        std::cout << "User PATH = " << getenv("MYENV") << std::endl;
    else std::cout << "User PATH = (null)" << std::endl;
    std::cout << "About to exit test..." << std::endl;
    sleep(1);
}

cfile.c 的内容如下:

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

int main(void)
{
    char *const args[] = {"./test", NULL};
    char *const envp[] = {"PATH=/usr/local/bin", "MYENV=hhh", NULL};
    const char *path = "/Users/Sky_Lee/Documents/Linux/Test/test";

    printf("Now in cfile[PID = %d]...\n", getpid());

    // exec ... 这里分别对应了六种示例代码

    printf("About exit cfile...\n");
    sleep(1);
}

并且,我们使用 export 导入了环境变量 MYENV="hello exec"

现在来依次看看这六种示例:

示例 1

execl(path, "./test", "./test", NULL);

运行,输出如下:

Now in cfile[PID = 4786]...
Now in test[PID = 4786]...
OS PATH = /usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/VMware Fusion.app/Contents/Public:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin
User PATH = hello exec
About to exit test...

可以验证,调用 exec 函数 不会 创建新进程,只是 进行了程序的替换,因为两个程序的 PID 是相同的

并且,OS PATHUser PATH 使用的是当前的环境变量

示例 2

execlp("./test", "./test", NULL);

输出与示例 1 一致,加上 p 只是让我们不用提供可执行程序的路径,会自动搜索环境变量,以此找到路径

示例 3

execle(path, "./test", "./test", NULL, envp);

与示例 1 不同的是:

OS PATH = /usr/local/bin
User PATH = hhh

因为加上了 e,表明自己维护环境变量,exec 就不会使用当前的环境变量了

示例 4、5、6
execv(path, args);
execvp("./test", args);
execve(path, args, envp);

三个示例的输出与示例 1、2、3 对应,因为加上 v,只是说明提供的是数组,不是列表

总结

在这里插入图片描述

exec 函数的本质

事实上,只有 execve 是真正的系统调用,其它五个函数最终也会调用 execve

这张图很好地说明了它们之间的关系:

在这里插入图片描述

简易 Shell

#include <iostream>
#include <bitset>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

class Shell
{
    static constexpr int MAX_COMMAND_LENGTH = 1024;
    static constexpr int DO_EXEC_ERROR = -1;
    static constexpr int DO_EXEC_SUCCESS = 0; // 后面发现没啥用(想想为啥没用
    static constexpr int PARSE_SUCCESS = 1;
    static constexpr int PARSE_ERROR = 2;
public:
    void Interface(void)
    {
        std::cout << "mysh % ";
        std::cout.flush();
        pid_t pid = fork();
        if(pid == 0)
            doExec();
        else
        {
            int status;
            wait(&status);
            if(status == PARSE_ERROR)
                std::cout << "mysh: parse error, your command might be to long!" << std::endl;
        }
    }
private:

    int doExec(void)
    {
        char* args[MAX_COMMAND_LENGTH];

        // 解析命令
        auto parse = [&](void) -> int
        {
            std::string command;
            std::getline(std::cin, command);
            size_t cur = 0, size = command.size();
            size_t curArgPos = 0;
            while (true)
            {   
                if(curArgPos == MAX_COMMAND_LENGTH - 1) // 还要留一个空间放 NULL
                    return PARSE_ERROR;
                auto next = command.find(' ', cur);
                auto temp = command.substr(cur, next - cur);
                args[curArgPos] = new char[temp.size()];
                strcpy(args[curArgPos++], temp.c_str());
                // std::cout << "debug: " << args[curArgPos - 1] << std::endl;
                if(next == std::string::npos)
                    break;
                cur = next + 1;
            }
            args[curArgPos] = NULL;
            return PARSE_SUCCESS;
        };

        if(parse() == PARSE_ERROR)
            exit(PARSE_ERROR);
        execvp(args[0], args);
        // 注意是 exit,而不是 return,
        // exit 会导致子进程退出
        // 而 return 不会,因为 return 只是让函数 doExec 退出
        // 这也说明了 exit 与 return 是有区别的
        std::cout << "mysh: command not found: " << args[0] << std::endl; // 如果 execvp 正常执行,这一句不会被执行
        exit(DO_EXEC_ERROR);  
    }
};

int main(void)
{
    Shell shell;
    while (1)
    {
        shell.Interface();
    }
}

注意: 简易 Shell 不支持 cd 操作(想想为什么),此外,简易 Shell 在遇到有 ' 的指令时,往往不能做出正确的动作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值