目录
谢谢帅气美丽且优秀的你看完我的文章还要点赞、收藏加关注
没错,说的就是你,不用再怀疑!!!
希望我的文章内容能对你有帮助,一起努力吧!!!
1、IPC
IPC: Internal Procces Communication 进程间通信
实质:信息(数据)的交换(通信)
思考?要想实现通信--在父进程内定义一个全局变量 A ,然后再给 A 进行赋值。然后再让子进程去读取,不就实现通信了吗?
答案是否定的
原因: 两个进程的地址空间是独立的。
如果两个进程要进行通信,必须要把数据放到一个大家都可以访问的地方。
<文件>:可以支持进程间的通信,大家都可以访问。有一个很大的问题:速度太慢了。
有没有其他方式?
在操作系统内核中开辟一段空间(某种机制),进程去访问它。这个内核空间对于进程而言是共享的。
1.1IPC方式
- 管道
- pipe 无名管道
- fifo 有名管道
- 信号
- signal
- 消息队列
- system V 消息队列
- POSIX 信号量
- 共享内存
- system V共享内存
- POSIX共享内存
- socket
- unix 域协议
2、管道
在很久以前,进程间通信方式,都是通过文件。这种方式有个缺点:效率(速度)太低了。但是这种方 式有一个天大的好处:简单,不需要额外 API 函数支持(直接利用文件系统的 API 操作)
因为弊端所有需要改进,首先问题存在于:文件内容是在外设上面,文件系统上====>访问效率低。
管道:管道文件,它的文件内容是在内核/内存中
2.1无名管道
它在文件系统中没有名字( inode ),它的内容存放在内核中,访问pipe的方式是通过文件系统的 API (read/write) 。
它不能用 open ,但是 read/write 有需要一个文件描述符。所以创建这个pipe的时候,就必须要返回 一个文件描述符!!!!
pipe 在创建的时候,在内核中开辟一块区域作为缓冲区,作为pipe的文件的内容区域的存储空间,同 时返回两个文件描述符(一个是用来读,一个用来写)
它有如下特点:
- pipe 有两端,一端用于写,一端用于读
- 按顺序读,不支持 lseek 光标偏移
- 内容读走,就莫有了
- pipe(无名管道)随内核持续性
2.1.1pipe 的API
创建 pipe 管道
关闭 pipe 时候,需要注意,要先关闭写端,再关闭读端。
pipe (无名管道)只能用于有亲缘关系的进程间通信,它为什么有这个限制?
- 因为它没有名字
- 假设它如果在文件系统中有个一个名字 “ inode ”它就可以用于任意进程间的通信。
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
// 创建pipe管道文件
int pipefd[2]={0};
if(pipe(pipefd) == -1)
{
return -1;
}
// 创建子进程进行通信
pid_t pid = fork();
if(pid == 0)
{
while(1)
{
char buf[2]={0};
printf("PID<%d>Enter:",pid);
scanf("%s",buf);
if(buf[0] == 'e')
{
write(pipefd[1],"e",1);
close(pipefd[1]);
return 0;
}
write(pipefd[1],buf,1);
}
}
else if(pid > 0)
{
while(1)
{
char buf[2]={0};
read(pipefd[0],buf,1);
if(buf[0] == 'e')
{
close(pipefd[0]);
return 0;
}
printf("PID<%d>RECV:%s\n",pid,buf);
}
}
return 0;
}
2.2有名管道
fifo 是在 pipe 基础上,给它在文件系统中创建一个 inode (它会在文件系统中有一个名字),但是 fifo 文件的内容却是在内核中!!!!
- fifo 的文件名随文件系统持续性
- fifo 的文件内容存在于内核,随内核持续性
fifo 同 pipe 一样,除了 fifo 在文件系统中有一个文件名外。
2.2.1fifo 的API
创建fifo 文件
注意: FIFO 特殊文件(命名管道)类似于管道,只是它是作为文件系统的一部分访问的。它可以通过 多个进程打开以进行读取或写入。当进程通过 FIFO 交换数据时内核在内部传递所有数据,而不将其写 入文件系统
因此, FIFO 特殊文件没有内容在文件系统上;文件系统条目只是作为一个参考点,这样进程就可以访 问管道使用文件系统中的名称。内核为至少一个进程打开的每个 FIFO 特殊文件保留一个管道对象。
这个必须先打开 FIFO 的两端(读取和写入),然后才能传递数据。通常,打开 FIFO 一端直到另一端也 被打开。进程可以在非阻塞模式下打开 FIFO 。在Linux下,打开 FIFO 进行读写操作在阻塞和非阻塞模 式下都会成功。
- 阻塞的读/写
- 读的时候,如果没有数据,则read会阻塞
- 写的时候,如果没有空间,则write会阻塞
- 非阻塞的读/写
- 读的时候,如果没有数据,则立即返回,设置对应错误码
- 写的时候,如果没有空间,则立即返回,设置对应错误码
在使用open的时候,默认是阻塞方式,非阻塞则需要或上一个宏: NONBLOCK
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FIFPPATH ("/home/thirteen/fifo")
int main()
{
mkfifo("/home/thirteen/fifo",0777);
int data = -1;
int fifo_id = open("/home/thirteen/fifo",O_RDONLY);
if(fifo_id == -1)
{
perror("");
return -1;
}
while(1)
{
read(fifo_id,&data,sizeof(data));
if(data == -1)
break;
printf("data:%d\n",data);
}
close(fifo_id);
return 0;
}
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int data = -1;
int fifo_id = open("/home/thirteen/fifo",O_WRONLY);
if(fifo_id == -1)
{
perror("");
return -1;
}
while(1)
{
scanf("%d",&data);
write(fifo_id,&data,sizeof(data));
if(data == -1)
break;
}
close(fifo_id);
return 0;
}
3、信号
3.1信号的含义
信号是进程间通信一种方式,这个种方式没有传输数据。只是在内核中传一个信号(整数),信号的表 示是一个整数,不同信号值,代表不同含义,当然用户可以自定义信号,以及自定义信号的含义解释权 归用户所有。
进程在收到一个信号的时候,通常会有三种处理方式:
- 捕捉信号:
- 把一个信号 与 用户自定义的信号处理函数关联起来
- 那么在收到该信号的时候,就会自动调用该函数处理
- 默认行为:
- 收到一个信号的时候,采用操作系统默认的行为
- 大部分信号的默认行为,是会直接干掉进程。
- 只有一个信号 SIGCHLD 是被忽略的。
- 忽略该信号
3.2信号的处理过程
通过 “ 软件中断/软中断 ” 来实现,信号处理函数起始在 “ 中断上下文 ” 执行,信号处理函数----->" 软中 断函数 "
进程上下文:
- 进程在大环境下 , “ 时间片轮转 ”
- 一个进程的执行状态又分为:
- 用户态:执行用户自己的代码的时候
- 内核态:进入操作系统执行内核代码的时候
- 在状态切换的时候,要保存用户自己代码的运行到的位置,保存的这个就是上下文。
3.3linux下信号相关的API
发送信号
#include <signal.h>
#include <cstdio>
#include <unistd.h>
#include <cstdlib>
int main(int argc,const char *argv[])
{
int pid = atoi(argv[1]);
kill(pid,SIGKILL);
return 0;
}
3.4设置闹钟信息
alarm :定时发送一个闹钟信号给本进程,“ 闹钟 ” 每一个进程都有一个属于自己的 “ 闹钟 ”。闹钟的时 间到了,进程就会收到一个 SIGALRM 的信号,但是同一时刻一个进程只有一个 “ 闹钟 ”生效。
大部分的信号的默认行为,是把收到信号的进程Kill掉。那么如果要改变接收到信号之后的行为:捕获信 号。
3.5捕获信号
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <cstdlib>
int count = 0;
// 自定义处理函数
void my_handler_function(int sig)
{
puts("非法内存访问");
count++;
if(count == 3)
exit(0);
}
int *a = NULL;
int main()
{
signal(SIGSEGV,my_handler_function);
*a = 10;
while(1);
return 0;
}
3.6等待信号
pause:让进程停在那里等待一个信号的到来
4、共享内存
共享内存是进程间通信一种方式,多个进程共享一段内存,“ 共享内存 ”。由于多个进程共享了同一段内 存,这个段内存既是你的也是我的。也就是你往这个内存里面写入数据,实际上就相当于往我的内存里 面写入数据。比起其他 IPC 方式( pipe fifo message.. )少拷贝操作,相对而言共享内存的效率高 于其他的。
4.1共享内存的生存期:随内核持续性
实现方式:
- 在内核中开辟一块共享内存,其他的进程通过 “ 映射 ” 方式,获取这个段共享内存的引用(指针)
- 进程 P1 可以映射这段内存,同时其他的进程(如: P2..Pn )也可以映射这段内存, P1 往内存里 面写入数据,实际就是往 P2..Pn 进程中写入数据,反之亦然。
4.2System V共享内存的API
System V ( msg/shm/sem )操作流程:
4.2.1创建或者是打开一个IPC 设施
ftok :创建或者是打开一个 IPC 设施( msg/shm/sem )也就是一个 System V IPC 对象的" 钥 匙 "( KEY )
4.2.2创建或打开System V 共享内存
shmget :通过 ftok 获取到的 IPC 设施的钥匙来创建或打开一个 System V 共享内存
#include <iostream>
#include <sys/types.h>
#include <sys/shm.h>
// 通过宏定义来指定IPC设施的路径和工程ID
#define IPC_SHM_PATH ("/home/thirteen")
#define IPC_SHM_CODE (20240710)
// 共享内存区域的大小
#define IPC_SHM_SIZE (4)
int main()
{
// 第一步:获取钥匙
key_t key = ftok(IPC_SHM_PATH,IPC_SHM_CODE);
// 第二步:创建或打开共享内存,拿到共享内存的ID号
int shm_id = shmget(key,IPC_SHM_SIZE,IPC_CREAT | 0777);
// 第三步:映射共享内存到用户内存空间
int *point = (int *)shmat(shm_id,NULL,0);
// 第四步:进行操作
while(1)
{
std::cin >> *point;
if(*point == -1)
{
break;
}
}
// 最后解除映射
shmdt(point);
// 删除掉共享内存
shmctl(shm_id,IPC_RMID,NULL);
return 0;
}
4.2.3映射/解映射
映射:把内核或者设备的文件中的一段内存映射到进程的地址空间去,用进程的一个指针,去访问这段 内存。
shmat :通过拿到的共享内存 ID 映射共享内存
#include <iostream>
#include <sys/types.h>
#include <sys/shm.h>
// 通过宏定义来指定IPC设施的路径和工程ID
#define IPC_SHM_PATH ("/home/thirteen")
#define IPC_SHM_CODE (20240710)
// 共享内存区域的大小
#define IPC_SHM_SIZE (4)
int main()
{
// 第一步:获取钥匙
key_t key = ftok(IPC_SHM_PATH,IPC_SHM_CODE);
// 第二步:创建或打开共享内存,拿到共享内存的ID号
int shm_id = shmget(key,IPC_SHM_SIZE,0);
// 第三步:映射共享内存到用户内存空间
int *point = (int *)shmat(shm_id,NULL,0);
// int tmp = *point;
// 第四步:进行操作
while(1)
{
// if(tmp != *point)
// {
std::cout << *point << std::endl;
// tmp = *point;
// }
}
// 最后解除映射
shmdt(point);
return 0;
}
4.2.4其他操作
shmctl :对于共享内存的操作的。
5、信号量
有两个以上的任务(进程/线程)并发的实体,去访问同一个共享资源(硬件上,软件上)的时候,那么 要保证访问的这个共享资源是有序访问,如果不是有序访问有可能造成不可预知后果。
very_important_i = 5;
func()
{
very_important_i ++;
}
有两个实例(任务),调用 func 函数,那么 very_important_i 最后的值是多少。
有可能是6,还有可能是7。
6的结果不是我们想要,所有我们要保证多个实例能够有序的去访问,就需要对共享资源进行某种保护, 以便实例可以有序的访问,避免竞争
分析:
- 并发---->竞争---->共享资源的非法访问 ---->程序行为异常...
解决方法:
- 能不能不用并发?
- 显然不行
- 在保留并发前提下,“ 避免竞争 ” ===> 访问共享资源的时候,严格串行!!!!
5.1信号量机制
- 信号量是个什么玩意?
- 信号量的作用是什么?
- 为什么要用到信号量?
- 信号量是怎么达到目的的?
5.2信号量是个什么玩意?
信号量(semaphore)是一种用于提供不同进程的间或一个进程内部不同线程间的同步的一种机制。
- 进程/线程:任务,并发的实体
- 同步:并发实体间,相互等待相互约束的,有序的,有条件的访问。
信号量就是为了保护共享资源,让共享资源有序的访问的一种机制
信号量目标:为了保护共享资源,使其能够被有序访问。
信号量是我们程序界最高尚的一种东西,因为它不是为了自己存在而存在,是为了别人而存在的。(它 保护的对象,共享资源)“ 保镖 ”
5.3什么时候使用信号量?
- 有保护对象的时候,才需要信号量
- 首先搞清楚,谁需要保护,保护谁
- 一个被保护对象,需要一个信号量。
5.4如何来保护?
“ 保护 ”是指,让这个被保护对象( 共享资源 )有序的访问。如: “ 互斥 ”
“ 共享资源 ”:大家都访问的资源。
信号量机制,其实是程序员之间的一种约定,用来保护共享资源的。比如说进程A和进程B,都要访问一 个互斥设备,那么我们可以使用一个信号量来表示能不能访问该设备,然后每个进程访问该设备的时 候,先去访问信号量,如果能访问设备就把信号量设置为“ NO ”,访问完毕之后再将信号量设置为 “ YES ”。
在访问共享资源的时候,先去判断,共享资源是否能够访问。
- 能访问:你就获取到了该信号量( 变成不可访问 ),则进入能访问之后的代码。
- 不能访问: wait 直到信号量变成:能访问。
访问共享资源的代码区域叫做:临界区
- LOCK 上锁
- 操作共享资源的代码
- UnLOCK 解锁
5.5信号量是如何实现的?
“ 信号量 ”:大家都可以访问的一个整数。
一个进程/线程可以在某个信号量上执行以下三种操作:
- 创建 ( create ) 一个信号量:这还要求调用者指定信号量的初始值。
- 初始值表示该信号量保护的共享资源,可以同时被多少个任务访问。
- sem --> 5 表示此刻有5个进程或者线程去同时访问它所保护的共享资源。
- sem --> 1 表示此刻有一个进程或者线程可以去访问它所保护的共享资源。
- “ 互斥信号量"
- 等待( wait )一个信号量
- 该操作会测试这个信号量的值,如其值 0,这个时候,将 它-1,并继续往下执行临界区代码。
- 其函数实现如下:
- 上述操作必须是 “ 原子操作 ”:不允许有两个及以上的进程同时操作
- P操作: proberen ( 尝试 )荷兰语
- down/lock 上锁
- 释放一个信号量:该操作将信号量的值+1,其函数实现类型如下
- V操作: verhogen ( 增加 )荷兰语
- up / unlock 解锁
信号量保护的目标是通过如下方式实现:
- 在临界区的前面加上一个:P操作
- 在临界区的后面加上一个:V操作
Linux内核信号量的具体实现:
- System V 信号量
- POSIX 信号量
- System V semaphore
- System V 信号量的大概流程
- ftok :获取 System V IPC 设施对象的key
- semget :在内核中创建或打开一个 System V 信号量
- P/V操作
- System V信号量:
- 计数信号量集(计数信号量数组):
- 计数信号量:
- 该信号量的值可以是> 1 的值,它所保护共享资源允许多个任务同时访问它。
- 计数值 1 , 0 ===> 互斥信号量
- 互斥信号量:
- 该信号量的值要么是1,要么是0,它所保护的共享资源同一时刻只能允许一个任务访 问。
- 为什么 System V 要把信号量弄成一个信号量集( 信号量数组 )呢?
- P(S1 & S2) 的这种情况...
5.6信号量的API函数
5.6.1SystemV IPC 信号量
semget :用来创建或打开一个System V信号量
注意:在一个新创建的信号量集中的信号量的值,是不确定的。
semctl :控制操作
semop : System V 信号量的 PV 操作
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#define SEMPATH ("/home/thirteen")
#define PROJECTID (20230828)
int main()
{
// 获取IPC设施的key
key_t key = ftok(SEMPATH,PROJECTID);
if(key == -1)
{
perror("");
return -1;
}
pid_t pid = fork();
if(pid == 0)
{
sleep(1);
// 创建一个共享内存
int shm_id = shmget(key,0,0);
// 映射
int * num_p = (int *)shmat(shm_id,NULL,0);
// 创建或打开一个信号量集
int sem_id = semget(key,0,0);
if(sem_id == -1)
{
perror("");
return -1;
}
// 操作
while(1)
{
struct sembuf sem_PV;
sem_PV.sem_num = 0;
sem_PV.sem_op = -1; // P操作
sem_PV.sem_flg = SEM_UNDO; // 撤销
semop(sem_id,&sem_PV,1); // P操作
(*num_p) ++;
sem_PV.sem_op = 1;
sleep(1);
printf("子进程:%d\n",*num_p);
semop(sem_id,&sem_PV,1); // V操作
if(*num_p >= 20)
break;
}
}
else if(pid > 0)
{
// 创建一个共享内存
int shm_id = shmget(key,128,IPC_CREAT|0777);
// 映射
int * num_p = (int *)shmat(shm_id,NULL,0);
*num_p = 0;
// 创建或打开一个信号量集
int sem_id = semget(key,1,IPC_CREAT|0777);
if(sem_id == -1)
{
perror("");
return -1;
}
// 设置信号量集的初始值
semctl(sem_id,0,SETVAL,1);
// 操作
while(1)
{
struct sembuf sem_PV;
sem_PV.sem_num = 0; // 信号量下标
sem_PV.sem_op = -1; // P操作
sem_PV.sem_flg = 0; // 撤销
semop(sem_id,&sem_PV,1); // P操作
(*num_p) ++;
sem_PV.sem_op = 1;
sleep(1);
printf("父进程:%d\n",*num_p);
semop(sem_id,&sem_PV,1); // V操作
if(*num_p >= 20)
break;
}
shmdt(num_p);
shmctl(shm_id,IPC_RMID,NULL);
semctl(sem_id,0,IPC_RMID);
}
}