Linux平台下的进程控制
  PzTaj2xFbKXN 2023年11月22日 16 0

进程创建

关于进程的创建,在Linux进程状态与进程优先级部分已进行过讨论,为了保证文章的完整性,这里再进行简述。

在linux平台下,创建进程有两种方式:运行指令和使用系统调用接口,前者是在指令层面创建进程,后者是在代码层面创建进程。在C/C++代码中,使用 fork(2) 创建子进程,fork(2)的工作有3步:创建进程、填充进程内核数据结构和值返回,fork(2) 在值返回时分别在父、子进程中返回两次。关于fork(2)的使用和更多细节,请参考上述文章。

为了叙述方便,本文以 func(2) 表示func是一个2号文档的系统调用,而以 func(3) 表示func是一个3号文档的C接口。

进程终止

进程终止的三种场景

一个进程终止,无外乎三种情况:

  • 代码运行完毕,结果正确。这正是我们想要的,此时不需要做其他处理。
  • 代码运行完毕,结果不正确。
  • 程序异常终止,代码未运行完毕。

针对第二种和第三种情况,我们需要知道程序的错误信息或异常信息,以对程序进行调整。对于第二种情况的错误信息,一般可以通过进程的退出码获悉。

进程的退出码

下面是一个经典的"Hello World"实例:

/*
代码2.1
*/
#include <stdio.h>
int main()
{
  printf("Hello World\n");
	return 0;
}

在C/C++程序中,main 函数是被系统中的其他函数调用的,上面的代码第8 行在 return 后,其实是将 0 返回给了 exit(3) 函数,exit(3) 的参数即为这个进程的退出码(exit code)。对于 exit(3) 函数,这是一个使进程主动退出的函数,下文会进行详谈,这个函数的参数即为进程的退出码。在 main 函数中,当执行 return 时,main 函数对应的进程已经可以认为结束,所以 return 返回一个值与用该值调用 exit(3) 是等价的。

退出码标识了进程的退出状态,规定,退出码为 0,表示程序正常结束且结果正确,否则认为程序运行错误。进程退出后,会将退出码返回给其父进程,父进程最终会将退出码转交给用户,供用户做出判断和决策。即,退出码是服务于用户的

承上,在C/C++中,全局变量 errno 会保存最近一次C库函数运行后的退出码,同时,C接口 strerror 可以将错误码转化为错误信息(错误码描述)。

#include <string.h>
char *strerror(int errnum);

上述的,用户接收错误码,并根据错误码对程序进行调整,只针对于程序运行完毕的情况,而不考虑程序异常退出的情况。程序异常退出时,首先,其是否返回了退出码是无法确定的,假设在进程退出时没有返回有效的退出码而用户使用了这个退出码,就会使用户对程序的退出情况进行误判;其次,假设进程退出时确实返回了有效的退出码,此时用户依然无法确定这个退出码是否有效。承上,判断程序的执行结果时,首先要判断其是否异常退出,再看退出码。

可以认为,程序异常退出,本质是接收到了某种信号。相关内容会在有关进程信号的文章中讨论。

进程终止的方式

不考虑程序异常终止的情况和线程概念,有 3 种方式使进程终止:

  • 从 main 函数返回。
  • 调用 exit(3)。
  • 调用 _exit(2)
#include <stdlib.h>
void exit(int status);

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

exit(3) 是一个C语言接口,在任何地方被调用时,进程直接退出并返回退出码;_exit(2) 是一个系统调用,在任何地方被调用时,进程直接退出并返回退出码。exit(3) 与 _exit(2) 的不同之处在于,exit(3)在被调用时,会先刷新缓冲区,关闭流,再调用 exit(2) 使进程退出。即,_exit(2)与exit(2)是调用者与被调用关系

Linux平台下的进程控制_进程终止

进程等待

为什么要进行进程等待 & 什么是进程等待

