c++高级篇(一) —— 初识Linux下的进程控制

news2024/11/20 16:24:26

linux的信号

信号的概念

在Linux中,信号是一种用于进程间通信和处理异步事件的机制,用于进程之间相互传递消息和通知进程发生了事件,但是,它不能给进程传递任何数据。

信号产生的原因有很多种,在shell中,我们可以使用killkillall来发送信号

kill -信号的类型  进程编号
killall -信号的类型 进程名

信号的类型

常见信号类型:

  • SIGINT:终止进程(键盘快捷键 ctrl+c
  • SIGKILL: 采用kill -9 进程编号,强制杀死程序

信号的处理

进程对信号的处理方法一般有三种:

  • 对该信号进行默认处理,一般是终止该进程
  • 设置中断的处理函数,受到该型号的函数进行处理
  • 忽略该信号,不做如何处理

signal()函数可以设置程序对信号的处理方式

函数的声明:

sighandler_t signal(int signum,sighandler_t handler)

注释

参数signum表示信号的编号

参数handler表示信号的处理方式,有三种情况:

  1. SIG_DFL:恢复参数signum所指信号的处理方法为默认值
  2. 一个自定义的处理信号的函数,信号的编号为这个自定义函数的参数
  3. SIG_IGN:忽略signum所指的信号

示例代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

void func(int signum)
{
    cout<<"收到了信号"<<signum<<endl;
    signal(1,SIG_DFL);//将函数的处理方式由自定义函数改为了默认方式处理
}

int main(int argc,char *argv[],char *envp[])
{
    signal(1,func);
    signal(15,func);
    signal(2,SIG_IGN);//忽略信号2
    while(1)
    {
        cout<<argc<<endl;
        sleep(1);
    }
    return 0;
}

信号的作用

​ 服务程序运行在后台,如果想终止它,一般不会直接杀死它,以防止出现意外。

​ 我们·一般会选择向进程去发送一个信号,当程序收到这个信号的时候能够调用函数,并通过函数中英语善后的代码,原计划的退出。

​ 我们也可以向其发送0的信号来确保程序是否存活

示例代码:

#include <iostream>
#include <signal.h>

using namespace std;

void Exit(int signum)
{
    cout<<"收到了"<<signum<<"信号"<<endl;
    cout<<"开始释放资源并退出"<<endl;
    //释放资源的代码
    cout<<"退出程序"<<endl;
    exit(0);
}
int main()
{
    for(int i=1;i<=64;i++) 
    {
        signal(i,SIG_IGN);
    }
    signal(2,Exit);
    signal(15,Exit);
    while(1)
    {
        cout<<"fengxu\n";
    }
}

进程终止

进程的终止

main()函数中,return的返回值就是终止状态,如果没有return语句或者调用exit(),那么该进程终止状态为0

我们可以通过

echo $?

来查看线程终止的状态

正常终止进程函数有三个:

void exit(int status);
void _exit(int status);
void _Exit(int status);

status也是进程终止的状态

注意:进程如果被异常终止,终止状态也为非0

资源释放

return表示函数返回,会调用局部对象的析构函数,main()函数中的return还会调用全局对象的析构函数

exit()表示进程终止,它不会调用局部对象的析构函数,只会调用全局变量的析构函数

注意:exit()会执行清理工作再退出,但是_EXIT()——exit()不会执行清理工作

进程的终止函数

进程可以利用atexit函数来登记终止函数(最多32个),这些函数将由exit()自动调用。

**注意:**运行登记函数的顺序与登记函数顺序相反

示例代码:

#include <iostream>
#include <stdlib.h>

using namespace std;

void fuc1()
{
    cout<<"调用了fuc1()"<<endl;
}

void fuc2()
{
    cout<<"调用了fuc2()"<<endl;
}

int main()
{
    atexit(fuc1);
    atexit(fuc2);
    exit(0);
}

输出:

在这里插入图片描述

调用可执行程序

system函数

system()函数提供了一种简单的执行程序的方法,把需要执行的参数用一个字符串传给system()函数就行了

函数声明:

int system(const char *string);

返回值:0成功,非0失败

示例代码:

#include <iostream>
#include <stdlib.h>

using namespace std;

int main()
{
    int ret=system("ls -l");
    cout<<ret<<endl;
    perror("system");
    return 0;
}

输出

在这里插入图片描述

exec函数族

前言

我们在用fork()函数去创建一个进程的时候,当我们想继续使用这个进程去去执行其他函数的时候,我们可以去调用exec函数,这样该进程将被替换为全新的程序,而且调用exec函数,前后函数的进程不变

exec函数族函数的功能

它能够在调用进程内部去执行一个可执行文件,它既可以是二进制文件,也可以是任何Linux下的可执行脚本文件

exec函数的返回值

exec函数族的函数在执行成功后不会返回,调用失败则会返回-1并设置error值,并从原程序调用点继续往下执行

exec函数的种类

exec族函数一个有六种:

  1. execl(const char *path, const char *arg, ...):接受一个以NULL结尾的参数列表,第一个参数是要执行的可执行文件的路径,后面的参数是传递给可执行文件的命令行参数。
  2. execlp(const char *file, const char *arg, ...):与execl函数类似,但它在可执行文件的搜索路径中查找该文件,而不是仅使用给定的路径。
  3. execle(const char *path, const char *arg, ..., char *const envp[]):类似于execl函数,但允许指定新的环境变量参数(通过envp数组传递)。
  4. execv(const char *path, char *const argv[]):与execl函数类似,但接受一个以NULL结尾的参数数组来传递命令行参数。
  5. execvp(const char *file, char *const argv[]):与execv函数类似,但在可执行文件的搜索路径中查找文件,而不是仅使用给定的路径。
  6. execvpe(const char *file, char *const argv[], char *const envp[]):与execvp函数类似,但允许指定新的环境变量参数

对exec参数的说明

path:可执行文件的路径

arg:可执行文件所带的参数,第一个为文件的名字,不带路径且以NULL结尾

file:如果参数file中带有\,则将其视作路径处理,否则即在当前PATH环境变量按照其指定的各个目录去搜寻可执行文件

exec族函数的分类

exec族函数参数比较难记忆,但是当我们可以通过函数名中的字符来辅助我们记忆

  • l:使用参数列表
  • P:使用文件名,并从PATH环境中寻找可执行文件
  • V:构造一个指向各参数的指针数组,将数组的地址作为这些

函数的参数

  • e:多了envp数组,利用写的环境变量代替了进程的环境变量

接下来是对每种类型的具体描述:

l类函数

带l的一类exac函数(l表示list),包括execlexeclpexecle,要求将新程序的每个命令行参数都说明为 一个单独的参数。这种参数表以空指针结尾。

execl函数为例

//echoarg.cpp
#include <iostream>

using namespace std;

int main(int argc, char *argv[])
{
  for(int i=0;i<argc;i++)
  {
    printf("argv[%d]=:%s\n",i,argv[i]);
  }
  return 0;
}
//execl.cpp
#include <iostream>
#include <unistd.h> 

using namespace std;

int main()
{
    cout<<"before execl"<<endl;
    if(execl("/home/lib/项目课源码/app/进程与通信/exec族函数/out/echoarg","echorag","abc",NULL)==-1)
    {
        cout<<"execl error"<<endl;
    }
    cout<<"after execl"<<endl;
}

输出结果:
在这里插入图片描述

说明

我们先用g++编译echoarg.cpp,生成可执行文件echoarg并放在目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。

p类函数

带p的一类exac函数,包括execlpexecvpexecvpe,如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。举个例子:

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    cout<<"before execlp\n";
    if(execlp("ls","ls","-l",NULL)==-1)
    {
        cout<<"execlp error\n";
    }
    cout<<"after execlp\n";
    return 0;
}

