Linux基础——进程控制

news2024/11/23 19:23:37

1. 进程创建

在这之前我们曾了解过进程创建(详见进程初识(二)),我们在这里对fork函数做一些补充

其实对于父子进程来说,若是有一方试图修改数据时,会向物理内存中申请一份新空间,并将数据拷贝到其中,拷贝完成后将自己对应页表中的只读属性去掉。

2. 进程终止

我们之前都知道,在main函数的最后我们一般都有return 0;这个语句,那么为什么要返回0呢?返回1,2怎么样?这个值返回给了谁?以及为什么要返回这个值呢?

在这里,返回的这个0其实是程序的退出码,来表征进程的运行结果是否正常(0表示success)。对于一个进程来说,当它终止时无外乎三种情况:

1. 代码运行完,运行结果正确

2. 代码运行完,运行结果不正确

3. 代码异常终止

对于前两种情况来说,我们怎么知道运行结果是否正确呢?——可以使用return 返回不同数字,来表示不同的出错原因,这就是进程的退出码。因此,对于代码运行完后判断运行结果是否正确,统一采用进程的退出码来判定。

那么除了代码运行完以外,代码也有可能异常终止,此时代码可能没有跑完,因此在这里进程的退出码毫无意义,即不关心退出码,我们需要关注的是为什么异常?发生了什么异常? 在这个过程中,先确定是否出现异常,未出现就返回退出码,而出现了异常操作系统会发出信号,然后退出。

我们可以用如下方式来验证,

对于上面这段代码我们运行可以发现

在kill指令中我们可以找到与之相对应的信号

我们对一个正常运行的进程使用kill -8 PID有

可以得到相同的结果。

main函数的返回值,本质表示进程运行完成时是否是正确结果,若不是可以用不同数字来表示不同的出错原因,而对于进程来说谁最关心当前进程的情况呢?——父进程,因此main函数的返回值其实是返回给了父进程。而对于退出码,我们可以使用

echo $?

来获取最近一次的退出码,如

而在C语言的库里面有一个将错误码转换为错误信息的函数,即strerror,其手册如下

我们可以使用如下代码,将所有错误信息打印出来

运行有

在之前我们ls一个不存在文件时,有

可以看到,这里返回的是错误码为2的错误信息,我们获取错误码也有

即系统提供的错误码和错误码描述是有对应关系的,当然我们也可以自己设计一份,举个例子

而除了使用main函数以外,我们还可以使用exit函数和_exit函数来退出进程,如exit(0);,它们与return的区别在于

exit函数在任意地方被调用,都表示调用进程直接退出

return 只表示当前函数返回

而exit与_exit之间亦有差距,_exit是系统调用,而exit是用户函数,他会在函数实现的过程中调用_exit,对于下面这个代码

在使用exit时结果为

在使用_exit时结果为

在这里printf函数其实是先把数据写入到缓存区,在合适的时候进程刷新,exit属于用户函数它在实现时,内部应该会调用一些函数进行冲刷缓存区的操作,这之后再调用_exit,而_exit则是系统层面直接将这个进程关闭,因此也不会有冲刷其缓存区的情况。

3. 进程等待

①进程等待是什么?

进程等待就是通过系统调用wait/waitpid,来进行对子进程进行状态检测与回收的功能。

②为什么需要进程等待?

在之前我们曾经提到过僵尸进程的存在,由于进程在变成僵尸进程后无法被杀死,我们需要使用进程等待来解决内存泄漏问题(这个问题必须解决)。此外,一个父进程需要通过进程等待,进而获得子进程的退出情况,这样做是为了知道父进程给子进程布置的任务完成地怎么样(也有可能不关心)。

③进程等待是怎么做的?

在代码方面,父进程通过调用wait/waitpid来解决僵尸进程问题,我们可以查看手册,有

在目前看来,进程等待是必须的,对于wait函数,它是等待任意一个子进程退出,如果子进程一直不退出,父进程默认在wait,调用这个系统调用的时候也就不返回,此时就处于阻塞状态。对于waitpid函数,它共有三个参数,对于第一个参数如果传入的pid>0时,表示等待特定的子进程,如果传入的pid=-1时,表示等待任意一个子进程,第二个参数需要解释一下,它是一个输出型参数(即为了把值带出来),这里的int是被当做几部分来使用的,图解如下

前面我们知道,父进程关心子进程,那么父进程期望获得子进程退出后的哪些信息呢?