进行进程等待,即是要解决三个问题:

  • 如文章Linux进程状态与进程优先级所说,当一个进程退出后,如果其父进程没有查看和回收子进程,子进程就会进入僵尸状态。僵尸进程无法被杀死(kill),只能通过父进程进行进程等待来处理,进而解决僵尸进程的资源泄露问题。这一点是必须要处理的。
  • 父进程创建子进程的目的,即是要让子进程完成某些任务,子进程退出后,通过进程等待,父进程可以获取子进程的退出状态以获悉子进程的任务完成情况,以最终使用户获取进程的退出情况。
  • 通过进程等待可以保证父进程最后退出,避免产生孤儿进程。

进程等待,即是通过系统调用 wait(2)waitpid(2) 对子进程进行进程回收状态检测的过程。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

wait和waitpid

由于 waitpid 的功能是 wait 功能的全集,所以只详细介绍waitpid,wait的使用及原理与 waitpid 同理。waitpid的函数原型如上。

参数和返回值

pid参数用来指定等待对象。这里考虑pid参数的两种情况:

  • pid == -1 表示等待任意的子进程,任意的子进程僵尸,都会被waitpid 进行资源回收。
  • pid > 0 等待指定的子进程,这个子进程的pid为指定的实参。只有这个指定的子进程僵尸时,waitpid才会对其进行资源回收。

status参数是用来进行状态收集的。获取子进程的退出信息,本质是获取子进程的数据,而由于进程之间的独立性,这个工作必须由操作系统(系统调用)完成。status 是一个输出型参数,由操作系统通过指针对其进行修改。承上,进程退出的情况无外乎三种:异常退出、正常退出结果正确和正常退出结果不正确,而作为父进程/用户, 最期望获得的子进程退出的信息为:

  1. 子进程是否异常
  2. 如果没有异常,结果是否正确
  3. 如果结果不正确,错误信息是什么

作为一个32位(32位和64位机器下)的整数,status 只有后16位被使用:

  • 当进程正常退出时,status前16位的低8位为0,高8位存储退出状态。
  • 当进程异常退出时,status前16位的低7位存储终止信号,第8位存储core dump标志(这里先不做讨论);高8位不被使用。

Linux平台下的进程控制_进程终止_02

在用户层面,如果要获取 status 中的信息,可以手动进行位运算,不过最常见的做法是使用系统提供的宏。最常用的两个宏为:

  • WIFEXITED(int status) 判断是否异常。
  • WEXITSTATUS(int status) 提取子进程的退出码。

如果不关心子进程的退出状态,可以将 status 置为 NULL。

options参数用来指定父进程的等待方式。options有两个值可供选择:

  • options为0 进行阻塞等待。父进程在运行至waitpid,且子进程当前还未退出,父进程便会进行阻塞等待子进程,进入子进程的阻塞队列,直至子进程终止(僵尸)。
  • options为宏WNOHANG 进行非阻塞等待。父进程在运行至waitpid时:
  • 如果子进程当前还未退出,则父进程不阻塞,waitpid 直接返回 0
  • 如果子进程已经退出,则waitpid对子进程进行资源回收与状态收集。

waitpid的返回值是一个 int 类型:

  • 如果返回值大于0,则返回值为被回收进程的 pid。
  • 如果父进程进行非阻塞等待,且此时子进程尚未退出,则waitpid返回0
  • waitpid也会出现等待失败的情况,例如当等待的进程不是自己的子进程,此时waitpid返回 -1

非阻塞轮询

承上,waitpid 可以进行非阻塞等待,为了以非阻塞方式最终成功对子进程进行回收,父进程需要进行轮询,即不断调用 waitpid 对子进程进行检查。相比阻塞等待,非阻塞轮询期间父进程可以进行其他工作,灵活性更强。

/*
*代码 3.1
*/
int main()
{
    pid_t id = fork();
    if(id < 0) { perror("fork err"); exit(1); }
    else if(id == 0)
    {
        /*child do work...*/
        sleep(5);
        exit(0);
    }
    else
    {
        int status = 0;
        //父进程:进行非阻塞轮询
        while(waitpid(id, &status, WNOHANG) == 0) {
            printf("child still working...\n");
            sleep(1);
            /*father do other work...*/
        }
        printf("child exit\n");
        exit(0);
    }
    return 0;
}
//运行结果:
child still working...
child still working...
child still working...
child still working...
child still working...
child exit