带v不带l的函数

带v不带l的一类exac函数,包括execvexecvpexecve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。

以下面代码作为例子

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    cout<<"before execvp";
    char *argv[]={"ls","-l",NULL};//最后一个元素必须为NULL
    if(execvp("ls",argv)==-1)
    {
        cout<<"execvp error"<<endl;
    }
    cout<<"after execvp";
    return 0;
}

输出结果:

在这里插入图片描述

e类函数

带e的一类exac函数,包括execleexecvpe,可以传递一个指向环境字符串指针数组的指针。

进程

进程的创建

整个Linux系统全部的进程是一个树状结构

0号进程(系统进程):所有进程的祖先,创建了1号进程与2号进程

1号进程(systemd):负责执行内核的初始化工作与继续系统配置

2号进程(kthreadd):负责所有内核线程的调度与管理

我们可以使用pstree命令来查看进程树,命令为:

pstree -p 进程编号

示例:

在这里插入图片描述

进程标识

每一个进程的有一个非负整数标识的唯一的进程ID,虽然是唯一,但是我们可以复用进程ID,当一个进程终止以后,该进程ID自然就成为了复用的候选者,但Linux本身采用的是延迟服用算法,让新建进程的ID不同于最近计数进程的ID,防止被误以为进程尚未终止