1. 首先是子进程代码是否异常?——对于status的0-7位来说,当操作系统没有信号发出的时候默认都是0,因此只有0-7位都是0就认为没有收到信号。

2. 没有异常发生,那么结果对吗?不对是因为什么?——对于status的8-15位来说,程序正常退出时默认为0,即0->success,若是结果不对则从其中读取不同的错误码。

举个例子,status值为256时,低16位为0000 0001 0000 0000此时程序正常退出,退出码为1。我们可以使用以下代码来验证

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

int main()
{
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork\n");
        exit(1);
    }
    else if (id == 0)
    {
        // child
        int cnt = 5;
        while (cnt--)
        {
            printf("this is child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);
            sleep(1);
        }
    }
    else
    {
        // father
        int cnt = 10;
        while (cnt--)
        {
            printf("this is father, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);
            sleep(1);
        }
        // pid_t ret = wait(NULL);
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if (ret == id)
        {
            // 0x7f:0111 1111
            printf("wait success, ret:%d, exit sig:%d, exit code:%d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
        }
    }
    return 0;
}

运行有

既然如此,那么进程等待原理是怎么样的呢?

因为操作系统不会相信任何用户,因此他会提供一个接口来让用户访问数据。此外,操作提供提供了两个宏来供我们查看信息

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)

之前我们的代码可以修改一部分,即

而对于第三个参数options来说,其一般默认为0,即以阻塞方式等待,除了0以外还可以传入一个参数——WNOHANG(HANG意思是夯住(指的是系统或进程在执行某个任务时变得非常慢或停滞,导致系统或应用程序不再响应),整体代表wait no HANG),即等待的时候不要夯住,举个例子来理解,在一个男生等女友出门的时候,WNOHANG表示的是男生每隔几分钟向女生打一次电话确认女生出门了没有,而阻塞表示的是男生给女生打电话并且说不要挂电话,等你出门了再挂。

④非阻塞轮询

在上面举的例子中,男生打电话询问之后,如果女友未出门(未准备好),也不进入阻塞状态,再加上打电话的间隔时间,就形成了非阻塞+循环的形式,我们将其称为非阻塞轮询。对于非阻塞轮询来说,相对于阻塞最大的优势就是可以在这个等待的期间做一些自己的事情,此时对于返回的ret来说,当等待事件未就绪时就返回0。但是对于一个父进程来说,当它处于非阻塞轮询的状态时,等待子进程退出才是它的主要工作,因此在此时,能做的事只能是一些轻量化工作(如打印日志等)。我们可以定义如下的一些工作列表

#define TASK_NUM 10

typedef void(*task_t)();
task_t tasks[TASK_NUM];

void task1()
{
    printf("这是一个执行打印日志的任务, pid: %d\n", getpid());
}

void task2()
{
    printf("这是一个执行检测网络健康状态的一个任务, pid: %d\n", getpid());
}

void task3()
{
    printf("这是一个进行绘制图形界面的任务, pid: %d\n", getpid());
}

int AddTask(task_t t);

// 任务的管理代码
void InitTask()
{
    for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;
    AddTask(task1);
    AddTask(task2);
    AddTask(task3);
}

int AddTask(task_t t)
{
    int pos = 0;
    for(; pos < TASK_NUM; pos++) {
        if(!tasks[pos]) break;
    }
    if(pos == TASK_NUM) return -1;
    tasks[pos] = t;
    return 0;
}

void DelTask()
{}

void CheckTask()
{}

void UpdateTask()
{}

void ExecuteTask()
{
    for(int i = 0; i < TASK_NUM; i++)
    {
        if(!tasks[i]) continue;
        tasks[i]();
    }
}

而我们可以在主函数代码中这样调用

int status = 0;
InitTask();
while (1) // 轮询
{
    pid_t ret = waitpid(id, &status, WNOHANG); // 非阻塞
    if (ret > 0)
    {
        if (WIFEXITED(status))
        {
            printf("进程是正常跑完的, 退出码:%d\n", WEXITSTATUS(status));
        }
        else
        {
            printf("进程出异常了\n");
        }
        break;
    }
    else if (ret < 0)
    {
        printf("wait failed!\n");
        break;
    }
    else
    {
        ExecuteTask();
        usleep(500000);
    }
}

 这样封装也带来了非阻塞轮询和执行任务之间的解耦。

4. 进程程序替换

1. 单进程的进程程序替换

我们以下面这段代码为例

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    printf("before: this is a process pid: %d, ppid: %d\n", getpid(), getppid());

    // 标准进程程序替换接口
    execl("usr/bin/ls", "ls", "-a", "-l", NULL);

    printf("after: this is a process pid: %d, ppid: %d\n", getpid(), getppid());
    return 0;
}

