一、原理
解释:
- 如何实现进程间通信呢?
解释:
- 拓展
- 进程通信是要有一定的标准,当所有人都遵循一套标准时,可以扩大标准的影响范围,同时还能降低通信的成本。
- 进程通信采取的标准为IPC(System V 与POSIX),而没有这套通信标准时,通常采用管道的方式进行通信。
二、方式
1.管道
这样数据从一端进去,然后从一端出去,这样的具有单向通信
的特点的,我们就称之为管道。
如何存储数据呢?我们通常都会想到文件,那文件是可以多进程共享吗?答案是可以的,既然这样文件也可以实现通信,那根据之前我们所学的文件,如果文件通信具体的过程是如何的?我们画图演示。
下面我们进一步来看两种管道——匿名管道与命名管道。
1.1匿名管道
1.1.1通信原理
- 首先看名字便知是没有名字的管道。
- 最后没有名字的管道如何实现共享。
因此:进程通过匿名管道进行通信,我们可以断定进程之间必然拥有血缘关系。
1.1.2接口使用
- 初识接口
头文件:
#include<unistd.h>
函数声明:
int pipe(int pipefd[2]);
函数参数:
* 一个至少有两个元素的数组,实际上传参传的是数组名。
* 这里的pipefd[0]是读端,pipefd[0]是写端。
返回值:
* 成功返回0。
* 失败返回-1,并设置合适的错误码。
下面我们细讲一下为什么要设计int pipefd[2]这样的接口:
- 从管道的定义来看,管道具有单向通信的特点。
- 这就意味着只能一端读一端写,因此需要读写两个文件描述符。
- 其次父进程打开读和写,子进程继承之后,假设父进程关闭读端,子进程关闭写端,这样父端写子端读,进而达成了单向通信。
图解:
- 为什么要单向通信呢?
解释:
- 使用与论证
说明:为了验证下面的结论我们只给出读写方法,因为这里的整体框架大致相同,只有读写方法不同。
#include<iostream>
#include<string>
#include<cstdio>
#include<cstdlib>
#include<unistd.h>
using namespace std;
#define SIZE (4096)
int main()
{
int pipe_fd[2];
int ret_pipe = pipe(pipe_fd);
if(ret_pipe != 0)
{
perror("pipe");
exit(1);
}
int pid = fork();
if(pid == 0)
{
//子进程
//关闭写端,保留读端
close(pipe_fd[1]);
Read(pipe_fd[0]);
exit(0);
}
else if(pid < 0)
{
perror("fork");
exit(1);
}
//父进程
//关闭读端,保留写端
close(pipe_fd[0]);
Write(pipe_fd[1],pid);
return 0;
}
- 既然只能进行单向通信,那么我们可能会关心如下问题:
- 读端与写端都打开,如何正常运行。
验证代码:
void Read(int rfd)
{
while(true)
{
char buf[SIZE];
int sz = read(rfd,buf,sizeof(buf));
if(sz == 0) break;
if(sz == -1)
{
perror("read");
exit(-1);
}
//cout << sz << endl;
buf[sz] = '';//添上字符串结尾标识符。
cout << "my pid is " << getpid() << " What I read from father is " << buf << endl;
}
}
void Write(int wfd,int pid)
{
while(true)
{
string buf;
cout << "fahter say to "<< pid << " :";
getline(cin,buf);
int ret = write(wfd,buf.c_str(),buf.size());
if(ret == -1)
{
perror("write");
exit(1);
}
sleep(1);
}
}
- 观察现象:
- 实验结论:
- 读端与都打开,写端写满之后,会发生什么。
void Read(int rfd)
{
while(true);
}
void Write(int wfd,int pid)
{
int cnt = 0;
while(true)
{
cout << cnt++ << endl;
int ret = write(wfd,"C",1);
}
}
- 观察现象
- 实验结论:
- 读端关闭,写端会发生什么。
- 读写方法
void Read(int rfd)
{
int cnt = 3;
while(true)
{
char buf[SIZE];
int sz = read(rfd,buf,sizeof(buf));
buf[sz] = '';//添上字符串结尾标识符。
cout << "my pid is " << getpid() << " What I read from father is " << buf << endl;
if(cnt-- == 0)//读写4次之后读端进行关闭
{
close(rfd);
break;
}
}
}
void Write(int wfd,int pid)
{
while(true)
{
string buf;
cout << "fahter say to "<< pid << " :";
getline(cin,buf);
int ret = write(wfd,buf.c_str(),buf.size());
sleep(1);
}
}
- 现象分析
- 首先这里读写四次是正常的,当我们关闭读端时,写端还没有异常现象,此时我们再进行写入,发现代码异常退出了。
- 其次这里我们打印出退出信息发现,这里的信息异常为代码异常退出的结果。
- 最后我们分析141,发现其是管道破裂信号。
- 实验结论:
- 写端关闭,读端会发生什么。
- 读写方法
void Read(int rfd)
{
while(true)
{
char buf[SIZE];
int sz = read(rfd,buf,sizeof(buf));
if(sz == 0)
{
cout << "I read nothing" << endl;
break;
}
buf[sz] = '';//添上字符串结尾标识符。
cout << "my pid is " << getpid() << " What I read from father is " << buf << endl;
}
}
void Write(int wfd,int pid)
{
int cnt = 3;
while(true)
{
string buf;
cout << "fahter say to "<< pid << " :";
getline(cin,buf);
int ret = write(wfd,buf.c_str(),buf.size());
if(--cnt == 0)//读写3次之后读端进行关闭
{
close(wfd);
cout << "wfd is closed"<< endl;
break;
}
sleep(1);
}
}
- 代码解读:首先正常的读取3次之后,写端进行关闭,此时我们看看读端会发生什么。
- 实验现象:
- 观察现象:
- 实验结论:
- 总结一下:
- 管道读写端打开,正常写入时,读端陷入阻塞,且读完之后会刷新缓存区和文件指针。
- 管道读写端打开,一直写,不进行读取。管道写满之后,写端会陷入阻塞状态。
- 管道读端关闭,写端打开。无法正常进行读写,强行写会收到管道破裂信号。
- 管道写端关闭,读端打开。读端从阻塞状态变为非阻塞状态,一直进行读(读到文件末尾)。
- 实现方法:
- 对进程进行初始化
struct conduit//管道段
{
conduit()
{}
conduit(string pipename,int pipepid,int pipefd)
:_pipename(pipename),_pipepid(pipepid),_pipefd(pipefd)
{}
//1.管道名
string _pipename;
//2.管道的pid
int _pipepid;
//3.写端的文件描述符(写端)
int _pipefd;
};
const int PIPENUM = 10;
//所创建的管道(进程池)
vector<conduit> pipe_array;
void Init()
{
for(int i = 0; i < PIPENUM; i++)
{
//先创建管道
int pipefd[2];
int pipe_ret = pipe(pipefd);
if(pipe_ret == -1)
{
perror("pipe");
exit(errno);
}
//创建子进程
int pid = fork();
if(pid == 0)
{
//此处我们在后面还会补上一段代码。
//...
//子进程关闭写端
close(pipefd[1]);
//管道进行重定向,重定向到键盘,减少传参。
dup2(pipefd[0],0);
//子进程从管道读信息
Read();//之后会给出接口。
//退出子进程
exit(1);
}
if(pid == -1)
{
//进程创建失败,没有创建子进程
perror("fork");
exit(errno);
}
//父进程关闭读端
close(pipefd[0]);
//将创建的子进程放进vector中
string pipe_name = "process_" + to_string(i);
pipe_array.push_back({pipe_name,pid,pipefd[1]});
}
}
图解:
首先我们先设计一下,看看通过父进程让子进程完成什么任务,此处简单设计了一个LoL的资源更新的接口。
#include<functional>
#include<map>
#include<iostream>
using namespace std;
void Menu()
{
//打印功能
cout << "+-----------------------------------+" << endl;
cout << "|-----1.刷新野区--------2.刷新兵线---|" << endl;
cout << "|-----3.初始化防御塔-----4.退出------|" << endl;
cout << "+-----------------------------------+" << endl;
}
void Exit()
{
exit(0);
}
void ClearWlidArea()
{
cout << "野区已被刷新" << endl;
}
void ClearSolidLine()
{
cout << "兵线已被刷新" << endl;
}
void InitDefenseTower()
{
cout << "防御塔初始化" << endl;
}
//对功能进行包装。
map<int,function<void()>> Hash = {
{1,ClearWlidArea},{2,ClearSolidLine},
{3,InitDefenseTower}
};
//... 更多功能敬请期待!
void Write(vector<conduit>& pipe_array)
{
while(true)
{
Menu();
cout << "请选择:";
int cmd;
cin >> cmd;
if(cmd == 4) return;
int pipe = rand() % pipe_array.size();
int w_ret = write(pipe_array[pipe]._pipefd,&cmd,sizeof(cmd));
if(w_ret == -1)
{
perror("write");
exit(errno);
}
cout << "I am father, I write " << cmd << " to "
<< pipe_array[pipe]._pipename << " It Pid is"
<< pipe_array[pipe]._pipepid << endl;
sleep(1);
}
}
void Read()
{
while(true)
{
int cmd;
int r_ret = read(0,&cmd,sizeof(cmd));
if(r_ret == sizeof(cmd))
{
cout << "I am child, my pid is:" << getpid() <<
", which cmd I read is " << cmd << endl;
if(cmd >= 1 && cmd < 4)
Hash[cmd]();//执行任务
}
else if(r_ret == 0)
break;
sleep(1);
}
}
- 回收释放子进程的资源。
void DeletePipeArray(const vector<conduit>& pipe_array)
{
for(int i = 0; i < pipe_array.size(); i++)
{
//将子进程的写端进行关闭
close(pipe_array[i]._pipefd);
}
for(int i = 0; i < pipe_array.size(); i++)
{
//等待子进程,阻塞等待
waitpid(pipe_array[i]._pipepid,NULL,0);
}
}
- 这里我们画图可以看出,创建两个进程之后,一个子进程的读端虽然只有一个,但是在创建两个子进程之后,第二个创建的子进程会从父进程那里继承第一个子进程的写端,从而可以向第一个子进程里面进行写入。
- 这样创建进程是不好的,因为管道是单向通信的,多个写端可能会导致数据错乱。
void DeletePipeArray(const vector<conduit>& pipe_array)
{
for(int i = pipe_array.size() - 1; i >= 0; i--)
{
close(pipe_array[i]._pipefd);
waitpid(pipe_array[i]._pipepid,NULL,0);
}
}
其次如果我们只要一个读写端,还需要对子进程的所有的管道写端进行关闭。
for(int i = 0; i < pipe_array.size(); i++)
{
close(pipe_array[i]._pipefd);
}
加上这个补丁之后,我们再来看管道的清理,不管正着删还是倒着删,其实都只需一次循环。
- 拓展——日志
趁热打铁,我们回过头看一看日志的图解,顺便实现一个简单的日志类。
#include<map>
#include<iostream>
#include<cstdio>
#include<stdarg.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<time.h>
using namespace std;
#define SIZE (4096)
#define EMRGE 1
#define ALERK 2
#define CRIT 3
#define ERRO 4
#define WARNNING 5
#define NOTICE 6
#define INFORE 7
#define DEBUG 8
#define NONE 9
#define DEFAULTFILE 1
#define CLASSFILE 2
#define SCREAN 0
//说明:一般我们在传参时一般都是以宏的方式进行传参的,如果需要打印出字符串可以用KV类型进行映射转换。
map<int,string> Mode = {
{1,"EMERG"},{2,"ALERK"},{3,"CRIT"},
{4,"ERRO"},{5,"WARNING"},{6,"NOTICE"},
{7,"INFOR"},{8,"DEBUG"},{9,"NONE"}
};
//分类文件处理的后缀。
map<int,string> file = {
{1,"emerg"},{2,"alerk"},{3,"crit"},
{4,"erro"},{5,"warning"},{6,"notice"},
{7,"infor"},{8,"debug"},{9,"none"}
};
class log
{
public:
void operator()(int level,int where,const char* format,...)
{
//将输入的字符串信息进行输出。
va_list arg;
va_start(arg,format);
char buf[SIZE];
vsnprintf(buf,SIZE,format,arg);
va_end(arg);
//获取时间
time_t date = time(NULL);
struct tm* t = localtime((const time_t *)&date);
char cur_time[SIZE] = {0};
snprintf(cur_time,SIZE,"[%d-%d-%d %d:%d:%d]",t->tm_year + 1900,t->tm_mon + 1,、
t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
//输入再进行合并
string Log = "[" + Mode[level] + "]" + cur_time + string(buf) + "n";
//处理输出方向
PrintClassFile(level,where,Log);
}
void PrintDefaultFILE(string& file_name,const string& mes)
{
int fd = open(file_name.c_str(),O_CREAT | O_WRONLY | O_APPEND,0666);
write(fd,mes.c_str(),mes.size());
close(fd);
}
//将文件进行分类进行输出。
void PrintClassFile(int level,int where,const string& mes)
{
if(where == SCREAN)
cout << mes;
else
{
string file_name = "./log.txt";
if(where == CLASSFILE)
file_name += ("." + file[level]);
PrintDefaultFILE(file_name,mes);
}
}
};
1.2命名管道
- 顾名思义,就是有名字的管道。既然有名字,那就可以实现不同进程的之间的通信了。
如何实现呢?涉及一条命令。一条系统调用接口。
- 命令:
mkfifo 【管道名】
- 系统调用接口
头文件:
#include<sys/type.h>
#include<sys/stat.h>
函数声明:
int mkfifo(const char* pathname,mode_t mode);
函数参数:
1. pathname:要创建的所在路径 + 管道名。
2. mode:权限。
函数返回值:
1.创建管道成功,返回0.
2.创建管道失败,返回-1并设置合适的错误码。
- 聊天的基本原理:
首先我们建立两个管道,即两个输入端分别连到一个管道的写端,共享信息区与两个管道的读端。
实现代码:
为了便于理解,先将头文件进行给出:
- pipe.h
#pragma once
#include<iostream>
#include<cstdlib>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
using namespace std;
const string server_name = "shun_hua";
const string client_name = "舜华";
const string server_pipe = "server_pipe";
const string client_pipe = "client_pipe";
#define SIZE (4096)
//实现管道管理
//此处我们实现一个简单的窗口聊天菜单
void menu(const string& username)
{
//获取时间
time_t date = time(NULL);
struct tm* t = localtime((const time_t *)&date);
char cur_time[SIZE] = {0};
snprintf(cur_time,SIZE,"[%d:%d:%d]",t->tm_hour,t->tm_min
,t->tm_sec);
cout << cur_time << username << ":";
}
- 服务端写端(serverw.cc)
#include"pipe.h"
int main()
{
//创建管道文件
mkfifo(server_pipe.c_str(),0666);
mkfifo(client_pipe.c_str(),0666);
//打开写端。
int fd = open(server_pipe.c_str(),O_WRONLY);
if(fd == -1)
{
perror("open server_pipe");
exit(1);
}
//打开成功
while(true)
{
menu(server_name);
//输入你想说的话:
string str;
getline(cin,str);
write(fd,str.c_str(),str.size());
}
return 0;
}
#include"pipe.h"
int main()
{
//创建管道文件
//打开服务端管道的写端。
int fd = open(client_pipe.c_str(),O_WRONLY);
if(fd == -1)
{
perror("open client_pipe");
exit(1);
}
//打开成功
while(true)
{
menu(client_name);
//输入你想说的话:
string str;
getline(cin,str);
write(fd,str.c_str(),str.size());
}
return 0;
}
- 服务端的读端(serverr.cc)
#include"pipe.h"
int main()
{
//一个打开读端,一个打开写端。
int fd = open(server_pipe.c_str(),O_RDONLY);
if(fd == -1)
{
perror("open server_pipe");
exit(1);
}
//打开成功
while(true)
{
char buf[SIZE];
int sz = read(fd,buf,SIZE);
buf[sz] = '';
if(sz == 0) break;
//从client读到的话
menu(client_name);
cout << buf << endl;
}
return 0;
}
#include"pipe.h"
int main()
{
//一个打开读端,一个打开写端。
int fd = open(client_pipe.c_str(),O_RDONLY);
if(fd == -1)
{
perror("open server_pipe");
exit(1);
}
while(true)
{
char buf[SIZE];
int sz = read(fd,buf,SIZE);
if(sz == 0) break;
buf[sz] = '';
//从client读到的话
menu(client_name);
cout << buf << endl;
}
return 0;
}
我们实现的还是较为粗糙的代码,有兴趣的小伙伴可以进行丰富与补充。
- 除此之外,在读端未打开,写端会陷入阻塞状态,这是正常现象。与此同理写端未打开,读端打开,读端也会陷入阻塞状态。
- 拓展
- piper.cc
#include<iostream>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
using namespace std;
char buf[4096];
int main()
{
int rfd = open("pipe",O_RDONLY);
cout << "open success" << endl;
sleep(10);
read(rfd,buf,sizeof(buf));
cout << buf;
return 0;
}
- pipew.cc
#include<iostream>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
using namespace std;
int main()
{
mkfifo("pipe",0666);
int wfd = open("pipe",O_WRONLY);
cout << "open true" << endl;
int cnt = 10;
while(cnt--)
{
string str = "hello worldn";
write(wfd,str.c_str(),str.size());
cout << "writing " << cnt << endl;
sleep(1);
}
return 0;
}
- 实验现象:
2.共享内存
2.1原理
- 简要说明: 此共享内存是System V 标准中的。
- 大前提:进程间要进行通信,必须要看到同一份资源。
共享内存如何让进程看到同一份资源呢?我们画图进行解释:
我们对这张图进行深入分析:
2.2接口使用
- 共享内存的创建
* 接口1
头文件:
#include<sys/ipc.h>
#include<sys/shem.h>
函数声明:
int shmget((key_t key, size_t size, int shmflg);
函数参数:
1. 共享内存的内核标识符。
2. 共享内存的开辟的字节数。
3. 共享内存的创建方式。其中包括:
1. IPC_CREAT:没有创建,有则返回。
2. IPC_EXIT: 有则出错返回。若要使用也得跟上方式1
返回值:
4. 如果成功,返回共享内存的用户标识符。
5. 如果失败,返回-1,并设置合适的错误码。
*接口2
头文件:
#include<sys/ipc.h>
#include<sys/shm.h>
函数声明:
key_t ftok(const char *pathname, int proj_id);
函数参数:
1. 生成路径,是生成key_t类型内核共享内存表示符(这只是我这样叫的)的key.
2. 生成码,生成key_t类型的内核共享内存标识符的key,不能是0.
说明:这两个参数都是为了生成key_t类型的内核的标识符。
返回值:
1. 成功,返回key_t类型的内核共享内存标识符。
2. 失败,返回-1
- 其实光看这两个接口是有点懵逼的,我们把这两个接口联系起来。
接下来我们简单的使用一下接口。
- shm.h
#include<iostream>
#include<string>
#include<sys/ipc.h>
#include<sys/shm.h>
using std::string;
const string pathname = "/home/shun_hua";
const int proj_id = 0xFFFF;
- shm.cc
#include"shm.h"
using namespace std;
int main()
{
key_t key = ftok(pathname.c_str(),proj_id);
if(key == -1) return;
int ud = shmget(key,4096,IPC_CREAT);
if(ud == -1) return;
cout << "创建成功!" << endl;
return 0;
}
- 此时由于共享内存是由操作系统进行管理的,因此在没有调用对应的系统调用接口时,进程退出时不会进行释放共享内存。
如何验证呢?涉及一条命令:
ipcs -m
ipcrm -m 【shmid】
int ud = shmget(key,4096,IPC_CREAT | 0666);
- 共享内存的链接与取消
*产生链接
头文件:
#include<sys/shm.h>
函数声明:
void *shmat(int shmid, const void *shmaddr, int shmflg);
函数参数:
1. 用户共享内存标识符。
2. 指定的进程的共享区地址,若被占用失败,返回-1,为空,系统自动分配地址。
3. 进程映射共享内存的方式。这里只介绍SHM_RDONLY,以只读方式映射共享内存区域。
一般设为0。
返回值:
1. 成功,返回共享内存的进程空间的地址。
2. 失败,返回-1,并设置合适的错误码。
*取消链接
头文件:
#include<sys/shm.h>
函数声明:
int shmdt(const void *shmaddr);
函数参数:
*共享内存的地址
返回值
1.成功返回0
2.失败返回-1,并设置合适的错误码。
简单使用:
#include"shm.hpp"
int main()
{
key_t key = ftok(pathname.c_str(),proj_id);
if(key == -1) return -1;
int shmid = shmget(key,4096,IPC_CREAT | 0666);
if(shmid == -1) return -1;
cout << "获取成功!" << endl;
//产生链接
char* shmptr = (char*)shmat(shmid ,nullptr,0);
if(shmptr != -1) cout << "链接成功"<< endl;
sleep(5);
//断开链接
int ret = shmdt(shmptr);
if(ret == 0) cout << "链接已断开" << endl;
sleep(5);
return 0;
}
- 删除共享内存
头文件:
#include<sys/ipc.h>
#include<sys/shm.h>
函数声明:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
函数参数:
1.用户共享内存标识符。
2.选项,常见的有:
1.IPC_STAT,获取共享内存的状态信息,放在buf指向的变量中。
2.IPC_SET,设置共享内存的状态,需要将buf变量传进去,方便修改
3.IPC_RMID,删除共享内存。
4.SHM_LOCK,锁定共享内存。
5.SHM_UNLOCK,解锁共享内存。
3.输入或者输出型参数,用于存放共享内存的信息。
返回值:
1.成功,返回0.
2.失败返回-1.并设置合适的错误码。
- 简单使用:
#include"shm.hpp"
int main()
{
key_t key = ftok(pathname.c_str(),proj_id);
if(key == -1) return -1;
int shmid = shmget(key,4096,IPC_CREAT | 0666);
if(shmid == -1) return -1;
cout << "获取成功!" << endl;
//产生链接
char* shmptr = (char*)shmat(shmid,NULL,0);
if(shmptr != (void*)(-1)) cout << "链接成功"<< endl;
sleep(5);
//断开链接
int ret = shmdt(shmptr);
if(ret == 0) cout << "链接已断开" << endl;
sleep(5);
//上面这一坨代码,我们已经循序渐进讲过了。
//下面是核心代码.
//将共享内存进行删除。
ret = shmctl(shmid,IPC_RMID,NULL);
if(ret == -1) cout << "删除失败" << endl;
cout << "删除成功!"<< endl;
sleep(5);
return 0;
}
- 实验结果:
3.消息队列
原理
顾名思义,消息队列就是能看到消息的队列,不同进程之间必须看到这个队列,那是如何做到的呢?同理我们先画一张图,进行辅助理解:
- 其次我们再来根据这张图进行深入。
再来谈接口(具体细节我们等到后面再讲):
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数声明:
key_t ftok(const char *pathname, int proj_id);
*此函数共享内存的创建处已经提及到过。
int msgget(key_t key, int msgflg);
*此函数用于消息队列的创建。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
*此函数用于消息的发送。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz
, long msgtyp,
int msgflg);
*此函数用于消息的获取。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
*此函数用于消息队列信息的获取与删除。
通过此类接口,我们不难发现此类接口与共享内存的接口高度类似,这是为什么呢?
- 其实很简单,消息队列用的也是是System V标准的。
既然这样,那操作系统是如何统一的进行管理的呢?我们通过两个结构体进行分析:
共享内存:struct shmid_ds
消息队列:struct msqid_ds
通过查看这两个结构体的声明看是否有相同之处,下面以图解的方式进行呈现:
4.信号量
引入
在共享内存的讲解中,我们可以直接在进程中直接访问共享内存,但是若有多个进程同时进行访问那就可能会出现这样的问题:
- 数据错乱无章,数据可能会紊乱。
假如有两个进程,一个进程一边从进程读数据,一个进程一边从进程写数据,两者同时发生,那就可能会导致一个进程没写完的数据被另一个进程读走了
,从而导致读到的数据并不完整,这样的问题我们叫做数据不一致问题
。
- 说明:管道是具有原子性的,因此不会出现这样的问题。
此时我们需要对资源进行加锁,也就是一个进程访问时,另一个进程不能进行访问。这种现象我们称之为互斥
。
而有的资源,只能一次被一个进程进行占用,比如说显示器,键盘等,像这种资源我们称之为临界资源
。
若我们将临界资源的访问,限制在一段代码里面,也就是通过代码限制临界资源的访问,这样的代码我们称之为临界区
。
原理
- 前面我们已经讲过,像有些资源只能一次被一个进程访问占用。
这里标记我们就可以看做计数器,像这种只有0,1两种状态的我们称之为二元信号量。
- 在现实生活中,像一些去ATM机里面取钱,为了防止其它人,干扰这个过程,一般进去之后我们会自动上锁,还有一些比如上厕所,为了不让在蹲坑的时候,被其它人看见,通常都会把门锁上……诸如此类的现象都体现出了信号量的概念。
还有一些可以多执行流进行访问的资源,那信号量可能就不只是0,1两种状态,可能是[0,n]种状态。
- 比如我们去看一场电影,放映厅里面的座位是固定的,要想看电影,就得买电影票预定座位,即获取座位的在一定时间内的使用权,且座位的数量是固定的,这就意为电影票的数量最多与座位的数量一致。不可能出现票的数量大于座位的数量的情况。
回归到信号量:
- 首先,假设有这样的共享资源,这个资源分为40份,供进程进行使用。
- 信号量初始值为40。
- 当共享资源被访问时,信号量就减减,最多被减到0。
- 当减到0时,其它资源不能被继续访问。
- 拓展
- 既然要申请共享资源,必须要通过信号量,那也就意为着信号量也是共享资源,那信号量安不安全?
总结
如果本篇对您有所帮助,不妨点个赞鼓励
一下吧!
原文地址:https://blog.csdn.net/Shun_Hua/article/details/134584104
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_4935.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!