一篇文章教会你什么是Linux进程控制(下)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 3.3 进程等待示例#include <stdio.h>#include <unistd.h>#include <stdlib.h>

3.3 进程等待示例

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int code = 0; // 定义一个全局变量code,用于存储子进程的退出码
int main()
{
    pid_t id = fork(); // 创建一个子进程
    if(id < 0)
    {
        perror("fork"); // 如果创建子进程失败,则输出错误信息
        exit(1); // 退出程序,返回状态码1
    }
    else if(id == 0)
    {
        // 子进程
        int cnt = 5; // 定义一个计数器
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid()); // 打印子进程的信息
            cnt--;
            sleep(1); // 子进程休眠1秒
        }
        code = 15; // 将全局变量code的值设置为15
        exit(15); // 子进程退出,返回退出码15
    }
    else
    {
        // 父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid()); // 打印父进程的信息
        int status = 0; // 定义一个变量用于存储子进程的状态
        pid_t ret = waitpid(id, &status, 0); // 阻塞式的等待子进程退出
        if(ret > 0)
        {
            printf("等待子进程成功, ret: %d, 子进程收到的信号编号: %d, 子进程退出码: %d\n",\
                    ret, status & 0x7F ,(status >> 8) & 0xFF); // 打印子进程的退出信息
            printf("code: %d\n", code); // 打印全局变量code的值
        }
    }
}

这段代码创建了一个父进程和一个子进程,父进程通过fork()函数创建子进程。子进程会打印自己的信息,并在循环中每秒减少计数器的值,直到计数器为0。然后,子进程将全局变量code的值设置为15,并退出。父进程会打印自己的信息,并使用waitpid()函数阻塞等待子进程退出。当子进程退出后,父进程会打印子进程的退出信息,包括子进程收到的信号编号和退出码,以及全局变量code的值。

第一次运行让其正常终止,所以没有信号编号的返回,正常走exit函数退出,退出码为15,即为我们自己定义的退出码。

第二次运行期间我们使用kill -9 子进程pid 命令终止子进程,信号编号返回9,此时的退出码并无意义,因为程序非正常退出。

还需要注意的是,这里不管以何种方式终止进程,全局变量code始终为0,这是因为子进程和父进程是两个独立的进程,它们有各自独立的内存空间。在子进程中修改全局变量 code 的值,不会影响父进程中的 code 变量。子进程的修改只影响子进程内部的变量,而不会影响父进程的变量。

要实现子进程修改全局变量并使其对父进程可见,可以使用进程间通信机制,例如管道(Pipe)或共享内存(Shared Memory)。这样父子进程之间可以共享一块内存区域,使得修改在两个进程中都可见。

常见的信号编号如下(这里只做了解):

SIGHUP (1): 终端挂起或控制进程终止。

SIGINT (2): 中断信号,通常是Ctrl+C。

SIGQUIT (3): 退出信号,通常是Ctrl+\,会产生核心转储。

SIGILL (4): 非法指令。

SIGABRT (6): 终止信号,通常是abort()函数发出的信号。

SIGFPE (8): 浮点异常。

SIGKILL (9): 强制终止,不能被忽略、阻塞或捕获。

SIGSEGV (11): 段错误,试图访问无法访问的内存。

SIGPIPE (13): 管道破裂。

SIGALRM (14): 定时器超时。

SIGTERM (15): 终止信号,常用于请求进程正常终止。

SIGUSR1 (10): 用户自定义信号1。

SIGUSR2 (12): 用户自定义信号2。

SIGCHLD (17): 子进程状态改变,例如子进程终止时发送给父进程。

SIGCONT (18): 继续执行一个已停止的进程。

SIGSTOP (19): 停止信号,用于停止进程的执行。

SIGTSTP (20): 终端停止信号,通常是Ctrl+Z。

SIGTTIN (21): 后台进程试图从控制终端读取。

SIGTTOU (22): 后台进程试图向控制终端写入。

SIGBUS (7): 总线错误,试图访问不属于你的内存地址。

