linux(信号量)

article/2025/11/7 4:02:50

信号量

    • 几个基本概念
      • 临界资源
      • 临界区
      • 原子性
      • 互斥
    • 信号量
      • 后台进程
      • 前台进程
    • 信号
      • 储存信号
      • 处理信号(信号捕捉)
    • 发送信号
      • 1、键盘产生:
      • 2、系统调用接口发送信号
      • 3、由软件条件产生信号
      • 4、硬件异常发送信号
    • 内核中的信号量
      • **信号量在内核中的数据结构**
      • **信号集操作函数**
    • 信号的检查和处理
    • 可重入函数
    • volatile关键字

几个基本概念

临界资源

临界资源:被多个进程能够看到的资源

如果没有对临界资源进行任何保护,对于临界资源的访问,双方进程在进行访问的时候,就都是乱序的,可能会因为读写交叉而导致的各种乱码、废弃数据、访问控制访问的问题

临界区

临界区:对多个进程而言,访问临界资源的代码

我们写的进程的代码中,有大量的代码,只有一部分代码,会访问临界资源

原子性

原子性:一件事情要么不做,要么做完了,没有中间状态

互斥

互斥:任何时刻,只允许一个进程,访问临界资源

信号量

信号量的本质就是计数器,且这个计数器的操作是原子性

信号量对应的操作:

​ 申请资源:P操作

​ 释放资源:V操作

共享内存不具有访问控制,但可以通过信号量进行对资源的保护

共享内存

shmget	//创建
shmctl	//删除
shmat	//关联
shmdt	//去关联

消息队列

msgget
msgctl
msgsnd
msgrcv

信号量

semget
semctl
semop	+1 P	//申请资源
semop	-1 V	//释放资源

查看

ipcs -m/-q/-s	//共享内存/消息队列/信号量

删除

ipcrm -m/-q/-s	//共享内存/消息队列/信号量

共享内存、消息队列、信号量的生命周期都是随内核(操作系统)的

管道文件的生命周期都是随进程的

对于进程来讲,即便信号还没有产生,进程已经具有识别和处理这个信号的能力了。

后台进程

./myproc &

后台进程运行时,可以使用bash进程,后台进程不能使用ctrl+c终止,前台进程可以使用ctrl+c终止

jobs			//查看后台进程fg 作业号	  	  //把后台进程提到前台

动画

前台进程

./myproc //前台任务,运行时不能使用bash进程

kill -l			//查看信号man 7 'singal' 	//查看信号详细信息

image-20230109210753827

其中131为普通信号,3464为实时信号

可以同时运行一个前台进程和若干个后台进程

信号

因为信号产生是异步的(信号随时都有可能产生),当信号产生的时候,对应的进程可能正在做更重要的事情,我们进程可以暂时不处理这个信号,进程暂时不处理信号,就需要先将信号先储存起来

储存信号

那么信号如何储存?

使用位图记录信号量(在进程的task_struct中)

1、有没有产生【比特位的内容1/0】

2、什么信号产生【比特位的位置】

我们要对信号进行存储就需要对进程的task_struct中记录信号量的位图进行修改,而task_struct是在内核空间中的,那么只有os可以对task_struct做修改,无论信号如何产生,都是os帮我们进行设置的

处理信号(信号捕捉)

处理信号有三种动作

1、默认动作

2、忽略

3、自定义动作

sighandler_t signal(int signum, sighandler_t handler);	//设置信号的自定义方法
void handler(int signum)
{cout<<"捕捉到"<<signum<<"号信号"<<endl;}
int main()
{signal(SIGINT,handler);while(1){cout<<"hello linux"<<endl;sleep(1);}return 0;
}

image-20230110103435953

ctrl+c就是给前台进程发送2号信号(终止自己)

注意:

1、无法对9号和19号信号设置自定义动作,忽略,阻塞

2、6号信号虽然可以设置自定义动作,但执行完自定义动作后依旧会执行默认动作

发送信号

用户层产生信号方式

1、键盘产生:

ctrl+c:发送2号信号

ctrl+\:发送3号信号

我们在之前说过,无论信号如何产生,都是由os来发送的,本质上发送信号就是修改task_struct中的位图

2、系统调用接口发送信号