我们编译运行可以看到

在结果中我们可以看到,before的打印成功了,而after的打印未成功,那么这究竟是怎么回事呢?

2. 进程程序替换的原理

在一个正常运行的进程中,各部分对应关系如下,而当遇到exec*函数时,会将exec函数中的一个参数中文件的代码与数据替换当前进程的代码与数据,即

从这个基本原理我们可以看到,整个过程没有创建新的进程,同时也没有修改页表中的对应关系。

3. 多进程的进程程序替换

接下来我们使用多进程版来进行测试,代码如下

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

int main()
{
    pid_t id = fork();
    if (id == 0) // child
    { 
        printf("before: I am a process,pid: %d, ppid:%d\n",getpid(), getppid());
        //这类方法的标准写法
        //execl"/usr/bin/ls",“ls","=a","-l",NULL);
        execl( "/usr/bin/top", "top", NULL);
        printf("after: I am a process,pid: %d, ppid:%d\n", getpid(), getppid());
        exit(0);
    }
    // father
    pid_t ret = waitpid(id, NULL, 0);
    if (ret > 0) printf("wait success, father pid: %d,ret id: %d\n", getpid(), ret);
    return 0;
}

 在这里我们让子进程退出后,可以看到

在子进程退出后,父进程仍能对子进程进行进程等待,由此我们可以得出结论——子进程的程序替换不会影响到父进程,那么在这个过程中代码肯定发生了写实拷贝。在程序替换成功后,exec*函数后的代码不会执行,如果替换失败才可能执行后面的代码,所以exec*函数只有失败的返回值而没有成功的返回值。

4. 多进程中验证exec*接口

我们使用man手册查看execl有

这6个程序替换的接口都提供加载器的效果,即在shell中我们输入一条指令,shell会创建一个新的进程并在其中调用exce*函数来加载指令。

①execl

首先,先解释一下我们已经使用过的execl函数,这里的l意为list

execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

我们要想执行一个程序,第一件事应该是什么呢?——先找到程序在哪,因此在这个函数中传入的第一个参数就决定了如何找到这个程序,后面的参数是命令行如何写就如何传参。在找到了程序后,又干什么呢?——根据传入的参数选项,具体执行对应程序。

②execlp

然后,我们介绍一下execlp函数,l我们已经解释过,这里的p意为PATH,execlp会在默认的PATH环境变量中查找指令,我们可以用下面的代码验证

int main()
{
    printf("before: I am a process,pid: %d, ppid:%d\n",getpid(), getppid());
    execlp( "ls", "ls", "-a", "-l", NULL);
    printf("after: I am a process,pid: %d, ppid:%d\n", getpid(), getppid());
        
    return 0;
}

③execle

再然后,我们介绍一下execle函数,l不多说,这里的e意为env,即环境变量,使用方式如下

extern char** environ;

execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);

需要注意的是,在这里传入自己所定义的环境变量采取的措施是覆盖而非追加。那么我们如何追加环境变量呢?—— 我们可以调用脚本来将一个程序的环境变量导入到另一个程序中,举个简单的例子

我们可以使用下面的代码来测试,即

int main()
{
    printf("before: I am a process,pid: %d, ppid:%d\n",getpid(), getppid());
    execl( "/usr/bin/bash", "bash", "shell.sh", NULL);
    printf("after: I am a process,pid: %d, ppid:%d\n", getpid(), getppid());
        
    return 0;
}

看到这里我们能提出一个问题——为什么无论是可执行程序还是脚本,都能跨语言调用呢?其实所有语言运行的程序本质上都是进程。回归正题,在了解了execle函数后,我们若是想给子进程传递环境变量如何传呢?

1. 新增:我们可以使用putenv函数来给自己(父进程)添加环境变量

2. 彻底替换:即使用execle函数来直接替换所有的环境变量

既然在这里谈到了我们之前讲过的环境变量,那么我们可以思考一下:环境变量是什么时候传入给进程的呢?——我们要知道,环境变量也是数据,创建子进程的时候,环境变量已经被子进程继承下去了。因此,在程序替换中,环境变量不会被替换的。

④execv

execv函数中的v意为vector,它其实是一个指针数组,我们以如下代码测试

