8.内存映射
8.1 内存映射相关定义
创建一个文件,将保存在磁盘中的文件映射到内存中,后期两个进程之间对内存中的数据进行操作,大大减少了访问磁盘的时间,也是一种最快的 IPC ,因为进程之间可以直接对内存进行存取
8.2 内存映射相关的系统调用
代码地址:lesson25
8.2.1 mmap & munmap 函数–内存的映射与释放
1.API
mmap : 将一个文件映射到内存中
这个函数返回的是内存映射出来的内存首地址
munmap : 解除一个文件和内存的映射
内存映射和管道不一样,内存映射不会发生阻塞
2.代码
通过内存映射的方法实现两个关系进程之间的通信
①导入相应的库文件
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>
②将一个文件映射到内存区
int fd = open("test.txt",O_RDWR);
int size = lseek(fd,0,SEEK_END); // 获取文件大小:从index0 到 index_end
// 创建映射
void* ptr = mmap(NULL,size,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0); // prot :内存映射区域的权限,一般读写都有;flag:本地文件是否和盘文件同步
if(ptr==MAP_FAILED){
perror("mmap");
exit(0);
}
③父进程:等待子进程写入数据并读出来;子进程:将数据写入到内存中
// 创建子进程
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,"xuboluo");
}
④关闭映射
munmap(ptr,size);
3.代码演示
最后可以看到这里在对内存映射时文件中的文字是进行覆盖的
8.2.2 mmap & munmap 内存的映射与释放 中常见问题
可以对其 ++ 操作更改内存映射的开始地址,这样在写文件时就可以从指定的位置开始写
但是在进行释放内存时要传入正确的释放地址,也就是一开始系统给分配的地址
映射的权限要小于等于文件 open 时的权限
3.
文件偏移量的作用就是:指明从文件的哪个地方开始映射,如下图:
一般页面大小为 4K ,虚拟内存空间的大小也为 4K
映射的文件大小必须要 >0 ,这样在分配虚拟内存时才能保证能分配到空间。可以使用相应的方法扩展内存
创建映射区时会对文件描述符进行一个拷贝,即使文件被关闭也不会有影响
8.2.3 使用内存映射实现文件拷贝
代码保存在 lesson25 copy.c 中
1.基本思路
现在硬盘中有两个文件 A 和 B ,A 文件和 B 文件分别映射到内存当中,并获得相应的 map_ptr ,两个 ptr 之间进行通讯
2.代码实现
#include <string.h>
#include <stdlib.h>
int main(){
// 1.得到两个文件的文件描述符
int fd_a = open("a.txt",O_RDWR);
int fd_b = open("b.txt",O_RDWR | O_CREAT,0664);
// 2.得到文件大小
int len = lseek(fd_a,0,SEEK_END);
// 对新建的文件进行扩展
truncate("b.txt",len);
write(fd_b," ",1);
// 3.得到文件映射
int* ptr_a = mmap(NULL,len,PROT_WRITE | PROT_READ,MAP_SHARED,fd_a,0);
int* ptr_b = mmap(NULL,len,PROT_WRITE | PROT_READ,MAP_SHARED,fd_b,0);
//4.内存拷贝
memcpy(ptr_b,ptr_a,len); // 目标文件,源文件,长度
// 5.释放资源
munmap(ptr_a,len);
munmap(ptr_b,len);
// 6.关闭两个文件流
close(fd_a);
close(fd_b);
return 0;
}
3.代码演示
b.txt 文件原本是不存在的空文件,拷贝后有了 a.txt 的内容
8.2.4 文件的匿名映射
代码保存在 lesson25 mmap-anon.c
有时候不存在真正的文件实体,不需要文件这个中介进行通信。比如说进程将读到的内容直接传递给子进程
文件的匿名映射只能存在于有关系的两个进程之间
1.代码实现
创建一个父进程将读到的 string 中的信息直接传递给子进程。关键代码是需要在 mmap 映射时添加匿名映射的属性
MAP_ANONYMOUS
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(){
// 1.定义内存映射区的大小。以往都是根据文件大小来
int len = 4096;
void* ptr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);
// 2.父子间进程通信
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);
}
//3.释放映射
int ret = munmap(ptr,len);
return 0;
}
2.代码演示
最后可以看到虽然没有创建文件但还是实现了两个进程之间的通信
9.信号
9.1 信号的基本概念
1.定义
软件中断就是指当有另一个进程到达时此进程暂停当前任务去执行另一个进程的任务。等待另一个进程执行完毕再返回到刚才的任务继续执行,是一种异步通信方式。
信号 == 软件中断
2.中断发生的场景
- 对于前台进程用户输入了特殊的符号比如 Ctrl+c ,这时就向进程发了一个中断信号
- 硬件发生异常,硬件检测到一个错误条件并通知内核,随即硬核再发送相关信号通知进程。比如执行了一条异常的机器指令或者被 0 除,或者访问了一块无法访问的内存区域
- 系统状态的变化,定时器到期引起 SIGALRM 信号,进程执行的 CPU 时间超限或者该进程的某个子进程退出
- 执行 kill 命令或者调用 kill 函数
信号主要是软中断,告诉另一个进程可以执行了,所以说信号(软中断)也是一种进程通讯的方式
3.信号特点
简单,不能携带大量信息,满足某个特定条件才发送,优先级高
9.2 信号介绍
使用 kill -l
就可以看到所有的信号
前 31 个为常规信号,其余为实时信号
只看红色部分
-9 SIGKILL
SIGCHLD:子进程结束时,父进程会收到这个信号
下图中的 Core 文件是指我们在终止进程时对于中止进程保存的相关信息
man 7 signal // 查看信号指令
可以发现有的信号用三个数字表示,这是因为不同的系统架构造成的,我们在使用这些信号时选择中间的数字即可
SIGKILL and SIGSTOP 不能被捕捉,不能被阻塞,也不能被忽略
9.2.1 Core 文件保存中断中间变量
2.代码
这里定义一个会报错的代码,定义一个 string ,但是不将这个 string 进行初始化,直接对其赋值,那么最终也会导致一个 “段错误” 的出现
3.代码演示
这里会爆出一个段错误,主要是因为没有进行一个争取的段地址映射而产生的
4.添加 Core 文件
首先使用下面的指令查看和更改 Core 文件的大小,并且看到系统分配的 core 文件大小为 0
ulimit -a
使用下面代码更改生层的 Core 文件大小
ulimit -c 1024 // 定义 Core 文件的大小为 1024
因为我的代码没有显示对应的效果,所以这里用了视频中的演示
我们通过 gdb 的方式对 core 文件进行查看,里面说他是在执行 a.out 文件时产生的,执行了进程的终止并且是因为段错误发送了 SIGSEGV 信号
9.3 发送信号的相关函数
9.3.1 Kill–给指定进程发送信号
1.API
2.代码
实现 kill -9 操作
关键代码:
kill(pid, SIGINT);
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
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(2);
printf("kill child process now\n");
kill(pid, SIGINT);
}
return 0;
}
3.代码演示
9.3.2 raise --给当前进程发送信号
1.API
在 man 3 raise
中查看相关文档
这个函数可以由 kill 函数实现
9.3.3 abort–杀死当前进程
1.API
这个函数可以由 kill 函数实现
9.3.4 alarm 定时器–在一定的时长之后发送信号
1.API
定时器只能设置一次,后面再设置定时器还是按照第一次的时间进行倒计时
而且不管进程是什么状态 alarm 都会执行
2.代码
设置一个定时器
#include<stdio.h>
#include<unistd.h>
int main(){
int seconds = alarm(5); // 设置一个 5 s 是定时器
sleep(2);
seconds = alarm(1); // 这个闹钟无效
printf("second=%d\n",seconds); // 3
while(1){
}
return 0;
}
3.代码演示
虽然在 5 秒的闹钟后加闹钟加快,但是最后得到的闹钟还是 3 秒的剩余时间,也就是按照第一次闹钟定时
4.计数器代码实现
计数器实现电脑 1s 可以数多少个数
#include<stdio.h>
#include<unistd.h>
int main(){
alarm(1); // 1s 之后杀死自己
int i = 0;
while(1){
printf("%i\n",i++);
}
return 0;
}
5.程序的运行时间
程序真正的运行时间 = 内核时间+用户时间(程序真正的运行时间)+消耗的时间(比如文件 IO 操作打开关闭消耗的时间)
9.3.5 setitimer–实现周期性定时
1.API
这个定时器可以根据周期设置在多少时间之后启动,每隔多少秒启动一次
2.代码
关键代码:
如何定义结构体 new_value
int ret = setitimer(ITIMER_REAL,&new_value,NULL);
#include<sys/time.h>
#include<stdlib.h>
#include<stdio.h>
int main(){
// 定义结构体属性参数
struct itimerval new_value;
// 过3秒后每隔2秒进行一次定时
new_value.it_interval.tv_sec=2; // 每隔 2s 唤醒一次定时器
new_value.it_interval.tv_usec = 0;
new_value.it_value.tv_sec = 3; // 3 秒之后在设置这个定时器
new_value.it_value.tv_usec = 0;
// 设置一个周期定时器
int ret = setitimer(ITIMER_REAL,&new_value,NULL);
printf("定时器开始了");
if(ret==-1){
perror("setitimer");
exit(0);
}
getchar(); // 防止进程提前终止
return 0;
}
3.代码实现
首先是先打印:定时器开始了
在 3s 之后打印 Alarm clock
9.4信号捕捉函数
9.4.1 signal 捕捉信号
1.API
在定义信号之前定义捕捉信号的函数
捕捉到信号,然后执行自己想执行的事情
在形参中需要传入一个 sighandler ,通过前面定义发现这是一个回调函数
2.代码
signal(SIGALRM,SIG_IGN); // 忽略信号
3.代码演示
忽略信号:
刚才使用 setitimer 定义了一个信号,这里使用 signal 对信号忽略,可以看到它就一直在要求用户输入的地方,不会终止程序
默认信号执行:
时隔3s后执行 Alarm clock
执行回调函数:
在 3s 之后 signal 捕捉到第一个信号;然后每隔 2s 就会捕捉到另一个信号,捕捉到信号后执行相应的 handler 函数。执行完 handler 函数后程序继续执行下一个信号的发送(并没有终止程序)
9.4.2 信号集的捕捉
1.API
(1)什么是信号集
PCB 中有两个信号集:阻塞信号集和未决信号集。我们可以借助信号集操作函数对这两个信号集进行操作
阻塞信号集:
将某些信号加⼊集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(处理发⽣在解除屏蔽后)
未决信号集:
这个信号还没有被处理。信号产⽣,未决信号集中描述该信号的位⽴刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回为0。这⼀时刻往往⾮常短暂。
(2)如何处理信号
根据信号集中的值处理信号,1 代表这个信号被阻塞不能执行(未决),0 代表信号可以抵达,未决信号集中的值根据阻塞信号集中信号的变化而变化
e.g. : 当前的处理的信号 index 为 2 也就是 SIGINT 信号:
阻塞信号集中的值为 0 ,未决信号集看到阻塞信号集他的值为 0 ,也就是说不阻塞,则未决信号集中的值也变成 0
阻塞信号集中的值为 1 ,未决信号集看到阻塞信号集他的值为 1 ,先暂时将这个信号进行挂起,直到信号不阻塞了,未决信号的值再变为0
如果有更多的阻塞信号到达只能被丢弃
信号集
(3)操控信号集的常见函数
#include <signal.h>
int sigemptyset(sigset_t *set); // 将set集合置空
int sigfillset(sigset_t *set); // 将所有信号加⼊set集合
int sigaddset(sigset_t *set, int signo); // 将signo信号加⼊到set集合
int sigdelset(sigset_t *set, int signo); // 从set集合中移除signo信号
int sigismember(const sigset_t *set, int signo); // 判断信号是否存在
信号集的数据结构
sigset_t
2.代码
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(){
// 1.定义一个信号集
sigset_t set;
// 2.清空信号集中的内容
sigemptyset(&set);
// 3. 判断信号是否还在信号集 set 中,即它是否被阻塞
int ret = sigismember(&set,SIGINT);
if(ret==1){
printf("设置阻塞成功\n");
}else if(ret==0){
printf("设置阻塞失败\n");
}
// 4. 添加几个信号到信号集中
sigaddset(&set,SIGINT);
sigaddset(&set,SIGQUIT);
// 5.判断添加的信号是否在信号集中
ret = sigismember(&set,SIGINT);
if(ret==1){
printf("被阻塞了\n");
}else if(ret==0){
printf("没有被阻塞\n");
}
return 0;
}
3.代码演示
根据上面的代码可以看出,我们先对信号集中的信号进行清空,然后查看信号集中的成员。然后再设置信号再次查看信号集中的成员
9.4.3 sigaction—信号捕捉函数
代码保存在 lesson26 sigaction.c 中
1.API
类似于 signal 函数捕捉信号
#include <signal.h>
/**
* 检查或修改指定信号的设置(或同时执⾏这两种操作).
* @param signum 要操作的信号.
* @param act 要设置的对信号的处理⽅式(传⼊参数).
* @param oldact:一般不用:原来对信号的处理⽅式(传出参数) . *
* 如果 act 指针⾮空,则要改变指定信号的处理⽅式(设置),
* 如果 oldact 指针⾮空,则系统将此前指定信号的处理⽅式存⼊ oldact.
*
* @return 成功: 0; 失败: -1.
*/
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
数据结构:
struct sigaction {
void(*sa_handler)(int); // 信号处理函数指针
void(*sa_sigaction)(int, siginfo_t *, void *); // 一般不用:新的信号处理函数指针
sigset_t sa_mask; // 临时信号阻塞集,在信号捕捉函数执行过程中,临时阻塞某些信号
int sa_flags; // 使用哪个信号处理函数处理信号,0:sa_handler,SA_SIGINFO :sa_sigaction
void(*sa_restorer)(void); //已弃⽤
};
sigaction 讲解
2.代码
首先设置一个定时器不断的发送信号,然后使用 sigaction 捕捉信号,捕捉到信号后执行相应的操作,操作结束后程序继续执行
关键代码:
定义信号捕捉函数,重点在于如何定义结构体变量
// 定义 act
struct sigaction act;
act.sa_flags = 0;// 使用 handelr 进行处理
act.sa_handler = my_alarm;
sigemptyset(&act.sa_mask); // 清空临时信号集
// 注册信号捕捉函数
sigaction(SIGALRM,&act,NULL);
捕捉到信号后进行的操作
// 捕捉信号后处理的函数
void my_alarm(int num){
printf("捕捉到一个信号%d\n",num);
printf("进行相应的处理\n");
}
3.代码演示
9.4.5 信号捕捉流程及特性
1.流程
(1)程序在 main 函数中执行,运行到某一行时发现需要中断然后程序就运行到内核
(2)内核使用 do_signal 函数处理递送的信号
(3)内核判断信号处理函数是否是用户自定义的,如果是用户自定义的函数则再返回到用户区执行相应的信号处理函数
(4)信号处理函数处理完成后调用 sigreturn 返回到内核
(5)从内核再返回用户区,继续刚才没有执行完的 main 程序执行
2.特性
(1)内核中有一个阻塞信号集,信号捕捉时也有一个临时信号集,当信号捕捉结束后会返回到系统中的阻塞信号集
(2)当两个信号同时发生,后面的阻塞信号要等前面的阻塞信号处理完毕才能接着处理
(3)阻塞的常规信号是不支持排队的,因为信号集的个数只能由 01表示,其他来的阻塞信号要全部被丢弃
9.4.6 SIGCHLD 信号
代码保存在 lesson26 sigchld.c 中
1.子进程什么时候会向父进程发送 SIGCHLD 信号
子进程会向父进程发送 SIGCHLD 信号,以下三种情况发生时子进程就会想父进程发送 SIGCHLD 信号,默认情况下父进程会忽略该信号
1.子进程终止时
2.代码
首先注册阻塞信号集,并将其添加到系统阻塞信号集中。其次开启子进程,子进程执行相应方法。当子进程执行完毕后父进程对子进程进行回收,回收完毕接着执行自己的代码
调用相应的库文件:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
注册信号阻塞集,将其添加到系统信号阻塞集中
// 提前设置好阻塞信号集阻塞 SIGCHLD ,有可能因为子进程结束父进程还没有注册阻塞信号集就没有办法阻塞子进程
sigset_t set; // 定义信号集
sigemptyset(&set); // 将信号集中的信号清空
sigaddset(&set,SIGCHLD); // 添加对子进程阻塞的监听
sigprocmask(SIG_BLOCK,&set,NULL); // 将定义的阻塞信号集添加到系统的阻塞信号集中
信号处理函数,即回收子进程
void my_handler(int num){
printf("捕捉到的信号:%d\n",num);
// 回收子进程 pcb 资源,防止有漏掉的子进程
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;
}
}
}
创建子进程
// 创建子进程
pid_t pid;
for(int i=0;i<5;i++){
pid = fork();
if(pid==0) break;
}
父进程不断执行,并不断监听子进程发出的 SIGCHLD 信号
if(pid>0){
// 捕捉子进程死亡时 SIGCHLD 信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = my_handler;
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());
}
3.代码演示