int kill(pid_t pid,int sig)	//给任意进程发送任意信号
void handler(int signum)
{
cout<<"捕捉到"<<signum<<"号信号"<<endl;
exit(1);
}
int main()
{
signal(SIGINT,handler);
cout<<"进程运行中 pid:"<<getpid()<<endl;
cout<<"等待3秒后,发送2号信号,进程退出"<<endl;
sleep(3);
kill(getpid(),SIGINT);		//给当前进程发送2号信号
return 0;
}

动画

int raise(int sig)		//给自己发送任意信号
void handler(int signum)
{
cout<<"捕捉到"<<signum<<"号信号"<<endl;
exit(1);
}
int main()
{
signal(SIGINT,handler);
cout<<"进程运行中 pid:"<<getpid()<<endl;
cout<<"等待3秒后,发送2号信号,进程退出"<<endl;
sleep(3);
raise(SIGINT);		//给当前进程发送2号信号
return 0;
}
void abort(void)		//向自己发送SIGABRT信号(终止进程)
void handler(int signum)
{
cout<<"捕捉到"<<signum<<"号信号"<<endl;
exit(1);
}
int main()
{
signal(SIGABRT,handler);
cout<<"进程运行中 pid:"<<getpid()<<endl;
cout<<"等待3秒后,发送6号信号,进程退出"<<endl;
sleep(3);
abort();
return 0;
}

动画

3、由软件条件产生信号

unsigned int alarm(unsigned int seconds);
//调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号 
//该信号的默认处理动作是终止当前进程
int main()
{
cout<<"进程运行中 pid:"<<getpid()<<endl;
cout<<"等待3秒后,发送14号信号,进程退出"<<endl;
alarm(3);
sleep(1000);
return 0;
}

动画

4、硬件异常发送信号

首先我们需要知道进程崩溃的本质就是该进程收到了异常信号

一般情况,导致进程崩溃主要是除零错误越界野指针问题

①除零错误

void handler(int signum)
{cout<<"捕获到"<<signum<<"号信号"<<endl;exit(1);
}int main()
{for(int i=1;i<32;i++){signal(i,handler);}int num=10/0;return 0;
}

image-20230111205724168

②越界野指针问题

野指针

void handler(int signum)
{cout<<"捕获到"<<signum<<"号信号"<<endl;exit(1);
}
int main()
{for(int i=1;i<32;i++){signal(i,handler);}int* num=nullptr;*num=1000;return 0;
}

image-20230111214127014

越界

void handler(int signum)
{cout<<"捕获到"<<signum<<"号信号"<<endl;exit(1);
}
int main()
{for(int i=1;i<32;i++){signal(i,handler);}int arr[10];for(int i=10;i<10000;i++){arr[i]=100;cout<<i<<endl;}return 0;
}

image-20230111213843822

**注意:**我们的进程发生崩溃退出,是因为操作系统给进程发信号,进程合适的时候对于这个信号做出默认动作,终止进程,如果我们对信号设置(不终止进程的)自定义动作,这个进程就不会终止

那么进程发生崩溃时,是如何收到异常信号的?

①除零

在计算机中,运算都是在CPU中进行的,在CPU的内部有一个状态寄存器(硬件),这个状态寄存器的作用是检查计算是否出错

CPU进行计算时,发生除零错误,CPU内部的状态寄存器就会被设置为:有报错,浮点数错误

OS就会根据这个状态寄存器得知CPU内有报错,OS就会构建信号,并把这个信号发送给出错的这个进程,进程会在合适的时候处理这个信号,终止进程

②越界&&野指针

我们在语言层面使用的地址(指针)都是虚拟地址,我们使用的地址都是通过虚拟地址经过页表映射到物理地址,再通过物理地址找到物理内存,再读取对应的数据和代码的

虚拟地址转换到物理地址的工作是由(MMU(硬件)+页表(软件))来完成的,如果虚拟地址有问题,地址转化过程就会引起问题,表现在硬件MMU上,OS发现硬件出现问题,OS会构建信号,向出错的进程发送信号,目标进程会在合适的时候处理该信号,终止进程

补充:core_dump

某些信号的默认动作是Core,这些信号基本都是因为代码出现的问题导致的

image-20230112162957957

Core动作会将core_dump置为1,会产生一个大文件core.进程pid

那么这个core_dump 在在哪里,什么作用?

在父进程等待子进程时, waitpid(pid_t pid, int *status, int options),status是一个输出型参数,从子进程的pcb中获取,我们只取低16位,其中次低8位为子进程退出码,低七位为终止信号,剩下1位为core_dump

core_dump会把进程在运行中,对应的异常上下文数据,core_dump到磁盘上,方便调试