pid_t getpid(void) //获取当前进程的ID
pid_t getppid(void) //获取父进程的ID

说明:

pid_t:非负整数

进程的创建

fork函数

一个现有的进程能够调用fork()函数去创建一个新的进程

pid_t fork(void);

fork()创建的进程叫做子进程,下面演示一个例子:

//demo1.cpp
#include <iostream>
#include<unistd.h>

using namespace std;

int main()
{
    fork();
    cout<<"hello world"<<endl;
    sleep(100);
    cout<<"over";
    return 0;
}

我们运行一下结果如下:

在这里插入图片描述

我们使用命令来查看一下它的进程

ps -ef |grep demo1

在这里插入图片描述

我们用上面的查看进程树命令来试一下:

pstree -p 214251

在这里插入图片描述

我们可以看到它创建了一个子进程

分割子进程与父进程

fork()会返回值,而子进程与父进程的返回值不同,示例代码如下:

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    fork();
    int pid=fork();
    cout<<pid<<endl;
}

输出结果:

在这里插入图片描述

我们可以发现:

子进程的返回值:0

父进程的返回值:父进程的进程ID

所以我们可以通过这个来选择父进程与子进程所执行的代码

示例代码:

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    int pid = fork();
    if(pid==0)  cout<<"现在执行的是子进程"<<endl;
    if(pid>0)  cout<<"现在执行的是父进程"<<endl;
}

输出结果:

在这里插入图片描述

子进程与父进程之间的关系

在子进程被创建之后,它与父进程之间并不是共享堆栈以及数据空间的而是子进程获得了父进程的数据空间以及堆栈的副本

fork()的两种用法

  1. 父进程复制自己,然后父进程与子进程分别执行不同的代码,多见于网络服务程序,父进程等待客户端的连接请求,当请求到达的时候,父进程调用fork(),让子进程处理请求,而父进程等待下一个连接请求
  2. 进程需要执行另一个程序,这种多见于shell中,让子进程去执行exec族函数

共享文件

fork()的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程与子进程共享一个文件偏移量(子进程所写的内容会在父进程所写内容的后面)

注意:如果父进程与子进程写同意描述符指向的文件,但是每一然后显示的同步,那么它们的输出可能相互混合

vfork()函数

vfork()函数的调用与fork函数相同,但两者的语义不同

vfork()函数用于创建一个新进程,而新进程的目的是exec一个新程序,由于我们要求子进程必须立即执行,所以它不复制父进程的地址空间

vfork()fork()的宁一个区别:vfork()保证子进程先执行,保证了子进程调用exec函数或exit()之后父进程才恢复执行

僵尸进程

前言

如果父进程比子进程先退出,子进程将被1号进程所托管(这是一种让进程在后台运行的方法),而如果子进程比父进程先退出,且父进程并没有处理子进程退出的信息的话,那么子进程将成为僵尸进程。

