0630
Linux高并发服务器开发—笔记1
(视频课从01:30:30开始)
进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信( IPC:Inter Processes Communication )。
进程间通信的目的:
管道(匿名管道、有名管道)
信号量(互斥锁)
共享内存
内存映射(匿名映射、内存映射)
消息队列
信号
socket
管道也叫无名(匿名)管道,它是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。
统计一个目录中文件的数目命令:ls | wc –l
,为了执行该命令,shell 创建了两个进程来分别执行 ls 和 wc。
管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机的访问数据。
匿名管道只能在具有公共祖先(父进程与子进程,或者两个兄弟进程,具有亲缘关系)的进程之间使用。
创建匿名管道:
#include
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2]
这个数组是一个传出参数。
返回值: 成功 0;失败 -1
管道默认是阻塞的:如果管道中没有数据,read阻塞;如果管道满了,write阻塞;
注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)
示例:
// 子进程发送数据给父进程,父进程读取到数据输出
#include
#include
#include
#include
#include
int main() {
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1) {
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n", getpid());
// 关闭写端
close(pipefd[1]);
// 从管道的读取端读取数据
char buf[1024] = {0};
while(1) {
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv : %s, pid : %d\n", buf, getpid());
// 向管道中写入数据
// const char * str = "hello,i am parent";
// write(pipefd[1], str, strlen(str));
// sleep(3);
}
} else if(pid == 0){
// 子进程
printf("i am child process, pid : %d\n", getpid());
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while(1) {
// 向管道中写入数据
const char *str = "hello,i am child";
write(pipefd[1], str, strlen(str));
sleep(3);
// int len = read(pipefd[0], buf, sizeof(buf));
// printf("child recv : %s, pid : %d\n", buf, getpid());
// bzero(buf, 1024);
}
}
return 0;
}
ulimit –a
root@VM-16-2-ubuntu:~# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15343
max locked memory (kbytes, -l) 65536
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 15343
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
其中pipe size (512 bytes, -p) 8
表示管道大小为512B* 8 = 4KB。
#include
long fpathconf(int fd, int name);
//gets a value for the configuration option name for the open file descriptor fd.
示例:
#include
#include
#include
#include
#include
int main() {
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1) {
perror("pipe");
exit(0);
}
// 获取管道的大小
long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
printf("pipe size : %ld\n", size);
return 0;
}
结果:
pipe size : 4096
4096字节 = 4KB
// 子进程发送数据给父进程,父进程读取到数据输出
#include
#include
#include
#include
#include
int main() {
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1) {
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n", getpid());
// 关闭写端
close(pipefd[1]);
// 从管道的读取端读取数据:
char buf[1024] = {0};
while(1) {
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv : %s, pid : %d\n", buf, getpid());
// 向管道中写入数据:
// const char * str = "hello,i am parent";
// write(pipefd[1], str, strlen(str));
// sleep(3);
}
} else if(pid == 0){
// 子进程
printf("i am child process, pid : %d\n", getpid());
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while(1) {
// 向管道中写入数据:
const char *str = "hello,i am child";
write(pipefd[1], str, strlen(str));
sleep(3);
// 从管道的读取端读取数据:
// int len = read(pipefd[0], buf, sizeof(buf));
// printf("child recv : %s, pid : %d\n", buf, getpid());
// bzero(buf, 1024);
}
}
return 0;
}
编译运行:
i am child process, pid : 2799345
i am parent process, pid : 2799340
parent recv : hello,i am child, pid : 2799340
parent recv : hello,i am child, pid : 2799340
parent recv : hello,i am child, pid : 2799340
parent recv : hello,i am child, pid : 2799340
通过命令创建有名管道:
mkfifo 名字
通过函数创建有名管道:
#include
#include
int mkfifo(const char *pathname, mode_t mode);
参数:
open
的参数 mode
是一样的,是一个八进制的数返回值:成功返回0,失败返回-1,并设置错误号
一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件I/O 函数都可用于 fifo。如:close、read、write、unlink 等。
FIFO 严格遵循先进先出(First in First out),对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek() 等文件定位操作。
示例:
mkfifo.c
#include
#include
#include
#include
#include
int main() {
// 判断文件是否存在
int ret = access("fifo1", F_OK);
if(ret == -1) {
printf("管道不存在,创建管道\n");
ret = mkfifo("fifo1", 0664);//0664是权限
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
return 0;
}
read.c
#include
#include
#include
#include
#include
#include
// 从管道中读取数据
int main() {
// 1.打开管道文件
int fd = open(" ", O_RDONLY);
if(fd == -1) {
perror("open");
exit(0);
}
// 读数据
while(1) {
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
if(len == 0) {
printf("写端断开连接了...\n");
break;
}
printf("recv buf : %s\n", buf);
}
close(fd);
return 0;
}
write.c
#include
#include
#include
#include
#include
#include
#include
int main() {
// 3.以只写的方式打开管道
int fd = open("fifo1", O_WRONLY);
if(fd == -1) {
perror("open");
exit(0);
}
// 写数据
for(int i = 0; i < 100; i++) {
char buf[1024];
sprintf(buf, "hello, %d\n", i);
printf("write data : %s\n", buf);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
分别生成对应的以.o结尾的可执行文件,然后在两个终端分别运行读和写:
补充:
有名管道的注意事项:
1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道
读管道:
管道中有数据,`read`返回**实际读到的字节数**
管道中无数据:
管道写端被全部关闭,read返回0,(相当于读到文件末尾)
写端没有全部被关闭,read阻塞等待
写管道:
管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
管道读端没有全部关闭:
管道已经满了,write会阻塞
管道没有满,write将数据写入,并返回实际写入的字节数。
管道的读写特点:
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程
向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。
4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。
总结:
读管道:
0
(相当于读到文件的末尾);写管道:
SIGPIPE
信号)。#include
#include
#include
#include
#include
#include
/*
设置管道非阻塞
int flags = fcntl(fd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(fd[0], F_SETFL, flags); // 设置新的flag
*/
int main() {
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1) {
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n", getpid());
// 关闭写端
close(pipefd[1]);
// 从管道的读取端读取数据
char buf[1024] = {0};
int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(pipefd[0], F_SETFL, flags); // 设置新的flag
while(1) {
int len = read(pipefd[0], buf, sizeof(buf));
printf("len : %d\n", len);
printf("parent recv : %s, pid : %d\n", buf, getpid());
memset(buf, 0, 1024);
sleep(1);
}
} else if(pid == 0){
// 子进程
printf("i am child process, pid : %d\n", getpid());
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while(1) {
// 向管道中写入数据
char * str = "hello,i am child";
write(pipefd[1], str, strlen(str));
sleep(5);
}
}
return 0;
}
可以看①Linux简明系统编程(嵌入式公众号的课)—总课时12h中的mmap函数(用在信号量中):
(在当前进程的虚拟地址空间中创建一个新的映射;
如果成功创建了共享映射,就返回此映射区域的指针(地址)void*
;)
内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
修改内存映射区中的内容,内存映射会将修改后的内容同步到磁盘文件;这样如果有多个进程映射的是同一个磁盘文件,这样就可以通过以这个磁盘文件为中介实现进程间的通信(每个进程对映射到自己虚拟地址空间中的内存映射区进行操作即可),有点类似于有名管道中通过mkfifo
创建的那个文件。
使用内存映射实现进程间通信:
1.有关系的进程(父子进程)间通信
还没有子进程的时候,通过唯一的父进程,先创建内存映射区;
有了内存映射区以后,创建子进程;
父子进程共享创建的内存映射区
2.没有关系的进程间通信
准备一个大小不是0的磁盘文件;
进程1 通过磁盘文件创建内存映射区,得到一个操作这块内存的指针;
进程2 通过磁盘文件创建内存映射区,得到一个操作这块内存的指针;
使用内存映射区通信;
注意:内存映射区通信,是非阻塞。
①mmap()函数:
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:将一个文件或者设备的数据映射到内存中
参数:
NULL
, 系统会自动分配一个空间来放这个共享映射stat() 或者 lseek()
0
)。如果要用就必须指定的是4k
的整数倍,0
表示不偏移。返回值:返回创建的内存的首地址;失败返回MAP_FAILED,(void *) -1
②munmap()函数:
#include
int munmap(void *addr, size_t length);
功能:释放内存映射
参数:
匿名映射:不需要文件实体进程一个内存映射
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main() {
// 1.创建匿名内存映射区
int len = 4096;
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);//如果是匿名映射MAP_ANONYMOUS,那么倒数第二个参数fd就写-1
if(ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}
// 父子进程间通信
pid_t pid = fork();
if(pid > 0) {
// 父进程
strcpy((char *) ptr, "hello, world");
wait(NULL);
}else if(pid == 0) {
// 子进程
sleep(1);
printf("%s\n", (char *)ptr);
}
// 释放内存映射区
int ret = munmap(ptr, len);
if(ret == -1) {
perror("munmap");
exit(0);
}
return 0;
}
(共同映射到一个文件,如果这个文件不存在,就会返回错误mmap: Bad file descriptor
)
#include
#include
#include
#include
#include
#include
#include
#include
int main() {
// 1.打开一个文件
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END); // 获取文件的大小
// 2.创建内存映射区
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}
// 3.创建子进程
pid_t pid = fork();
if(pid > 0) {
wait(NULL);
// 父进程:读数据
char buf[64];
strcpy(buf, (char *)ptr);
printf("read data : %s\n", buf);
}else if(pid == 0){
// 子进程:写数据
strcpy((char *)ptr, "nihao a, son!!!");
}
// 关闭内存映射区
munmap(ptr, size);
return 0;
}
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
与管道等要求发送进程 将数据从用户空间的缓冲区复制进内核内存 和 接收进程将数据从内核内存复制进用户空间的缓冲区 的做法相比,这种 IPC 技术的速度更快。
shmget()
创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符;shmat()
来附上(连接attach)共享内存段,即使该段成为调用进程的虚拟内存的一部分;addr
值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。shmdt()
来分离detach共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。shmctl()
来删除control共享内存段。只有当当前所有附加内存段的进程都与之分离后内存段才会销毁。只有一个进程需要执行这一步。头文件:
#include
#include
①shmget()函数
int shmget(key_t key, size_t size, int shmflg);
功能:创建一个新的共享内存段,或者获取一个已有的共享内存段的标识。新创建的内存段中的数据都会被初始化为0
参数:
返回值:
②shmat()函数
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:和当前的进程进行关联
参数:
NULL
,内核指定返回值:
成功:返回共享内存的首(起始)地址。 失败(void *) -1
③shmdt()函数
int shmdt(const void *shmaddr);
功能:解除当前进程和共享内存的关联
参数:shmaddr
,共享内存的首地址
返回值:成功 0, 失败 -1
④shmctl()函数
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。
参数:
⑤ftok()函数
key_t ftok(const char *pathname, int proj_id);
功能:根据指定的路径名,和int值,生成一个共享内存的key
参数:
共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch;
shm_nattach 记录了关联的进程个数。
shmctl
(这块可以看视频课中的02:20:45)
可以的,因为shmctl 标记删除共享内存,不是直接删除;
那么什么时候真正删除呢?
当和共享内存关联的进程数为0的时候,就真正被删除;
当共享内存的key为0的时候,表示共享内存被标记删除了;
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存,也不能进行关联。
(参考mmap映射区和shm共享内存的区别总结)
linux中的两种共享内存:一种是我们的IPC通信System V版本的共享内存shm,另外的一种就是我们今天提到的存储映射I/O(mmap函数);
(一共有三种方式创建共享内存:POSIX共享内存对象
、System V共享内存段
、使用mmap()函数创建的共享映射区
)
总结mmap和shm:
1、mmap是在磁盘上建立一个文件,每个进程的地址空间中开辟出一块空间进行映射;
而对于shm而言,shm每个进程最终会映射到同一块物理内存。shm保存在物理内存,这样读写的速度要比磁盘要快,但是存储量不是特别大。
2、相对于shm来说,mmap更加简单,调用更加方便,所以这也是大家都喜欢用的原因。
3、另外mmap有一个好处是当机器重启,因为mmap把文件保存在磁盘上,这个文件还保存了操作系统同步的映像,所以mmap不会丢失,但是shmget就会丢失。
mmp的中介是磁盘上的一个文件,每个进程在自己的虚拟地址空间中有一个独立的内存,所以涉及到i/O操作,因此读写速度慢,但电脑宕机后文件依然存在;
shm的中介是同一块物理内存,所以读写速度快,但如果电脑重启,创建的共享内存就没了。
(参考链接:点这里)
可以看到内存映射中需要的一个参数是int fd(文件的标识符),可见函数是通过fd将文件内容映射到一个内存空间;
访问共享内存的执行速度比直接访问文件的快N倍(N>>10),这对于要求快速输入输出的场合非常有效;
共享内存主要是为了提高程序的执行速度,方便多个进程进行快速的大数据量的交换;
内存映射是用来加快对文件/设备的访问(如果是大文件,而且还想提高读写速度的话,建议使用内存映射);
共享内存是用来在多个进程间进行快速的大数据量的交换;
可以在程序中指定要将文件内容映射到哪块内存。对于多个进程打开同一个文件,不同的内存映射可以开辟多块内存区域。
内存映射是为了加快对文件/设备的访问速度,不是用来进行数据通信的;
我对内存映射的理解就是通过操作内存来实现对文件的操作,这样可以加快执行速度,因为操作内存比操作文件的速度快多了!
共享内存,顾名思义,就是预留出的内存区域,它允许一组进程对其访问。
共享内存是system vIPC中三种通信机制最快的一种,也是最简单的一种。对于进程来说,
获得共享内存后,他对内存的使用和其他的内存是一样的。由一个进程对共享内存所进行的
操作对其他进程来说都是立即可见的,因为每个进程只需要通过一个指向共享内存空间的指针就可以来读取
共享内存中的内容(说白了就好比申请了一块内存,每个需要的进程都有一个指针指向这个内存)
就可以轻松获得结果。使用共享内存要注意的问题:共享内存不能确保对内存操作的互斥性。
一个进程可以向共享内存中的给定地址写入,而同时另一个进程从相同的地址读出,这将会导致不一致的数据。
因此使用共享内存的进程必须自己保证读操作和写操作的的严格互斥。
可使用锁和原子操作解决这一问题。也可使用信号量保证互斥访问共享内存区域。
共享内存在一些情况下可以代替消息队列,而且共享内存的读/写比使用消息队列要快!
ipcs 用法:
ipcs -a // 打印当前系统中所有的进程间通信方式的信息
ipcs -m // 打印出使用共享内存进行进程间通信的信息
ipcs -q // 打印出使用消息队列进行进程间通信的信息
ipcs -s // 打印出使用信号进行进程间通信的信息
ipcrm 用法:
ipcrm -M shmkey // 移除用shmkey创建的共享内存段
ipcrm -m shmid // 移除用shmid标识的共享内存段
ipcrm -Q msgkey // 移除用msqkey创建的消息队列
ipcrm -q msqid // 移除用msqid标识的消息队列
ipcrm -S semkey // 移除用semkey创建的信号
ipcrm -s semid // 移除用semid标识的信号
这块的示例不好,可以看①Linux简明系统编程(嵌入式公众号的课)—总课时12h中的 ☆☆☆②任务间的通信 之 共享内存 shared memory。
read_shm.c
#include
#include
#include
#include
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;
}
write_shm.c
#include
#include
#include
#include
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 = "helloworld";
// 3.写数据
memcpy(ptr, str, strlen(str) + 1);
printf("按任意键继续\n");
getchar();//按任意键
// 4.解除关联
shmdt(ptr);
// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
分别生成对应的可执行文件,用两个终端分别打开。
(视频课从12:34开始)
(可以参考①Linux简明系统编程(嵌入式公众号的课)—总课时12h中的 第20节课:信号signal)
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
Ctrl+C
通常会给进程发送一个中断信号;alarm
定时器到期将引起 SIGALRM
信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。kill
命令或调用 kill
函数。(前台进程./a.out
; 后台进程 ./a.out &
)
使用信号的两个主要目的是:
信号的特点:
查看系统定义的信号列表:
kill –l
前 31 个信号为常规信号,其余为实时信号。
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
常用的信号:
编号 | 信号名称 | 对应事件 | 默认动作 |
---|---|---|---|
2 | SIGINT interrupt |
当用户按下了 |
终止进程 |
3 | SIGQUIT | 用户按下 |
终止进程 |
9 | SIGKILL | 无条件终止进程。该信号不能被忽略,处理和阻塞 | 终止进程,可以杀死任何进程(除了僵尸进程) |
11 | SIGSEGV | 指示进程进行了无效内存访问(段错误segment fault) | 终止进程,并产生core文件 (视频课中26:45到31:13) |
13 | SIGPIPE | Broken pipe向一个没有读端的管道写数据 | 终止进程 |
14 | SIGALARM | 定时器超时,超时的时间 由系统调用alarm设置 | 终止进程 |
17 | SIGCHLD child |
子进程结束时,父进程会收到这个信号 | 忽略这个信号 |
18 | SIGCONT continue |
如果进程已停止,则使其继续运行 | 继续/忽略 |
19 | SIGSTOP | 停止进程的执行。该信号不能被忽略,处理和阻塞 | 终止进程 |
如果程序产生了段错误(Segment Fault),一般是因为访问了非法内存,那么就可以通过生成core文件的方式来找到程序具体哪里出了问题;
先修改core文件大小;
编译生成可执行程序时加上GDB选项;
然后运行可执行程序时就会生成一个core文件;
通过GDB调试,输入core-file core
,就可以看出来是哪里的代码导致了段错误。
core.c
#include
#include
int main() {
char * buf;
strcpy(buf, "hello");
return 0;
}
查看信号的详细信息:
man 7 signal
信号的 5 种默认处理动作:
信号的几种状态:产生、未决、递达
SIGKILL(kill -9) 和 SIGSTOP(kill -19) 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。
int kill(pid_t pid, int sig);
int raise(int sig);
void abort(void);
unsigned int alarm(unsigned int seconds);
int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);
#include
#include
int kill(pid_t pid, int sig);
功能:给任何的进程或者进程组pid, 发送任何的信号 sig
参数:
kill(getppid(), 9);
和 kill(getpid(), 9);
示例:
#include
#include
#include
#include
int main() {
pid_t pid = fork();
if(pid == 0) {
// 子进程
int i = 0;
for(i = 0; i < 5; i++) {
printf("child process\n");
sleep(1);
}
} else if(pid > 0) {
// 父进程
printf("parent process\n");
sleep(6);
printf("kill child process now\n");
kill(pid, SIGINT);
}
return 0;
}
结果:
child process
parent process
child process
child process
child process
child process
kill child process now
int raise(int sig);
功能:给当前进程发送信号
参数: sig
: 要发送的信号
返回值: 成功 0;失败 非0
相当于是kill(getpid(), sig);
void abort(void);
功能: 发送SIGABRT
信号给当前的进程,杀死当前进程
相当于是kill(getpid(), SIGABRT);
#include
unsigned int alarm(unsigned int seconds);
功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:SIGALARM
,
参数:seconds
倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号),就相当于是取消一个定时器,通过alarm(0)。
返回值:
信号SIGALARM
:默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
例如:
alarm(10); -> 返回0
过了1秒
alarm(5); -> 返回9
alarm(100) -> 该函数是不阻塞的
#include
#include
int main() {
设置一个ding
int seconds = alarm(5);
printf("seconds = %d\n", seconds); // 0 之前没有定时器,所以返回0
sleep(2);
seconds = alarm(2); // 不阻塞
printf("seconds = %d\n", seconds); // 3 之前有定时器,所以返回之前的定时器剩余的时间
while(1) {}//这里虽然是死循环,但是当定时时间到了的时候就进程就会结束
return 0;
}
#include
#include
/*
实际的时间 = 内核时间 + 用户时间 + 消耗的时间
进行文件IO操作的时候比较浪费时间
定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
*/
int main() {
alarm(1);//定时1秒
int i = 0;
while(1) {
printf("%i\n", i++);
}
return 0;
}
由于打印操作很耗费时间,所以结果不准确(才93361
),可以直接将数字存到一个文件中./a.out >> a.txt
(>>
重定向操作),试了一下文件中有500多万行数据,文件大概70多M。
(见linux c setitimer用法说明)
#include
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时。
参数:
ITIMER_REAL
: 真实时间,时间到达,发送 SIGALRM;(常用)ITIMER_VIRTUAL
: 用户时间,时间到达,发送 SIGVTALRM;ITIMER_PROF
: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROFNULL
返回值:成功 0;失败 -1,并设置错误号。
结构体struct itimerval
:
// 定时器的结构体
struct itimerval {
struct timeval it_interval; // 每个阶段的时间,间隔时间
struct timeval it_value; // 延迟多长时间执行定时器
};
struct timeval { // 时间的结构体
time_t tv_sec; // 秒数
suseconds_t tv_usec; // 微秒
};
例如:it_value
设定为10s,it_interval
设定为2s,表示先倒计时10秒,然后循环倒计时2秒。
示例:
#include
#include
#include
#include
void signalHandler(int signo)
{
switch (signo){
case SIGALRM:
printf("Caught the SIGALRM signal!\n");
break;
}
}
//先倒计时5s,然后循环倒计时10s
int main(int argc, char *argv[])
{
//捕捉SIGALRM信号:
signal(SIGALRM, signalHandler);
struct itimerval new_value, old_value;
//延迟的时间,5s之后开始第一次定时
new_value.it_value.tv_sec = 5;
new_value.it_value.tv_usec = 0;
//间隔的时间:每次定时10s
new_value.it_interval.tv_sec = 10;
new_value.it_interval.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, &old_value);
//printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
for(int i = 0; i < 30; +i) {
printf("%d\n", ++i);
sleep(1);
}
return 0;
}
结果:先倒计时5s,然后循环倒计时10s
1
2
3
4
5
Caught the SIGALRM signal!
6
7
8
9
10
11
12
13
14
15
Caught the SIGALRM signal!
16
17
18
19
20
21
22
23
24
25
Caught the SIGALRM signal!
26
27
28
29
30
①信号捕捉函数 — signal()函数:
(见①Linux简明系统编程(嵌入式公众号的课)—总课时12h中的博客1)
#include
typedef void (*sighandler_t)(int);//函数指针
sighandler_t signal(int signum, sighandler_t handler);
功能:设置某个信号的捕捉行为
参数:
SIG_IGN
: 忽略信号SIG_DFL
: 使用信号默认的行为handler
: 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。返回值:
NULL
;注意:SIGKILL
和 SIGSTOP
不能被捕捉,不能被忽略。
示例:
(见上面setitimer()函数中的示例)
②sigaction()函数:
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:检查或者改变信号的处理;信号捕捉
参数:
NULL
返回值: 成功 0;失败 -1
结构体struct sigaction
:
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
#include
#include
#include
#include
void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
printf("xxxxxxx\n");
}
void signalHandler(int signo)
{
switch (signo){
case SIGALRM:
printf("Caught the SIGALRM signal!\n");
break;
}
}
// 过3秒以后,每隔2秒钟定时一次
int main() {
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;//signalHandler;//
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 ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
//printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
//while(1);
for(int i = 0; i < 30; +i) {
printf("%d\n", ++i);
sleep(1);
}
return 0;
}
结果:
1
2
3
捕捉到了信号的编号是:14
xxxxxxx
4
5
捕捉到了信号的编号是:14
xxxxxxx
6
7
捕捉到了信号的编号是:14
xxxxxxx
8
9
捕捉到了信号的编号是:14
xxxxxxx
10
11
捕捉到了信号的编号是:14
xxxxxxx
(视频课从56:50开始到01:02:10)
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t
。
在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” ,另一个称之为 “未决信号集” 。这两个信号集都是内核使用位图机制来实现的,但操作系统不允许我们直接对这两个信号集进行位操作,而需要自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
阻塞信号机 & 未决信号机:
一、对自定义的信号集进行修改:
int sigemptyset(sigset_t *set);//清空临时阻塞信号集
int sigfillset(sigset_t *set);//全部置一
int sigaddset(sigset_t *set, int signum);//将某个信号置一
int sigdelset(sigset_t *set, int signum);//将某个信号清零
int sigismember(const sigset_t *set, int signum);//判断某个信号的状态是否为1
二、对内核中的两个信号集进行修改:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set);
下面分别介绍一和二:
一、对自定义的信号集进行修改
以下信号集相关的函数都是对自定义的信号集进行操作。
①sigemptyset()函数
int sigemptyset(sigset_t *set);
功能:清空信号集中的数据,将信号集中的所有的标志位 置为0
参数:set,传出参数,需要操作的信号集
返回值:成功返回0, 失败返回-1
②sigfillset()函数
int sigfillset(sigset_t *set);
功能:将信号集中的所有的标志位置为1
参数:set,传出参数,需要操作的信号集
返回值:成功返回0, 失败返回-1
③sigaddset()函数
int sigaddset(sigset_t *set, int signum);
功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
参数:
返回值:成功返回0, 失败返回-1
④sigdelset()函数
int sigdelset(sigset_t *set, int signum);
功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
参数:
返回值:成功返回0, 失败返回-1
⑤sigismember()函数
int sigismember(const sigset_t *set, int signum);
功能:判断某个信号是否阻塞,即它对应的标志位是否为1
参数:
返回值:
⑥示例:对自定义的信号集进行修改
#include
#include
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);//Ctrl+c
sigaddset(&set, SIGQUIT);//Ctrl+\
// 判断SIGINT是否在信号集中
ret = sigismember(&set, SIGINT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
// 判断SIGQUIT是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0) {
printf("SIGQUIT 不阻塞\n");
} else if(ret == 1) {
printf("SIGQUIT 阻塞\n");
}
// 从信号集中删除一个信号
sigdelset(&set, SIGQUIT);
// 判断SIGQUIT是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0) {
printf("SIGQUIT 不阻塞\n");
} else if(ret == 1) {
printf("SIGQUIT 阻塞\n");
}
return 0;
}
编译运行:
SIGINT 不阻塞
SIGINT 阻塞
SIGQUIT 阻塞
SIGQUIT 不阻塞
二、对内核中的两个信号集进行修改
①sigprocmask()函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
参数:
SIG_BLOCK
: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变SIG_UNBLOCK
: 根据用户设置的数据,对内核中的数据进行解除阻塞SIG_SETMASK
:覆盖内核中原来的值NULL
返回值:成功:0;失败:-1,设置错误号:EFAULT、EINVAL
②sigpending()函数:
int sigpending(sigset_t *set);
功能:获取内核中的未决信号集
参数:set,传出参数,保存的是内核中的未决信号集中的信息。
③示例:对内核中的两个信号集进行修改
// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号
#include
#include
#include
#include
int main() {
// 设置2、3号信号阻塞
sigset_t set;//自定义信号集
sigemptyset(&set);//全部清零
// 将2号和3号信号添加到信号集中
sigaddset(&set, SIGINT);//Ctrl+c
sigaddset(&set, SIGQUIT);//Ctrl+\
// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
int num = 0;
while(1) {
num++;
// 获取当前的未决信号集的数据
sigset_t pendingset;
sigemptyset(&pendingset);//全部清零
sigpending(&pendingset);//获取内核中的未决信号集中的信息
// 遍历前32位
for(int i = 1; i <= 31; 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) {//循环10次
// 解除阻塞,就只执行信号,而执行信号的结果就是结束进程
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 0;
}
SIGCHLD信号产生的条件:
SIGSTOP
信号时变为停止态;SIGCONT
后被唤醒时;以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号。
☆☆☆使用SIGCHLD信号解决僵尸进程的问题:
示例:
父进程会执行一个while死循环,而子进程输出自己的pid就结束了,这样子进程会变成僵尸进程,无法通过kill -9
指令杀死,只能通过杀死父进程或者让父进程循环
调用wait()函数或者waitpid()函数来回收子进程。
但是我们希望有更好的解决方法:父进程可以执行它自己的内容,而不用专门等待子进程结束然后对其进行回收,我们可以通过信号捕捉的方式,当有子进程结束时会向父进程发送SIGCHLD
信号,捕捉到这个信号时对其进行回收。
#include
#include
#include
#include
#include
#include
void myFun(int num) {
printf("捕捉到的信号 :%d\n", num);
// 回收子进程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 < 8; 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;
}
结果:
child process pid : 2928710
child process pid : 2928711
child process pid : 2928712
child process pid : 2928713
捕捉到的信号 :17
child die , pid = 2928710
child die , pid = 2928711
child die , pid = 2928712
child die , pid = 2928713
child process pid : 2928714
child die , pid = 2928714
捕捉到的信号 :17
parent process pid : 2928701
parent process pid : 2928701
parent process pid : 2928701
parent process pid : 2928701
见Linux高并发服务器开发—笔记3