Linux与cpp——进程
文章目录
- Linux与cpp——进程
- 信号
- 设置程序对信号的处理:signal()
- 信号类型
- 发送信号函数kill()
- 多进程和信号
- 终止进程
- 调用可执行程序
- system()
- exec()函数族
- 创建进程
- fork()创建子进程
- 僵尸进程
- 共享内存
- 1. 创建/获取共享内存
- 2. 连接共享内存到当前进程地址空间
- 3. 从进程中分离共享内存
- 4. 操作共享内存
- 案例
- 循环队列
- 信号量
- 生产消费者模型
本笔记中的进程→狭义中的Linux操作系统下运行的c++程序
信号
在Linux操作系统中,我们可以使用killall或kill命令向进程传递信号(用数值表示)。
# 以下的demo表进程killall-14demo# 查看进程是否存活killall-0demo进程在接收到信号后会进行处理,有以下三种处理方式
- 默认 -> 终止进程(宏为
SIG_DFL) - 设置信号处理函数,收到信号后执行函数
- 忽略信号(宏为
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);}信号类型
常用信号类型
| 信号名 | 信号值 | 默认处理动作 | 发出信号的原因 |
|---|---|---|---|
| SIGHUP | 1 | A | 终端挂起或者控制进程终止 |
| SIGINT | 2 | A | 键盘中断Ctrl+c |
| SIGQUIT | 3 | C | 键盘的退出键被按下 |
| SIGILL | 4 | C | 非法指令 |
| SIGABRT | 6 | C | 由abort(3)发出的退出指令 |
| SIGFPE | 8 | C | 浮点异常 |
| SIGKILL | 9 | AEF | 采用kill -9 进程编号 强制杀死程序。 |
| SIGSEGV | 11 | CEF | 无效的内存引用(数组越界、操作空指针和野指针等)。 |
| SIGPIPE | 13 | A | 管道破裂,写一个没有读端口的管道。 |
| SIGALRM | 14 | A | 由闹钟alarm()函数发出的信号。可以用作定时器 |
| SIGTERM | 15 | A | 采用“kill 进程编号”或“killall 程序名”通知程序。 |
| SIGUSR1 | 10 | A | 用户自定义信号1 |
| SIGUSR2 | 12 | A | 用户自定义信号2 |
| SIGCHLD | 17 | B | 子进程结束信号 |
| SIGCONT | 18 | 进程继续(曾被停止的进程) | |
| SIGSTOP | 19 | DEF | 终止进程 |
| SIGTSTP | 20 | D | 控制终端(tty)上按下停止键 |
| SIGTTIN | 21 | D | 后台进程企图从控制终端读 |
| SIGTTOU | 22 | D | 后台进程企图从控制终端写 |
| 其它 | <=64 | A | 自定义信号 |
处理动作一项中的字母含义如下:
- 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:
pid > 0代表具体的进程编号pid = 0将信号传给和当前进程相同进程组的所有进程。常用于父进程给(所有)子进程发送信号,且父进程也会收到。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);}终止进程
可以用
echo $?命令行查看进程终止状态正常终止的线程值为0 ,非0都为异常状态
| 终止方式 | 状态 | 析构 |
|---|---|---|
| return | 正常 | 正常析构释放 |
| exit() | 正常 | 调用全局对象析构,局部不会调用 |
| _exit() | 正常(了解) | 直接退出,无清理动作 |
| _Exit() | 正常(了解) | 直接退出,无清理动作 |
| abort() | 异常 | |
| 接受信号 | 异常 |
注:
atexit()函数
int atexit(void (*funtion)(void))登记终止函数,用于进程退出前的首尾工作
exit()调用终止函数的顺序与登记时相反
调用可执行程序
在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();// 关闭文件。}用法:
- 父进程复制自己,但执行不同代码(比如用if判断,copy过去的代码是一致的)
- 当进程需要执行(exec)另一个程序时。(可以理解为替身攻击,假设A需要使用exec()执行B程序,但exec()之后的代码不会继续执行,那么A可以fork出一个A’,由A’去执行B程序,那么A就可以执行完成自身代码。
vfork()
针对上述用法2,我们可以直接使用vfork(),vfork()不会复制父进程的地址空间,子进程会立即调用exec。
且,vfork()会保证子程序先运行,子程序调用exec或exit后,父进程才会恢复。
僵尸进程
通常指,子进程结束还没善后前,父进程已经结束,导致子进程的进程描述符还在系统中,依然占用资源。
产生的原因可能是:
- 父进程没有等待子进程结束并处理,就已经结束了。
避免方式:
- 子进程退出时,内核会向父进程发送
SIGCHLD信号。父进程在创建子进程前表明signal(SIGCHLD,SIG_IGN),即退其退出不感兴趣,那么子进程会退出后立刻释放。 - 父进程使用
wait()/waitpid()等函数等待子进程结束,在子进程退出前,父进程将会阻塞等待。
// 通常使用这个, 返回值为子进程编号pid_twait(int*stat_loc);stat_loc为子进程终止信息。- 宏
WIFEXITED(stat_loc)判断是否正常终止 - 宏
WEXITSTATUS(stat_loc)会获取终止状态(正常为1,详见终止状态) - 宏
WTERMSIG(stat_loc)可以获取终止进程的信号(如果该进程异常终止是由于信号造成的话)
- 宏
- 若父进程任务繁忙,可以通过捕获
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);}}共享内存
多线程可以通过全局变量来共享/传递数据,多进程通过共享内存来共享和传递数据。
- 共享内存只能使用内置数据类型
- 共享内存不能使用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: 共享内存idcommand: 操作指令- 删除:
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共享内存和信号量。
生产者代码