文章目录
前言
在文章【嵌入式Linux】<总览> 多进程中已经介绍了进程的相关概念和创建多个进程的方法。本篇聚焦于进程间通信的方式,若涉及版权问题请联系本人删除。
一、管道
1. 概念
- 管道是进程间通信的方式之一,其数据的流动是单向的。
- 管道本质上是内核中的一块内存,即内存缓冲区。这块内存中的数据存储在环形队列中,其默认空间为4K。这个环形队列的队头就是读指针,队尾就是写指针。
- 由于管道是内核中的内存,需要使用系统调用的文件IO函数read和write函数来读写。每次读完后,数据相当于出队了!读写默认是阻塞:①读管道:管道中没有数据就会阻塞,直到有数据到来;②写管道:管道空间满了就会阻塞,直到有数据出队。通过fcntl函数能修改为非阻塞。
- 管道是独立于任何进程的,并且充当了两个进程用于数据通信的载体,只要两个进程能够得到同一个管道的入口和出口(读端和写端的文件描述符),那么他们之间就可以通过管道进行数据的交互。
- 当读端关闭了,管道破裂,写端进程直接退出;当写端关闭了,读端就会将管道中剩余内容读取完,最后返回0。
2. 匿名管道
【1】介绍:匿名管道是管道的一种,其没有具体的名字,只能实现拥有血缘关系的进程进行通信。
【2】创建匿名管道:pipe函数
#include <unistd.h>
int pipe(int pipefd[2]);
//参数说明:pipefd是传出的参数
//pipefd[0]对应读端的文件描述符
//pipefd[1]对应写端的文件描述符
//返回值:0表示成功,-1表示失败
【3】进程通信的注意事项:在创建子进程之后,父进程中的读端和写端文件描述符都会被复制到子进程中。若子进程只有写的需求,那么可以将读端的文件描述符给close。若父进程只有读的需求,那么可以将写端的文件描述符给close。
程序示例:子进程执行"ps"命令,并将结果写入管道中;父进程读管道,将结果显示在终端上。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <sys/fcntl.h> #include <string.h> int main(int argc, char **argv) { /* 1.创建匿名管道 */ int fd[2]; int pipeRet = pipe(fd); if (pipeRet < 0) { perror("创建管道失败"); return -1; } /* 2.创建子进程 */ pid_t pid = fork(); if (pid < 0) { perror("创建子进程失败"); return -1; } /* 3.子进程操作 */ if (pid == 0) { close(fd[0]);//关闭管道的读端 dup2(fd[1], STDOUT_FILENO);//重定向标准输出 execlp("ps", "ps", NULL);//执行ps程序 perror("execlp错误"); } /* 4.父进程操作 */ if (pid > 0) { close(fd[1]);//关闭管道的写端 //循环读取管道中的数据 char buf[4096]; while (1) { memset(buf, 0, sizeof(buf));//清空缓存 int len = read(fd[0], buf, sizeof(buf));//读取管道 if (len <= 0) {//异常或读完 break; } printf("%s", buf);//输出内容至终端 } close(fd[0]); //等待子进程 wait(NULL); } return 0; }
3. 有名管道
【1】介绍:有名管道在磁盘上有实体文件,文件类型为p(不存储真实数据)。
- 有名管道可以称为fifo.
- 有名管道的磁盘大小永远是0,因为有名管道依旧是将数据存储到内存缓冲区。打开这个磁盘文件,就能获取对应的文件描述符。
- 任意两个进程都能利用有名管道来进行通信。
【2】创建有名管道:
- 方式一:终端命令"mkfifo 有名管道名字"
- 方式二:调用函数mkfifo,其细节如下:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
//参数说明:
//pathname: 保存的文件名
//mode: 创建的管道文件权限
//返回值:成功返回0,失败返回-1
【3】进程间通信注意事项:有名管道的读写首先需要调用open函数来打开管道文件,若管道文件中只有读端或只有写端被打开,那么就会阻塞在open函数,直到两端都被打开。
程序示例:写两个程序,一个作为写管道程序,一个作为读管道程序。①写管道:创建有名管道文件,打开该文件,向有名管道中写入数据,关闭文件。②读管道:打开有名管道文件,读取数据,并显示到终端上,关闭文件。
写管道程序:
#include <stdio.h> #include <sys/types.h> #include <sys/fcntl.h> #include <unistd.h> #include <sys/stat.h> //写管道程序 int main(int argc, char **argv) { /* 1.创建有名管道 */ int mkRet = mkfifo("./pipefile", 0664); if (mkRet < 0) { perror("创建有名管道失败"); return -1; } /* 2.打开有名管道 */ int fd = open("./pipefile", O_WRONLY); if (fd < 0) { perror("管道文件打开失败"); return -1; } /* 3.循环写入数据 */ for (int i = 0; i < 5; ++i) { char writeBuf[200] = {0}; sprintf(writeBuf, "Hello, Can! 我在努力学习中... %d\n", i); int writeRet = write(fd, writeBuf, sizeof(writeBuf)); if (writeRet < sizeof(writeBuf)) { printf("写入错误"); break; } sleep(1);//保证能够写入 } /* 4.关闭管道文件 */ close(fd); return 0; }
读管道程序:
#include <stdio.h> #include <sys/types.h> #include <sys/fcntl.h> #include <unistd.h> //读管道程序 int main(int argc, char **argv) { /* 1.打开有名管道 */ int fd = open("./pipefile", O_RDONLY); if (fd < 0) { perror("管道文件打开失败"); return -1; } /* 2.循环读取数据并打印 */ while (1) { char readBuf[1024] = {0}; int readRet = read(fd, readBuf, sizeof(readBuf)); if (readRet <= 0) {//异常或读完 break; } printf("%s", readBuf);//打印读取的内容 } /* 3.关闭管道文件 */ close(fd); return 0; }
二、内存映射区
1. 概念
- 通过mmap函数创建内存映射区是实现进程间通信的方法之一。
- 内存映射区位于每个进程的用户区(用于加载动态库的那个区域)。
- 内存映射区的读写是非阻塞的。
- 内存映射区创建成功之后,得到映射区内存的起始地址,使用内存操作函数读写数据。
- 机制:多个进程将内存映射区和同一个磁盘文件进行映射。当其中一个进程修改了它的内存映射区,数据会自动同步到磁盘文件。同时,和该磁盘文件建立映射关系的进程中的内存映射区的数据也会实时变化,因此能够实现进程间的通信。
- 注意:内存映射区使用完后通过munmap函数释放。
#include <sys/mman.h>
int munmap(void *addr, size_t length)
//参数说明:
//addr: 内存映射区的起始地址,就是mmap函数的返回值
//length: 内存映射区的大小,与mmap函数的第二个参数相同
//返回值:成功返回0,失败返回-1
2. 创建内存映射区---mmap函数
【1】头文件:#include <sys/mman.h>
【2】函数原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
【3】参数说明:
- addr:从动态库加载区的什么位置开始创建内存映射区,一般为NULL来委托内核分配
- length:创建的内存映射区的大小(单位:字节),实际大小是按4096的整数倍去分配
- prot:对内存映射区的操作权限
- PROT_READ: 读内存映射区
- PROT_WRITE: 写内存映射区
- PROT_READ | PROT_WRITE:读写内存映射区
- flags:用于确定是否共享内存映射区
- MAP_SHARED: 多个进程可以共享数据,进行映射区的数据同步
- MAP_PRIVATE: 映射区数据是私有的,不能同步给其他进程
- fd:对应打开的磁盘文件的文件描述符。内存映射区通过此与硬盘文件建立联系。
- offset:要求>=0并且是4096的倍数,表示硬盘文件从偏移到的位置进行映射。
【4】返回值:成功返回内存映射区的首地址,失败返回MAP_FAILED其实就是(void *) -1
【5】注意事项:
- length必须要大于0。
- prot一般都是PROT_READ | PROT_WRITE。
- 内存映射区创建成功之后, 关闭参数中的文件描述符fd不会影响进程间通信。
3. 进程间通信(有血缘关系)
父进程fork子进程,子进程就会将父进程的虚拟地址空间进行复制。因此,对于有血缘关系的进程来说,通过内存映射区的方式来进行通信是简单的。
【程序实例】如下代码,父进程创建子进程,父进程向内存映射区中写数据,子进程从内存映射区中读数据。注意:磁盘文件的大小不能为0,否则会报错误:Bus error (core dumped)。
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
/* 1.打开磁盘文件 */
int fd = open("./test.txt", O_RDWR);
if (fd < 0) {
perror("磁盘文件打开错误");
return -1;
}
/* 2.创建内存映射区 */
void * ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap失败");
close(fd);
return -1;
}
/* 3.创建子进程,复制父进程的内存映射区 */
pid_t pid = fork();
if (pid < 0) {
perror("fork失败");
close(fd);
return -1;
}
/* 4.父进程:写数据到内存映射区 */
if (pid > 0) {
const char *content = "Hello, Can!";
memcpy(ptr, content, strlen(content)+1);
wait(NULL);
}
/* 5.子进程:从内存映射区中读数据 */
if (pid == 0) {
sleep(1);//由于读写非阻塞,这里保证父进程先行
printf("从内存映射区读取的数据:%s\n", (char*)ptr);
}
/* 6.关闭硬盘文件,释放内存映射区 */
close(fd);
munmap(ptr, 4096);
return 0;
}
4. 进程间通信(没有血缘关系)
进程间没有血缘关系,需要各自创建内存映射区,并且对应的磁盘文件必须是同一个。
【程序实例】两个进程打开同一个磁盘文件,然后各自构建内存映射区。一个进程向内存映射区写数据,另一个进程读数据。
写数据的进程代码:
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
//写数据的进程
int main(int argc, char **argv)
{
/* 1.打开磁盘文件 */
int fd = open("./test.txt", O_RDWR);
if (fd < 0) {
perror("磁盘文件打开错误");
return -1;
}
/* 2.创建内存映射区 */
void * ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap失败");
close(fd);
return -1;
}
/* 3.向内存映射区中写数据 */
const char *content = "------Hello, Can!------";
memcpy(ptr, content, strlen(content)+1);
sleep(1);
/* 4.关闭硬盘文件,释放内存映射区 */
close(fd);
munmap(ptr, 4096);
return 0;
}
读数据的进程代码:
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
//读数据的进程
int main(int argc, char **argv)
{
/* 1.打开磁盘文件 */
int fd = open("./test.txt", O_RDWR);
if (fd < 0) {
perror("磁盘文件打开错误");
return -1;
}
/* 2.创建内存映射区 */
void * ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap失败");
close(fd);
return -1;
}
/* 3.从内存映射区中读数据 */
printf("从内存映射区中读数据:%s\n", (char*)ptr);
/* 4.关闭硬盘文件,释放内存映射区 */
close(fd);
munmap(ptr, 4096);
return 0;
}
5. 拷贝文件
鉴于内存映射区和磁盘文件的同步关系,可以用来拷贝文件,其流程如下:
①打开原文件,计算原文件大小size,并映射到内存映射区A。
②打开目标文件,拓展大小为size,并映射到内存映射区B。
③拷贝内存映射区A的内容到内存映射区B。
④关闭所有文件,释放所有内存映射区。
注意:mmap中还是需要MAP_SHARED,否则vim和cat都无法识别内容。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
int main(int argc, char **argv)
{
/* 1.打开原文件,并映射到内存映射区A */
int fd1 = open("./test.txt", O_RDWR);
if (fd1 < 0) {
perror("打开原文件失败");
return -1;
}
void *ptrA = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
if (ptrA == MAP_FAILED) {
perror("内存映射区A构建失败");
return -1;
}
int size = lseek(fd1, 0, SEEK_END);//原文件大小
/* 2.打开目标文件,拓展大小,并映射到内存映射区B */
int fd2 = open("./output.txt", O_RDWR | O_CREAT, 0664);
if (fd2 < 0) {
perror("打开输出文件失败");
return -1;
}
ftruncate(fd2, size);//将输出文件拓展到size大小
void *ptrB = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd2, 0);
if (ptrB == MAP_FAILED) {
perror("内存映射区B构建失败");
return -1;
}
/* 3.拷贝A的空间到B */
memcpy(ptrB, ptrA, size);
/* 4.关闭文件,释放所有内存映射区 */
munmap(ptrA, 4096);
munmap(ptrB, 4096);
close(fd1);
close(fd2);
return 0;
}
三、共享内存
1. 概念
- 是进程间通信的方式之一,且效率最高。
- 共享内存不属于任何进程,不受进程的生命周期影响。
- 进程需要和共享内存进行关联,得到共享内存的起始地址后就能对其进行读写。
- 进程关联之后仍然可以选择解除关联。
- 共享内存操作默认不阻塞,因此需要借助额外的机制来保证数据同步,例如信号量。
2. 创建/打开共享内存---shmget函数
【1】功能:创建或者打开共享内存。若不存在则创建,存在了就打开。
【2】头文件:#include <sys/ipc.h>、#include <sys/shm.h>
【3】函数原型:int shmget(key_t key, size_t size, int shmflg);
【4】参数说明:
- key:必须>0,通过key可以创建或打开一块共享内存。
- size:在创建时指定共享内存的大小,而在打开时是没有意义的。
- shmflg:创建共享内存时指定的属性
- IPC_CREAT:创建新的共享内存,指定操作权限。例如:IPC_CREAT | 0664
- IPC_EXCL:检测共享内存是否存在,需要和IPC_CREAT一起使用
【5】返回值:成功返回共享内存的唯一ID,失败返回-1.
【6】使用示例:
//创建一块大小为4k的共享内存
shmget(100, 4096, IPC_CREAT | 0664);
//创建一块大小为4k的共享内存,并检测是否存在
shmget(100, 4096, IPC_CREAT | 0664 | IPC_EXCL);
【7】ftok函数创建key:
#include <sys/types.h>
#include <sys/ipc.h>
//两个参数作为种子, 生成一个key_t类型的数值
key_t ftok(const char *pathname, int proj_id);
//参数说明:
pathname: 当前已存在的路径
proj_id: 只用到了int中的一个字节,传参的时候要将其作为char进行操作,取值范围: 1-255
//返回值:成功返回key,失败返回-1
//使用举例:
key_t key = ftok("./", 'a');
shmget(key, 4096, IPC_CREAT | 0664 | IPC_EXCL);
3. 关联与解除关联
3.1 shmat函数:
【1】功能:将共享内存和进程进行关联。只有关联了才能得到共享内存的起始地址。
【2】头文件:#include <sys/types.h>、#include <sys/shm.h>
【3】函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
【4】参数说明:
- shmid: 共享内存的id,是shmget函数的返回值。
- shmaddr:共享内存的起始地址,一般写NULL委托内核指定。
- shmflg:和共享内存关联的对共享内存的操作权限。
- 0:读写权限
- SHM_RDONLY:只读权限
【5】返回值:关联成功返回共享内存的起始地址,失败返回(void *)-1
3.2 shmdt函数:
【1】功能:将共享内存和进程解除关联。注意:进程退出后,结束的进程和共享内存的关联也就自动解除了。
【2】头文件:#include <sys/types.h>、#include <sys/shm.h>
【3】函数原型:int shmdt(const void *shmaddr);
【4】参数说明:shmaddr是共享内存的起始地址,也就是shmat函数的返回值。
【5】返回值:成功返回0,失败返回-1.
4. 删除共享内存---shmctl函数
【1】功能:设置、获取共享内存的状态也可以将共享内存标记为删除状态。由于只是标记为删除状态,多次调用没问题。当共享内存被标记为删除状态时,并不会马上删除共享内存,而是在所有进程和共享内存解除关联后才删除。同时,共享内存内部的key会变为0。通过"ipcs -m"可以查看系统中所有的共享内存信息。
【2】头文件:#include <sys/ipc.h>、#include <sys/shm.h>
【3】函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
【4】参数说明:
- shmid:共享内存的id。
- cmd:要执行的操作。
- IPC_RMID:标记为删除状态。
- IPC_STAT:获得当前共享内存的状态。
- IPC_SET:设置共享内存的状态。
- buf:根据cmd不同而不同。
- 当cmd为IPC_RMID时,buf没有意义,指定为NULL。
- 当cmd为IPC_STAT时,作为传出参数,将共享内存的信息存储于此。
- 当cmd为IPC_SET时,要设置的共享内存状态。
【5】struct shmid_ds结构体:
//参考:爱编程的大丙
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
// 引用计数, 多少个进程和共享内存进行了关联
shmatt_t shm_nattch; /* 记录了有多少个进程和当前共享内存进行了关联 */
...
};
【6】返回值:成功返回>=0,失败返回-1.
5. 进程间通信
【1】进程间通信步骤如下:
①调用shmget函数创建/打开共享内存。
②调用shmat函数将共享内存与进程关联。
③调用printf函数或memcpy函数对共享内存数据进行读写。
④调用shmdt函数将共享内存与进程解除关联。
⑤调用shmctl函数删除共享内存。
【2】案例演示:有一个写进程向共享内存中写数据,有一个读进程从共享内存中读数据。
写进程的程序:
#include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <string.h> //写进程的程序 int main(int argc, char **argv) { /* 1.创建共享内存 */ int shmID = shmget(100, 4096, IPC_CREAT | 0664);//共享内存id if (shmID < 0) { perror("shmget失败"); return -1; } /* 2.进程关联共享内存 */ void *ptr = shmat(shmID, NULL, 0);//共享内存起始地址 if (ptr == (void *)-1) { perror("shmat失败"); return -1; } /* 3.写数据 */ const char *content = "Hello, 灿姐!"; memcpy(ptr, content, strlen(content)+1); /* 4.阻塞程序,便于调试 */ printf("请按任意键继续\n"); getchar(); /* 5.解除关联, 删除共享内存 */ shmdt(ptr); shmctl(shmID, IPC_RMID, NULL); printf("共享内存已经被删除!\n"); return 0; }
读进程的程序:
#include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <string.h> //读进程的程序 int main(int argc, char **argv) { /* 1.打开共享内存 */ int shmID = shmget(100, 0, 0);//共享内存id if (shmID < 0) { perror("shmget失败"); return -1; } /* 2.进程关联共享内存 */ void *ptr = shmat(shmID, NULL, 0);//共享内存起始地址 if (ptr == (void *)-1) { perror("shmat失败"); return -1; } /* 3.读数据 */ printf("从共享内存中读取的数据:%s\n", (char*)ptr); /* 4.阻塞程序,便于调试 */ printf("请按任意键继续\n"); getchar(); /* 5.解除关联, 删除共享内存 */ shmdt(ptr); shmctl(shmID, IPC_RMID, NULL); printf("共享内存已经被删除!\n"); return 0; }
四、信号
1. 信号概述
1.1 信号的概念
- Linux中的信号是一种消息处理机制。本质上是一个整数,不同的信号对应不同的值。
- 虽然信号结构简单不能携带很大的信息量,但是信号在系统中的优先级非常高。
- 信号产生例子:①按下Ctrl+C;②执行了kill命令;③调用了sleep函数等等。
- 信号能够实现进程间通信,但是不建议采用信号进行通信。原因:①信号传递的消息很少,不能满足大部分需求;②它对应的处理动作是通过回调完成的,会打乱程序原有流程。
1.2 信号的编号
如下图所示,执行kill -l能够显示所有的信号及对应的数字。每个信号对应的含义见信号 | 爱编程的大丙 (subingwen.cn)。
1.3 信号的信息
【1】通过man 7 signal可以查看信号的详细信息。
【2】信号的五种处理动作:
- Term:信号终止进程。
- Ign:信号产生之后被忽略。
- Core:信号终止进程,并生成core文件用于gdb调试。
- Stop:信号暂停进程。
- Cont:信号让暂停的进程继续运行。
【3】常用信号:
- 9号信号:无条件杀死进程。
- 19号信号:无条件暂停进程。
- 注意:9号和19号信号无法被捕捉、阻塞和忽略。
1.4 信号的状态
Linux中信号有三种状态:产生、未决、递达。
- 产生:键盘输入Ctrl+C、调用函数、执行kill命令、对硬件进行非法访问都会产生信号。
- 未决:信号产生后,但是还没有被处理。
- 递达:信号被某个进程处理了。
2. 信号相关函数
2.1 kill函数
【1】功能:给指定进程或进程组发送指定信号。
【2】头文件:#include <signal.h>、#include <sys/types.h>
【3】函数原型:int kill(pid_t pid, int sig);
【4】参数说明:pid为进程ID,sig为信号。
【5】返回值:成功返回0,失败返回-1.
【6】举例:kill(getpid(), 9); 杀死当前进程。
2.2 raise函数
【1】功能:给当前进程发送指定信号。
【2】头文件:#include <signal.h>
【3】函数原型:int raise(int sig);
【4】参数说明:sig为信号。
【5】返回值:成功返回0,失败返回-1.
2.3 abort函数
【1】功能:给当前进程发送固定信号SIGABRT,杀死当前进程。
【2】头文件:#include <stdlib.h>
【3】函数原型:void abort(void);
2.4 alarm函数
【1】功能:单次定时,定时完成后发出一个信号SIGALRM,中断当前进程。
【2】头文件:#include <unistd.h>
【3】函数原型:unsigned int alarm(unsigned int seconds);
【4】参数说明:seconds为定时的秒数。
【5】返回值:大于0表示倒计时还剩多少秒;0表示倒计时完成,信号被发出。
程序实例:定时1s,查看1s内能数多少个数。
#include <stdio.h> #include <unistd.h> int main(int argc, char **argv) { //定时1秒,当时间到时会中断该进程 alarm(1); for(int i = 0; 1; ++i) { printf("%d\n", i); } return 0; }
2.5 setitimer函数
【1】功能:周期式定时,每次触发都会发出信号。
【2】头文件:#include <sys/time.h>
【3】函数原型:int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
【4】参数说明:
- which:定时器使用什么计时法则
- ITIMER_REAL: (最常用)计算消耗的所有的时间=用户区+内核+程序消耗,发出信号SIGALRM。
- ITIMER_VIRTUAL: 只计算程序在用户区执行的时间,发出信号SIGVTALRM。
- ITIMER_PROF: 只计算内核运行的时间,发出信号SIGPROF。
- new_value:传入参数,给定时器设置的定时信息。
- old_value:传出参数,获得上一次给定时器设置的定时信息。若不需要则为NULL。
【5】struct itimerval 结构体:
struct itimerval {
struct timeval it_interval; /* 时间间隔 */
struct timeval it_value; /* 第一次触发定时器的时长 */
};
【6】struct timeval 结构体:
//表示时间=tv_sec+tv_usec
struct timeval {
time_t tv_sec; /* 秒 */
suseconds_t tv_usec; /* 微秒 */
};
【7】返回值:成功返回0,失败返回-1.
程序实例:设置周期性定时器,并且将定时器时间到产生的信号捕捉。
#include <stdio.h> #include <unistd.h> #include <sys/time.h> #include <signal.h> /* function of capturing the signal*/ void timeout(int arg) { static int count = 0; printf("signal: %d, count = %d\n", arg, ++count); } int main(int argc, char **argv) { /* set the periodic timer */ struct itimerval timer; timer.it_interval.tv_sec = 1; timer.it_interval.tv_usec = 0; timer.it_value.tv_sec = 3; timer.it_value.tv_usec = 0; setitimer(ITIMER_REAL, &timer, NULL); /* capture the signal: SIGALRM */ signal(SIGALRM, timeout); while (1) { sleep(10); } return 0; }
3. 信号集
3.1 概念
- 信号集就是信号的集合。
- 两个重要信号集:阻塞信号集、未决信号集。这两个信号集在内核中是两张表,但用户无法直接操作,而是需要自定义一个集合通过相关函数来修改。
- 内核中的结构:它们都是整型数组int [32],一共1024个标志位。前31个标志位每个都对应一个标准信号,通过标志位来标记信号的状态。
- 在阻塞信号集中:标志位为0表示信号未阻塞,标志位为1表示信号阻塞。
- 在未决信号集中:标志位为0表示信号解除阻塞并且马上要被处理,标志位为1表示信号阻塞。
3.2 信号集函数
【1】sigprocmask函数:读/写阻塞信号集
①头文件:#include <signal.h>
②函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
③参数说明:
- how:相关读写操作指令
SIG_BLOCK: 将set集合中的数据追加到阻塞信号集中
SIG_UNBLOCK: 将set集合中的信号在阻塞信号集中解除阻塞
SIG_SETMASK: 使用set集合中的数据覆盖内核的阻塞信号集的数据
set:传入的参数,用户自定义的信号集。
oldset:传出的参数,将设置之前的阻塞信号集数据传出,不需要时指定为NULL。
④返回值:成功返回0,失败返回-1.
【2】初始化sigset_t 类型的函数:
//来源:爱编程的大丙
#include <signal.h>
// 如果在程序中读写 sigset_t 类型的变量
// 阻塞信号集和未决信号集都存储在 sigset_t 类型的变量中, 这个变量对应一块内存
// 阻塞信号集和未决信号集, 对应的内存中有1024bit = 128字节
// 将set集合中所有的标志位设置为0
int sigemptyset(sigset_t *set);
// 将set集合中所有的标志位设置为1
int sigfillset(sigset_t *set);
// 将set集合中某一个信号(signum)对应的标志位设置为1
int sigaddset(sigset_t *set, int signum);
// 将set集合中某一个信号(signum)对应的标志位设置为0
int sigdelset(sigset_t *set, int signum);
// 判断某个信号在集合中对应的标志位到底是0还是1, 如果是0返回0, 如果是1返回1
int sigismember(const sigset_t *set, int signum);
【3】sigpending函数:读未决信号集
①头文件:#include <signal.h>
②函数原型:int sigpending(sigset_t *set);
③参数说明:set为传出参数,是内核中未决信号集的拷贝。
④返回值:成功返回0,失败返回-1。
程序实例:在阻塞信号集中设置2号和3号信号阻塞,通过"Ctrl+C"产生2号信号,通过"Ctrl+\"产生3号信号,然后读未决信号集, 最后再解除这些信号的阻塞。
#include <stdio.h> #include <signal.h> #include <unistd.h> int main(int argc, char **argv) { /* 1.初始化信号集 */ sigset_t myset; sigemptyset(&myset);//标志位全0,不阻塞 /* 2.设置指定信号阻塞 */ sigaddset(&myset, SIGINT); //2号 sigaddset(&myset, SIGQUIT); //3号 /* 3.将自定义信号集设置给内核 */ sigset_t old; sigprocmask(SIG_BLOCK, &myset, &old); /* 4.死循环,用于观察现象 */ int i = 0; while(1) { sleep(1); printf("%d\n", i++); //内核恢复原始的信号集后, //Ctrl+C和Ctrl+\能够终止进程 if (i == 5) { sigprocmask(SIG_SETMASK, &old, NULL); } } return 0; }
4. 信号捕捉
4.1 概念
Linux中信号有默认的处理行为。若想修改该行为,就要捕捉指定的信号,并告诉应用程序执行什么样的行为。
4.2 signal函数
【1】功能:捕捉产生的信号,并且修改相关行为。
【2】头文件:#include <signal.h>
【3】函数原型:sighandler_t signal(int signum, sighandler_t handler);
【4】参数说明:
- signum: 需要捕捉的信号
- handler: 捕捉后的信号处理函数,这是一个函数指针,函数原型为typedef void (*sighandler_t)(int);
【5】返回值:成功返回信号处理函数先前的值,失败返回SIG_ERR。
signal函数的程序实例:调用signal函数来捕捉定时器产生的信号 SIGALRM
#include <stdio.h> #include <signal.h> #include <sys/time.h> #include <unistd.h> void doing(int arg) { printf("捕捉的信号:%d\n", arg); } int main(int argc, char **argv) { //捕捉SIGALRM信号,执行doing函数 signal(SIGALRM, doing); //设置定时器: 第一次3秒后触发,后面每隔1s触发 struct itimerval timer; timer.it_interval.tv_sec = 1; timer.it_interval.tv_usec = 0; timer.it_value.tv_sec = 3; timer.it_value.tv_usec = 0; setitimer(ITIMER_REAL, &timer, NULL); //进程自身不结束,等待信号 while (1) { sleep(1000000); } return 0; }
4.3 sigaction函数
【1】功能:是signal函数的升级版。
【2】头文件:#include <signal.h>
【3】函数原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
【4】参数说明:
- signum: 需要捕捉的信号
- act: 包含捕捉后的信号处理函数的结构指针
- oldact: 上一次调用该函数进行信号捕捉设置的信号处理动作,一般指定为NULL
【5】struct sigaction结构体:
//参考:爱编程的大丙
struct sigaction {
void (*sa_handler)(int); // 指向一个函数(回调函数)
void (*sa_sigaction)(int, siginfo_t *, void *); //一般不用
sigset_t sa_mask; // 初始化为空即可, 处理函数执行期间不屏蔽任何信号
int sa_flags; // 0,捕捉信号执行sa_handler
void (*sa_restorer)(void); //不用
};
【6】返回值:成功返回0,失败返回-1。
程序示例:调用sigaction函数捕捉2号和3号信号。先让2号和3号加入阻塞信号集,然后i增加到5的时候再修改其为非阻塞,它们才会去执行处理函数。
#include <stdio.h> #include <signal.h> #include <unistd.h> //信号处理函数 void doing(int arg) { printf("捕捉的信号:%d\n", arg); } int main(int argc, char **argv) { /* 1.初始化自定义信号集 */ sigset_t myset; sigemptyset(&myset);//全部非阻塞 /* 2.设置信号为阻塞 */ sigaddset(&myset, SIGINT); //2号 sigaddset(&myset, SIGQUIT); //3号 /* 3.将自定义信号集写入到内核 */ sigset_t old; sigprocmask(SIG_BLOCK, &myset, &old); /* 4.捕捉2号和3号信号 */ struct sigaction sa; sa.sa_flags = 0; sa.sa_handler = doing; sigemptyset(&sa.sa_mask); sigaction(SIGINT, &sa, NULL); sigaction(SIGQUIT, &sa, NULL); /* 5.死循环,等待信号到来 */ int i = 0; while (1) { sleep(1); if (++i == 5) {//恢复原始的阻塞信号集 sigprocmask(SIG_SETMASK, &old, NULL); } } return 0; }
5. SIGCHLD信号
当子进程退出、暂停、从暂停恢复运行时,在子进程中会产生一个SIGCHLD信号,并将其发送给父进程。默认处理方式是忽视的,但我们可以通过signal函数来捕捉,然后调用waitpid函数来释放子进程。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//信号处理函数:回收子进程资源
void recycle(int num) {
//由于SIGCHLD信号不会排队,因此采用waitpid非阻塞
//多个子进程同时退出, 父进程同时收到了多个SIGCHLD信号
//若不用循环,则会使得多个子进程成为僵尸进程
while (1) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid == 0) {//子进程还在运行,退出循环捕捉其它
break;
} else if (pid < 0) {//所有子进程已经释放完毕
printf("所有子进程释放完毕\n");
break;
} else {//释放了某个子进程
printf("释放的子进程PID: %d\n", pid);
}
}
}
//主函数
int main(int argc, char **argv)
{
/* 1.捕捉SIGCHLD信号,执行recycle */
signal(SIGCHLD, recycle);
/* 2.创建多个子进程,形成进程扇 */
pid_t pid = getpid();//默认为父进程
for (int i = 0; i < 3; ++i) {
pid = fork();
if (pid == 0) {
break;
}
}
/* 3.子进程执行 */
if (pid == 0) {
printf("我是子进程PID: %d, PPID: %d\n", getpid(), getppid());
}
/* 4.父进程死循环等待捕捉信号 */
if (pid > 0) {
while (1) {
sleep(1);
}
}
return 0;
}