首页 > 系统相关 >Linux进程控制

Linux进程控制

时间:2024-11-01 20:44:32浏览次数:5  
标签:status 控制 函数 程序 退出 exit Linux 进程

目录

1.进程创建

1.1.fork()函数常规用法

1.2 .fork()函数创建失败的原因 

2.进程退出 

2.1退出码

2.2将错误码转化为错误描述 

2.2.1利用系统自带的方法进行转化

2.2.2自定义的方式

2.3普通函数的返回值

2.4错误码

2.4.2错误码和退出码的区别 

2.5进程退出的几种情况

2.6.进程退出的方式

2.6.1.正常退出

2.6.2.exit()函数和_exit()函数

一、exit()函数

二、_exit()函数

2.6.3. exit()、_exit()函数的区别

3.进程等待

3.1为什么要进行进程等待 

3.2进程等待的方式

3.2.1.wait

3.2.1.waitpid

3.3.进程等待的两种方式:阻塞等待和非阻塞等待

3.3.1阻塞等待

3.3.2非阻塞等待

3.4获取子进程的退出状态

3.4.1位操作获取子进程的退出状态

3.4.2利用宏来获取子进程退出状态

4.进程程序替换

4.1为什么要有进程程序替换

4.2进程程序替换的概念和原理

4.3直接写代码----exec*接口

4.3.1 execl的单进程场景 

4.3.2 execl的多进程进程场景 

4.4学习另外的exec*接口

 4.4.1 exec*接口 + p

4.4.2.exec*接口 + e

4.4.3.exec*接口 + v

5.补充知识

5.1.putenv

5.2.为什么子进程进行替换,父进程无任何影响?

5.3.shell是如何执行起来一个指令的?

6. 应用场景


1.进程创建

1.1.fork()函数常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

1.2 .fork()函数创建失败的原因 

系统中有太多的进程。

实际用户的进程数超过了限制 。


2.进程退出 

2.1退出码

 概念:main函数的返回值,又叫做进程的退出码。

代码执行成功,程序能够执行到main函数的末尾并返回,而不是说程序中的每一行都按预期执行了,因为有些错误不能被捕获或者导致程序提前退出了。 

非0返回值,通常用于表示不同类型错误/异常的原因,退出码的字符串含义取决于程序的设计者。 

退出码:分为 0 和 !0

0:表示程序正常运行,进程执行成功。

 !0:表示程序异常退出,进程执行失败。非零又用1 2 3 4等等,数字表示不同的错误信息

bash会自动记录上一个程序的退出码 : echo $?

2.2将错误码转化为错误描述 

2.2.1利用系统自带的方法进行转化

错误信息大概133种,自己可以去看看。 

2.2.2自定义的方式

#include<stdio.h>
#include<string.h>

enum{
    success=0,
    malloc_err,
    open_err
};
  
const char* errorDesc(int code)
{
    switch(code)
    {
        case success:
            return "running sucess!";
        case malloc_err:
            return "malloc failure!";
        case open_err:
            return "file open failure!";
        default:
            return "unkown error!";                                                
     }
}
 
int main()
{
    int exit_code = malloc_err;
    printf("%s\n", errorDesc(exit_code));
 
   return 0;
}

2.3普通函数的返回值

普通函数一般返回值或者状态。 

以fopen为例

执行结果:文件打开成功,fopen()返回指向该文件的指针;文件打开失败,fopen()返回NULL。

执行情况:返回了非空的FILE*指针,则可认为函数执行成功;返回了NULL,则可认为函数执行失败,需要进一步检查错误的原因(errno变量或调用perror()函数)。

普通函数退出,仅仅表示函数调用完毕。

函数也被称为子程序,与进程退出时返回退出码类似,函数执行完毕也会返回一个值,这个值通常用于表示函数的执行结果或状态。

调用函数,我们通常想看到两种结果:a.函数的执行结果(函数的返回值);b.函数的执行情况(函数是否成功执行了预期的任务),例如:fopen()函数的执行情况是通过其执行结果来间接表示。


2.4错误码

为了获取普通函数的错误信息,操作系统提供了错误码这个接口

errno是错误码,它是记录系统最后一次错误代码的一个整数值,不同值表示不同含义,在#include<errno.h>中定义。

当函数运行成功时,errno值不会被修改,因此我们不能通过测试errno的值来判断是否有错误存在,而应该在被调用的函数提示有错误发生时,再检查errno的值。

注:错误码只有当库函数调用失败了才会被设置。