3.4 进程的阻塞等待方式

1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <sys/wait.h>
  5 int main()
  6 {
  7         pid_t id = fork();
  8         if(id == 0)
  9         {
 10             //子进程
 11             printf("子进程开始运行, pid: %d\n", getpid());
 12             sleep(3);
 13         }
 14         else
 15         {
 16             //父进程
 17             printf("父进程开始运行, pid: %d\n", getpid());
 18             int status = 0;                                                                                                                                                                                                      
 19             pid_t id = waitpid(-1, &status, 0); //阻塞等待, 一定是子进程先运行完毕,然后父进程获取之后,才退出!
 20             if(id > 0)
 21             {
 22                 printf("wait success, exit code: %d\n", WEXITSTATUS(status));
 23             }
 24         }
 25     return 0;
 26 }

运行结果

[kingxzq@localhost Documents]$ ./test1
父进程开始运行, pid: 12554
子进程开始运行, pid: 12555
wait success, exit code: 0

3.5 进程的非阻塞等待方式

1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <sys/wait.h>
  5 int main()
  6 {
  7     pid_t pid;
  8     pid = fork();
  9     if(pid < 0){
 10         printf("%s fork error\n",__FUNCTION__);
 11         return 1;
 12     }
 13     else if( pid == 0 ){ //child
 14         printf("child is run, pid is : %d\n",getpid());
 15         sleep(5);
 16         exit(1);
 17     }   
 18     else{
 19         int status = 0;
 20         pid_t ret = 0;
 21         do
 22         {
 23             ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
 24             if( ret == 0 ){
 25                 printf("child is running\n");
 26             }
 27             sleep(1);
 28         }while(ret == 0);
 29         if( WIFEXITED(status) && ret == pid ){
 30             printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
 31         }
 32         else{
 33             printf("wait child failed, return.\n");
 34             return 1;
 35         }
 36     }
 37     return 0;
 38 }

运行结果

[kingxzq@localhost Documents]$ ./test
child is running
child is run, pid is : 13231
child is running
child is running
child is running
child is running
wait child 5s success, child return code is :1.

kill命令终止运行结果

[kingxzq@localhost Documents]$ ./test
child is running
child is run, pid is : 13268
child is running
child is running
child is running
wait child failed, return.

3.6 进程的阻塞等待方式和进程的非阻塞等待方式有什么区别

进程的阻塞等待方式和进程的非阻塞等待方式是两种不同的等待子进程状态变化的方式:

阻塞等待:当父进程调用等待函数(如wait、waitpid等)等待子进程退出时,父进程会一直阻塞(即挂起自己的执行),直到子进程退出或发生其他指定的状态变化。在等待期间,父进程不会继续执行其他任务。

非阻塞等待:当父进程调用非阻塞等待函数(如waitpid函数的使用WNOHANG标志)等待子进程退出时,父进程会继续执行自己的任务,不会被阻塞。父进程会立即返回等待函数,无论子进程的状态是否发生变化。非阻塞等待允许父进程在等待子进程的同时继续执行其他任务。

总之,阻塞等待会导致父进程在等待子进程状态变化期间被挂起,而非阻塞等待允许父进程在等待子进程的同时继续执行其他任务。选择使用哪种等待方式取决于具体的应用场景和需求。

进程程序替换

1.替换原理

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

2.替换函数

这些函数是用于在Linux/Unix操作系统中执行新的程序的系统调用函数。它们的作用是在一个进程内启动一个新的程序执行,取代当前进程的执行。这些函数在C语言标准库头文件<unistd.h>中声明。

下面是对这些函数的简要说明:

execl:

原型int execl(const char *path, const char *arg, ...);

功能:用于执行指定路径的可执行文件,第一个参数是要执行的程序的路径,后面的参数是传递给新程序的命令行参数,以NULL为结束标志。

示例execl("/bin/ls", "ls", "-l", NULL);

execlp:

原型int execlp(const char *file, const char *arg, ...);

功能:类似于execl,但是它会在系统的路径中搜索可执行文件。

