IPC方法-信号

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IPC方法-信号相关的知识,希望对你有一定的参考价值。

参考技术A

信号通过软件方法实现(软中断), 会导致延时性. 每个进程接收到的信号都是由内核发送处理的, 内核作为中转.

未决: 产生和递达之间的状态, 主要由于阻塞(屏蔽)导致.

未决信号集和阻塞信号集(信号屏蔽字)都存放在PCB中, 数据结构是集合, 不重复但无序, 内部存放0/1值. 正常传递时, 未决信号集中从0变到1(未决)再从1变到0(被处理)的过程可以看作是瞬时的. 但当阻塞信号集某编号因为某种原因为1时, 当内核发送信号后, 未决信号集中对应的该编号也会变为1(不发送就仍为0), 所以说阻塞信号集影响未决信号集.

man 7 signal 查看信号, 当一个名字对应多个编号时, 取中间那列的, 左右两列为其他平台的编号.
9)SIGKILL和19)SIGSTOP比较特殊, 不允许忽略和捕捉, 甚至不允许阻塞, 只能执行默认动作

ctrl + c 2)SIGINT 中断信号interrupt, 终止进程(Term)
ctrl + z 20)SIGTSTP 暂停与终端交互进程, 放到后台(Stop). 注意与19)SIGSTOP区分, SIGSTOP可以暂停任何进程(Stop).
ctrl + \\ 3)SIGQUIT 退出进程(Core)

除0操作 8)SIGFPE(浮点数例外)
非法访问内存 11)SIGSEGV(段错误)
总线错误 7)SIGBUS

int kill(pid_t pid, int sig) 可以调用kill函数
对于管道连接的进程, 当写端进程终止后, 写端都关闭, 读端也会跟着关闭, 所以只kill写端进程就会把管道两端的进程都杀死.

alarm()传新的时间进去后会覆盖旧的, 所以返回的上一次剩下的秒数没什么用, 如果alarm(0)就是关闭定时器, 直接终止进程. 注意终止进程只是默认行为, 可以设置一个handler函数如 signal(SIGALRM, handler) 来注册信号的捕捉函数(真正捕捉信号的是内核), 这样就可以在时间到了之后不终止进程. 信号捕捉函数是一个典型的回调函数(封装了一个函数, 但是没有直接调用).

可以使用time命令查看程序运行的时间, 总时间=用户空间时间+内核空间时间+等待时间, 等待时间I/O占了大部分.

new_value是传入参数, 表示要定时的时间; old_value是传出参数, 表示上次剩的时间, 相当于alarm()函数的返回值. setitimer成功返回0, 失败返回-1.
需要先把结构体成员赋初值, 再把结构体指针传到函数里, 分别指定val和interval的秒值/毫秒值:

只有第一次设定闹钟后5秒会接收到SIGALRM信号, 之后每3秒接收到一个SIGALRM信号.

sigset_t myset; 自己的信号集, 即"位图"

影响阻塞信号集3号位的方法:
1.先 sigemptyset(&myset) 再 sigaddset(&myset, 3) 把自己的信号集3号位设置为1(注意信号集编号从1开始, 长度为64位unsigned long int, 对应kill -l列出来的信号宏, 一般操作前31个).
2.如果是设置阻塞, 调用 sigprocmask(SIG_BLOCK, &myset, &oldset) 把阻塞集mask的3号位置为1, 其中 sigset_t oldset 是传出参数, 用来保存原来的阻塞集状态, 可以传NULL. 之后当内核发送3号信号(SIGQUIT)时, 未决信号集的第3位也根据阻塞集变为1.
3.如果是解除阻塞, 调用 sigprocmask(SIG_UNBLOCK, &myset, &oldset) 把阻塞集mask的3号位置为0, 则未决集的3号位也会从1变0. 也就是说无论是设置阻塞还是解除阻塞, 位图myset的对应号位都要置为1.

类似sigaddset()的的还有 sigfillset(&myset) 全置1, sigdelset(&myset, 3) 把3号位改为0(若已为0则无动作), sigismember(&myset, 5) 判断5号位是否为1.

阻塞信号集不可读, 但可以使用 int sigpending(sigset_t *set) 来读取未决信号集, set是传出参数. 打印信号集要使用sigismember函数.

信号捕捉函数由内核调用, 即所谓的回调模式, 只有在确认信号已抵达后才会被调用, 在用户空间执行, 执行完通过sigreturn返回内核进行报道, 再从上次被中断的地方继续执行.

