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

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

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