示例execlp("ls", "ls", "-l", NULL);

execle:

原型int execle(const char *path, const char *arg, ..., char *const envp[]);

功能:类似于execl,但是可以指定新程序的环境变量。最后一个参数是一个指向环境变量的指针数组,以NULL为结束标志。

示例char *const envp[] = {"PATH=/bin", NULL}; execle("/bin/ls", "ls", "-l", NULL, envp);

execv:

原型int execv(const char *path, char *const argv[]);

功能:类似于execl,但是参数传递方式是使用一个指向参数字符串数组的指针。第一个参数是要执行的程序的路径,第二个参数是指向参数字符串数组的指针,以NULL为结束标志。

示例char *const argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv);

execvp:

原型int execvp(const char *file, char *const argv[]);

功能:类似于execv,但是它会在系统的路径中搜索可执行文件。

示例char *const argv[] = {"ls", "-l", NULL}; execvp("ls", argv);

execvpe:

原型int execvpe(const char *path, char *const argv[], char *const envp[])

功能:类似于execv,但是可以指定新程序的环境变量,最后一个参数是一个指向环境变量的指针数组,以NULL为结束标志。

示例char *const argv[] = {"ls", "-l", NULL}; char *const envp[] = {"PATH=/bin", NULL}; execve("/bin/ls", argv, envp);

这些函数通常用于在一个进程内部启动一个新的程序,新程序取代当前进程的执行。执行成功则不会返回,失败则会返回-1并设置errno。它们通常用于在C程序中执行其他程序,比如在Shell中运行命令。

看下面的两段代码:

mycmd.c

1 #include <stdio.h>
  2 #include <string.h>
  3 #include <stdlib.h>
  4 
  5 int main(int argc, char *argv[])
  6 {
  7     if(argc != 2)
  8     {
  9         printf("can not execute!\n");
 10         exit(1);
 11     }
 12 
 13     printf("获取环境变量: MY_VAL: %s\n", getenv("MY_VAL"));                                                 
 14 
 15     if(strcmp(argv[1], "-a") == 0)
 16     {
 17         printf("hello a!\n");
 18     }
 19     else if(strcmp(argv[1], "-b") == 0)
 20     {
 21         printf("hello b!\n");
 22     }
 23     else{
 24         printf("default!\n");
 25     }
 26 
 27     return 0;
 28 }

test.c

1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include <unistd.h>
    4 #include <sys/wait.h>
    5 
    6 #define NUM 16
    7 
    8 const char *myfile = "./mycmd";
    9 
   10 int main(int argc, char*argv[], char *env[])
   11 {
   12        char *const _env[NUM] = {
   13            (char *)"MY_VAL=23333333",
   14            NULL
   15        };
   16             printf("进程开始运行, pid: %d\n", getpid());        
   17             sleep(3);                                           
   18             char *const _argv[NUM] = {                          
   19                 (char*)"ls",                                    
   20                 (char*)"-a",                                    
   21                 (char*)"-l",                                    
   22                 (char*)"-i",                                    
   23                 NULL                                            
   24             };                                                  
   25                                                                 
   26             //execl(myfile, "mycmd", "-b", NULL);//调用自己的进程   
   27             //execl("/usr/bin/ls", "ls", "-a", "-l", NULL);//调用系统ls进程,自己输入字符命令
   28             //execlp("./test.py", "test.py", NULL);//调用自建的python进程   
   29             //execlp("python", "python", "test.py", NULL);//结果同上,调用形式不同
   30             //execlp("bash", "bash", "test.sh", NULL); //调用自建的shell进程
   31             //execlp("ls", "ls", "-a", "-l", NULL); //调用系统ls进程,自己输入字符命令
   32             execle(myfile, "mycmd", "-a", NULL, _env);          
   33                                                                 
   34             //execv("/usr/bin/ls", _argv); //和上面的execl只有传参方式的区别
   35             //execvp("ls", _argv);//调用系统ls进程,输入字符串数组名
   36             //execvpe("/usr/bin/ls",_argv,env);//效果同execv,多了一个环境变量参数                        
   37     return 0;                                                               
   38 }