注册 信号捕捉函数除了 signal(signum, handler) 之外, 还可以使用 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact) 函数, 用结构体的sigset_t sa_mask成员设置信号处理期间要被屏蔽的信号集合(使用sigemptyset()和sigaddset()设置), 防止调用sa_handler(函数指针型成员变量)期间被打断, sa_mask会覆盖内核的阻塞集mask. 另外sa_flags也要设置为0, 表示sa_handler处理某信号期间忽略相同信号.
捕获完信号后最好恢复成信号的默认动作 sigaction(signum, &oldact, NULL) , 且sigaction()调用前后都要对要接收的信号进行屏蔽(设置mask), 以防止注册信号捕捉函数的过程中信号到来.

pause()函数可以让进程主动被挂起, 当捕获并处理完信号后返回-1, 设置errno为EINTR. 需和信号捕捉函数配合使用
如果alarm()后失去CPU, 且过了定时的时间, 当重获CPU时会先处理已发送并阻塞的SIGALRM信号, 这样pause()后就无法接收到本应等待的SIGALRM, 从而一直挂起, 导致时序竞态问题. 使用sigsuspend可解决该问题.

sigsuspend()函数实质上就是在内核阻塞信号集mask上把SIGALRM屏蔽, 当执行sigsuspend()时再使用临时阻塞集解除屏蔽. 这样即使alarm后失去CPU, SIGALRM信号被阻塞, 等恢复执行后也是先调用sigsuspend()并用捕捉函数处理信号, 而不是先处理SIGALRM再pause()导致一直挂起.

对于进程间通信, 全局变量存在异步I/O的情况, 应尽量少用全局变量(使用锁的机制可以避免这种情况). 即可能主函数信号发出后失去CPU, 当恢复CPU后, 另一端已发送的信号会优先被处理(信号捕捉函数里可以改变全局变量), 使得对全局变量的修改顺序出现错误, 导致进程挂起.

使用全局变量的函数容易变成不可重入函数, 一旦被信号打断容易发生错误, 执行结果会和预期的不同(如递归操作). 所以信号捕捉函数要设计成可重入函数, 避免使用全局变量和static变量, 避免使用malloc/free.

当多个子进程同时死亡时, 虽然信号捕捉函数一次只能处理一个信号, 但如果waitpid()第一个参数设为回收全部子进程, 在这一次调用的过程中waitpid()就会把当前剩下死亡的子进程都回收掉, 此处信号的作用只是激活waitpid()函数, 回收几个子进程由waitpid自己决定.

read在读文件时不会阻塞, 在读网络/设备/管道时可能发生阻塞, 此时收到信号就会被中断.

中断的慢速系统调用要么重启要么执行默认动作, 如果重启需要again和goto参数, 同时判断EINTR信号

进程间通信 (IPC) 方法总结

进程间通信 (IPC) 方法总结(三)

信号量(SEMAPHORE)

信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。
为了获得共享资源,进程需要执行下列操作:

  1. 创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。
  2. 等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞。也称为P操作。
  3. 挂出一个信号量:该操作将信号量的值加1,也称为V操作。

为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。Linux环境中,有三种类型:Posix(可移植性操作系统接口)有名信号量(使用Posix IPC名字标识)、Posix基于内存的信号量(存放在共享内存区中)、System V信号量(在内核中维护)。这三种信号量都可用于进程间或线程间的同步。

Posix有名信号量

技术图片

Posix基于内存的信号量

技术图片

System V信号量

技术图片

信号量与普通整型变量的区别

  1. 信号量是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap) , signal(semap) ; 来进行访问;
  2. 操作也被成为PV原语(P来源于荷兰语proberen"测试",V来源于荷兰语verhogen"增加",P表示通过的意思,V表示释放的意思),而普通整型变量则可以在任何语句块中被访问;

    信号量与互斥量之间的区别

  3. 互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。

    互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

    同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。

    在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源

  4. 互斥量值只能为0/1,信号量值可以为非负整数。

    也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。

  5. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

套接字(SOCKET)

套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。
技术图片

套接字是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

套接字特性

套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。

套接字的域