代码示例:

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    if(fork()==0) return 0;
    for(int i=0;i<1000;i++)
    {
        cout<<"hello world"<<endl;
        sleep(100);
    }
    return 0;
}

在这里插入图片描述

在这里插入图片描述

我们可以看到哪怕子进程已经退出了,但是我们查找进程的时候,子进程依旧存在,这时候它就成为了一个僵尸进程。

僵尸进程的危害

Linux内核给每一个子进程都保留了一个数据结构,它包括了进程编号,终止状态,使用cpu时间等等。当父进程处理了子进程的退出之后内核会将这个数据结构释放掉,而父进程如果没有将子进程的退出处理掉,内核就不会释放这个数据结构,这样会导致子进程的基础编号一直被占用,而进程编号的数量是有限的,这样将影响系统去创建新的进程

如何避免僵尸进程

  • 子进程退出的时候,内核需要向父进程发出SIGCHLD信号,如果父进程用signal(SIGCHLD,SIG_INT)来表示对子进程的退出不做处理,内核将自动释放子进程的数据结构

  • 父进程通过wait/waitpid函数等待子进程结束,子进程退出前,父进程将被阻塞

     pid_t wait(int *stat_loc);
     pid_t waitpid(pid_t pid, int *stat_loc, int options);
     pid_t wait3(int *stat_loc, int options, struct rusage *rusage);
     pid_t wait4(pid_t pid, int *stat_loc, int options, struct rusage *rusage);
    

    返回值是子进程的编号

    变量的说明

    • pid_t pid:要等待的进程的进程ID。
    • int *stat_loc:用于保存进程退出状态的指针。如果不关心进程的退出状态,可以传递 NULL
    • int options:等待选项,可用于指定等待行为的一些附加选项。常见的选项包括 WNOHANG (非阻塞等待)和 WUNTRACED (等待暂停子进程状态)。
    • struct rusage *rusage:用于保存子进程资源使用情况的结构体指针。如果不关心子进程的资源使用情况,可以传递 NULL
    • stzt_loc是子进程终止的信息,如果是正常终止,宏WIFEEXITED(stat_loc)返回真,WEXITSTAUTS(stat_loc)可获取终止状态,如果是异常状态,宏WTERMSIG可获取终止进程的信号

    我们来用一段代码实验一下上述知识点:

    #include <iostream>
    #include<unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    using namespace std;
    
    int main()
    {
        //父进程
       if(fork()>0)
       {
            int sts;
            pid_t pid=wait(&sts);
            cout<<"已经终止子进程的进程编号为:"<<pid<<endl;
            if(WIFEXITED(sts))
            {
                cout<<"子进程正常退出"<<"子进程的退出状态为"<<WEXITSTATUS(sts)<<endl;
            }
            else
            {
                cout<<"子进程异常退出"<<"子进程的退出状态为"<<WTERMSIG(sts)<<endl;
            }   
       }
       //子进程
       else
       {
            sleep(30);
            cout<<"byebye"<<endl;
            exit(1);
       }
    }
    

在这里插入图片描述

我们如果尝试使用kill指令去强行结束子进程:

在这里插入图片描述

在这里插入图片描述

  • 如果父进程很忙,我们可以考虑捕获SIGCHLD信号,在信号处理函数里面调用wait()/waitpid()

    代码示例:

    #include <iostream>
    #include <unistd.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    
    using namespace std;
    
    void  func(int signal)
    {
        int sts;
        pid_t pid=wait(&sts);
        cout<<"子进程pid为"<<pid<<endl;
        if(WIFEXITED(sts))
        {
            cout<<"子进程正常退出\n"<<"子进程的退出状态为"<<WEXITSTATUS(sts)<<endl;
        }
        else
        { 
            cout<<"子进程异常退出\n"<<"子进程的退出状态为"<<WTERMSIG(sts)<<endl;
        }   
    }
    int main()
    {
        signal(SIGCHLD,func);
        if(fork()>0)
        {
            while(true)
            {
                sleep(1);
                cout<<"父进程忙碌中"<<endl;
            }
        }
        else
        {
            sleep(10);
            int *p=0;
            *p=10;
            exit(1);
        }
    }
    

    在这里插入图片描述

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

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