2.4.2错误码和退出码的区别 

退出码是进程结束时给系统返回的状态码,通常简单地表示成功或失败
错误码是函数调用或操作失败时的具体错误信息,提供了更详细的错误类型 

要是本身你给退出码定义了详细的分类,那么就会进而编程错误码 


2.5进程退出的几种情况

任何程序退出的情况我们都可以分为一下三种:用两个数字表示退出情况。

进程信号  =   0  ; 退出码   =   0  进程正常结束,也是成功执行

进程信号  =   0  ; 退出码   =   !0  进程正常结束,但是进程执行结果不正确

进程信号  =   !0  ; 退出码   =   0/!0  进程没有正常运行,退出码没有任何意义


2.6.进程退出的方式

2.6.1.正常退出

就是在正常的程序代码中,如main函数走到结尾,或者是遇到return。这样的正常结束。

2.6.2.exit()函数和_exit()函数

一、exit()函数

  1. 调用exit()是程序主动退出的方式,即:正常终止进程。
  2. exit()函数是C标准库提供的一个函数,在#include<stdlib.h>中定义,用于立即终止当前进程的执行,它会接受一个整形作为参数,该整形为进程的退出码。
  3. 调用exit(),程序会立即终止,exit()同时会执行清理操作(如:刷新所有的输出缓冲区、关闭通过fopen打开的文件、malloc开辟的内存等),然后向操作系统返回退出码。
二、_exit()函数

  1. 调用_exit()是程序主动退出的方式,即:正常终止进程。
  2. _exit()函数是系统调用函数,在#include<unistd.h>中定义,用于立即终止当前进程的执行,它会接受一个整形作为参数,该整形为进程的退出码。
  3. _exit()不会自动执行exit()函数所执行的清理工作,需要确保在调用它之前,手动处理所有必要的清理工作,然后向操作系统返回退出码。

注:main函数返回、调用exit()、_exit()函数,都表示程序主动退出,即:正常终止;接受到信号(如:ctrl c,信号终止),表示程序被动退出,即:异常退出。


2.6.3. exit()、_exit()函数的区别

  1. exit()支持刷新缓冲区,_exit()不支持刷新缓冲区,因为exit中有缓存区,_exit中无缓冲区。

  2. 它们都是终止进程,但只有OS才有能力终止进程,因此exit()底层封装了_exit(),两者是上下层关系

为什么语言具有可移植性和跨平台性?在库层面上,对系统强相关的接口进行了封装,从而屏蔽了底层差异。


3.进程等待

3.1为什么要进行进程等待 

1.需要父进程去回收子进程的资源(内存空间),如果子进程结束了,需要父进程去回收空间,否则子进程就会变成僵尸进程,造成内存泄漏。

2.一般来说,进程都会有一个目的,进程等待就可以让父进程得到子进程的运行结果。

子进程的退出信息(exit code、exit signal),需要通过内核数据结构来维护,保存在子进程的task_struct中,属于内核数据

3.2进程等待的方式

3.2.1.wait

pid_t wait(int *status) 

  1. 返回值:调用成功,返回已经结束进程的PID,同时获取到了子进程的退出状态码;调用失败,返回-1,并设置错误码以指示错误的原因。
  2. 参数status:输出型参数,用于存储子进程的退出状态,由OS填充,如果不需要这个信息,可以传递NULL,否则,OS会根据该参数,将子进程的信息反馈给父进程。

3.2.1.waitpid

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

  1. 参数pid:如果pid = -1,等待任意一个子进程,与wait等效;如果pid > 0,等待其进程的PID与pid相等的子进程。
  2. 参数option:如果option = 0,则为阻塞等待;如果option = WNOHANG,则为非阻塞等待。
  3. 返回值:调用成功,返回收集到的子进程的PID,同时获取到了子进程的退出状态码;调用失败,返回-1,并设置错误码以指示错误的原因;如果为非阻塞等待,waitpid调用成功且没有收集到已结束的子进程,则返回0。

3.3.进程等待的两种方式:阻塞等待和非阻塞等待

3.3.1阻塞等待

定义:进程在发出某个请求(如:I/O操作、等待某个条件成立等)后,如果请求不能立即得到满足(如:数据未准备好、资源被占用等),进程会被挂起,在此期间无法继续执行其他任务,直到等待条件满足或被唤醒。

 a.行为 -> 进程在等待期间无法执行其他任务(干等着)。