image-20230112164245870

在云服务器上,默认把core file size设置为0,无法生成core文件,我们需要ulimit命令修改core file size

image-20230112191036360

代码

int main()
{pid_t id=fork();if(id==0){cout<<"子进程 pid"<<getpid()<<endl;int* num=nullptr;*num=1000;cout<<"子进程 pid"<<getpid()<<endl;exit(1);}else{int status=0;waitpid(id,&status,0);cout<<"子进程退出码:"<<((status>>8)&0xFF)<<"终止信号:"<<(status&0X7F)<<"core_dump:"<<((status>>7)&0x1)<<endl;}return 0;
}

image-20230112192712660

使用core文件进行调试:

image-20230112193644337

上图中,红线中的调试信息,11号信号终止进程,段错误错误定位在第14行,*num=1000;

内核中的信号量

信号量在内核中的数据结构

信号存储在进程的task_struct中,task_struct中有三个表,block表(阻塞信号集),pending表(未决信号集),handler表,

其中pending表就是发送信号给进程,存储信号的位图

block表也是一个位图,这个位图上表示的是哪些信号被阻塞,信号被阻塞表示进程依旧可以收到这些信号,但是不会递达(处理)这些信号

hanlder表是一个函数指针数组,处理信号使用信号编号为数组下标(对应的处理方法默认动作或自定义方法或忽略)

image-20230112200322400

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号

    产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler

  • 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,也就是如果解除该信号的阻塞,只会处理一次该信号

sigset_t

专门为信号量设计的类型

信号集操作函数

虽然block表和pending表都是位图,但是不同系统的实现不同,位图的内部实现可能数组,所以不能直接使用位操作,需要使用特定的信号集操作函数

int sigemptyset(sigset_t *set);
//函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号
int sigfillset(sigset_t *set);
//函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示 该信号集的有效信号包括系统支持的所有信号
int sigaddset(sigset_t *set,int signo);
//向set所指向的信号集添加signo对应的信号
int sigdelset(sigset_t *set,int signo);
//向set所指向的信号集删除signo对应的信号
int sigismember(const sigset_t *set,int signo);
//检查set所指向的信号集,是否包含signo对应的信号
  • 注意,在使用sigset_t类型的变量之前,一定要调用sigemptysetsigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddsetsigdelset在该信号集中添加或删除某种有效信号
  • 前四个函数都是成功返回0,出错返回-1;sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1

sigprocmask

int sigprocmask(int how, const sigset_t *restrict set,sigset_t *restrict oset);
//调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
//若成功返回0,失败返回1
  • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。

  • 如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。

  • 如果osetset都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据sethow参数更改信号屏蔽字。

  • how的取值

    SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字

    SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号

    STG_SETMASK:设置当前信号屏蔽字为set所指向的值

  • 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号抵达

sigpending

int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

代码

void printpending(sigset_t* p)
{for(int i=1;i<32;i++){int ret = sigismember(p,i);if(ret==1){cout<<"1";}else{cout<<"0";}}cout<<endl;
}
int main()
{sigset_t s,p;sigemptyset(&s);sigaddset(&s,SIGINT);sigprocmask(SIG_BLOCK,&s,NULL);while(1){sigpending(&p);printpending(&p);sleep(1);}return 0;
}

动画

信号的检查和处理

在前面,都在说进程会在合适的时候处理信号,那么什么时候是合适的时候?

合适的时候,就是进程从内核态切换到用户态时会对信号做检测和处理

我们来讲一下用户态和内核态

在进程的虚拟地址空间0~3G为用户空间,3~4G为内核空间,所有进程的内核空间都相同,所有进程使用同一个内核空间,当我们的进程访问内核空间时,进程处于内核态,进程访问用户空间时,进程处于用户态。

那么进程如何访问内核空间

我们知道进程通过页表进行虚拟地址和物理地址的转换,访问用户代码和数据,那这个页表叫做用户级页表,用户级页表不能访问内核空间,除了用户级页表还有一个内核级页表,内核具有访问所有空间的权限(内核级空间和用户级空间),

用户态切换到内核态切换的时机

当进程的时间片到了,需要进行进程间切换时,进程会切换到内核态,执行进程调度算法,等执行完内核的代码,会切换回用户态

进行系统调用,

进行信号的检查和处理

在进程由于某些原因(进程切换,系统调用)切换到内核态后,再切换回用户态时,回进行信号的检测和处理