相关文章

Neuralink首个脑机接口患者:打游戏、搞研究两不误,重获自主能力

今年1月28日&#xff0c;Neuralink首次将侵入式脑机接口植入人类患者Noland Arbaugh的大脑。100天后&#xff0c;这家由埃隆马斯克创立的公司公布了最新的进展。从Neuralink的更新中我们可以看到&#xff0c;Arbaugh的恢复情况超出预期&#xff0c;他的用户体验也非常积极。 原…

Arduino-点亮TFT触摸屏一

Arduino-点亮TFT触摸屏一 1.概述 这篇文章主要介绍Arduino操作TFT触摸屏入门操作&#xff0c;通过SPI通信协议方式点亮TFT触摸屏。 2.硬件电路 2.1.硬件列表 名称数量Arduino Uno12.8" TFT彩色液晶触摸屏模块&#xff08;ILI9431&#xff09;110K 电阻5面包板1杜邦线…

YOLO数据集制作(一)|Labelme标注的矩形框json文件转txt

以下教程用于将使用Labelme软件标注生成的json格式文件转成YOLO使用的txt格式&#xff0c;适用场景&#xff1a;矩形框 使用方法&#xff1a;将json文件夹路径填到json_dir后&#xff0c; 将保存转化后txt的路径填入txt_dir后&#xff0c;运行即可转化。 运行完成后会生成label…

【Ajax零基础教程】-----第四课 简单实现

一、XMLHttpRequest对象 通过XMLHttpRequest对象来向服务器发送异步请求&#xff0c;从服务器获取数据。然后用JavaScript来操作DOM而更新页面。XMLHttpRequest是ajax的核心机制&#xff0c;它是IE5中首先引入的&#xff0c;是一种支持异步请求的技术。 简单的说&#xff0c;也…

分布式存储CephFS最佳实践

文章来源于知乎文章&#xff1a; 分布式存储CephFS最佳实践 背景 近日&#xff0c;一朋友说他们企业内部想落地CephFS&#xff0c;让我帮忙写一份能落地的CephFS最佳实践。然后就顺便把文章整理到了这里。因能力水平以及认知有限&#xff0c;如有错漏烦请指正。 简介 CephF…

Python学习笔记------json

json简介 JSON是一种轻量级的数据交互格式。可以按照JSON指定的格式去组织和封装数据 JSON本质上是一个带有特定格式的字符串 主要功能&#xff1a;json就是一种在各个编程语言中流通的数据格式&#xff0c;负责不同编程语言中的数据传递和交互 为了让不同的语言能够相互通…

重载,重写,重定义,纯虚函数,多态习题

只要不够成重写就是重定义。 重定义&#xff1a; 抽象类&#xff1a; 包含纯虚函数的类就是抽象类。 1.纯虚函数的作用&#xff0c;强制子类去完成重写。 2.表示抽象的类型。 抽象就是在现实中没有对应的实体。 1. 下面哪种面向对象的方法可以让你变得富有( a) A 继承 B…

创新案例|搜索新王Perplexity如何构建生成式AI产品开发的新模式

Perplexity AI&#xff1a;生成式搜索的颠覆者 刚刚成立满两年&#xff0c;Perplexity AI已经变成了我日常频繁使用的工具&#xff0c;甚至取代了我对 Google搜索的依赖 —— 而我并非个案。该公司仅凭不到 50 名员工&#xff0c;已经吸引了数千万用户。他们目前的年收入超过 …

C++ 多态 - 下

目录 1. 多态的原理 1.1. 虚函数表 1.2. 多态原理 1.3. 静态绑定和动态绑定 1.3.1. 运行时决议 1.3.2. 编译时决议 1.4. 为什么基类的对象调用虚函数不能构成多态 2. 单继承中的虚函数表 2.1. 同类型对象的虚表 2.2. 单继承的对象的虚表 2.2.1. 内存窗口查看 2.2.2…