基本原理

wait/waitpid 的基本原理为:子进程僵尸时,代码和数据被销毁,而进程task_struct 不销毁,操作系统(waitpid/wait)通过读取子进程 task_struct 中的信息,将错误信息通过位运算集成在 status 中,并将子进程的task_struct释放。

Linux平台下的进程控制_非阻塞轮询_03

程序替换

当用 fork 函数创建子进程后,子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程执行的程序完全被替换为新程序,这个新程序会被从其入口开始执行。这即是程序替换。调用 exec 函数不创建新进程,只是用磁盘上的一个新程序替换了当前程序(的正文段、数据段、堆段和栈段,当前进程的 task_struct 和 mm_struct 大体不变,只修改其中的部分信息。

exec系列接口

有 7 种不同的 exec 函数可供使用,用户可以根据情况进行选择调用:

#include <unistd.h>

extern char **environ;

/*1.*/ int execl(const char *path, const char *arg, ...);
/*2.*/ int execlp(const char *file, const char *arg, ...);
/*3.*/ int execle(const char *path, const char *arg, ..., char * const envp[]);
/*4.*/ int execv(const char *path, char *const argv[]);
/*5.*/ int execvp(const char *file, char *const argv[]);
/*6.*/ int execvpe(const char *file, char *const argv[], char *const envp[]);
/*7.*/ int execve(const char *filename, char *const argv[], char *const envp[]);
//上述所有函数,程序替换成功返回0,否则返回-1

其中前 6 个函数是C语言标准库提供的,第 7 个函数是2号手册中的系统调用。在实现层面,前6个接口最终都会调用最后一个系统调用。

这些 exec 函数,第一个参数需要用户指定需要新程序的位置;后面的参数需要用户指定如何执行这个新程序,一般以命令行参数说明;某些函数还会有环境变量相关的参数。观察这些 exec 函数,除了统一的 exec 前缀之外,还有以下后缀:

  • l(list) 表示用户需要以命令行参数列表的形式指定程序的执行方法。
  • p(PATH)系统会自动在环境变量 PATH 中寻找这个程序。
  • v(vector) 表示用户需要以命令行参数数组的形式指定程序的执行方法。
  • e(env) 表示用户需要手动组装环境变量表。每个进程的地址空间中都有一份环境变量,环境变量在进程被创建时就已经存在。进行程序替换时,默认使用原来的环境变量。exece* 需要用户手动组装环境变量表,在这个过程中可以使用当前的环境变量表environ ,也可以自定义环境变量表,对原来的环境变量表进行覆盖

承上,在这些 exec 函数的执行层面,用户传入的命令行参数列表都会最终被转化为命令行参数数组,并使用 environ 环境变量,最终执行 execve 系统调用。

Linux平台下的进程控制_进程终止_04

一个程序替换实例(mini_shell)

下面是一个模拟 shell 的mini_shell,可以实现最基本的shell功能。用户输入后,mini_shell会解析命令,将命令和命令参数存储在一个数组中,然后 fork 出一个子进程,在子进程中调用 execvp 进行程序替换,以完成用户的任务。

/*
* 代码 4.1
*/
#define LEFT "["
#define RIGHT "]"
#define COMMAND_SIZE 1024
#define ARG_MAX 50
#define PATH_LENGTH 100
#define ENV_LENGTH 100
#define DELIM_STR " \t"

int quit = 0; //shell是否退出
int last_code = 0; //最近一次命令的退出码
char command_line[COMMAND_SIZE]; //存储输入命令
char* command_vector[ARG_MAX]; //存储解析后的输入命令

const char* get_user_name()
{
  return getenv("USER");
}

const char* get_host_name()
{
  return getenv("HOSTNAME");
}

const char* get_pwd()
{
  return getenv("PWD");
}

//普通命令执行
int normalCommand(int argCunt)
{
    //fork一个子进程,并将子进程替换为欲执行命令
    pid_t id = fork();
    if(id < 0) { perror("fork err"); exit(1); }
    else if(id == 0)
    {
      if(!strcmp(command_vector[0], "ls") || !strcmp(command_vector[0], "ll"))
      {
        command_vector[argCunt++] = "--color";
        command_vector[argCunt] = NULL;
      }
      if(!strcmp(command_vector[0], "ll"))
      {
        command_vector[0] = "ls";
        command_vector[argCunt++] = "-l";
        command_vector[argCunt] = NULL;
      } //进行程序替换
      int ret = execvp(command_vector[0], command_vector);
      if(ret == -1) { exit(1); }
    }
    else
    { //等待子进程和获取退出信息,更新最近一次的退出信息
      int status = 0;
      waitpid(id, &status, 0);
      last_code = WEXITSTATUS(status);
    }
    return 1;
}

//命令解析
int stringSplit()
{
  checkRedirect();//检查和判断重定向操作
  //printf("check complete, is_redirect:%d, filename:%s\n", is_redirect, filename);
  int index = 0;
  command_vector[index++] = strtok(command_line, DELIM_STR);
  if(command_vector[index - 1] != NULL) while(command_vector[index++] = strtok(NULL, DELIM_STR));
  return index - 1;
}

//用户交互
void interAct()
{
  printf(LEFT"%s@%s %s"RIGHT" ", get_user_name(), get_host_name(), get_pwd());
  fgets(command_line, COMMAND_SIZE, stdin);
  command_line[strlen(command_line) - 1] = '\0';
}

void mini_shell_init()
{
  filename = NULL;
}

int main()
{
  while(!quit)
  {
    mini_shell_init();
    interAct(); //用户交互(输入命令)
    int argCount = stringSplit(); //命令解析
    if(argCount == 0) { continue; }
    normalCommand(argCount); //执行命令
  }
  return 0;
}

使用效果:

[@shr Tue Nov 21 16:12:05 10.28_mini_shell]$ ./testShell 
[shr@VM-24-8-centos /home/shr/code/2023_y/10.28_mini_shell] ls -a -l
total 44
drwxrwxr-x  2 shr shr  4096 Nov  3 10:44 .
drwxrwxr-x 82 shr shr  4096 Nov 21 14:26 ..
-rw-rw-r--  1 shr shr   303 Oct 29 19:58 makefile
-rw-rw-r--  1 shr shr  5189 Nov  3 10:47 mini_shell.c
-rwxrwxr-x  1 shr shr 17960 Nov  3 10:44 testShell
-rw-rw-r--  1 shr shr    27 Nov  3 10:02 txt
[shr@VM-24-8-centos /home/shr/code/2023_y/10.28_mini_shell] cowsay hello
 _______
< hello >
 -------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
[shr@VM-24-8-centos /home/shr/code/2023_y/10.28_mini_shell] exit
[@shr Tue Nov 21 16:12:26 10.28_mini_shell]$ 
[@shr Tue Nov 21 16:12:26 10.28_mini_shell]$

在这个过程中shell进程与子进程的运行大致情况如下:

Linux平台下的进程控制_非阻塞轮询_05

程序替换的原理

如上文所说,进行程序替换操作时,操作系统会将程序的代码和数据加载到内存,此时进程的用户区的代码段和数据段完全被新程序替换,从新程序的程序入口处开始执行。程序替换不创建新进程,只会对进程的某些内核数据结构字段进行调整,所以进程的 PID 不变。在原来的程序中,如果 exec 执行成功,后面的、原来的代码已经被替换,不继续执行;如果 exec 执行失败,则继续执行后面的、原来的代码。

Linux平台下的进程控制_进程终止_06

承上,exec 具有加载器的效果,因为其可以做到将硬盘中的可执行程序加载到内存中。

类似上述的 mini_shell 程序,之所以支持多进程下的程序替换,是因为进程具有独立性,当子进程调用 exec 将新程序的代码和数据进行替换时,会触发代码和数据的写时拷贝,此时子进程的程序替换不影响父进程。

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月22日 0

暂无评论

PzTaj2xFbKXN