news 2026/7/7 11:55:09

【Linux下的cpp】进程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Linux下的cpp】进程

Linux与cpp——进程

文章目录

  • Linux与cpp——进程
    • 信号
      • 设置程序对信号的处理:signal()
      • 信号类型
      • 发送信号函数kill()
      • 多进程和信号
    • 终止进程
    • 调用可执行程序
      • system()
      • exec()函数族
    • 创建进程
      • fork()创建子进程
    • 僵尸进程
    • 共享内存
      • 1. 创建/获取共享内存
      • 2. 连接共享内存到当前进程地址空间
      • 3. 从进程中分离共享内存
      • 4. 操作共享内存
      • 案例
    • 循环队列
    • 信号量
    • 生产消费者模型

本笔记中的进程→狭义中的Linux操作系统下运行的c++程序

信号

在Linux操作系统中,我们可以使用killallkill命令向进程传递信号(用数值表示)。

# 以下的demo表进程killall-14demo# 查看进程是否存活killall-0demo

进程在接收到信号后会进行处理,有以下三种处理方式

  1. 默认 -> 终止进程(宏为SIG_DFL)
  2. 设置信号处理函数,收到信号后执行函数
  3. 忽略信号(宏为SIG_IGN

设置程序对信号的处理:signal()

头文件:<unistd.h> <signal.h>

函数声明:sighandler_t signal(int signum, sighandler_t handler);

  • signum: 信号的编号
  • handler: 宏(SIG_DFL,SIG_IGN) OR 可调用对象
voidEXIT(intsig){...}intmain(){// 忽略全部的信号,防止程序被信号异常中止。for(intii=1;ii<=64;ii++)signal(ii,SIG_IGN);// 如果收到2和15的信号(Ctrl+c和kill、killall),本程序将主动退出。signal(2,EXIT);signal(15,EXIT);}

信号类型

常用信号类型

信号名信号值默认处理动作发出信号的原因
SIGHUP1A终端挂起或者控制进程终止
SIGINT2A键盘中断Ctrl+c
SIGQUIT3C键盘的退出键被按下
SIGILL4C非法指令
SIGABRT6C由abort(3)发出的退出指令
SIGFPE8C浮点异常
SIGKILL9AEF采用kill -9 进程编号 强制杀死程序。
SIGSEGV11CEF无效的内存引用(数组越界、操作空指针和野指针等)。
SIGPIPE13A管道破裂,写一个没有读端口的管道。
SIGALRM14A由闹钟alarm()函数发出的信号。可以用作定时器
SIGTERM15A采用“kill 进程编号”或“killall 程序名”通知程序。
SIGUSR110A用户自定义信号1
SIGUSR212A用户自定义信号2
SIGCHLD17B子进程结束信号
SIGCONT18进程继续(曾被停止的进程)
SIGSTOP19DEF终止进程
SIGTSTP20D控制终端(tty)上按下停止键
SIGTTIN21D后台进程企图从控制终端读
SIGTTOU22D后台进程企图从控制终端写
其它<=64A自定义信号

处理动作一项中的字母含义如下:

  • A 默认的动作是终止进程。
  • B 默认的动作是忽略此信号,将该信号丢弃,不做处理。
  • C 缺省的动作是终止进程并进行内核映像转储(core dump)。
  • D 缺省的动作是停止进程,进入停止状态的程序还能重新继续,一般是在调试的过程中。
  • E 信号不能被捕获。
  • F 信号不能被忽略。

发送信号函数kill()

等价kill/killall

声明int kill(pid_t pid, int sig)

  • sig指定信号(若为0,则没有任何信号送出,但系统会检查执行是否错误,常用于检查某个进程是否仍在运行)
  • pid指定进程
  • 作用:将指定的信号传给指定的进程
  • 返回值:成功0,失败-1,errno会被设置

参数pid:

  1. pid > 0代表具体的进程编号
  2. pid = 0将信号传给和当前进程相同进程组的所有进程。常用于父进程给(所有)子进程发送信号,且父进程也会收到。
  3. pid = -1将信号广播推送给系统内所有进程(系统关机时会用。)

多进程和信号

在多进程的服务程序中:

  • 如果子进程收到退出信号,子进程自行退出。
  • 如果父进程收到退出信号,则应该先向全部的子进程发送退出信号,然后自己再退出。
#include<iostream>#include<unistd.h>#include<signal.h>usingnamespacestd;voidFathEXIT(intsig);// 父进程的信号处理函数。voidChldEXIT(intsig);// 子进程的信号处理函数。intmain(){// 忽略全部的信号,不希望被打扰。for(intii=1;ii<=64;ii++)signal(ii,SIG_IGN);// 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程// 但请不要用 "kill -9 +进程号" 强行终止signal(SIGTERM,FathEXIT);signal(SIGINT,FathEXIT);// SIGTERM 15 SIGINT 2while(true){if(fork()>0)// 父进程的流程。{sleep(5);continue;}else// 子进程的流程。{// 子进程需要重新设置信号。signal(SIGTERM,ChldEXIT);// 子进程的退出函数与父进程不一样。signal(SIGINT,SIG_IGN);// 子进程不需要捕获SIGINT信号。while(true){cout<<"子进程"<<getpid()<<"正在运行中。\n";sleep(3);continue;}}}}// 父进程的信号处理函数。voidFathEXIT(intsig){// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。signal(SIGINT,SIG_IGN);signal(SIGTERM,SIG_IGN);cout<<"父进程退出,sig="<<sig<<endl;kill(0,SIGTERM);// 向全部的子进程发送15的信号,通知它们退出。// 在这里增加释放资源的代码(全局的资源)。exit(0);}// 子进程的信号处理函数。voidChldEXIT(intsig){// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。signal(SIGINT,SIG_IGN);signal(SIGTERM,SIG_IGN);cout<<"子进程"<<getpid()<<"退出,sig="<<sig<<endl;// 在这里增加释放资源的代码(只释放子进程的资源)。exit(0);}
运行中,不断创建子进程
执行杀死某个子线程:kill 14844
杀死父进程后

终止进程

可以用echo $?命令行查看进程终止状态

正常终止的线程值为0 ,非0都为异常状态

终止方式状态析构
return正常正常析构释放
exit()正常调用全局对象析构,局部不会调用
_exit()正常(了解)直接退出,无清理动作
_Exit()正常(了解)直接退出,无清理动作
abort()异常
接受信号异常

注:

atexit()函数

  • int atexit(void (*funtion)(void))

  • 登记终止函数,用于进程退出前的首尾工作

  • exit()调用终止函数的顺序与登记时相反

会先运行func2,再运行func1

调用可执行程序

在Cpp文件中调用Linux的可执行程序(如,命令行,另一个cpp程序等)

system()

声明:int system(const char * string)

  • “程序不存在” 或者 (成功但终止状态非0),返回非0
  • 成功且终止状态为0 ,返回0
// 等价于命令行 ls -l /tmpintret=system("/bin/ls -l /tmp");

exec()函数族

// 声明方式如下intexecl(constchar*path,constchar*arg,...);intexeclp(constchar*file,constchar*arg,...);intexecle(constchar*path,constchar*arg,...,char*constenvp[]);intexecv(constchar*path,char*constargv[]);intexecvp(constchar*file,char*constargv[]);intexecvpe(constchar*file,char*constargv[],char*constenvp[]);
  • 失败返回-1,失败原因存在errno
  • 在A进程中使用exec调用程序B,则B将取代A进程的进程号、代码段、数据段和堆栈。换句话说,进程不会切换回A继续执行代码。
// 如下// 参数:运行程序,运行程序,选项,操作对象,0不能省略// 最后一个参数0不能省略。intret=execl("/bin/ls","/bin/ls","-lt","/tmp",0);

创建进程

查看进程树:pstree -p 进程编号(需要先安装psmisc)

获取进程ID

pid_tgetpid(void);// 获取当前进程的ID。pid_tgetppid(void);// 获取父进程的ID。

fork()创建子进程

fork()可以用于创建子进程,也就是父进程的副本。

fork()函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是子进程的进程ID。

  • 子进程和父进程执行内容一致,且都从fork()之后开始执行
  • 当子进程和父进程同时运行时,其顺序不确定(需要使用同步机制,来确保执行顺序正确)
  • 子进程将获得父进程的数据空间、堆栈的副本,但不是原本的那一份,虽然父子拥有的数据虚拟内存地址一致,但物理内存不一样。
  • 父子进程共享同一个文件偏移量(加入父进程在a.txt写入1000行数据,父进程在运行时创建了子进程,那么结束后,a.txt写入的内容将是2000行)
#include<iostream>#include<fstream>#include<unistd.h>usingnamespacestd;intmain(){ofstream fout;fout.open("/tmp/tmp.txt");// 打开文件。fork();for(intii=0;ii<10000000;ii++)// 向文件中写入一千万行数据。{fout<<"进程"<<getpid()<<"西施"<<ii<<"极漂亮"<<"\n";// 写入的内容无所谓。}fout.close();// 关闭文件。}

用法:

  1. 父进程复制自己,但执行不同代码(比如用if判断,copy过去的代码是一致的)
  2. 当进程需要执行(exec)另一个程序时。(可以理解为替身攻击,假设A需要使用exec()执行B程序,但exec()之后的代码不会继续执行,那么A可以fork出一个A’,由A’去执行B程序,那么A就可以执行完成自身代码。

vfork()

针对上述用法2,我们可以直接使用vfork(),vfork()不会复制父进程的地址空间,子进程会立即调用exec

且,vfork()会保证子程序先运行,子程序调用execexit后,父进程才会恢复。

僵尸进程

通常指,子进程结束还没善后前,父进程已经结束,导致子进程的进程描述符还在系统中,依然占用资源。

产生的原因可能是:

  1. 父进程没有等待子进程结束并处理,就已经结束了。

避免方式:

  1. 子进程退出时,内核会向父进程发送SIGCHLD信号。父进程在创建子进程前表明signal(SIGCHLD,SIG_IGN),即退其退出不感兴趣,那么子进程会退出后立刻释放。
  2. 父进程使用wait()/waitpid()等函数等待子进程结束,在子进程退出前,父进程将会阻塞等待。
// 通常使用这个, 返回值为子进程编号pid_twait(int*stat_loc);
  • stat_loc为子进程终止信息。
    • WIFEXITED(stat_loc)判断是否正常终止
    • WEXITSTATUS(stat_loc)会获取终止状态(正常为1,详见终止状态)
    • WTERMSIG(stat_loc)可以获取终止进程的信号(如果该进程异常终止是由于信号造成的话)
  1. 若父进程任务繁忙,可以通过捕获SIGCHLD信号,在信号处理函数中调用wait()/waitpid()进行处理

实例1:演示第二种避免方式的check

#include<iostream>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>usingnamespacestd;intmain(){if(fork()>0){// 父进程的流程。intsts;pid_t pid=wait(&sts);cout<<"已终止的子进程编号是:"<<pid<<endl;if(WIFEXITED(sts)){cout<<"子进程是正常退出的,退出状态是:"<<WEXITSTATUS(sts)<<endl;}else{cout<<"子进程是异常退出的,终止它的信号是:"<<WTERMSIG(sts)<<endl;}}else{// 子进程的流程。// sleep(100);// 以下两种写法会引起异常终止11// int *p = 0;// *p = 10;cout<<1<<endl;exit(1);}}

实例2:演示第三种避免方式

#include<iostream>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>usingnamespacestd;voidfunc(intsig)// 子进程退出的信号处理函数。{intsts;pid_t pid=wait(&sts);cout<<"已终止的子进程编号是:"<<pid<<endl;if(WIFEXITED(sts)){cout<<"子进程是正常退出的,退出状态是:"<<WEXITSTATUS(sts)<<endl;}else{cout<<"子进程是异常退出的,终止它的信号是:"<<WTERMSIG(sts)<<endl;}}intmain(){signal(SIGCHLD,func);// 捕获子进程退出的信号。if(fork()>0){// 父进程的流程。while(true){cout<<"父进程忙着执行任务。\n";sleep(1);}}else{// 子进程的流程。sleep(5);// int *p=0; *p=10;exit(1);}}
使用killall bodythread即可退出

共享内存

多线程可以通过全局变量来共享/传递数据,多进程通过共享内存来共享和传递数据。

  • 共享内存只能使用内置数据类型
  • 共享内存不能使用STL
  • 共享内存不能使用move语义

1. 创建/获取共享内存

函数声明:int shmget(key_t key, size_t size, int shmflg);

  • key(十六进制整数):共享内存键值
  • size:共享内存的大小,以字节为单位。
  • shmflg: 共享内存的访问权限(与文件的权限一样)
    • 0666:全用户可读写
    • IPC_CREAT:不存在则创建。
  • 返回值:成功返回共享内存的id(非负整数), 失败返回-1(通常由于内存不足,没有权限)

tips: 可以使用ipcs -m查看系统共享内存,ipcs -m sm_id手工删除该共享内存

2. 连接共享内存到当前进程地址空间

声明:void *shmat(int shmid, const void *shmaddr, int shmflg);

  • shmid:共享内存id(由shmget()函数返回的共享内存标识)
  • shmaddr:指定连接地址,通常填0(表系统自动选择连接地址)
  • shmflg:标志位,通常填0
  • 返回:成功则返回连接起始地址,失败返回(void*)-1

3. 从进程中分离共享内存

声明:int shmdt(const void *shmaddr);

  • shamddr: 分离地址
  • 返回:成功返回0,失败返回-1

4. 操作共享内存

最常用操作是删除

声明:int shmctl(int shmid, int command, struct shmid_ds *buf);

  • shmid: 共享内存id
  • command: 操作指令
    • 删除:IPC_RMID
  • buf: 操作的共享内存的地址
    • 如果要删除,填0。
  • 返回:成功(0),失败(-1)

案例

创建一个共享内存连接当前进程,更改共享内存内容,然后断开连接并释放。

#include<iostream>#include<cstdio>#include<cstdlib>#include<cstring>#include<unistd.h>#include<sys/ipc.h>#include<sys/shm.h>usingnamespacestd;// super girl 结构体structstgirl{intno;// 编号charname[51];// 姓名,不能用string(其实是不支持STL容器,因为会调用堆内存,我们这里暂时不要涉及)};intmain(intargc,char*argv[]){if(argc!=3){cout<<"Using: ./spgirl no name\n";return-1;}// 1. 创建共享内存,intshmid=shmget(0x5005,sizeof(stgirl),0640|IPC_CREAT);if(shmid==-1){cout<<"shmget(0x5005) failed.\n";return-1;}cout<<"shmid="<<shmid<<endl;// 2. 把共享内存连接到当前进程的地址空间stgirl*ptr=(stgirl*)shmat(shmid,0,0);// 连接失败返回-1if(ptr==(void*)-1){cout<<"shmat() failed\n";return-1;}// 3. 使用共享内存,对共享内存进行读/写cout<<"原值:no="<<ptr->no<<",name="<<ptr->name<<endl;// 显示共享内存中的原值。ptr->no=atoi(argv[1]);// 对超女结构体的no成员赋值。strcpy(ptr->name,argv[2]);// 对超女结构体的name成员赋值cout<<"新值:no="<<ptr->no<<",name="<<ptr->name<<endl;// 显示共享内存中的当前值。// 4. 把共享内存从当前进程分离shmdt(ptr);// 5. 删除共享内存if(shmctl(shmid,IPC_RMID,0)==-1){cout<<"shmctl failed\n";return-1;}return0;}
运行效果

循环队列

循环队列即数据结构中的循环队列,有一个头指针和一个尾指针,维护一个队列容器。

有空的时候可以手写一个循环队列。

注意:当循环队列和共享内存结合使用时,一定要额外定义初始化函数,因为在共享内存分配循环队列对象时,并不会调用构造函数。且类型不能为STL容器,需要为内置类型

信号量

用于多进程同步机制。

P 加锁(减值)

V 解锁(加值)

生产消费者模型

没什么不同,只是用法上复杂了一点,用到了共享内存。

但因为本章探讨的是进程同步,那么需要用到POSIX共享内存和信号量。

生产者代码

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/7 11:49:29

收藏!从零开始学习大模型,小白程序员必备的完整路线图

本文分享了一份70多天的AI应用开发学习路线&#xff0c;强调AI学习不应只关注技术&#xff0c;更要理解技术背后的原理和解决的问题。文章分为五个阶段&#xff1a;让AI说人话&#xff08;Prompt基础&#xff09;、解决AI幻觉问题&#xff08;RAG应用&#xff09;、AI项目评测体…

作者头像 李华
网站建设 2026/7/7 11:47:52

口碑好的家装门窗生产厂家

在家装过程中&#xff0c;门窗的选择至关重要&#xff0c;它不仅关系到家居的美观度&#xff0c;还影响着居住的舒适度和安全性。然而&#xff0c;面对市场上众多的门窗生产厂家&#xff0c;业主们往往会感到无从下手。今天&#xff0c;我就站在业主的视角&#xff0c;为大家剖…

作者头像 李华
网站建设 2026/7/7 11:47:45

如何在3分钟内完成网易云音乐插件安装:BetterNCM-Installer完整指南

如何在3分钟内完成网易云音乐插件安装&#xff1a;BetterNCM-Installer完整指南 【免费下载链接】BetterNCM-Installer 一键安装 Better 系软件 项目地址: https://gitcode.com/gh_mirrors/be/BetterNCM-Installer BetterNCM-Installer是一款专为网易云音乐PC客户端设计…

作者头像 李华
网站建设 2026/7/7 11:44:00

视频内容自动化解析技术:从语音识别到画面分析的完整实现方案

&#x1f680; 30款热门AI模型一站整合&#xff0c;DeepSeek/GLM/Qwen 随心用&#xff0c;限时 5 折。 &#x1f449; 点击领海量免费额度 这次我们来看一个游戏预告相关的技术话题——《赛博朋克&#xff1a;边缘行者2》Netflix韩版预告。虽然这不是一个可以直接部署的AI模…

作者头像 李华