【机器学习300问】84、AdaGrad算法是为了解决什么问题?

神经网络的学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题&#xff0c;解决这个问题的过程称为最优化。因为参数空间非常复杂&#xff0c;无法轻易找到最优解&#xff0c;而且在深度神经网络中&#xff0c;参数的数量非常庞大&#xff0c;导致最优化问…

数据库表自增主键超过代码Integer长度问题

数据库自增主键是 int(10) unsigned类型的字段&#xff0c;int(M) 中 M指示最大显示宽度&#xff0c;不代表存储长度&#xff0c;实际int(1)也是可以存储21.47亿长度的数字&#xff0c;如果是无符号类型的&#xff0c;那么可以从0~42.94亿。 我们的表主键自增到21.47亿后&#…

【Pytorch】6.torch.nn.functional.conv2d的使用

阅读之前应该先了解基础的CNN网络的逻辑 conv2d的作用 是PyTorch中用于执行二维卷积操作的函数。它的作用是对输入数据进行二维卷积操作&#xff0c;通常用于图像处理和深度学习中的卷积神经网络&#xff08;CNN&#xff09;模型。 conv2d的使用 我们先查看一下官方文档 inpu…

Linux系统调用过程详解:应用程序调用驱动过程

Linux下应用程序调用驱动程序过程&#xff1a; &#xff08;1&#xff09;加载一个驱动模块(.ko)&#xff0c;产生一个设备文件&#xff0c;有唯一对应的inode结构体 a、每个设备文件都有一个对应的’inode‘结构体&#xff0c;包含了设备的主次设备号&#xff0c;是设备的唯一…

AScript纯本地离线文字识别插件

目的 AScript是一款可以模拟鼠标和键盘操作的自动化工具。它可以帮助用户自动完成一些重复的、繁琐的任务&#xff0c;节省大量人工操作的时间。但按键精灵是不包含图色功能&#xff0c;无法识别屏幕上的图像&#xff0c;根据图像的变化自动执行相应的操作。本篇文章主要讲解下…

全网最详细使用war包的方式结合Tomcat和向量数据库PostgreSQL本地部署Xwiki教学

部署 XWiki 在 CentOS 7 上的过程涉及多个步骤&#xff0c;主要包括安装环境依赖、配置数据库以及安装和配置XWiki。以下是一个详细的步骤说明&#xff1a; 1. 系统准备 首先&#xff0c;确保您的系统是最新的&#xff1a; sudo yum update2. 安装 Java 这里需要注意一下版本…

综述列表(~2024.05.10)

&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 每周末更新&#xff0c;完整版进群获取。 Q 群在群文件&#xff0c;VX 群每周末更新。

vivado新版本兼容老版本,vitis classic兼容sdk教程

new version: vivado版本2023.2 和vitisv classic 2023.2 old version: vivado 2018.3以及之前的版本 打开工程 自动升级到当前版本&#xff0c;选择OK 点击Yes,合并当前的目录架构 点击OK 点击Report IP status 勾选要升级的IP核&#xff0c;点击升级 在项目工程文件夹…

leetcode-括号生成-101

题目要求 思路 1.左括号的数量等于右括号的数量等于n作为判出条件&#xff0c;将结果存到res中 2.递归有两种&#xff0c;一种是增加左括号&#xff0c;一种是增加右括号&#xff0c;只要左括号的数量不超过n&#xff0c;就走增加左括号的递归&#xff0c;右括号的数量只要小于…

Microsoft Edge浏览器,便携增强版 v118.0.5993.69

01 软件介绍 Microsoft Edge浏览器&#xff0c;便携增强版&#xff0c;旨在无需更新组件的情况下提供额外的功能强化。这一增强版专注于优化用户体验和系统兼容性&#xff0c;具体包含以下核心功能的提升&#xff1a; 数据保存&#xff1a;通过优化算法增强了其数据保存能力&…