b.触发方式 -> 等待由外部条件触发(如:数据到达、资源释放等)。

c.管理层面:由操作系统或者底层系统资源管理。

d.效率与并发性:效率低。

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

int main()
{
    pid_t id = fork();
      
    if(id == 0) //子进程
    {
        int cnt = 5;
        while(cnt)
        {
            printf("child is running, id:%d, ppid:%d\n", getpid(), getppid());
            sleep(1);
            cnt--;
        }
        exit(1); //子进程退出
    }
  
    int status = 0; //存储子进程退出状态
    pid_t rid = waitpid(id, &status, 0); //父进程等待 —— 阻塞等待
    if(rid > 0) //等待成功
        printf("wait success, status:%d\n", status);
    else if(rid == -1) //调用失败
        perror("wait error!\n");                                     
    
    return 0;
}

3.3.2非阻塞等待

定义:进程在发出某个请求后,不会被立即挂起已等待请求的完成,即使请求不能立即得到满足,进程在等待期间可以继续执行其他任务,同时可能会以某种方式(轮询访问、回调等)定期检查请求状态或者等待结果的通知。

行为 -> 进程在等待期间可以执行其他任务;

b.触发方式 -> 可能通过编程的方式实现,如:轮询、回调等。

c.管理层面:在应用层通过编程实现。

d.效率与并发性:效率高,提高并发性和响应能力。


3.4获取子进程的退出状态

status不能简单的当作整形来看,他是一种类似于位图的,

它有自己的格式,只研究status低16位比特位。

3.4.1位操作获取子进程的退出状态

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

int main()
{
    pid_t id = fork();
      
    if(id == 0) //子进程
    {
        int cnt = 5;
        while(cnt)
        {
            printf("child is running, id:%d, ppid:%d\n", getpid(), getppid());
            sleep(1);
            cnt--;
        }
        exit(1); //子进程退出
    }
  
    int status = 0; //存储子进程退出状态
    pid_t rid = waitpid(id, &status, 0); 
    if(rid > 0) //等待成功
        printf("wait success, status:%d, exit code:%d, exit sign:%d\n", status, (status>>8)&0xff, status&0x7f);   //位操作获取子进程的退出码、退出信号        
 
     return 0;
}

3.4.2利用宏来获取子进程退出状态

WIFEXITED(status):检查子进程是否正常退出。


如果子进程通过调用exit函数或main函数return返回而退出,则WIFEXITED返回非0值(真) ->正常退出;(进程是正常退出的,进程信号返回的是0,当进程信号为0时,WIFEXITED的返回值是非0)

如果子进程是由于接收到信号而退出,则WIFEXITED返回0(假) -> 异常退出。(进程是异常退出的,进程信号返回的是非,当进程信号为非时,WIFEXITED的返回值是0)

WEXITSTATUS(status):只有当WIFEXITED为真时(即进程是正常退出的,进程信号为0),接着才会使用WEXITSTATUS获取子进程的退出码。

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

int main()
{
    pid_t id = fork();
      
    if(id == 0) //子进程
    {
        int cnt = 5;
        while(cnt)
        {
            printf("child is running, id:%d, ppid:%d\n", getpid(), getppid());
            sleep(1);
            cnt--;
        }
        exit(1); //子进程退出
    }
  
    int status = 0; //存储子进程退出状态
    pid_t rid = waitpid(id, &status, 0); 
    if(rid > 0) //等待成功
    {
        if(WIFEXITED(status)) //子进程正常退出
            printf("wait success, status:%d, exit code:%d\n", status, WEXITSTATUS(status)); //提取退出码 宏
        else  //子进程异常退出                                                      
            printf("child process error!\n");
      }
   
     return 0;
}

问题1:在父进程中定义两个全局变量(exit code、exit sign),子进程修改exit code值,父进程可以获取到子进程的退出信息吗?

  • 不能。因为进程具有独立性,子进程对共享数据的修改,父进程是不可见的。

问题2:为什么要有wait、waitpid?

  • 为了避免子进程僵尸,造成内存泄漏,父进程需要通过wait、waitpid等函数来回收子进程资源,同时可以获取到子进程的退出信息。
  • 子进程的退出码、退出信号等内核数据,需要被拷贝到用户层的某个变量(如:wait、waitpid中的status参数等),这个过程需要调用系统调用接口,因为用户空间的程序无法直接访问内核空间的数据。

4.进程程序替换

