50万县城做地方网站,wordpress 弹窗登录插件,网站原创文章在哪里找,定制旅游网站开发目录 一、关于进程间通信
二、管道
pipe函数
管道的特点
匿名管道
命名管道
mkfifo
三、system v共享内存
shmget函数(创建)
ftok函数(生成key)
shmctl函数(删除)
shmat/dt函数(挂接/去关联)
四、初识信号量 一、关于进程间通信
首先我们都知道#xff0c;进程运…目录 一、关于进程间通信
二、管道
pipe函数
管道的特点
匿名管道
命名管道
mkfifo
三、system v共享内存
shmget函数(创建)
ftok函数(生成key)
shmctl函数(删除)
shmat/dt函数(挂接/去关联)
四、初识信号量 一、关于进程间通信
首先我们都知道进程运行是具有独立性的所以两个进程想通信的话难度较大
进程间通信的本质
让不同的进程看到同一份资源内存空间
这里的能看到的同一块内存不能属于任何一个进程而应该是共享的
进程间通信的目的
为了交互数据、控制、通知等目的
进程间通信的发展
管道
system V进程间通信
POSIX进程间通信
进程间通信分类
管道匿名管道pipe、命名管道
system V IPCsystem V 消息队列、共享内存、信号量
POSIX IPC消息队列、共享内存、信号量、互斥量、条件变量、读写锁 二、管道
管道是非常古老的进程间通信的形式
管道是计算机通信领域的设计者设计得一种单向通信的方式
从一个进程连接到另一个进程的一个数据流称为一个管道
管道通信的背后是进程间通过管道进行通信的 下面举个管道的例子方便理解
pipe函数
需要用到pipe创建管道man查看pipepipe是系统调用接口 需要的头文件是unistd.h
作用是创建管道
参数是pipefd[2]是输出型参数希望通过调用它得到被打开的文件fd
返回值创建成功返回0失败返回-1
代码如下
#include iostream
#include unistd.h
#include assert.h
#include string
#include cstdio
#include cstring
#include sys/types.h
#include sys/wait.husing namespace std;int main()
{// 创建管道int pipefd[2] {0};// 文件描述符0/1/2分别是stdin/stdout/stderr// 这里的pipefd[0]为3表示读端// 这里的pipefd[1]为4表示写端int n pipe(pipefd);assert(n ! -1);// debug下assert起作用release下不起作用// 所以需要(void)n表示n被使用过// 如果不(void)n在release下会被认为n没有被使用(void)n;// 创建子进程pid_t id fork();assert(id ! -1);if (id 0){// 子进程 - 读// 构建单向通信的信道父进程写入子进程读取// 关闭子进程不需要的fd关闭写close(pipefd[1]);char buffer[1024]; // 缓冲区用于读数据while (true) // 一直循环{// ssize_t类型是long intssize_t s read(pipefd[0], buffer, sizeof(buffer) - 1);if (s 0) // 读成功{// read是系统调用所以读取完结尾不会有\0自己加buffer[s] 0;cout 子进程[ getpid() ]获得父进程的message, buffer endl;}}// 最后关闭读close(pipefd[0]);exit(0);}// 父进程 - 写// 构建单向通信的信道父进程写入子进程读取// 关闭父进程不需要的fd关闭读close(pipefd[0]);int count 0; // 发送消息的条数string message 父进程正在发送消息;char send_buffer[1024];while (true){// 构建变化的字符串// 往send_buffer中写入snprintf(send_buffer, sizeof(send_buffer), %s[%d] : %d,message.c_str(), getpid(), count);// 写入write(pipefd[1], send_buffer, strlen(send_buffer));// 每次sleep方便观察sleep(1);}pid_t ret waitpid(id, nullptr, 0);assert(ret 0);(void)ret;// 最后关闭写close(pipefd[1]);return 0;
}
观察运行结果 通过结果可以发现父进程每次写入后子进程都能读取到以管道的方式实现了进程间通信 管道的特点
1、管道是用来进行具有血缘关系的进程进行进程间通信的常用于父子进程
2、管道通过让进程间协同提供了访问控制
访问控制就是指如果父进程写的慢例如上述代码每隔一秒写一次子进程即使没有sleep也只能每隔一秒再读而如果写的很快当把缓冲区写满了在读取前也就不能再继续写了
3、管道提供的是面向流式的通信服务面向字节流需要定制协议进行数据区分后面博客会说到
流式服务如果写的很快读的很慢读的时候一次就可以读一批消息
4、管道是基于文件的文件的生命周期是随进程的所以管道的生命周期也是随进程的
写入的一方的fd如果没有关闭读取的一方有数据就读没有数据就等
写入的一方的fd如果关闭了读取的一方read返回0表示读到了文件的结尾
5、管道是单向通信的就是半双工通信的一种特殊情况
半双工通信就是指两个人通信我发你就不能发你发我就不能发
下面总结四种情况
①写端快读端慢写满就不能再写了
②写端慢读端快管道没有数据时读端必须等待
③写端关闭fd读端read返回0表示读到了文件结尾
④读端关闭fd写端继续写OS会自动终止写端
前两种也就是上面提到的访问控制 匿名管道
下面引入一个例子了解匿名管道的使用一个父进程有5个子进程父进程与每一个子进程都建立对应的管道每个子进程内部都有处理任务的方法如果用户给了一个任务父进程可以给子进程派发该任务让子进程完成该任务也就是实现一个小型的进程池
首先实现Makefile
ProcPool:ProcPool.ccg -o $ $^ -stdc11
.PHONY:clean
clean:rm -f ProcPool
ProcPool.cc(.cc和.cpp一样)代码
#include iostream
#include cstdlib
#include ctime
#include sys/types.h
#include sys/wait.h
#include unistd.h
#include cassert
#include vector#include task.hppusing namespace std;#define PROCESS_NUM 5int waitcommand(int waitfd, bool q)
{//uint32_t是4字节uint32_t command 0;//从文件描述符waitfd中读取读取的内容写到command里ssize_t s read(waitfd,command,sizeof(command));if(s 0){q true;return -1;}assert(s sizeof(uint32_t));return command;
}//给一个进程id通过文件描述符fd发送命令command
void sendAndExec(pid_t id,int fd, uint32_t command)
{write(fd,command,sizeof(command));cout 调用进程[ id ] 执行: desc[command] endl;
}int main()
{//将task中的方法装载进来load();//pair键值对pid、pipefdvectorpairpid_t, int slots;//创建多个进程for(int i 0; i PROCESS_NUM; i){//创建管道int pipefd[2] {0};int n pipe(pipefd);assert(n 0);//这里的(void)n与前一个例子一样的作用//release下assert就没用了n会被认为没有使用(void)n;pid_t id fork();assert(id ! -1);//子进程进行读取if(id 0){//子进程关闭写端close(pipefd[1]);while(true){//false表示不退出bool q false;//子进程等命令,不发命令就阻塞int command waitcommand(pipefd[0], q);//执行对应的命令if(command 0 command tasksize()){//执行call中command对应的方法call[command]();}else{cout 非法command: command endl;}}exit(1);}//父进程关闭读端close(pipefd[0]);//存每个子进程的pidslots.push_back(pairpid_t,int(id,pipefd[1]));}//父进程派发任务//让数据源更加随机srand((unsigned long)time(nullptr) ^ getpid() ^ 89745213L);while(true){//下面屏蔽的部分是自动选择任务不需要人为输入//选择任务//int command rand() % tasksize();//选择进程//int selectID rand() % slots.size();//将任务交给子进程//sendAndExec(slots[selectID].first,slots[selectID].second,command);//sleep(1);int num;int command;cout ########################################## endl;cout ##### 1.展示功能 2.发送命令 ##### endl;cout ########################################## endl;cout 请输入你的选择;cin num;if(num 1)show();else if(num 2){cout 请选择你的命令;//选择任务cin command;//选择进程int selectID rand()%slots.size();//将任务交给子进程sendAndExec(slots[selectID].first,slots[selectID].second,command);}}//关闭fd子进程会退出for(const auto e : slots){close(e.second);}//等待子进程退出回收子进程信息for(const auto e : slots){//默认在阻塞状态去等待子进程waitpid(e.first,nullptr,0);}return 0;
}
Task.hpp
(以hpp结尾.cc的实现代码混入.h头文件当中定义与实现都包含在同一文件)
#pragma once#include iostream
#include string
#include unistd.h
#include vector
#include functional
#include unordered_map//这里表示func这个函数类型返回值都是void没有参数
typedef std::functionvoid() func;
//call中存各种func类型的函数
std::vectorfunc call;
//desc是描述各个命令编号所对应的任务名称的
std::unordered_mapint, std::string desc;void execadd()
{std::cout 程序[ getpid() ] 执行加法操作的任务 std::endl;
}void execsub()
{std::cout 程序[ getpid() ] 执行减法操作的任务 std::endl;
}void execmul()
{std::cout 程序[ getpid() ] 执行乘法操作的任务 std::endl;
}void execdiv()
{std::cout 程序[ getpid() ] 执行除法操作的任务 std::endl;
}void load()
{//即0号编号的任务是execadddesc.insert({call.size(),execadd : 加法});call.push_back(execadd);desc.insert({call.size(),execsub : 减法});call.push_back(execsub);desc.insert({call.size(),execmul : 乘法});call.push_back(execmul);desc.insert({call.size(),execdiv : 除法});call.push_back(execdiv);
}void show()
{for(const auto e : desc){std::cout e.first - e.second std::endl;}
}int tasksize()
{return call.size();
}人为输入的结果如下 自动选择任务的结果如下 小型的进程池实现完毕 命名管道
关于匿名管道和命名管道上面所举的例子都是匿名管道匿名管道缺点是只能由有亲缘关系的进程进行通信如果想要两个毫不相关的进程进行通信就需要用到命名管道了
匿名管道和命名管道一样都是两个进程通过同一份文件进行通信只不过看到同一份文件的手段、途径是不一样的
匿名管道是子进程通过继承父进程的方式打开同一份文件
命名管道是创建一个管道文件并且让两个不相关的进程打开同一个文件 创建管道文件需要用到mkfifo可以在指定路径下创建命名管道
man查看 下面就是创建一个name_pipe的管道文件 圈中的p就是指管道文件
下面具体演示现象
首先复制SSH渠道形成左右两个窗口即有两个毫不相关的进程在同一个路径下 左边的窗口先往name_pipe中重定向一句话hello写到name_pipe中 这时由于左边的窗口写了内容但是右边窗口并没有打开所以此时处于阻塞状态
所以此时右边窗口cat从管道中把数据读取出来 此时完成了一个进程向另一个进程通过管道的方式写入消息的过程
如果想删除管道文件可以rm也可以unlink mkfifo
man 3 mkfifo查看mkfifo函数 头文件
sys/types.h和sys/stat.h
参数
第一个参数pathname表示特定的路径
第二个参数modeopen时也见过表示需要指定权限例如06666表示rw-
返回值
mkfifo成功了返回0小于0表示创建失败 下面用样例更清楚的理解命名管道的知识
创建两个.cc.cc、.cxx、.cpp是一样的文件分别是client.cc和server.cc分别表示服务器端和顾客端
common.hpp表示client.cc和server.cc必须包含的文件
log.hpp表示每次执行完操作打印提示信息
makefile当然也是有的方便操作
下面看具体演示首先复制一个ssh渠道让两个毫不相干的进程都在一个路径下具体代码在演示结果的下面 左边窗口当做server端右边窗口当做client端
左边先运行server 这时打印显示创建管道文件成功再在右边窗口运行client 这时server端会显示打开管道文件成功
接着在client端输入信息分别输入你好再见 这时serve端也会显示对应输入的信息最后client端Ctrl c 退出进程 server端会显示左边红框的信息整个过程结束 下面是具体代码
Makefile
.PHONY:all
all:client serverclient:client.ccg -o $ $^ -stdc11
server:server.ccg -o $ $^ -stdc11.PHONY:clean
clean:rm -f client server
common.hpp
//如果_COMMON_H_不存在就define
#ifndef _COMMON_H_
#define _COMMON_H_#include iostream
#include string
#include cstdio
#include cstring
#include unistd.h
#include sys/types.h
#include sys/stat.h
#include fcntl.h
#include log.hppusing namespace std;#define SIZE 128
//设置MODE权限为0666
#define MODE 0666
//设置路劲path为当前路径的fifo.ipc文件
string path ./fifo.ipc;#endif
log.hpp
#ifndef _LOG_H_
#define _LOG_H_#include iostream
#include ctimestd::ostream log(std::string message)
{std::cout | (unsigned)time(nullptr) | message;return std::cout;
}#endif
client.cc
#include common.hppint main()
{//获取管道文件int fd open(path.c_str(), O_WRONLY);if(fd 0){perror(open);exit(1);}//通信过程string buffer; while(true){cout 请输入信息;getline(cin, buffer);write(fd,buffer.c_str(),buffer.size());}//关闭fdclose(fd);return 0;
}
server.cc
#include common.hppint main()
{//创建管道文件if(mkfifo(path.c_str(), MODE) 0){perror(mkfifo);exit(1);}log(创建管道文件成功) endl;//文件操作int fd open(path.c_str(), O_RDONLY);if(fd 0){perror(open);exit(2);}log(打开管道文件成功) endl;//编写通信代码char buffer[SIZE];while(true){//清空buffermemset(buffer,\0,sizeof(buffer));//从管道文件中读取//sizeof(buffer)-1是因为系统接口不考虑\0结尾ssize_t s read(fd,buffer,sizeof(buffer) - 1);if(s 0){//读取成功cout client : buffer endl;}else if(s 0){//读到了文件结尾cerr 读到文件结尾,client和server都退出 endl;break;}else{//读取失败perror(read);break;}}//关闭文件close(fd);log(关闭管道文件成功) endl;//通信完毕就删除文件unlink(path.c_str());log(删除管道文件成功) endl;return 0;
} 三、system v共享内存
操作系统提供了共享内存共享内存的本质
共享内存 共享内存块 共享内存对应的内核数据结构
共享内存映射到各自进程的地址空间后不用经过系统调用直接可以访问意味着双方进程要通信直接进行内存级的读和写即可
shmget函数(创建)
首先共享内存需要用到shmget函数作用是创建并获取共享内存
man 2 shmget查看 包含了两个头文件
sys/ipc.h和sys/shm.h
函数参数
第一个参数keykey_t其实就是32位的整数类型是用于保证通信的双方进程看到的是同一份共享内存key在系统是唯一的通信双方使用同一个key只要key值相同通信双方就是看到了同一个共享内存
第二个参数size表示要创建的共享内存有多大
第三个参数shmflg一般由两种选项IPC_CREAT、IPC_EXCL
IPC_CREAT单独使用时如果底层已经存在就获取并返回它如果底层不存在就创建并返回它
IPC_EXCL单独使用是无意义的
IPC_CREAT、IPC_EXCL共同使用如果底层不存在创建并返回如果底层已经存在出错返回
创建共享内存要有权限还需 | 0666
函数返回值
成功则返回一个合法的标识符失败就返回-1 下面具体讲解关于第一个参数key的相关知识
ftok函数(生成key)
为了能够形成唯一的key值需要用到函数ftok ftok其实是一套算法只是为了将pathname和proj_id联合起来形成一个唯一值
ftok包含头文件sys/types.h和sys/ipc.h
参数pathname是路径proj_id是项目id一般是0~255之间
ftok的返回值成功就返回key失败返回-1
下面是ftok的使用
shmserver.cc shmclient.cc common.hpp 此时运行代码观察shmserver.cc和shmclient.cc中创建出来的key值 通过结果发现key值相同 共享内存的声明周期是随内核的
查看共享内存ipcs -m 上图的perm就是权限nattch与该共享内存关联的进程个数status是状态
手动删除共享内存ipcrm -m [shmid] 删除后shmid为0的共享内存就被删除了
这是手动删除共享内存比较麻烦也有自动删除共享内存的函数
shmctl函数(删除)
man查看shmctl 包含头文件sys/ipc.h和sys/shm.h
函数参数
第一个参数shmid就是创建的共享内存用户管理的id
第二个参数cmd有不同的选项对这个共享内存有不同的操作方案删除一般用IPC_RMID
IPC_RMID即使有进程与当前共享内存的shm挂接依旧删除共享内存
第三个参数通常用于获取共享内存的属性一般只进行删除操作时设为空即可
函数返回值
失败会返回-1成功会返回0
使用如下shmid是自己创建的 shmat/dt函数(挂接/去关联)
at就是attch的意思
man shmat 包含头文件sys/types.h和sys/shm.h
shmat函数参数
第一个参数shmid就是要挂接的共享内存的用户管理的id
第二个参数shmaddr就是要挂接这个共享内存的虚拟地址由于虚拟地址我们并不清楚所以一般设为空让OS帮我们自由挂接
第三个参数shmflg就是挂接共享内存的挂接方式例如SHM_RDONLY只读如果是0表示默认
shmat函数返回值
成功时会返回挂接成功的共享内存的地址虚拟地址失败返回-1
shmdt函数参数
shmaddr就是刚刚shmat函数成功后的返回值
shmdt函数返回值
成功返回0失败返回-1 共享内存总结的结论
只要通信双方使用共享内存一方直接向共享内存中写入另一方马上就能看到对方写入的数据所以共享内存是所以进程间通信中速度最快的因为不需要过多的拷贝
与管道不同共享内存缺乏访问控制所以会带来并发问题 由于共享内存缺乏访问控制而管道是有访问控制的所以我们可以借助管道使得共享内存拥有访问控制
下面例子具体演示代码下演示结果的下方
有以下文件 首先复制SSH渠道使得两个窗口都处于同一路径下一端作为server端一端作为client端 接着make创建两个可执行文件 左边窗口当做server端右边当做client端 左边窗口server端显示没有数据等待中等待右边窗口client端输入数据
右边输入数据后结果如下 最后想退出时client端输入quit即可 下面是上述例子的代码部分
Makefile
.PHONY:all
all:shmclient shmservershmclient:shmclient.ccg -o $ $^ -stdc11
shmserver:shmserver.ccg -o $ $^ -stdc11.PHONY:clean
clean:rm -f shmclient shmserver
log.hpp:
#ifndef _LOG_H_
#define _LOG_H_#include iostream
#include ctimestd::ostream log(std::string message)
{std::cout | (unsigned)time(nullptr) | message;return std::cout;
}#endif
common.hpp:
#pragma once#include iostream
#include cstdio
#include unistd.h
#include cstring
#include sys/ipc.h
#include sys/shm.h
#include sys/types.h
#include sys/stat.h
#include cassert
#include fcntl.h#include log.hppusing namespace std;#define PATH_NAME /home/fcy
#define PROJ_ID 0x55
//共享内存的大小最好是页的整数倍(4096)
#define SHM_SIZE 4096
#define FIFO_NAME ./fifo//由于共享内存没有访问控制
//所以借助管道实现访问控制
class Init
{
public:Init(){umask(0);int n mkfifo(FIFO_NAME,0666);assert(n 0);(void)n;log(创建管道文件成功);}~Init(){unlink(FIFO_NAME);log(删除管道文件成功);}
};#define READ O_RDONLY
#define WRITE O_WRONLY//使用下面的函数进行读写操作
int OpenFIFO(std::string pathname, int flags)
{int fd open(pathname.c_str(),flags);assert(fd 0);return fd;
}
//读取需要等待别人写入
void Wait(int fd)
{log(没有数据等待中) endl;uint32_t str 0;ssize_t s read(fd,str,sizeof(uint32_t));assert(s sizeof(uint32_t));(void)s;
}
//写入信息
int Signal(int fd)
{uint32_t str 1;ssize_t s write(fd,str,sizeof(uint32_t));assert(s sizeof(uint32_t));(void)s;log(正在等待输入数据) endl;}
//关闭该文件描述符
void CloseFIFO(int fd)
{close(fd);
}
shmserver.cc:
#include common.hpp//程序加载时自动构建全局变量自动调用该类的构造函数
//程序退出时全局变量被析构自动调用析构函数删除管道文件
Init init;//将10进制数转化为16进制方便查看共享内存时观察
string Trans(key_t k)
{char buffer[32];snprintf(buffer, sizeof(buffer), 0x%x, k);return buffer;
}int main()
{//创建公共的keykey_t k ftok(PATH_NAME, PROJ_ID);assert(k ! -1);log(创建key成功) server key- Trans(k) endl;//创建共享内存建议创建全新的共享内存int shmid shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);if(shmid -1){perror(shmget);exit(1);}log(创建共享内存成功) shmid- shmid endl;//将指定的共享内存挂接到自己的地址空间char* shmaddr (char*)shmat(shmid,nullptr,0);log(挂接共享内存成功) shmid- shmid endl;int fd OpenFIFO(FIFO_NAME,READ);//将共享内存当做字符串使用for(;;){//刚开始先wait数据为空先不读取Wait(fd);printf(%s\n,shmaddr);//当client端不写入了server端就停止读取共享内存的数据if(strcmp(shmaddr, quit) 0)break;}//将指定的共享内存从自己的地址空间中去除关联int ret shmdt(shmaddr);assert(ret ! -1);(void)ret;log(去关联共享内存成功) shmid- shmid endl;//删除共享内存ret shmctl(shmid,IPC_RMID,nullptr);assert(ret ! -1);(void)ret;log(删除共享内存成功) shmid- shmid endl;//关闭文件描述符CloseFIFO(fd);return 0;
}
shmclient.cc:
#include common.hppint main()
{//创建公共的keykey_t k ftok(PATH_NAME, PROJ_ID);if(k 0){perror(ftok);exit(1);}log(创建key成功) client key- k endl;//获取共享内存int shmid shmget(k,SHM_SIZE,0);if(shmid 0){perror(ftok);exit(2);}log(创建共享内存成功) shmid- shmid endl;//挂接共享内存char* shmaddr (char*)shmat(shmid,nullptr,0);if(shmaddr nullptr){perror(ftok);exit(3);}log(挂接共享内存成功) shmid- shmid endl;int fd OpenFIFO(FIFO_NAME,WRITE);//将共享内存当做char类型的bufferwhile(true){ssize_t s read(0,shmaddr,SHM_SIZE-1);if(s 0){//将quit后面的回车(\n)变为\0//否则无法判断是quit这个字符串shmaddr[s-1] 0;Signal(fd);if(strcmp(shmaddr,quit) 0)break;}}//关闭CloseFIFO(fd);//将指定的共享内存从自己的地址空间中去除关联int ret shmdt(shmaddr);assert(ret ! -1);(void)ret;log(去关联共享内存成功) shmid- shmid endl;//client不需要删除共享内存return 0;
} 四、初识信号量
我们之前所学的为了进程间通信都是要让不同的进程看到同一份资源而不同的进程的进程看到同一份资源会带来时序问题造成数据不一致的问题
多个执行流互相运行是相互干扰主要是访问临界资源时不加以保护而在非临界区是不会互相干扰的
下面引入几个概念
临界资源多个进程执行流看到的公共的一份资源称为临界资源
临界区自己的进程访问临界资源的代码称为临界区
互斥为了更好的进行临界区的保护可以让多个执行流在任意时刻只能有一个进程进入临界区这种方式称为互斥
原子性要么不做要么做完没有中间状态称之为原子性 每一个进程想要进入临界资源访问临界资源的一部分需要先申请信号量才能使用临界资源
信号量就像一个计数器一样申请信号量的本质是让信号量计数器--
申请信号量成功后临界资源内部就会预留你想要的资源是对临界资源的预定机制
所以申请信号量信号量计数器--称之为信号量的P操作必须是原子的
而释放信号量信号量计数器会称之为信号量的V操作也必须是原子的
访问临界资源也就是进程执行自己的临界区代码 剩下详细的信号量知识在多线程部分会说到