它指定套接字通信中使用的网络介质,最常见的套接字域有两种:

  1. AF_INET,它指的是Internet网络。当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址和端口来指定一台联网机器上的某个特定服务,所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。
  2. AF_UNIX,表示UNIX文件系统,它就是文件输入/输出,而它的地址就是文件名。

    套接字的端口号

    每一个基于TCP/IP网络通讯的程序(进程)都被赋予了唯一的端口和端口号,端口是一个信息缓冲区,用于保留Socket中的输入/输出信息,端口号是一个16位无符号整数,范围是0-65535,以区别主机上的每一个程序(端口号就像房屋中的房间号),低于256的端口号保留给标准应用程序,比如pop3的端口号就是110,每一个套接字都组合进了IP地址、端口,这样形成的整体就可以区别每一个套接字。

    套接字协议类型
  3. 流套接字
    流套接字在域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。流套接字提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有一定的出错后重新发送的机制。
  4. 数据报套接字
    它不需要建立连接和维持一个连接,它们在域中通常是通过UDP/IP协议实现的。它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输,它可能会丢失、复制或错乱到达,UDP不是一个可靠的协议,但是它的速度比较高,因为它并一需要总是要建立和维持一个连接。
  5. 原始套接字
    原始套接字允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW。

原始套接字与标准套接字的区别

原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。

套接字通信的建立

技术图片

  • 服务端
    1. 首先服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他的进程共享。(socket)
    2. 服务器进程会给套接字起个名字,我们使用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个套接字。(bind)
    3. 系统调用listen来创建一个队列并将其用于存放来自客户的进入连接。(listen)
    4. 服务器通过系统调用accept来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理来自其他客户的连接(建立客户端和服务端的用于通信的流,进行通信)。(accept--read/write)
  • 客户端
    1. 客户应用程序首先调用socket来创建一个未命名的套接字。(socket)
    2. 将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。(connect)
    3. 一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信(通过流进行数据传输)(read/write)

eg.

服务端代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> //socket listen bind
#include <sys/socket.h>//socket listen bind 
#include <unistd.h>//unlink
#include <sys/un.h>//struct sockaddr_un
  
int main()  
  
  /* delete the socket file */  
  unlink("server_socket");  
    
  /* create a socket */  
  int server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);  
    
  struct sockaddr_un server_addr;  
  server_addr.sun_family = AF_UNIX;  
  strcpy(server_addr.sun_path, "server_socket");  
    
  /* bind with the local file */  
  bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));  
    
  /* listen */  
  listen(server_sockfd, 5);  
    
  char ch;  
  int client_sockfd;  
  struct sockaddr_un client_addr;  
  socklen_t len = sizeof(client_addr);  
  while(1)  
    
    printf("server waiting:\\n");  
      
    /* accept a connection */  
    client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &len);  
      
    /* exchange data */  
    read(client_sockfd, &ch, 1);  
    printf("get char from client: %c\\n", ch);  
    ++ch;  
    write(client_sockfd, &ch, 1);  
      
    /* close the socket */  
    close(client_sockfd);  
    
    
  return 0;  
  

客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> //socket listen bind
#include <sys/socket.h>//socket listen bind 
#include <unistd.h>//unlink
#include <sys/un.h>//struct sockaddr_un  
  
int main()  
  
  /* create a socket */  
  int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);  
    
  struct sockaddr_un address;  
  address.sun_family = AF_UNIX;  
  strcpy(address.sun_path, "server_socket");  
    
  /* connect to the server */  
  int result = connect(sockfd, (struct sockaddr *)&address, sizeof(address));  
  if(result == -1)  
    
    perror("connect failed: ");  
    exit(1);  
    
    
  /* exchange data */  
  char ch = 'A';  
  write(sockfd, &ch, 1);  
  read(sockfd, &ch, 1);  
  printf("get char from server: %c\\n", ch);  
    
  /* close the socket */  
  close(sockfd);  
    
  return 0;  
  

如果我们首先运行tcp_client,会提示没有这个文件:

技术图片

因为我们是以AF_UNIX方式进行通信的,这种方式是通过文件来将服务器和客户端连接起来的,因此我们应该先运行tcp_server,创建这个文件,默认情况下,这个文件会创建在当前目录下,并且第一个s表示它是一个socket文件:

技术图片

程序运行的结果如下图:

技术图片


参考文章:

  1. 进程间通信IPC (InterProcess Communication)

  2. 进程间通信--管道

  3. UNIX/Linux进程间通信IPC系列(四)消息队列

  4. Linux进程间通信(四) - 共享内存

  5. 本地socket通讯

以上是关于IPC方法-信号的主要内容,如果未能解决你的问题,请参考以下文章

带信号/插槽的 PySide IPC

进程间通信(IPC)之信号量

进程间通信(IPC)之信号量

linux进程间通讯-System V IPC 信号量

IPC信号

IPC——信号