4.1为什么要有进程程序替换

  1. 我们创建的进程只能执行自己的代码。
  2. 当子进程被创建的时候如果想执行别的程序该怎么办呢? ------程序替换

4.2进程程序替换的概念和原理

  • 概念:允许一个进程在执行过程中用一个新的程序来替换当前正在执行的程序。这一过程通常是通过调用exec函数实现的。即:用全新的程序替换原有的程序。
  • 具体来说,当进程调用某种exec函数时,当前进程的用户空间中的代码和数据会被新程序的代码和数据完全覆盖,因此从新程序的启动例程开始执行。
  • 注:调用exec函数,并不会创建新的进程,而是对原有进程的资源进行替换,因此调用exec前后该进程的pid并未发生改变。
  • 原理:加载新程序 -> 替换当前程序 -> 更新页表 -> 执行新程序。
  • 加载新程序:当进程决定进行程序替换时(调用exec函数),它会请求OS将全新程序(代码和数据)从磁盘中加载到内存。
  • 更新页表:为了实现替换,OS需要更新页表,将原来指向旧程序代码的虚拟地址映射到新程序代码的物理地址上,这样,就会执行新程序的代码。
  • 所谓的把磁盘的数据加载到内存,把磁盘的数据拷贝到内存中,磁盘,内存都是硬件,只有操作系统具有将数据从一个硬件(磁盘)搬移到另一个硬件(内存)的能力,从而支持程序的加载和替换(所以,这里一般调用的都是系统调用)。
  • 注:进程替换的本质工作就是加载,充当的是加载器的角色!

4.3直接写代码----exec*接口

这里面有很多的接口,我们先来演示一个最简单的函数 execl  

4.3.1 execl的单进程场景 

 这里有四个细节:

  1. 这里我们也发现了,程序替换成功之后,exec*代码后的代码也就不会被执行了。
  2. exec*函数只有失败返回值,是没有成功的返回值的。
  3. 替换完成之后,是不会产生新的进程的。
  4. 进程创建的时候:实现创建PCB,地址空间和页表,然后才是把程序加载到内存当中。

补充一个点:我们这么写传参方式是标准传参,当然也可以非标准传参,就是在遇到非标准的情况也不要奇怪。

4.3.2 execl的多进程进程场景 


4.4学习另外的exec*接口

这七个接口在功能上没有不同(都是封装系统调用接口execve),只是在使用方式上有所不同,传参方式不同。 

我们发现其实前面的名字都差不,后面另外添加的字母就展现出区别。 

 4.4.1 exec*接口 + p

我们发现execlp,的第一个参数是file,而execl是path,说明execl第一个参数需要传递的是要替换的文件的路径,而第二个只需要传递文件名称就行了,会自动取环境变量指定的路径下去寻找文件。

4.4.2.exec*接口 + e

有了e,就代表这在参数当中添加了 env[]数组,说明了,可以自己写环境变量,要注意这个写入是覆盖式的写入,会将当前进程从父进程那里继承来的参数覆盖掉。

4.4.3.exec*接口 + v

 有了v以后,就说明后面的参数传递,不再使用可变参数来传递了,直接传递一个存储参数的字符指针数组。

5.补充知识

5.1.putenv

  • 功能:动态改变或新增环境变量。

如果指定的环境变量已经存在,那么它的值会被新的字符串中的值所替换;如果指定的环境变量不存在,那么它会被添加到环境变量表中。

5.2.为什么子进程进行替换,父进程无任何影响?

  • 进程具有独立性:每个进程都有自己的地址空间,意味着每个进程只能访问自己的内存区域,而不能访问其他进程的内存区域,所以子进程进行程序替换,只会改变自己的地址空间的内容,不会影响到父进程的地址空间。
  • exec函数的行为:仅在调用它的进程中生效,而不会影响到父进程。由于exec函数是在子进程中调用的,因此只有子进程的映像被替换,父进程的映像保持不变,父进程继续执行其后续代码。

5.3.shell是如何执行起来一个指令的?

  • 读取命令行输入 -> 解析命令 -> 创建子进程 -> 执行程序替换 -> 等待子进程结束。

6. 应用场景

  • 进程替换的应用场景有:Shell命令解释、服务器设计、在线OJ、搜索引擎等
  • Shell命令解释:当用户在Shell中输入一个命令,Shell会创建一个子进程来执行该命令,这个子进程会使用exec函数来替换需执行命令的代码和数据,从而执行用户指定的程序。
  • 服务器设计:在服务器程序中,父进程可以创建多个子程序来处理客户端的请求,每个子程序可以使用exec函数来执行特定的程序或者服务。
     

