首页 > 系统相关 >Linux平台下的进程控制

Linux平台下的进程控制

时间:2023-11-21 20:04:07浏览次数:43  
标签:int 平台 char 退出 exit Linux 进程 waitpid

进程创建

关于进程的创建,在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 将新程序的代码和数据进行替换时,会触发代码和数据的写时拷贝,此时子进程的程序替换不影响父进程。

标签:int,平台,char,退出,exit,Linux,进程,waitpid
From: https://blog.51cto.com/158SHI/8505952

相关文章

  • 7.1 Windows驱动开发:内核监控进程与线程回调
    在前面的文章中LyShark一直在重复的实现对系统底层模块的枚举,今天我们将展开一个新的话题,内核监控,我们以监控进程线程创建为例,在Win10系统中监控进程与线程可以使用微软提供给我们的两个新函数来实现,此类函数的原理是创建一个回调事件,当有进程或线程被创建或者注销时,系统会通过回......
  • 第12周linux课堂总结
        这周的linux课程我们学习了存储管理,从连接方式上,存储分为以下3种类型,分别是本地存储、外部存储和网络存储,从工作原理上,硬盘分为固态硬盘和机械硬盘,从硬盘接口上,硬盘分为以下几种类型,IDE——SATA硬盘,SCSI——SAS硬盘,其他——PCIe、FC硬盘,SAS是新一代的SCSI技术,SAS硬盘......
  • 硬盘录像机无法注册到视频监控平台EasyCVR上是什么原因?该如何解决?
    视频监控汇聚平台EasyCVR可拓展性强、视频能力灵活、部署轻快,可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等,以及支持厂家私有协议与SDK接入,包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安防视频监控的能力,也具备接入AI智能分析的能力,包括对人、车、物、行为等事......
  • Linux中的$符号的三种常见用法
    本文总结了Linux中的$符号的各种用法用法一:显示脚本参数($0、$?、$*、$@、$#、$$、$!)(本质上属于变量替换)$0:就是该bash文件名,个位数的,可直接使用数字,但两位数以上,则必须使用{}符号来括住,如${10}.$?:是上一指令的返回值,成功是0,不成功是1。一般来说,UNIX(linux)系统的进程以执行......
  • 安防视频监控管理平台EasyCVR定制首页开发与实现
    视频监控平台EasyCVR能在复杂的网络环境中,将分散的各类视频资源进行统一汇聚、整合、集中管理,在视频监控播放上,TSINGSEE青犀视频安防监控汇聚平台可支持1、4、9、16个画面窗口播放,可同时播放多路视频流,也能支持视频定时轮播。视频监控汇聚平台EasyCVR支持多种播放协议,包括:HLS、HTT......
  • CentOS7环境下Linux命令的基本指令(二)
    权限管理命令权限管理命令:chmod命令名称:chmod命令英文原意:changethepermissionsmodeofafile命令所在路径:/bin/chmod执行权限:所有用户语法:chmod[{ugoa}{±=}{rwx}][文件或目录]chmod[mode=421][文件或目录]-R递归修改功能描述:改变文件或目录的权限指令解析:可以对不同......
  • linux安装tomcat
    1.Centos+Tomcat在线安装sudoyuminstalljava-1.7.0-openjdk-devel#安装javayuminstalltomcat#在线安装tomcatsystemctlstarttomcat#启动tomcat服务器yuminstalltomcat-webappscd/usr/share/tomcat/webapps#DeleteunnecessarythingsmkdirROOT......
  • 安防监控视频云存储平台EasyCVR页面播放卡顿的优化方法
    视频监控平台EasyCVR能在复杂的网络环境中,将分散的各类视频资源进行统一汇聚、整合、集中管理,在视频监控播放上,TSINGSEE青犀视频安防监控汇聚平台可支持1、4、9、16个画面窗口播放,可同时播放多路视频流,也能支持视频定时轮播。视频监控汇聚平台EasyCVR支持多种播放协议,包括:HLS、HTT......
  • 安防监控视频云存储平台EasyCVR页面播放卡顿的优化方法
    视频监控平台EasyCVR能在复杂的网络环境中,将分散的各类视频资源进行统一汇聚、整合、集中管理,在视频监控播放上,TSINGSEE青犀视频安防监控汇聚平台可支持1、4、9、16个画面窗口播放,可同时播放多路视频流,也能支持视频定时轮播。视频监控汇聚平台EasyCVR支持多种播放协议,包括:HLS、HTTP......
  • 羚通视频智能分析平台工地安全帽、反光背心AI智能算法检测系统算法识别
    羚通视频智能分析平台是一款专门用于工地安全帽和反光背心的AI智能检测系统算法识别的工具。该平台利用深度学习和计算机视觉技术,提供一种安全帽佩戴识别检测的智能算法方案,具有高精度检测、实时性强、可扩展性强、自定义配置和智能分析和预警等优点,能够满足工地安全管理的需求,提......