当进程在内核态执行完内核的代码(比如:系统调用等),准备返回,要从内核态切换回用户态时,会检查和处理信号,

下图中,会查看pending表中为1的信号量是否被阻塞,如果没被阻塞,执行handler表中的动作

  • 如果handler表中信号量编号下标对应的动作是SIG_DFL(默认动作),当前进程正处于内核态,执行默认动作
  • 如果handler表中信号量编号下标对应的动作是SIG_IGN(忽略动作),当前进程正处于内核态,直接把pending表中对应的信号量置为0
  • 如果handler表中信号量编号下标对应的动作是自定义动作(函数指针),进程就会从内核态切换到用户态执行用户代码,执行完用户的自定义动作代码后,再从用户态切换回内核态,在内核态中,再使用特定的系统调用返回,切换到用户态。再执行接下来的用户代码。
  • 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号

image-20230112200322400

sigaction

int sigaction(int sig, const struct sigaction *restrict act,struct sigaction *restrict oact);
//sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1
  • signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。actoact指向sigaction结构体:

可重入函数

一个链表头插节点函数,同时设置了一个自定义动作函数,也是链表头插

node node1,node2;
node* head;
void insert(node* p)
{p->next = head;head = p;
}
int sighandler(int signo)
{insert(&node2);
}
int main()
{insert(&node1);…………
}

如果在主函数中进行链表头插时,当p->next = head;这句代码后,如果该进程的时间片到了,要进行进程间切换,由用户态切换到内核态,执行调度算法,执行完调度算法后,需要从内核态切换回用户态,这时会进行信号检测和处理,如果该进程收到信号,那么这时执行进程的自定义动作方法,进行链表头插节点,就会出现下图这种情况,导致node2丢失,内存泄漏

image-20230118105504914

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称
为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,
如果一个函数只访问自己的局部变量或参数,则称为可重入函数

如果一个函数符合以下条件之一则是不可重入的

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile关键字

一个死循环

int flag=1;void handler(int signo)
{flag=0;
}int main()
{signal(SIGINT,handler);while(flag){cout<<"hello world"<<endl;sleep(1);}return 0;
}

flag该为0,死循环停止

image-20230118194047707

使用GCC优化选项-O2cpu就不会从内存中取值,一直使用CPU中的flag值循环不会结束

使用volatile保持内存的可见性,死循环可以退出


http://chatgpt.dhexx.cn/article/edgywcKx.shtml

相关文章

Linux--信号量

1.信号量的定义: 信号量是一个特殊的变量&#xff0c;一般取正数值。它的值代表允许访问的资源数目&#xff0c; 获取资源时&#xff0c;需要对信号量的值进行原子减一&#xff0c;该操作被称为p操作。当信号量值为0时&#xff0c;代表没有资源可用&#xff0c;p操作会阻塞。释…

【Linux】Linux的信号量集

所谓信号量集&#xff0c;就是由多个信号量组成的一个数组。作为一个整体&#xff0c;信号量集中的所有信号量使用同一个等待队列。Linux的信号量集为进程请求多个资源创造了条件。Linux规定&#xff0c;当进程的一个操作需要多个共享资源时&#xff0c;如果只成功获得了其中的…

linux信号量简介

一、什么是信号量 为了防止多个程序同时访问一个共享资源而引发的一系列问题&#xff0c;我们需要一种访问机制&#xff0c;它可以通过生成并使用令牌来授权&#xff0c;在同一时刻只能有一个线程访问代码的临界区域。 临界区域是指执行数据更新的代码需要独占式地执行。而信…

Linux下信号量使用总结

目录 1.Linux下信号量简介 2.POSIX信号量 2.1 无名信号量 2.2 有名信号量 3.System V信号量 1.Linux下信号量简介 信号量是解决进程之间的同步与互斥的IPC机制&#xff0c;互斥与同步关系存在的症结在于临界资源。 临界资源是在同一个时刻只容许有限个&#xff08;一般只有…

Linux信号量详解

Linux信号量详解 1.什么是信号量信号量是一种特殊的变量&#xff0c;访问具有原子性。只允许对它进行两个操作&#xff1a;1)等待信号量当信号量值为0时&#xff0c;程序等待&#xff1b;当信号量值大于0时&#xff0c;信号量减1&#xff0c;程序继续运行。2)发送信号量将信号量…

Linux进程间通信—信号量

