C++linux高并发服务器项目实践 day9
- 信号集
- 信号集相关函数
- 以下信号集相关的函数都是对自定义的信号集进行操作
- sighandler_t函数
- sigaction函数
- SIGCHLD信号
- 共享内存
- 共享内存使用步骤
- 共享内存操作函数
信号集
- 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为sigset_t
- 在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改
- 信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间
- 信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生
- 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作
信号集相关函数
以下信号集相关的函数都是对自定义的信号集进行操作
int sigemptyset(sigset_t *set);
- 功能:清空信号集中的数据,将信号集中的所有标志位置为0
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0,失败返回-1
int sigfillset(sigset_t *set);
- 功能:将信号集中的所有标志位置为1
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0,失败返回-1
int sigaddset(sigset_t *set , int signum);
- 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置阻塞的那个信号
- 返回值:成功返回0,失败返回-1
int sigdelset(sigset_t *set , int signum);
- 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置阻塞的那个信号
- 返回值:成功返回0,失败返回-1
int sigismember(const sigset_t *set , int signum);
- 功能:判断某个信号是否阻塞
- 参数:
- set:需要的信号集操作
- signum:需要判断的那个信号
- 返回值:
- signum被阻塞
- signum不是一个成员
-1. 失败
#include <signal.h>
#include <stdio.h>
int main(){
//创建一个信号集
sigset_t set;
//清空信号集的内容
sigemptyset(&set);
//判断SIGINT是否在信号集set里
int ret = sigismember(&set,SIGINT);
if(ret == 0){
printf("SIGINT 不阻塞\n");
}else if(ret == 1){
printf("SIGINT 阻塞\n");
}
//添加几个信号到信号集中
sigaddset(&set,SIGINT);
sigaddset(&set,SIGQUIT);
//判断SIGINT是否在信号集set里
ret = sigismember(&set,SIGINT);
if(ret == 0){
printf("SIGINT 不阻塞\n");
}else if(ret == 1){
printf("SIGINT 阻塞\n");
}
//判断SIGQUIT是否在信号集set里
ret = sigismember(&set,SIGQUIT);
if(ret == 0){
printf("SIGQUIT 不阻塞\n");
}else if(ret == 1){
printf("SIGQUIT 阻塞\n");
}
//从信号集中删除一个信号
sigdelset(&set , SIGQUIT);
//判断SIGQUIT是否在信号集set里
ret = sigismember(&set,SIGQUIT);
if(ret == 0){
printf("SIGQUIT 不阻塞\n");
}else if(ret == 1){
printf("SIGQUIT 阻塞\n");
}
return 0;
}
sighandler_t函数
#include <signal.h>
int sigpromask(int how,const sigset_t *set,sigset_t *oldset);
- 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
- 参数:
-
how:如何对内核阻塞信号集进行处理
SIG_BLOCK:将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
假设内核中默认的阻塞信号集是mask, mask | set
SIG_UNBLOCK:根据用户设置的数据,对内核中的数据进行解除阻塞
mask &= ~set
SIG_SETMASK:覆盖内核中原来的值 -
set:已经初始化好的用户自定义的信号集
-
oldset:保存设置之前的内核中的阻塞信号集的状态,可以是NULL
-
- 返回值:
成功:0
失败:-1
设置错误号:EFAULT、EINVAL
int sigpending(sigset_t *set);
- 功能:获取内核中的未决信号集
- 参数:set,传出参数,保存的是内核中的未决信号集中的信号
//编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
//设置某些信号是阻塞的,通过键盘产生这些信号
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
//设置2、3号信号阻塞
sigset_t set;
sigemptyset(&set);
//将2号和3号信号添加到信号集中
sigaddset(&set,SIGINT);
sigaddset(&set,SIGQUIT);
//修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK,&set,NULL);
int num =0;
while(1){
num++;
//获取当前的未决信号集的数据
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);
//遍历前32位
for(int i = 1;i <= 32 ;i++){
if(sigismember (&pendingset,i) == 1){
printf("1");
}else if(sigismember (&pendingset,i) == 0){
printf("0");
}else{
perror("sigismember");
exit(0);
}
}
printf("\n");
sleep(1);
if(num == 10){
//解除阻塞
sigprocmask(SIG_UNBLOCK,&set,NULL);
}
}
return 0;
}
sigaction函数
#include <signal.h>
int sigaction(int signum, const struct siaction *act,struct sigaction *oldact);
- 功能:检查或者改变信号的处理。信号捕捉
- 参数:
- signum:需要捕捉的信号的编号或者宏值(信号的名称)
- act:捕捉到醒后之后的处理动作
- oldact:上一次对信号捕捉相关的设置,一般不使用,传递NULL
- 返回值:
成功返回0,失败返回-1
struct sigaction {
//函数指针,指向的函数就是信号捕捉到之后的处理函数
void (*sa_handler)(int);
//函数指针,不常用
void (*sa_sigaction)(int, siginfo_t *, void *);
//临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号
sigset_t sa_mask;
//使用哪一个信号处理对捕捉到的信号进行处理
//这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
int sa_flags;
//被废弃掉了
void (*sa_restorer)(void);
};
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num){
printf("捕捉到了信号的编号是:%d\n",num);
printf("XXXXXXXXXX\n");
}
//过3秒以后,每隔2秒定时一次
int main(){
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;
sigemptyset(&act.sa_mask); //清空临时阻塞信号集
//注册信号捕捉
sigaction(SIGALRM,&act,NULL);
struct itimerval new_value;
//设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
//设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int retur = setitimer(ITIMER_REAL ,&new_value,NULL);//非阻塞
printf("定时器开始了...\n");
if(retur == -1){
perror ("setitimer");
exit(0);
}
while(1);
return 0;
}
SIGCHLD信号
SIGCHLD信号产生的条件:
- 子进程终止时
- 子进程从接收到SIGSTOP信号停止时
- 子进程处在停止态,接受到SIGCONT后唤醒时
以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号
可以用这个信号来解决僵尸进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
void myFun(int num ){
printf("捕捉到的信号:%d\n",num);
//能捕捉到17号信号,可以通过kill -l来查看
//回收子进程PCB的资源
// while(1){
// wait(NULL);
// }
while (1)
{
int ret = waitpid(-1,NULL,WNOHANG);
if(ret > 0){
printf("child die,pid = %d\n",ret);
}else if (ret == 0)
{
//说明还有子进程
break;
}else if (ret == -1)
{
/*没有子进程*/
break;
}
}
}
int main(){
//提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_BLOCK,&set,NULL);
//创建一些子进程
pid_t pid;
for(int i = 0;i < 20;i++){
pid = fork();
if(pid == 0){
break;
}
}
if(pid > 0){
//父进程
//捕捉子进程死亡时发送的SIGCHLD信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myFun;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD,&act,NULL);
//注册完信号捕捉后,解除阻塞
sigprocmask(SIG_UNBLOCK,&set,NULL);
while (1)
{
printf("parent process pid :%d\n",getpid());
sleep(2);
}
}else if(pid == 0){
//子进程
printf("child process pid :%d\n",getpid());
}
return 0;
}
共享内存
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种IPC无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用
与管道等要求发送进程将数据从用户控件的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC技术的速度更快
共享内存使用步骤
- 调用shmget()创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中欧冠需要用到的共享内存标识符
- 使用shmat()来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分
- 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由shmat()调用返回的addr值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针
- 调用shmdt()来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步
- 调用shmctl()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步
共享内存操作函数
int shmget(key_t key,size_t size,int shmflg);
void *shmat(int shmid,const void *shmaddr ,int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
key_t ftok(const char *pathname,int proj_id);
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);
- 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识
新创建的内存段中的数据都会被初始化为0 - 参数:
- key:key_t类型是一个整形,通过这个找到或者创建一个共享内存
一般使用16进制表示,非0值 - size:共享内存的大小
- shmflg:属性
- 访问权限
- 附加属性:创建/判断共享内存是不是存在
- 创建:IPC_CREAT
- 判断共享内存是否存在:IPC_EXCL ,需要和IPC_CREAT一起使用
IPC_CREAT | IPC_EXCL | 0664
- key:key_t类型是一个整形,通过这个找到或者创建一个共享内存
- 返回值:
失败返回-1,设置错误号
成功>0,返回共享内存的引用的ID,后面操作共享内存都是通过这个值
void *shmat(int shmid,const void *shmaddr ,int shmflg);
- 功能:和当前的进程进行关联
- 参数:
- shmid:共享内存的标识(ID),由shmget返回值获取
- shmaddr:申请的共享内存的起始地址,指定为NULL,由内核指定
- shmflg:对共享内存的操作
- 读:SHM_RDONLY,必须要有读权限
- 读写:0
- 返回值:
成功:返回共享内存的首(起始)地址
失败:返回(void*)-1,设置错误号
int shmdt(const void *shmaddr);
- 功能:解除当前进程和共享内存的关联
- 参数:
shmaddr:共享内存的首地址 - 返回值:成功 0,失败 -1
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
- 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响
- 参数:
- shmid:共享内存的ID
- cmd:要做的操作
- IPC_STAT:获取共享内存的当前的状态
- IPC_SET:设置共享内存的状态
- IPC_RMID:标记共享内存被销毁
- buf:结构体指针,需要设置或者获取的共享内存的属性信息
- IPC_STAT:buf存储数据
- IPC_SET:buf中需要初始化数据,设置到内核中
- IPC_RMID:没有用,NULL
key_t ftok(const char *pathname,int proj_id);
- 功能:根据指定的路径名,和int值,生成一个共享内存的key
- 参数:
- pathname:指定一个存在的路径
- proj_id:int类型的值,但是这系统调用只会使用其中的1个字节
范围:0-255 一般指定一个字符,如’a’
写
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main(){
//1.创建一个共享内存
int shmid = shmget(100,4096,IPC_CREAT|0664);
printf("shmid = %d\n",shmid);
//2.和当前进程进行关联
void * ptr = shmat(shmid, NULL,0);
char* str = "hello world";
//3.写数据
memcpy(ptr,str ,strlen(str)+1);
printf("按任意键继续\n");
getchar();
//4.解除关联
shmdt(ptr);
//5.删除共享内存
shmctl(shmid ,IPC_RMID,NULL);
return 0;
}
读
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main(){
//1.创建一个共享内存
int shmid = shmget(100,0,IPC_CREAT);
printf("shmid = %d\n",shmid);
//2.和当前进程进行关联
void * ptr = shmat(shmid, NULL,0);
//3.读数据
printf("%s\n",(char*)ptr);
printf("按任意键继续\n");
getchar();
//4.解除关联
shmdt(ptr);
//5.删除共享内存
shmctl(shmid ,IPC_RMID,NULL);
return 0;
}
问题1:操作系统如何知道一块共享内存被多少个进程关联
- 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattach
- shm_nattach 记录了关联的进程个数
问题2:可不可以对共享内存进行多次删除 shmctl
- 可以的
- 因为shmctl标记删除共享内存,不是直接删除
- 什么时候真正删除呢
当和共享内存关联的进程数为0的时候,就真正被删除 - 当共享内存的key为0的时候,表示共享内存被标记删除了
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存了
共享内存和内存映射的区别
- 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
- 共享内存效果更高
- 内存
所有的进程操作的是同一块共享内存
内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存 - 数据安全
- 进程突然退出
共享内存还存在
内存映射区小时 - 运行进程的电脑宕机了
数据存储在共享内存中,没有了
内存映射区的数据,由于磁盘文件中的数据还在,所以内存映射区的数还在
5.生命周期 - 内存映射区:进程退出,内存映射区销毁
- 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
如果一个进程退出,会自动和共享内存取消关联
- 进程突然退出