管道的回顾
进程是具有独立性的—>进程通信的成本比较高—>必须先解决一个问题—>要让不同的进程看到同一份资源(内存文件,内存,队列)【一定是需要OS提供的】----> pipe的本质:是通过子进程继承父进程资源的特性,达到一个目的让不同的进程看到同一份资源
我们通常标识一个文件:路径+文件名(具有唯一性)
命名管道
为了解决匿名管道只能在父子通信,我们就引入了命名管道 命名管道可以在命令行里面弄,也可以在程序里面弄
命令行
mkfifo filename
创建管道文件
测试 :我们虽然在是同一个文件,但是在不同的进程当中 一个进程中的数据通过管道文件传递到另一个进程
代码中的使用
失败返回-1
在进程中创建一个命名管道文件,他的权限要和掩码进行按位&,我们标准情况下的掩码都是002
可以将掩码设置成0
我们发现一方发的信息直接就被另一方给收到了
因为命名管道是基于字节流的,所以实际上信息传递的时候,是需要通信双方定制”协议“的,到网络的时候就可以了解 了,今天我们就单纯的进行字符串通信即可
进程间通信的目的:一个进程去控制另一个进程 命名管道的数据不会刷新到磁盘,为了效率
命名管道为什么一定要有名字
为了保证不同进程看到同一个文件(通过文件名来操作)所以命名管道 而匿名管道是通过父子进程的方式,看到同一份资源,所以就不需要名字,来标识同一个资源
client.c
#include"head.h"
int main()
{
int fd=open(MY_FIFO,O_WRONLY);
if(fd<0)
{
perror("open");
return 2;
}
while(1)
{
printf("请输入: ");
fflush(stdout);
char buffer[64]={0};
ssize_t s=read(0,buffer,sizeof(buffer));
if(s>0)
{
buffer[s]=0;
buffer[s-1]=0;
printf("%s\n",buffer);
write(fd,buffer,strlen(buffer));
}
}
close(fd);
return 0;
}
server.c
#include"head.h"
int main()
{
umask(0);
if(mkfifo(MY_FIFO,0666)<0)
{
perror("mkfifo");
return 2;
}
int fd=open(MY_FIFO,O_RDONLY,0666);
if(fd<0)
{
perror("open");
return 1;
}
while(1)
{
char buffer[64]={0};
sleep(5);
ssize_t s=read(fd,buffer,sizeof(buffer));
if(s>0)
{
if(strcmp(buffer,"show")==0)
{
if(fork()==0)
{
execlp("ls","ls","-a",NULL);
exit(1);
}
waitpid(-1,NULL,0);
}
if(strcmp(buffer,"sl")==0)
{
if(fork()==0)
{
execlp("sl","sl",NULL);
exit(1);
}
waitpid(-1,NULL,0);
}
buffer[s]=0;
printf("client# %s\n",buffer);
}
else if(s==0)
{
printf("client quit....\n");
}
else
{
perror("read");
break;
}
}
close(fd);
return 0;
}
System V标准进行通信方式
在OS层面上,专门为进程间通信设计的一个方案,要不要给用户用,以什么方式给用户用? 肯定要给用户用, OS不相信任何用户,给用户提供功能的时候,采用系统调用! System V进程间通信,一定会存在专门原来通信的接口(system call)
所以就需要有个人组织机构来定制标准,就要有人来定制标准,在同一主机上的通信方案: system V
system V
- 共享内存
- 消息队列
- 信号量
共享内存
准备工作:
- OS 内存可不可能存在多个进程,同时使用不同的共享内存来通信??
可能—> 共享内存在系统中你可能有多份!—》操作系统也要管理这些共享内存,—》如何管理这些共享内存呢?—》先描述再组织(一定要有内核数据结构) - 怎么保证两个或两个以上的进程会看到同一个共享内存,(共享内存一定要有一定的标识唯一的ID),方便让不同的进程就能识别同一个共享内存的资源!!
这个ID在哪里呢,这个id就在描述的结构体里面
shmget
失败返回-1
创建共享内存段
- size:创建这共享内存的大小(我们一般建议是4KB的整数倍),共享内存在内核当中申请的基本单位是页,内存页(4KB)
如果我申请来了4097个字节>4KB,所以内核会向上取整,变成8KB,我们就给你4097,但是操作系统是按照4096*2的方式给你的
-
IPC_CREAT:创建一个新的共享内存段,如果单独使用IPC_CREAT,或者flag为0,创建一个共享内存,如果创建的共享内存已经存在,则直接返回当前已经存在的共享内存(不存在则创建,存在就获取一个),(基本不会空手而归) -
IPC_EXCL:一般不会单独使用的,IPC_CREAT|IPC_EXCL这两个单独使用的才有意义 如果不存在共享内存则创建,如果已经存在了共享内存,则返回出错!(意义在于,如果调用成功,得到的一定是一个最新的,没有被人使用的共享内存!) -
key:为了保证看到的是唯一的标识符,目的是为了让不同的进程来识别的,本质是可以用这标识符让不同的进程看到同一份资源:先让不同的进程看到同一个ID, 用ftok来操作
./server进程执行结束,该进程曾经创建的共享内存没有被释放, 第二次运行的时候发现他说文件已经存在(创建共享内存失败)
system V 的IPC 资源,生命周期是随内核的(只能通过程序员使用命令或者系统调用来释放,或者OS 重启) 于文件不同:文件只要和他相关的进程退出的话,那么这个文件所对应的资源也都全部被释放掉
ipc
ipcs
命令行中可以查看共享内存,消息队列,信号量
ipcs -m
ipcrm
把共享空间给删除掉 ipcrm -m shmid
key和shmid
- key:只是用来在系统层面进行标识唯一性的,不能原来管理共享内存
- shmid:是OS 给用户返回的id,用来在用户层对共享内存进行管理
shmid相当于fd,而key相当于文件的地址
命令行是属于用户层:所以肯定是使用shmid
ftok
先调用ftok,获得key值,传给shmget pathname :我们自定义的路径名 proj_id:我们自定义的项目id 失败了就返回-1
如我们的路径名“./temp” 项目id:0行66 这数字是什么不重要,只要保证唯一性就可以了
怎么保证不同的进程看到的是同一个共享内存?只要我们形成key的算法+原始数据是一样 的,。形成同一个id
这里的key就是会设置进内核的关于shm的内核中的数据结构中
shmctl
成功返回0, shmid:就是我们shmget里面创建获得的id cmd:IPC_RMID,把这段给销毁掉 buf: struct shmid_ds,共享内存的结构体
shmid也是数组下标 控制共享内存(我们只了解删除)
shmat shmdt
return:成功就返回那个共享空间段的地址(虚拟地址:我们程序员看到的地址都是虚拟地址,不可以看到物理地址),失败的话就返回-1 如malloc,返回的空间也都是虚拟的,相当于平白无故多了一个空间
attach:把我们调用的继承和共享内存关联起来 关联和去关联
- shmid:来表示一个共享内存,
- shmaddr:我们要挂接的时候,我们想要把它挂接到我的地址空间的什么地方去,但是我们一般不关心不清楚,所以我们就直接设置为NULL,就可以了
- shmflag:我们也直接设置为0就可以了
shmdt:去关联,并不是释放共享内存,而是取消当前进程和共享内存的关系,(和页表构建映射关系的页表项删掉)返回值不重要 参数是我们关联后的返回值
struct shmid_ds
shmid_ds:权限,内存段的大小,上一次挂接的时间,上一次取消挂接的时间,创建者的pid,一共有多少个挂接 ipc_perm里面还有很多的uid,,同时还有key,我们可以根据key值找到对应的消息队列 消息队列和这个也是类似的,
- 我们会发现消息队列,信号量,共享内存,的接口类似
- 数据结构的第一个结构类型是完全一样的(struct ipc_perm)
结论:所有的ipc资源都是通过数组组织起来的,
所有的System V 标准的IPC 资源,XXid_ds结构体的第一个成员都是ipc_perm(这个是一样的,)
这些数组可以给我们指向不同的结构体,使用数组的下标 指向一个一个IPC资源的结构体指针
struct ipc_perm* ipc_id_arry[64] ipc_id_arry[0]=(ipc_perm)&shmid_ds;我们通过强转, 当我们需要使用第一个资源的时候不需要强转,当我们使用其他资源的时候就需要强转 (shmid_ds*)ipc_id_arry[0]->shmid_ds的其他属性 相当于c++的切片功能
信号量
临界资源
所有可以被多个执行流同时访问的资源就是临界资源, 例如: 多进程启动后同时向显示器打印,这个显示器就是所谓的临界资源 而我们在学习进程间通信的时候,管道,共享内存,消息队列等,不同的进程能够同时访问 共享内存是最典型的共享资源
所以凡是进程间通信,必定需要引入可以被多个进程看见的资源(通信需要),这一份资源就变成了临界资源,同时,也引入一个新的问题:临界资源的问题
但是我们在进程中定义一个全局变量,之后fork子进程,但是这个全局变量不是临界资源,因为子进程如果对这个数据进程修改的话,会发生写实拷贝
临界区
进程的代码可以很多的,其中用来访问临界资源的代码叫做临界区 类似显示器: 显示器叫做临界资源,在显示器中printf叫做临界区
原子性
一件事情要么不做,要么就做完,没有中间态,就叫做原子性 非原子性:有中间过程
比如学习,要么啥也不学,要么就考第一名,
int cout=100;
cout--; cout--;
父和子对于cout处理不同 这里的cout不是原子的,cout是为了保护临界资源的 每个人想要进入电影院,必须要先看到cout, cout本身就是也是临界资源的 所以信号本身就是临界资源的,
互斥
在任意时刻只能允许一个执行流进入临界资源,执行他的临界区
sem=1
if(sem>0)
sem--;
else if(sem<=0)
{
}
sem++;
只有0和1就叫做2元信号量
什么是信号量
管道,匿名or命名,共享内存,消息队列,都是以传输数据为目的的! 信号量不是以传输数据为目的的!通过共享内存“资源的方式”,来达到多个进程的同步和互斥的目的! 信号量的本质:
是一个计数器,类似 int count :衡量临界资源的中资源的数目的
- 在电影院的某一个放映厅也是一个临界资源!可以被不同的人访问,我们在进去的时候别人也可以进去,
- 并不是我们坐在这个位置上,这个位置才属于我们,在外面买到票的时候,这座位就已经属于我们了,
- 而买票的本质:对临界资源的预定机制
一个放映厅最怕什么?一共有100个座位,卖了110张票,所以最多只能买100张票,---------用信号量来约束这行为
信号量本身一个临界资源 所以信号量的p操作和delete操作就是必须保证是原子的
int count=100;
cout--;
cout++;
信号量的伪代码
if(cout>0)
cout--;
else
{
}
cout++;
|