一、概述 进程间通信&#xff08;interprocess communication&#xff0c;简称 IPC&#xff09;指两个进程之间的通信。系统中的每一个进程都有各自的地址空间&#xff0c;并且相互独立、隔离&#xff0c;每个进程都处于自己的地址空间中。所以同一个进程的不同模块譬如不同的函…

Linux操作系统-信号量

信号量也属于一种进程间通信的机制&#xff0c;与其他的进程间通信不同&#xff0c;信号量不是用来传输数据的&#xff0c;而是用来进程间同步与互斥。除此之外&#xff0c;信号量还可以实现线程间的互斥。 信号量是什么&#xff1f; 信号量的本质是一个计数器。 一个信号量…

Linux·信号量全解

目录 信号量 进程间 【无名信号量完成 有血缘关系的进程间 互斥】 知识点2【有名信号量 没有血缘进程互斥】 1、创建一个有名信号量 2、信号量的关闭&#xff1a; 3、信号量文件的删除 4、P操作 sem_wait V操作sem_post 销毁信号量sem_destroy 知识点3【有名信号量 没…

Linux-----信号量

信号量 信号量原理信号量概念信号量函数基于环形队列的生产消费模型空间和数据资源生产者和消费者申请、释放信号量模拟实现基于环形队列的生产者消费者模型 信号量原理 之前我们知道被多个执行流同时访问的公共资源叫做临界资源&#xff0c;而临界资源不保护的话会造成数据不…

Linux信号量

文章目录 POSIX信号量信号量的原理信号量的概念信号量函数 二元信号量模拟实现互斥功能基于环形队列的生产消费模型空间资源和数据资源生产者和消费者申请和释放资源必须遵守的两个规则代码实现信号量保护环形队列的原理 POSIX信号量 信号量的原理 我们将可能会被多个执行流同…

Linux —— 信号量

目录 一、POSIX信号量 1. 什么是信号量 2. 信号量的基本原理 二、与信号量相关的操作 1. 初始化信号量 2. 销毁信号量 3. 等待信号量 4. 发布信号量 三、基于环形队列的生产者消费者模型 1. 空间资源和数据资源 2. 生产者和消费者申请和释放资源 四、模拟实现基于…

Double取值intValue()与doubleValue()之参数缺省

Double调用intValue()是四舍五入向下取整。 调用doubleValue()才是取double真实值。

java.lang.NullPointerException: Attempt to invoke virtual method ‘int java.lang.Integer.intValue()‘

问题 对于PreparedStatement 对象设置参数时&#xff0c; 提示该错误; java.lang.NullPointerException: Attempt to invoke virtual method ‘int java.lang.Integer.intValue()’ 具体问题 2022-09-06 21:28:10.695 11368-11755/com.example.electronicmall E/AndroidRunt…

IntValue()方法 和 ValueOf()方法

intValue() 1.intValue()是java.lang.Number类的方法&#xff0c;Number是一个抽象类。Java中所有的数值类都继承它。也就是说&#xff0c;不单是Integer有intValue方法&#xff0c;Double&#xff0c;Long等都有此方法。 2.此方法的意思是&#xff1a;输出int数据。每个数值类…

Double取值intValue()与doubleValue()

描述一个之前没注意&#xff0c;手误造成的bug。 可以看出&#xff0c;Double调用intValue()结果类似于RoundingMode.DOWN。 调用doubleValue()才是取double真实值。

java中valueof_JAVA中intValue()和ValueOf()什么意思,还有Value什么意思

展开全部 intValue()和ValueOf()是数据类62616964757a686964616fe59b9ee7ad9431333366306538型转化的两个方法。 intValue() 如Integer类型&#xff0c;就会有intValue()方法&#xff0c;意思是说&#xff0c;把Integer类型转化为Int类型。 valueOf() 如String就有valueOf()方法…

IDEA告警:Unnecessary unboxing ‘xxx.intValue()‘

显式编码拆箱已包装的原始数值。在Java5及以上的版本&#xff0c;拆箱是不必要的&#xff0c;可以安全地删除。那么 JDK5 到底做了啥&#xff1f; 自动装箱&#xff08;auto-boxing&#xff09;与自动拆箱&#xff08;auto-unboxing&#xff09; Java语言的基本类型都有包装&…

latex自定义插入空行或者空格

空行有几种方法&#xff1a; 1.~\\ 2.\\[行距] 例如&#xff1a;\\[3pt] 最后&#xff0c;我的选择是&#xff1a; \vspace*{n\baselineskip}空格&#xff1a;