int main()
{
    char* const myargv[] = {
        "ls",
        "-a",
        "-l",
        NULL
    };
    printf("before: I am a process,pid: %d, ppid:%d\n",getpid(), getppid());
    execv( "/usr/bin/ls", myargv);
    printf("after: I am a process,pid: %d, ppid:%d\n", getpid(), getppid());
        
    return 0;
}

运行有

在这个例子中,myargv是一个命令行参数,而ls内部含有main函数,这个main函数会去调用这个命令行参数去执行,这样就能完成指定任务。后面的execvp, execvpe大同小异,这里就不再赘述。

⑤execve

除了上面几个接口外,还有一个execve函数,其文档如下

它与前面六个函数的区别在于execve是系统调用,前面六个函数都是库函数,它们都要调用execve接口。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1493918.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

list链表的创建,排序,插入, test ok

1. 链表的建立&#xff0c;打印 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <stack> #include <iostream> #include <string.h> #include <string>using namespace std;struct node {int data;s…

让运维无忧,实战解析巡检报告功能实现方案

随着大数据技术的演进和信息安全性需求的提升&#xff0c;数据规模的持续扩张为数据运维工作带来了严峻考验。面对海量数据所形成的繁重管理压力&#xff0c;运维人员面临效率瓶颈&#xff0c;而不断攀升的人力成本也使得单纯依赖扩充运维团队来解决问题变得不再实际可行。 由…

双体系Java学习之全路线图

Java路线图 此路线图是为了我以后的Java学习指明方向。 希望大家都能在Java的路线上越走越远&#xff01;努力学习&#xff01;&#xff01;

自动驾驶革命:解密端到端背后的数据、算力和AI奇迹

作者 |毫末智行数据智能科学家 贺翔 编辑 |祥威 最近&#xff0c;特斯拉FSD V12的发布引发了业界对端到端自动驾驶的热议&#xff0c;业界纷纷猜测FSD V12的强大能力是如何训练出来的。从马斯克的测试视频可以大致归纳一下FSD V12系统的一些核心特征&#xff1a; 训练数据&am…

FreeRTOS操作系统学习——空闲任务及其钩子函数

空闲任务 当 FreeRTOS 的调度器启动以后就会自动的创建一个空闲任务&#xff0c;这样就可以确保至少有一任务可以运行。但是这个空闲任务使用最低优先级&#xff0c;如果应用中有其他高优先级任务处于就绪态的话这个空闲任务就不会跟高优先级的任务抢占 CPU 资源。空闲任务还有…

图机器学习(1)--导论

0 CS224W概况 斯坦福大学CS224W课程&#xff1a;http://cs224w.stanford.edu/ 图机器学习的库&#xff1a; 为什么是图&#xff1f;图是描述和分析具有关系/交互的实体的通用语言。 1 图数据举例 复杂域具有丰富的关系结构&#xff0c;可以表示为关系图。 通过显式地建模关…

比瓴科技强势领跑软件开发安全领域,ASPM名列赛道第一

近日&#xff0c;斯元商业咨询正式发布2024首版「网安新兴赛道厂商速查指南&#xff5c;短名单精选」。比瓴科技入围七个赛道&#xff0c;其中ASPM、ASOC、SDL位居赛道第一。 应用安全态势管理&#xff08;ASPM&#xff09; 降低应用安全漏洞及数据泄露风险 比瓴在软件安全领…

seliunx 基础规则介绍

一 SELinux的状态 enforcing&#xff1a;强制&#xff0c;每个受限的进程都必然受限 permissive&#xff1a;允许&#xff0c;每个受限的进程违规操作不会被禁止&#xff0c;但会被记录于审计日志 disabled&#xff1a;禁用 二 相关命令 getenforce: 获取selinux当前状…

SDWAN专线,解决银行网络搭建痛点

金融行业的不断发展和数字化转型&#xff0c;银行网络的搭建和管理面临着诸多挑战和痛点。SD-WAN&#xff08;Software-Defined Wide Area Network&#xff0c;软件定义广域网&#xff09;专线作为一种创新的网络解决方案&#xff0c;为银行解决了诸多网络搭建痛点&#xff0c;…

Hadoop集群配置与管理指南

目录 前言一、Hadoop集群配置历史服务器二、配置日志的聚集三、集群启动/停止方式总结四、编写Hadoop集群常用脚本五、常用端口号说明最后 前言 这篇文章内容覆盖了Hadoop集群中一些重要且常用的配置和管理任务。首先&#xff0c;我们将介绍如何配置Hadoop集群的历史服务器&am…

