Linux练级宝典->进程间通信
目录
进程间通信介绍
为什么要进程间通信
进程间通信的本质
进程间通信的分类
匿名管道
匿名管道
pipe函数
管道读写规则
管道的特点
管道的四种情况
编辑 管道的大小
命名管道
命令行创建命名管道
在代码中创建命名管道
实现server 和 client 通信
基于server实现一个计算器功能
服务器执行命令行指令
用命名管道实现文件拷贝
system V进程间通信
system V共享内存
共享内存的数据结构
共享内存的建立和释放
共享内存的创建
共享内存的释放
共享内存的关联
共享内存去关联
用共享内存实现serve & client 通信
共享内存和管道对比
system V消息队列
信息队列的基本原理
消息队列的数据结构
消息队列的创建:
消息队列的释放:
发送数据:
获取数据:
system V信号量:
概念:
信号量的数据机构
信号量相关函数
信号量集的创建
信号量集的删除
信号量集的操作
进程互斥
system V IPC
进程间通信介绍
进程间通信简称IPC(interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
为什么要进程间通信
数据传输,资源共享,通知事件,进程控制。
通知事件:子进程向父进程发送中止事件,父进程可以回收子进程了。
进程间通信的本质
让不同的进程看到一份资源。
我们知道进程具有独立性,每个进程都认为自己享有整个内存空间,但实际是用多少拿多少,只是虚拟地址给了进程自信,而在物理地址中则是用多少给多少。
所以进程的数据应该是独一份的,除了父子进程没有进行写时拷贝时共用一份数据。
而我们想要做的就是让多个进程看到同一份资源。那就需要规定一块内存第三方资源来进行摆放信息,这里的信息就是多个进程都可以看见的。
进程间通信的分类
管道:匿名管道,命名管道。
System V IPC:System V 消息队列 System 共享队列 System V 信号量
POSIX IPC:消息队列 共享内存 信号量 互斥量 条件变量 读写锁
匿名管道
平常我们使用指令连接时比如,统计当前云服务器上的登录用户个数
命令都是程序,所以上面有两个程序who和wc,who本来输出到屏幕的数据到了管道里,wc从管道里拿到数据。此时who的数据就被wc拿到了
who是以行表示当前登录用户个数 wc -l是统计当前的行数。
匿名管道
匿名管道的原理:用于父子进程之间的通信。
让不同的进程看到同一个资源,父子进程看到同一份被打开的文件,然后决定父子进程谁写入和读取。所以实现父子进程的通信
这个文件不会收到写时拷贝的影响,因为这个文件是共享的。
这里的数据不会刷新到磁盘里,因为增加了IO降低效率,所以共享资源当二者关闭后自然消失,而没有磁盘保存。
pipe函数
int pipe(int pipefd[2]);
因为传入的是一个数组,所以是一个输入输出型参数。数组名就是一个指针。
成功返回0,失败返回-1.
匿名管道流程
1.父进程调用pipe函数创建管道
2.父进程创建子进程,此时子进程和父进程对于管道是都连接的状态
3. 父进程关闭写端,子进程关闭读端,即父读子写
注意点:
管道是半双工的,即只可以一端写,一端读,所以需要确认谁写谁读。
写端写入的数据会被内核缓冲,直到管道的读端读取。
看看文件描述符的视角
1.父进程创建管道
2.父进程创建子进程
3.父进程关闭写端,子进程关闭读端
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//fd[0]是读端fd[1]是写端
int fd[2] = {0};
if(pipe(fd) < 0)
{
perror("pipe");
return 1;
}
pid_t id = fork();
if(id == 0)
{
close(fd[0]);
const char* msg = "hello father,i am child.......";
int count = 10;
while(count--)
{
write(fd[1],msg,strlen(msg));
sleep(1);
}
close(fd[1]);
exit(0);
}
//父进程关闭写端,然后读取管道数据并输出
close(fd[1]);
char buff[64];
while(1)
{
ssize_t s = read(fd[0],buff,sizeof(buff));
if(s > 0)
{
//说明有数据
buff[s] = '\0';
printf("child send to father:%s\n",buff);
}
else if(s == 0)
{
//说明文件读取完毕,后续没有
printf("read file end\n");
break;
}
else
{
printf("read error\n");
break;
}
}
close(fd[0]);
waitpid(id,NULL,0);
return 0;
}
管道读写规则
pipe2与pipe函数类似,都是用来创建匿名管道的,其函数原型如下:
int pipe2(int pipefd[2], int flags);
多了一个flags选项,
选项名字为O_NONBLOCK,即不阻塞的意思。
开启时,read函数如果没有数据读本来会把程序卡主,如果加了选项,此时read调用直接返回-1,并且错误码值为EAGAIN,即下次继续,先去执行后续逻辑。
write函数本来在管道写满时,就要被卡住,此时就不会卡住,一样返回EAGAIN,先去执行后续逻辑,直到管道有空间。
如果所有写端文件描述符都被关闭,read返回0.
如果所有读端文件描述符都被关闭,说明没有接收端,此时写端没意义,则会发送SIGPIPE信号终止进程。
原子性问题:
当PIPE_BUF能一次接收完全部数据时,此时就是原子性,即数据一次性全打入缓冲区。
如果数据量很大,此时要分多次打印,又由于我们是无阻塞状态,此时的整体数据写入的原子性了。
管道的特点
1.管道自带的同步与互斥机制
一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行读写,因此管道也称为一种临界资源。
所以当管道被多个进程使用时,这个临界资源是需要保护的,如果同一时间多个进程都对管道进行操作,同时读写,交叉读写,就会乱。
内核中对管道操作进行同步与互斥:
- 同步:两个或两个以上的进程按照先后次序运行,比如A任务的运行依赖于B任务产生的数据。
- 互斥:一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
同步就是我资源的完成,是有特定的进程接收,等会写好后,这个进程先运行,把数据接收了以后,后面排队的进程继续写入,重复上述步骤
互斥就是说,只有一个进程能使用这个公共资源,读也只能一个人读,写也是同理,不能同时有多个进程一起使用公共资源。
2.管道的生命周期随文件
管道也是通过文件进行通信的,如上面的图示一样,管道依赖于文件系统,文件描述符全部关闭后,此时管道就没用了,然后就被回收,所以说当使用管道的文件都关闭后,管道关闭
3.管道提供的是流式服务
进程A写入管道的数据,进程B每次读取都是任意的,这就是流式服务。
流式服务:数据没有明确的分割。
数据报服务:数据有明确的分割,数据按报文段拿。
4.管道是半双工通信
单工通信:一端固定为发送端,一端固定为接收端
半双工通信:双方都可以当发送端或者接收端,但是同一时间只能有一边进行传输
全双工通信:双方都可以当发送端或者接收端,同一时间可以一起传输 。
管道是半双工的,但是上面我们只看出他是一端接收一端发送,我们可以用两个管道就能实现
管道的四种情况
- 1.写端不写,读端读完后,没数据就挂起。直到写端写数据,读端唤醒
- 2.读端不读,写端一直写,管道写满后,写端就会挂起,直到数据被读取,写端唤醒。
- 3.写端进程将数据写完后将写端关闭,读端会把数据读取完后继续执行逻辑,而不是被挂起。
- 4.读端进程关闭,此时没人读,写端就没有必要写了,此时写端进程也会被关闭。
前两个情况很好说明了,管道自带的同步和互斥机制,读端和写端是有步调的,一个写一个读,没有两个一起动这个说法。这就是同步,互斥就是说二者同一时间只能一人使用管道。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//1.创建管道
int fd[2] = {0};
if(pipe(fd) < 0)
{
perror("pipe error");
return 1;
}
//2.创建子进程
pid_t id = fork();
if(id == 0)
{
//关闭释放操作一定要清清楚楚
//子进程
close(fd[0]);//关闭读端
int count = 10;
const char* msg = "father,this is child,over....";
while(count--)
{
write(fd[1],msg,strlen(msg));
sleep(1);
}
close(fd[1]);
exit(0);
}
//3.父进程关闭读端,子进程被强制关闭(看状态码)
close(fd[1]);//关闭写端
close(fd[0]);//强制关闭读端。
int status;
waitpid(id,&status,0);
printf("child get signal:%d\n",status);
return 0;
}
我们发现子进程和父进程这次没有打印数据,因为父进程读端关闭后,此时没人读,子进程被信号13杀掉,信号13:SIGPIPE
管道的大小
1.使用指令:
ulimit -a
下面有一个pipe size:512 * 8 = 4096字节。
2.自行测试
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//1.创建管道
int fd[2] = {0};
if(pipe(fd) < 0)
{
perror("pipe");
return 1;
}
//2.创建子进程,并且子进程一直写1字节,然后每次打印count
pid_t pid = fork();
if(pid == 0)
{
//子进程
char a = 'a';
//关闭读端
close(fd[0]);
int count = 0;
while(1)
{
count++;
write(fd[1],&a,1);
printf("%d\n",count);
}
//关闭写端
close(fd[1]);
exit(0);
}
//3.父进程
close(fd[1]);//关闭写端
waitpid(pid,NULL,0);
close(fd[0]);
return 0;
}
结果如下:
我们发现管道的大小有65536字节而不是4096字节。
命名管道
我们发现匿名管道,好像是和子进程和父进程相关,所以是有亲缘关系的进程才能使用。
但是如果我们想要两个不想干的进程使用管道通信呢?这时就要用到命名管道了。
- 命名管道实际就是一个文件,两个进程通过这个管道文件进行数据的运输和获取。
- 匿名管道就像C++中,匿名对象一样,用完就丢,而命名管道实际是创造了一个文件映像出来。
- 但是命名管道虽然有文件,但是大小依然为0,和匿名管道一样二者都是不刷新到磁盘中的。内存中用完就可以丢了。
命令行创建命名管道
mkfifo指令:
发现我们上面出现了一个fifo文件,并且文件类型为p ,pipe的意思
并且字节数为0,说明它没有占用磁盘。
我们用一个脚本指令,持续向fifo文件输入数据,然后另一个终端读取这个fifo的数据。
两个终端的指令如下:
while :; do echo "hello fifo";sleep 1; done > fifo
cat < fifo
持续打印hello fifo进入fifo,另一个把fifo中的数据打印出来。
在代码中创建命名管道
函数名也是mkfifo原型如下:
int mkfifo(const char *pathname, mode_t mode);
pathname:1.以路径给出,则在指定路径下创建一个命名管道。
2.以文件名给出,则在当前路径在当前路径下。
mode:创建命名管道文件的权限。
mode 666如下:
但是由于有umask(文件默认掩码)的影响,实际创建出来的权限会变成 mode&(~umask) , 就是说掩码的值为多少,在那个比特位的值都会变为0,如果umask == 2,上面的权限就会变为
mode 664如下:
所以可以把umask的值直接设置为0,在创建文件前使用umask(0)函数把umask的值提前关掉。
mkfifo的返回值:
创建成功,返回0,。
失败,返回-1.
实现代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
//设置宏变量文件名
#define FILE_NAME "myfifo"
int main()
{
//设置掩码值
umask(0);
//创建命名管道
if(mkfifo(FILE_NAME,0666) < 0)
{
perror("mkfifo");
return 1;
}
//创建成功后的逻辑 ......
return 0;
}
实现server 和 client 通信
服务器逻辑:
1.服务端创建一个命名管道
2.关闭写端
3.等待数据
#include "common.h"
int main()
{
//创建命名管道
umask(0);
if(mkfifo(FILE_NAME,0666) < 0)
{
perror("mkfifo");
return 1;
}
//管道创建成功
//读端打开管道文件
int fd = open(FILE_NAME,O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char msg[128];
while(1)
{
//读端重复读取命名管道
msg[0] = '\0';
ssize_t size = read(fd,msg,sizeof(msg)-1);
if(size > 0)
{
//说明此时管道中存在数据
msg[size] = '\0';
printf("client say: %s\n",msg);
}
else if(size == 0)
{
printf("client quit\n");
break;
}
else
{
printf("read error");
break;
}
}
close(fd);
return 0;
}
对于客户端来说,因为服务端创建了管道,此时客户端以写的方式打开管道写入即可。服务器一直在读取的。
我们启动两个终端,一个终端用作服务器,一个终端用作客户端,我们发现,启动服务器server后,客户端中myfifo文件创建,此时打开启动客户端。
我们客户端输入信息,此时我们的服务器就收到信息了。 这就是命名管道对不同进程的通信形式。
基于server实现一个计算器功能
我们的client就不用改了,因为只是发信息,而server因为要处理计算任务,所以逻辑不是显示了,而是计算后显示,所以就需要server在收到信息时做出相应的逻辑。
#include "common.h"
int main()
{
//创建命名管道
umask(0);
if(mkfifo(FILE_NAME,0666) < 0)
{
perror("mkfifo");
return 1;
}
//管道创建成功
//读端打开管道文件
int fd = open(FILE_NAME,O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char msg[128];
while(1)
{
//读端重复读取命名管道
msg[0] = '\0';
ssize_t size = read(fd,msg,sizeof(msg)-1);
if(size > 0)
{
//说明此时管道中存在数据
//对信息进行解析
msg[size] = '\0';
printf("client say: %s\n",msg);
char* label = "+-*/%";
char* p = msg;
int flag = 0;
while(*p)
{
//获取是什么符号
switch(*p)
{
case '+':
flag = 0;
break;
case '-':
flag = 1;
break;
case '*':
flag = 2;
break;
case '/':
flag = 3;
break;
case '%':
flag = 4;
break;
}
p++;
}
//解析出两个数字
char* snum1 = strtok(msg,"+-*/%");
char* snum2 = strtok(NULL,"+-*/%");
int num1 = atoi(snum1);
int num2 = atoi(snum2);
int ret = 0;
switch (flag)
{
case 0:
ret = num1 + num2;
break;
case 1:
ret = num1 - num2;
break;
case 2:
ret = num1 * num2;
break;
case 3:
ret = num1 / num2;
break;
case 4:
ret = num1 % num2;
break;
}
//打印计算结果
printf("%d %c %d = %d\n",num1,label[flag],num2,ret);
}
else if(size == 0)
{
printf("client quit\n");
break;
}
else
{
printf("read error");
break;
}
}
close(fd);
return 0;
}
由于我们只实现了计算逻辑,而对其他信息的逻辑没进行处理,所以会导致输入其他内容直接出错。
服务器执行命令行指令
#include "common.h"
int main()
{
//创建命名管道
umask(0);
if(mkfifo(FILE_NAME,0666) < 0)
{
perror("mkfifo");
return 1;
}
//管道创建成功
//读端打开管道文件
int fd = open(FILE_NAME,O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char msg[128];
while(1)
{
//读端重复读取命名管道
msg[0] = '\0';
ssize_t size = read(fd,msg,sizeof(msg)-1);
if(size > 0)
{
//说明此时管道中存在数据
msg[size] = '\0';
printf("client say: %s\n",msg);
if(fork() == 0)
{
//进程替换,使用对应的指令
execlp(msg,msg,NULL);
exit(1);
}
waitpid(-1,NULL,0);
}
else if(size == 0)
{
printf("client quit\n");
break;
}
else
{
printf("read error");
break;
}
}
close(fd);
return 0;
}
用命名管道实现文件拷贝
先来介绍一下为什么要学这个实现文件拷贝,事实就是我们文件的拷贝,其实就是网络在做的事情,网络就是从客户端或者服务器,把文件粘贴到另一端的过程。
所以这里实现文件拷贝,其实和网络下载的总体道理是差不多的。
逻辑就是我们客户端的一个文件,将数据传到管道,服务器端通过读取数据,在服务器下面创建一个副本。
服务器代码如下:
#include "common.h"
int main()
{
//创建命名管道
umask(0);
if(mkfifo(FILE_NAME,0666) < 0)
{
perror("mkfifo");
return 1;
}
//管道创建成功
//读端打开管道文件
int fd = open(FILE_NAME,O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
int fdout = open("file-copy.txt",O_CREAT | O_WRONLY,0666);
if(fd < 0)
{
perror("open");
return 3;
}
char msg[128];
while(1)
{
//读端重复读取命名管道
msg[0] = '\0';
ssize_t size = read(fd,msg,sizeof(msg)-1);
if(size > 0)
{
//说明此时管道中存在数据
write(fdout,msg,size);
}
else if(size == 0)
{
printf("client quit\n");
break;
}
else
{
printf("read error");
break;
}
}
close(fd);
close(fdout);
return 0;
}
客户端代码如下:
#include "common.h"
int main()
{
//打开管道
int fd = open(FILE_NAME,O_WRONLY);
if(fd < 0)
{
perror("open");
return 1;
}
//打开file.txt文件
int fdin = open("file.txt",O_RDONLY);
if(fdin < 0)
{
perror("open");
return 3;
}
//写入数据
char msg[128];
while(1)
{
msg[0] = '\0';
size_t size = read(fdin,msg,sizeof(msg));
if(size > 0)
{
msg[size - 1] = '\0';
//信息写入管道
write(fd,msg,size);
}
else if(size == 0)
{
printf("read end of file\n");
break;
}
else
{
printf("read error\n");
break;
}
}
close(fdin);
close(fd);
return 0;
}
结果如下:
我们发现此时就出现了copy文件了。
二者内容一样。
system V进程间通信
和管道一样,都是为了让不同的进程看到同一份资源。
三种通信方式:
1. system V共享内存
2. system V消息队列
3. system V信号量
共享内存和消息队列是用来进行数据传输的,而信号量则是为了进程间同步与互斥设计的。
system V共享内存
共享内存,顾名思义就是在物理内存申请一块共享内存,这块内存在每一个进程中的虚拟内存中都建立了映射,之后不管是任何进程只要使用这个共享内存,映射后都是同一个物理内存,所以实现了不同进程看到同一份信息。
共享内存的数据结构
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
因为共享内存不止一个,并且使用不同共享内存的进程也可能是不同的,所以要把这些数据管理起来,可以看到我们结构体中存在了cpid 和 lpid 这两个id 就是记录创造共享内存的id和最近使用共享内存id的进程。
其中的shm_perm中这里面就是用来识别不同的共享内存的,它的内部结构如下:
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
可以看到里面存在一个key变量,这个变量就类似于pid,pid用来识别不同的进程,key就是用来识别不同的共享内存。
共享内存的建立和释放
建立:
- 在物理内存中申请共享内存的空间。
- 申请到的共享内存的物理地址和创建的进程虚拟内存建立映射关系。
释放:
- 把建立好的映射关系都去掉。
- 释放共享内存的内存空间。
共享内存的创建
函数:
int shmget(key_t key, size_t size, int shmflg);
参数说明:
- key就是共享内存的id,但是是在系统层面的。
- size表示要创建的共享内存的大小。
- shmflg表示要创建的共享内存的方式。
返回值说明:
- 调用成功,返回一个用户层面的共享内存id。
- 调用失败,返回-1.
attention:
句柄的意思就是用来表达某种资源能力的东西,shmget返回的用户层面的共享内存id,就是一个句柄,后续要对共享内存进行操作,都要搭配这个句柄进行使用。
ftok函数
我们的key需要用ftok函数进行获取。
key_t ftok(const char *pathname, int proj_id);
传入一个pathname和proj_id 此时会转换出一个key值,pathname必须要存在且可取。
所以说这个ftok函数有什么用?如果让我们自己指定key值,用数字一个个来写有时候我们是记不住的。而用pathname + id 的方法构建出一个key,此时记忆路径名会相对简单。
shmget的第三个参数
上述两种方法就是:不存在都创建,上面那个存在则返回这个共享内存,下面的则是存在出错返回。
我们可以使用ftok和shmget函数创建共享内存观察一下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/ubuntu/blog-code"
#define PROJ_ID 0x6666//整数标识符
#define SIZE 4096
int main()
{
//1.ftok获取key
key_t key = (PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
//2.shmget 获取 用户级id句柄
int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL);
if(shmid < 0)
{
perror("shmget");
return 1;
}
//3.打印 key 和 id句柄
printf("key : %x\n",key);
printf("shmid : %d\n",shmid);
return 0;
}
使用ipcs,会默认弹出消息队列,共享内存以及信号量相关的信息
attention:key是内核层面用来保证共享内存唯一性的,shmid是用户层面保证唯一性的。
类似于fd 和 FILE*的关系,一个是给用户使用的,一个是系统使用的
共享内存的释放
和其他文件一样,共享内存也是要释放的,但是我们发现上述我们在程序结束后,共享内存依然存在,也就变相的说明共享内存不是进程管理的,虽然由进程创建但是不跟随进程的生命周期,所以共享内存的生命周期是跟随系统的。
释放共享内存的两种方法:
1.使用命令释放共享内存
ipcrm -m + shmid 删除指定shmid 的 共享内存
ipcrm -m 1
attention: 要用的是 shmid 因为我们是用户层。
2.使用程序释放共享内存资源
shmctl函数:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
shmid :用户层使用的共享内存标识符
cmd :控制动作
buf :获取或设置控制共享内存的数据结构
返回值说明:
shmctl成功,返回0。
失败,返回-1 。
第二个参数可传入的选项:
释放共享内存代码测试:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/ubuntu/blog-code"
#define PROJ_ID 0x6666//整数标识符
#define SIZE 4096
int main()
{
//1.ftok获取key
key_t key = (PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
//2.shmget 获取 用户级id句柄
int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL);
if(shmid < 0)
{
perror("shmget");
return 1;
}
//3.打印 key 和 id句柄
printf("key : %x\n",key);
printf("shmid : %d\n",shmid);
sleep(2);
//4.释放共享内存
shmctl(shmid,IPC_RMID,NULL);
sleep(2);
return 0;
}
我们发现这一次的共享内存直接被释放了。
使用脚本指令:持续的观察共享内存的状况
while :; do ipcs -m;echo "###################################";sleep 1;done
共享内存的关联
shmat函数
shared memory attach, 贴上共享内存,就可以当做,共享内存的关联了。
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
shmid:用户级共享内存标识符
shmaddr:指定共享内存映射到进程地址空间的某一位置,通常设置为NULL,让内核决定
shmflg:关联共享内存时设置的某些属性。
返回值说明:
调用成功,返回一个共享内存映射到进程地址空间中的起始地址
调用失败,返回(void*)-1.
第三个参数的选项:
进行共享内存关联的代码测试:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/ubuntu/blog-code" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小
int main()
{
//1.ftok函数做key
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("key");
return 1;
}
//2.shmget制造共享内存
int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);
if(shmid < 0)
{
perror("shmid");
return 1;
}
printf("key:%x\n",key);
printf("shmid:%d\n",shmid);
//3.shmat关联共享内存
printf("attach begin\n");
sleep(2);
//以读写打开共享内存
char* mem = shmat(shmid,NULL,SHM_RDONLY);
if(mem == (void*)-1)
{
perror("shmat");
return 1;
}
printf("attach end\n");
sleep(2);
//4.释放共享内存
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
上面可以看到我们d
attention:这里的权限要注意,普通用户不能打开共享内存,所以切换为root用户
当然也可以在创建共享内存时,就给它设置好权限,就是更改shmget函数传参如下:
int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);
发现此时我们除了root用户也可以执行shmat命令了。
共享内存去关联
int shmdt(const void *shmaddr);
shared memory detach 取消关联。
参数说明:
shmaddr:共享内存在进程空间的起始地址,在上面shmat中的返回值
返回值说明:
调用成功返回0,失败返回-1
解除关联的代码测试:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/ubuntu/blog-code" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小
int main()
{
//1.ftok函数做key
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("key");
return 1;
}
//2.shmget制造共享内存
int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);
if(shmid < 0)
{
perror("shmid");
return 1;
}
printf("key:%x\n",key);
printf("shmid:%d\n",shmid);
//3.shmat关联共享内存
printf("attach begin\n");
sleep(2);
//以读写打开共享内存
char* mem = shmat(shmid,NULL,SHM_RDONLY);
if(mem == (void*)-1)
{
perror("shmat");
return 1;
}
printf("attach end\n");
sleep(2);
//解除关联
printf("detach begin\n");
sleep(2);
shmdt(mem);
printf("detach end\n");
sleep(2);
//4.释放共享内存
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
上图我们发现我attach后 nattch数量为1,detach后 nattch 数量为0,最后共享内存被释放。
attention:
共享内存的解关联不是释放共享内存,只是当前进程和共享内存之间的关系解除,最后还是需要释放共享内存
用共享内存实现serve & client 通信
我们要实现的就是让客户端和服务器都连接到同一个共享内存,服务器创建并连接,客户端负责连接。
服务器代码:
#include "common.h"
int main()
{
//1.创造key值
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
//2.创建共享内存
int shmid = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);
if(shmid < 0)
{
perror("shmget");
return 1;
}
//3.连接并死循环等待client连接
char* mem = shmat(shmid,NULL,0);
if(mem == (void*)-1)
{
perror("shmat");
return 1;
}
while(1)
{
//......
}
//4.解除连接并释放共享内存
int ret = shmdt(mem);
if(ret < 0)
{
perror("shmdt");
return 1;
}
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
客户端代码:
#include "common.h"
int main()
{
//1.创造key值
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
//2.创建共享内存
int shmid = shmget(key,SIZE,IPC_CREAT);
if(shmid < 0)
{
perror("shmget");
return 1;
}
//3.连接并死循环等待client连接
char* mem = shmat(shmid,NULL,0);
if(mem == (void*)-1)
{
perror("shmat");
return 1;
}
while(1)
{
//......
}
//4.解除连接
int ret = shmdt(mem);
if(ret < 0)
{
perror("shmdt");
return 1;
}
return 0;
}
common.h:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/ubuntu/blog-code" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小
二者代码的区别就在于:服务器是创建并且打开,所以要释放 ,客户端只用打开就可以了。
但是注意我们的共享内存可能释放不了,但是可以用命令行关闭,因为我们代码死循环卡在一半了
执行情况:
可以发现我们此时客户端和服务器都连接上了共享内存 (nattch为2)。
客户端向共享内存不断的写入数据:
int i = 0;
while(1)
{
//向共享内存写入数据
mem[i] = 'A' + i;
i++;
mem[i] = '\0';
sleep(1);
}
服务器不断读取客户端数据:
while(1)
{
//不断从共享内存读取数据
printf("client say: %s\n",mem);
sleep(1);
}
结果:
共享内存和管道对比
共享内存创建后,只用向共享内存直接添加数据就好,而管道创建好后,是有文件创立的,我们要对文件写入和读取还是需要用系统调用的例如:open write read......
管道通信步骤:
由上图看出我们管道在一次通信中,有4次拷贝
第一次拷贝:从输入设备或者文件复制数据到服务器的缓冲区。
第二次拷贝:从服务器的缓冲区的数据复制到管道中。
第三次拷贝:从管道中将数据拷贝到客户端的缓冲区中。
第四次拷贝:从客户端的缓冲区中拷贝到要输出的设备或文件中。
共享内存通信步骤:
由上图我们看出我们的文件传输只用了两步:
首先我们创建好共享内存,之后的读取和输入都是直接到位的。
所以共享内存是所有进程间通信方式中最快的一种,因为该通信方式只需要两次拷贝。
缺点:我们知道管道自带同步与互斥机制,同步:输入给谁谁从管道拿。互斥:同一时间只有一个进程使用管道。共享内存没有任何保护机制。
system V消息队列
信息队列的基本原理
顾名思义:队列就是在系统中创建了一个队列,多个进程可以向这个队列发送数据,获取数据时都从队头获取数据,发送数据都是发送到队尾和队列一样。
怎么判别数据是给谁的,在数据块类型。
消息队列的数据结构
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
我们发现第一个变量依然是结构体 ipc_perm 和共享内存一样的。
内容如下:
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
消息队列的创建:
int msgget(key_t key, int msgflg);
我们发现和共享内存一样也需要key,所以也要用到ftok函数制作key。
第二个参数和共享内存的第三个参数一样。设定消息队列的信息。
创建成功返回一个用户级标识符,失败返回-1.
消息队列的释放:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
第一个参数:msqid 是 msgget的返回值。
第二个参数:cmd 是 要怎么操控信息队列
第三个参数:buf是消息队列结构体的相关数据结构
发送数据:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
第一个参数:消息队列用户级标识符
第二个参数:msgp 待发送的数据块
第三个参数:msgsz 待发送的数据块的大小
第四个参数:发送数据块的模式,一般填0就好
返回值:成功返回0,失败返回-1.
attention:第二个参数必须为以下结构
struct msgbuf{
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
第二个成员就是我们要发送的数据,当使用时可以自己改变大小。
获取数据:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
第一个参数:消息队列用户级标识符
第二个参数:msgp 表示获取的数据块,是输出型参数
第三个参数:msgsz表示获取的数据块的大小
第四个参数:msgtyp表示要接收数据块的类型
第五个参数:获取数据块的方式
返回值:调用成功返回实际获取到mtext数组中的字节数,失败返回-1.
system V信号量:
概念:
我们知道上述两种共享方式,消息队列和共享内存,如果多进程竞争使用,是不保证安全的。
互斥资源 临界资源:多进程竞争使用一个资源。这个资源一次只能一个进程正在使用。
在进程中涉及到临界资源的程序段叫临界区。
IPC资源必须删除,不会自动删除,因为IPC资源的生命周期跟随内核
信号量的数据机构
struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__kernel_time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};
我们可以看到信号量的第一个成员变量也是ipc_perm
信号量相关函数
信号量集的创建
int semget(key_t key, int nsems, int semflg);
我们可以看到有key 所以也要用 ftok生成一个key值。
第二个参数:nsems 代表要创建信号量的个数
第三个参数:semflg 表示怎么创建信号量
返回值:创建成功返回用户级标识符,失败返回-1.
信号量集的删除
int semctl(int semid, int semnum, int cmd, ...);
信号量集的操作
int semop(int semid, struct sembuf *sops, unsigned nsops);
进程互斥
我们进程共享资源是爽了,但是也有新的问题,就是我们进程共用的临界资源,若是不保护,就会出现数据不一致的情况。
信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量
假设我们有100字节的数据,如果算他25字节一份,此时就有4份,那就可以用4个信号量进行标识。
信号量本质就是一个计数器,在二元信号量中,信号量的个数为1(相当于将临界资源看成一整块)。
由上面的伪代码中我们可以看到,我们有一个sem,在sem==1时,我们进入的是 sem--这个逻辑,代表这个信号量我拿走了,其他进程此时进来就发现sem == 0走到else中挂起。而使用信号量的这个进程完成操作后对sem++就相当于将信号量归还了。
这样就保证了同一时间内只有一个进程能访问临界区。
PV操作
我们把计数器sem--的操作叫做P操作,sem++叫做V操作,P就是申请信号量,V就是释放信号量。
system V IPC
我们学习了上述三个共享内存,消息队列,信号量后我们发现他们的函数中都有一个ipc perm这个结构体大家都是一样的,所以操作系统维护system V 的东西的时候,就进行了统一管理。