linux 信号及处理过程
信号本质:
信号是软件中断,是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
其实,在头文件<signal.h>中,内核将信号都定义为正整数(信号编号)。
信号来源
信号事件的发生有两个来源。
1.硬件来源:
-
用户按终端键,引起终端产生的信号(比如Ctrl + C键产生SIGINT)。
-
硬件异常产生信号:除数为0、无效的内存引用等。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的进程产生相应的信号。
2.软件来源: -
最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数。
常见信号
Linux信号的编号是从1-64,其中32和33空缺,没有对应的信号。通过kill -l 可查看所有的信号。
- 1~31之间的信号叫做不可靠信号, 不支持排队, 信号可能会丢失, 也叫做非实时信号。
- 34~64之间的信号叫做可靠信号, 支持排队, 信号不会丢失, 也叫做实时信号。
信号代码从1到32是不可靠信号,不可靠信号主要有以下问题:
(1)每次信号处理完之后,就会恢复成默认处理,这可能是调用者不希望看到的(早期的signal函数,linux2.6.35.6内核经验证已经不再恢复默认动作)。
(2)存在信号丢失的问题(进程收到的信号不作排队处理,相同的信号多次到来会合并为一个)。
现在的Linux对信号机制进行了改进,因此,不可靠信号主要是指信号丢失。
信号代码从SIGRTMIN到SIGRTMAX之间的信号是可靠信号。可靠信号不存在丢失,由sigqueue发送,可靠信号支持排队。
- 可靠信号注册机制:
内核每收到一个可靠信号都会去注册这个信号,在信号的未决信号链中分配sigqueue结构,因此,不会存在信号丢失的问题。
- 不可靠信号的注册机制:
而对于不可靠的信号,如果内核已经注册了这个信号,那么便不会再去注册,对于进程来说,便不会知道本次信号的发生。
可靠信号与不可靠信号与发送函数没有关系,取决于信号代码,前面的32种信号就是不可靠信号,而后面的32种信号就是可靠信号。
信号响应的方式
- 忽略信号SIG_IGN ,但有两种信号不能被忽略SIGKILL,SIGSTOP。
- 捕捉信号处理,即用户自定义的信号处理函数来处理。
- 采用系统默认处理SIG_DFL,执行缺省操作。
信号的处理过程
信号产生->信号注册->信号在进程中注销->信号处理函数执行完毕
(1)信号的产生是指触发信号的事件的发生
(2)信号注册指的是在目标进程中注册,该目标进程中有未决信号的信息:
struct sigpending pending:
struct sigpending{undefined
struct sigqueue *head, **tail;
sigset_t signal;
};
struct sigqueue{undefined
struct sigqueue *next;
siginfo_t info;
}
其中 sigqueue结构组成的链称之为未决信号链,sigset_t称之为未决信号集。
*head,**tail分别指向未决信号链的头部与尾部。
siginfo_t info是信号所携带的信息。
信号注册的过程就是将信号值加入到未决信号集siginfo_t中,将信号所携带的信息加入到未决信号链的某一个sigqueue中去。
Linux IPC
管道(Pipe)
管道用来连接不同进程之间的数据流。
(1)在两个程序之间传递数据的最简单的方法是使用popen()和pclose()函数:
#include <stdio.h>
FILE *popen(const char *command, const char *open_mode);
int pclose(FILE *stream);
popen()函数首先调用一个shell,然后把command作为参数传递给shell。这样每次调用popen()函数都需要启动两个进程;但是由于在Linux中,所有的参数扩展(parameter expansion)都是由shell执行的,这样command中包含的所有参数扩展都可以在command程序启动之前完成。
(2)pipe()函数:
#include <unistd.h>
int pipe(int pipefd[2]);
popen()函数只能返回一个管道描述符,并且返回的是文件流(file stream),可以使用函数fread()和fwrite()来访问。pipe()函数可以返回两个管道描述符:pipefd[0]和pipefd[1],任何写入pipefd[1]的数据都可以从pipefd[0]读回;pipe()函数返回的是文件描述符(file descriptor),因此只能使用底层的read()和write()系统调用来访问。pipe()函数通常用来实现父子进程之间的通信。
(3)命名管道:FIFO
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *fifo_name, mode_t mode);
前面两种管道只能用在相关的程序之间,使用命名管道可以解决这个问题。在使用open()打开FIFO时,mode中不能包含O_RDWR。mode最常用的是O_RDONLY,O_WRONLY与O_NONBLOCK的组合。O_NONBLOCK影响了read()和write()在FIFO上的执行方式。
System V IPC
System V IPC指的是AT&T在System V.2发行版中引入的三种进程间通信工具:(1)信号量,用来管理对共享资源的访问 (2)共享内存,用来高效地实现进程间的数据共享 (3)消息队列,用来实现进程间数据的传递。我们把这三种工具统称为System V IPC的对象,每个对象都具有一个唯一的IPC标识符(identifier)。要保证不同的进程能够获取同一个IPC对象,必须提供一个IPC关键字(IPC key),内核负责把IPC关键字转换成IPC标识符。
System V IPC具有相似的语法,一般操作如下:
(1)选择IPC关键字,可以使用如下三种方式:
a)IPC_PRIVATE。由内核负责选择一个关键字然后生成一个IPC对象并把IPC标识符直接传递给另一个进程。
b)直接选择一个关键字。
c)使用ftok()函数生成一个关键字。
(2)使用semget()/shmget()/msgget()函数根据IPC关键字key和一个标志flag创建或访问IPC对象。如果key是IPC_PRIVATE;或者key尚未与已经存在的IPC对象相关联且flag中包含IPC_CREAT标志,那么就会创建一个全新的IPC对象。
(3)使用semctl()/shmctl()/msgctl()函数修改IPC对象的属性。
(4)使用semctl()/shmctl()/msgctl()函数和IPC_RMID标志销毁IPC实例。
System V IPC为每个IPC对象设置了一个ipc_perm结构体并在创建IPC对象的时候进行初始化。这个结构体中定义了IPC对象的访问权限和所有者:
struct ipc_perm{
uid_t uid; //所有者的用户id
gid_t gid; //所有者的组id
uid_t cuid; //创建者的用户id
gid_t cgid; //创建者的组id
mode_t mode; //访问模式
…
};
shell中管理IPC对象的命令是ipcs、ipcmk和ipcrm。
Socket
套接字(Socket)是由Berkeley在BSD系统中引入的一种基于连接的IPC,是对网络接口(硬件)和网络协议(软件)的抽象。它既解决了无名管道只能在相关进程间单向通信的问题,又解决了网络上不同主机之间无法通信的问题。
套接字有三个属性:域(domain)、类型(type)和协议(protocol),对应于不同的域,套接字还有一个地址(address)来作为它的名字。
域(domain)指定了套接字通信所用到的协议族,最常用的域是AF_INET,代表网络套接字,底层协议是IP协议。对于网络套接字,由于服务器端有可能会提供多种服务,客户端需要使用IP端口号来指定特定的服务。AF_UNIX代表本地套接字,使用Unix/Linux文件系统实现。
IP协议提供了两种通信手段:流(streams)和数据报(datagrams),对应的套接字类型(type)分别为流式套接字和数据报套接字。流式套接字(SOCK_STREAM)用于提供面向连接、可靠的数据传输服务。该服务保证数据能够实现无差错、无重复发送,并按顺序接收。流式套接字使用TCP协议。数据报套接字(SOCK_DGRAM)提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP协议。
代码练习
https://gitee.com/zhang_yu_peng/practice-code/blob/master/t.c
https://gitee.com/zhang_yu_peng/practice-code/blob/master/ts.s