基于ceph-deploy部署Ceph 集群

Ceph分布式存储一、存储基础1、单机存储设备1.1 单机存储的问题 2、分布式存储(软件定义的存储SDS)2.1 分布式存储的类型 二、Ceph简介1、Ceph优势2、Ceph架构3、Ceph 核心组件4、OSD 存储后端5、Ceph 数据的存储过程6、Ceph 版本发行生命周期 三、Ceph 集群部署1、 基于 ceph-…

java: No enum constant javax.lang.model.element.Modifier.SEALED报错

这里我的idea版本为2021.03&#xff0c;JDK版本为21.0.2。经过大量冲浪后大多数都是让修改JDK版本&#xff0c;原因是Modifier.SEALED是JDK15新增的&#xff0c;但是当我修改完JDK版本后并无卵用。 尝试在代码中声明&#xff0c;也没问题可以引用到&#xff0c;这就怪了&#…

AI付费课程水分大 网红博主李一舟卖课被下架

日前&#xff0c;OpenAI旗下的文生视频模型Sora爆火&#xff0c;网上的AI付费课程嗅到商机&#xff0c;开始上线大量相关教学视频&#xff0c;几元至百元就号称能从入门小白到大神&#xff0c;其中就包括自称清华博士的李一舟。不过&#xff0c;李一舟很快就翻车了&#xff0c;…

6个免费可商用的高清图片素材网站,建议收藏!

作为设计师或者是自媒体创作者&#xff0c;都需要寻找高质量的图片素材为作品增添色彩&#xff0c;但随意找的图片素材很容易侵权。为了让大家能找到免费又能商用的图片素材&#xff0c;这期分享我经常用的6个图片素材网站&#xff0c;免费下载还能商用&#xff0c;赶紧收藏起来…

【产品经理方法论——产品的基本概念】

1. 产品学三元素 产品学有三个元素&#xff1a;用户、需求、产品 产品学的内容&#xff1a;根据用户的需求设计产品&#xff0c;使用产品服务用户 仅仅通过三个元素无法说明每个元素的概念&#xff0c;因为三个元素互为说明关系。 通过引入人/群体来说明三个元素的关系。 需…

腾讯云最新活动_腾讯云促销优惠_代金券-腾讯云官网入口

腾讯云服务器多少钱一年&#xff1f;62元一年起&#xff0c;2核2G3M配置&#xff0c;腾讯云2核4G5M轻量应用服务器218元一年、756元3年&#xff0c;4核16G12M服务器32元1个月、312元一年&#xff0c;8核32G22M服务器115元1个月、345元3个月&#xff0c;腾讯云服务器网txyfwq.co…

20240305-2-海量数据处理常用技术概述

海量数据处理常用技术概述 如今互联网产生的数据量已经达到PB级别&#xff0c;如何在数据量不断增大的情况下&#xff0c;依然保证快速的检索或者更新数据&#xff0c;是我们面临的问题。 所谓海量数据处理&#xff0c;是指基于海量数据的存储、处理和操作等。因为数据量太大无…

985硕的4家大厂实习与校招经历专题分享(part2)

我的个人经历&#xff1a; 985硕士24届毕业生&#xff0c;实验室方向:CV深度学习 就业&#xff1a;工程-java后端 关注大模型相关技术发展 校招offer: 阿里巴巴 字节跳动 等10 研究生期间独立发了一篇二区SCI 实习经历:字节 阿里 京东 B站 &#xff08;只看大厂&#xff0c;面试…

抖店应该怎么运营,2024新版入门教程分享,快速起店玩法

我是王路飞。 抖店快速起店的方法有很多&#xff0c;爆款截流、低价引流、全店动销、货损起店、达人带货等等。 今天主要给你们说下达人带货爆款截流的玩法&#xff0c;你们相结合着去做。 感兴趣的可以先收藏并关注&#xff0c;文末也有免费的抖店资料和领取。 内容来源于…

容器化技术

容器化技术并不是由Docker引入&#xff0c;而是有其发展历程。容器有效地将由单个操作系统管理的资源划分到孤立的组中&#xff0c;以更好地在孤立的组之间平衡有冲突的资惊使用需求。容器可以在核心CPU运行指令&#xff0c;而不需要任何专门的解释机制。容器避免了准虚拟化(pa…