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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 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们的观看是我坚持下去的动力

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

相关文章
|
1月前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
37 0
|
3月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
652 2
|
3月前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
69 2
|
17天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
50 4
linux进程管理万字详解!!!
|
8天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
47 8
|
16天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
51 4
|
17天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
19天前
|
消息中间件 存储 Linux
|
25天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
30 1
|
1月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
23 1