标签:status,控制,函数,程序,退出,exit,Linux,进程
From: https://blog.csdn.net/2301_76653277/article/details/142991159

相关文章

  • 并查集---Linux发行版的数量
    题目描述Linux操作系统有多个发行版,distrowatch.com提供了各个发行版的资料。这些发行版互相存在关联,例如Ubuntu基于Debian开发,而Mint又基于Ubuntu开发,那么我们认为Mint同Debian也存在关联。发行版集是一个或多个相关存在关联的操作系统发行版,集合内不包含没有关联的发行......
  • 《Linux系统编程篇》fork/wait/waitpid/exit函数——基础篇
    文章目录引言fork()函数概述父子进程兄弟进程fork函数fork()的常见问题fork()的优势与限制引入`wait`和`waitpid`(解决僵尸进程)wait函数waitpid函数:exit函数结论命为志存。——朱熹引言《Linux系统编程篇》——基础篇首页传送门本节我们正式进入Linux的进......
  • 《Linux系统编程篇》消息队列(Linux 进程间通信(IPC))——基础篇
    文章目录引言消息队列(MessageQueue)消息队列的特点消息队列的特性消息队列的操作ipcs-q拓展ipcrm拓展注意事项结论“山重水复疑无路,柳暗花明又一村。”——陆游引言《Linux系统编程篇》——基础篇首页传送门想象一下,你正在开发一个多任务处理的应用程序,其中......
  • Chromium127编译指南 Linux篇 - 同步第三方库以及Hooks(六)
    引言在成功克隆Chromium源代码仓库并建立新分支之后,配置开发环境成为至关重要的下一步。这一过程涉及获取必要的第三方依赖库以及设置钩子(hooks),这些步骤对于确保后续的编译和开发工作能够顺利进行起着决定性作用。本指南旨在详细阐述这些配置步骤的执行方法,为开发者提供清晰......
  • Chromium127编译指南 Linux篇 - 编译前环境搭建(一)
    前言在当前的浏览器开发中,Chromium作为一个开源项目,已经赢得了广泛的关注和使用。它不仅构成了GoogleChrome的核心框架,同时也是诸如MicrosoftEdge、Opera和Brave等多款浏览器的基础。凭借其广泛的应用和出色的可定制性,许多开发者选择在Chromium的基础上进行再开发......
  • Linux nginx 配置
    Nginx的配置类型丰富多样,可以根据不同的需求进行灵活配置。以下是使用不同域名介绍的10种Nginx配置类型:基本Web服务器配置域名:http://www.example1.com配置说明:这是Nginx作为Web服务器的基本配置,包括监听端口、服务器名称、根目录设置等。示例配置:nginxserver{ listen8......
  • 2024年大湾区杯数学建模竞赛 A 题 证券市场投资风险控制模型设计 思路和代码(持续更新)
    目录任务一:风险计量指标计算与分析1.1平均收益率计算1.2市场流动性(换手率)1.3市场情绪指标(波动率)指标的经济意义和分析任务二:系统性风险预测模型构建2.1多因子模型示例2.2使用GARCH模型预测波动性任务三:事前风控体系构建任务四:合理收益预期设定任务一:风险计量......
  • Linux-shell实例手册-网络操作
    本文章讲解的是在linux下跟网络相关的一些操作和命令,喜欢就点赞收藏哦,方便随时查阅!文章目录1Linux下网络基本命令2netstat3ssh4网卡配置文件5route6解决ssh链接慢7ftp上传8nmap9 流量切分线路10snmp1Linux下网络基本命令   rz  #通过ssh上传......
  • Linux的常用命令
    普通用户不具备修改权限命令su-(进入root账号)查询资料是因为$代表普通用户模式,权限不够,可以进入root帐号在建立文件夹进入root帐号,打su-(su-切换到root用户,并转到root用户的家目录下,即改变到了root用户的环境。)命令选项传参command-optionsparameterCommand:命......
  • 如何实现跨境设备操作?ToDesk远程控制加持全球节点轻松搞定
    随着近年来国际化水平的逐步提高,跨境的学习、交流、工作、旅行等已愈发常见。然而虽然交通出行也算方便,但针对频繁两地往来、海内外人员协助互动等,从省时、省力、省财力精力等成本方面考量,通过来回往返来解决却并非是一个最佳选项。那么,面向例如外资企业远程协助境外同事处理任务......