运行结果:

[kingxzq@localhost Documents]$ ./test
进程开始运行, pid: 18076
获取环境变量: MY_VAL: 23333333
hello a!

3.函数解释

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。

如果调用出错则返回-1

所以exec函数只有出错的返回值而没有成功的返回值。

4.命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH

e(env) : 表示自己维护环境变量

下图exec函数族 一个完整的例子:

制作简易shell

就像系统中的bash(即shell),完成下面这类操作

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。

所以要写一个shell,需要循环以下过程:

1. 获取命令行

2. 解析命令行

3. 建立一个子进程(fork)

4. 替换子进程(execvp)

5. 父进程等待子进程退出(wait)

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串
char *g_argv[SIZE];
//环境变量的buffer
char g_myval[64];
// shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令
int main()
{
    extern char**environ;//获取全局环境变量的指针
    //0. 命令行解释器,一定是一个常驻内存的进程,不退出
    while(1)
    {
        //1. 打印出提示信息 [kingxzq@localhost myshell]# 
        printf("[kingxzq@localhost myshell]# ");
        fflush(stdout);
        memset(cmd_line, '\0', sizeof cmd_line);
        //2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"]
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        if (strlen(cmd_line) <= 1) { // 如果只有回车换行符,长度为1
            continue;//输入为空重新输入
        }
        cmd_line[strlen(cmd_line)-1] = '\0';//用\0将\n替换
        //"ls -a -l -i\n\0"
        //3. 命令行字符串解析:"ls -a -l -i" -> "ls" "-a" "-i"
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
        int index = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[index++] = "--color=auto";//添加自动颜色
        }
        if(strcmp(g_argv[0], "ll") == 0)//ll本身为ls -l,所以单独添加一个命令
        {
            g_argv[0] = "ls";
            g_argv[index++] = "-l";
            g_argv[index++] = "--color=auto";
        }
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL
        if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)//添加环境变量
        {
            strcpy(g_myval, g_argv[1]);
            int ret = putenv(g_myval);//输入环境变量
            if(ret == 0) printf("%s export success\n", g_argv[1]);
                continue;
        }
        //4.内置命令, 让父进程(shell)自己执行的命令,我们叫做内置命令,内建命令
        //内建命令本质其实就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //cd命令调用
        {
            if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd ..
            continue;
        }
        //5. fork()
        pid_t id = fork();
        if(id == 0) //child
        {
            printf("下面功能让子进程进行的\n");
            printf("child, MYVAL: %s\n", getenv("MYVAL"));//获取我们输入的环境变量MYVAL
            printf("child, PATH: %s\n", getenv("PATH"));//获取环境变量路径
            execvp(g_argv[0], g_argv); // ls -a -l -i
            exit(1);
        }
        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));//退出码接收
    }
    return 0;
}

测试ls命令

测试添加环境变量和cd命令

结语

有兴趣的小伙伴可以关注作者,如果觉得内容不错,请给个一键三连吧,蟹蟹你哟!!!

制作不易,如有不正之处敬请指出

感谢大家的来访,UU们的观看是我坚持下去的动力

在时间的催化剂下,让我们彼此都成为更优秀的人吧!!!

相关文章
|
20天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
45 1
|
2月前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
49 0
|
8天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
61 13
|
15天前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
23天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
1月前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
142 4
linux进程管理万字详解!!!
|
28天前
|
缓存 算法 Linux
Linux内核的心脏:深入理解进程调度器
本文探讨了Linux操作系统中至关重要的组成部分——进程调度器。通过分析其工作原理、调度算法以及在不同场景下的表现,揭示它是如何高效管理CPU资源,确保系统响应性和公平性的。本文旨在为读者提供一个清晰的视图,了解在多任务环境下,Linux是如何智能地分配处理器时间给各个进程的。
|
1月前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
85 8
|
1月前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
90 1
|
1月前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?