网站编程薪资,邳州网页设计,wordpress 安装 主题,wordpress带采集1. 共享内存 system V共享内存是最快的IPC形式#xff0c;之前的管道是基于Linux内核开发的通讯方案#xff0c;其读写接口都是现成的#xff0c;因此内核设计者为了完成进程间通讯任务并不需要新增太多代码。而共享内存属于system V标准#xff0c;是操作系统单独…1. 共享内存 system V共享内存是最快的IPC形式之前的管道是基于Linux内核开发的通讯方案其读写接口都是现成的因此内核设计者为了完成进程间通讯任务并不需要新增太多代码。而共享内存属于system V标准是操作系统单独设计的一种通讯方案。 简单来说共享内存的实现形式和动态库很像也是在物理内存中开辟一块空间通过页表映射到两个不同的进程中此时两个进程就能通过虚拟地址空间寻到同一块资源了。 共享内存交给页表映射虚拟地址的操作叫做将共享内存挂接到进程地址空间中。如果不用共享内存了就把页表中的映射关系删除这步操作叫去关联当所有进程都与这块共享内存去关联之后就可以把这块内存释放了。 共享内存在任何时刻可以在OS中存在很多个因此OS要先描述再组织把共享内存们管理起来。 shmget()创建共享内存的函数。 第一个参数key表示共享内存的唯一标识符后面我们再说它 第二个参数size表示创建共享内存块的大小。 第三个参数shmflg有很多选项我们就关注两个 IPC_CREAT 和 IPC_EXCL IPC_CREAT单独使用如果共享内存块shm不存在就创建它如果存在就获取它并返回 IPC_EXCL单独使用无意义只有 IPC_CREAT | IPC_EXCL 如此将两个选项组合在一起如果shm不存在就创建它如果存在就出错返回。 这两个选项第一个选项的作用是保证进程能拿到共享内存第二个选项是标识不拿老的共享内存。 返回值成功返回shmid共享内存面向用户的标识符如果创建失败返回-1 key是函数参数也就是要用户自己设定的之所以不让系统设置因为如果系统能设置岂不是系统自己就能有通讯方案了再一个共享内存空间是A进程创建的B进程与A进程之间是独立的即使key可以由系统自己设置B进程也无法得知A进程创建的共享内存空间的key。 如果让用户来创建key值那么在全局范围如果能把key值创建成功说明就能同时让AB进程拿到key值的同时保证这个key值对应的共享内存块是唯一的。 在理论上key值可以由用户任意设置但是有可能会出现该key值与现有key值冲突的情况那共享内存就创建失败了此时程序员就要手动修改key值说白了这个key值就是一个一个碰出来的。因此虽然这个key值需要程序员去设置但是不希望由程序员去生成因此我们退出ftok()函数来生成这个唯一码。 ftok()函数 这个函数可以把字符串和数字通过特定算法整合生成一个尽量的唯一值。第一个参数我们可以选择一个AB进程的公共路径第二个参数选择项目id 如果生成成功了就会返回一个合法内存标识符 此时我们让server进程创建共享内存空间 但是我们发现第一次运行server进程成功创建了共享内存空间但是后面就不能正确创建了。 这是因为今天这个共享内存看起来是由进程创建的但是实际上我们是用系统调用shmget()申请的内存空间而这块空间会被认为成操作系统申请的借了进程的手创建的因此这块空间并不会随进程的结束而释放而是随操作系统的退出而释放。 共享内存的声明周期随内核。要么用户要求OS释放要么操作系统重启。 1.2 共享内存的管理指令 查看共享内存 ipcs -m 指令 如果只用ipcs指令会把消息队列共享内存信号量全打出来选项-m可以筛选处共享内存。 这里我们也可以看到我们刚刚创建的共享内存nattch表示当前多少进程相关目前没有挂机就没有。 删除共享内存 ipcrm -m (shmid) 命令 shmid和key的区别shmid是只给用户用的一个标识shm的标识符key只作为内核中区分shm唯一性的标识符不作为用户管理shm的id值。 perms 表示共享内存的权限 bytes 表示共享内存的大小操作系统会以块为单元为用户申请空间也就是单次最小申请4KB但如果用户代码中要求4097个字节第二页中用户就只能使用1个字节也就是操作系统虽然申请的第二个4KB但是只让用户用一个字节。 代码删除共享内存 man shmctl 查看 这个函数集成了对于共享内存的删改查等操作功能。 第一个参数shmid不用说了指定某个共享内存 第二个参数 cmd 是选择操作方案删除就选 IPC_RMID 如果是删除就可以不要第三个参数。 shmat()函数把共享内存挂接到进程自己的地址空间上 第一个参数shmid共享内存面向用户的唯一标识符 第二个参数shmaddr可以由用户指定将共享内存挂接到虚拟地址的什么位置不过我们不用管它直接设置成nullptr就可以了 第三个参数shmflg可以选择共享内存的读写方案不过暂时也不用管只用共享内存通讯的话直接设置为 0 就可以 返回值挂接成功返回共享内存在虚拟地址空间中的起始地址如果挂接失败返回-1并设置错误码这个返回值很像malloc的返回值。 如果这样直接挂接的话就会发现是挂不上的这个函数的返回是-1这是因为共享内存空间也是有权限限制的。共享内存也是文件它在底层原理上和文件系统极像只是在操作方式上有所不同。 设置权限就是在创建共享内存的时候床架就好了。 shmdt() 函数共享内存去关联 参数就是刚才 shmat() 函数的返回值 去关联并不是删除而是取消挂接共享内存还在如果想删除共享内存还是要用shmctl(IPC_RMID) 最后我们可以写出这样一个关于共享内存操作的类 1.3 共享内存的通讯 使用共享内存通讯不需要像管道那样使用系统调用直接用地址向内存块中读写数据就好了。 共享内存是所有进程间通讯方法中速度最快的方案。 共享内存在真正意义上使两个进程共享了资源但是也没有加任何保护进程对共享内存的读写不会互相阻塞等待读完数据后数据也不会像管道那样就没了写进程关闭了读进程也不会退出也就是所谓数据不一致问题。 这样的话就需要用户自己去设定保护机制这个保护机制要用信号量去控制这个信号量我们后面会说暂时我们先用命名管道保护。 共享资源被保护起来我们一般称之为临界资源。 访问公共资源的代码叫临界区相反剩下的代码是非临界区。如果我们想把共享资源保护起来变成临界资源就要对临界区进行加锁这个话题我们也在后面会说。 补充获取时间 man localtime查看 localtime函数是一个系统调用 参数是一个时间戳地址返回值是tm结构体指针我们也可以看到tm结构体中都包含了什么内容。 获取时间戳就用time()函数就好了之前随机值种子就是用这个函数返回的时间戳生成的。 获取到的年和月要注意有固定的1900和1才能拿到正确数据。 1.3.1 通讯 我们给出的通讯保护方案就是client进程先向共享内存中写入信息写完后向管道中发送一个信号server进程一直等待管道中的信号接收到信号之后再读共享内存中的内容。 看起来不如直接使用管道但其实因为向管道中传递的数据量较少所以对整体通讯的速度影响也较小我们可以使用共享内存来传输大块数据用管道传输小块数据来保证整体的通讯速度。 我们让两个进程通讯以一定的顺序访问公共资源的方案叫进程间同步的过程。 我们借用上节管道通讯中的部分代码进行信号标识工作。 完整代码
Time.hpp
#pragma once#include iostream
#include string
#include time.hstd::string GetCurrTime()
{time_t t time(nullptr);struct tm *curr ::localtime(t);char currtime[32];snprintf(currtime, sizeof(currtime), %d-%d-%d %d:%d:%d,curr-tm_year 1900, curr-tm_mon 1, curr-tm_mday,curr-tm_hour, curr-tm_min, curr-tm_sec);return currtime;
}
Fifo.hpp
#pragma once#include iostream
#include string
#include unistd.h
#include sys/types.h
#include sys/stat.h
#include fcntl.hconst std::string gpipeFile ./fifo;
const mode_t gfifomode 0600; // 允许拥有者读写
const int gdefualt -1; // 默认错误文件描述符
const int gsize 1024; // 约定通讯缓冲区大小const int gForRead O_RDONLY;
const int gForWrite O_WRONLY;class Fifo
{
private:void OpenFifo(int flag){_fd ::open(gpipeFile.c_str(), flag);if (_fd 0){std::cerr open error std::endl;}}public:Fifo() : _fd(-1){umask(0);int n ::mkfifo(gpipeFile.c_str(), gfifomode);if (n 0){std::cerr mkfifo error std::endl;return;}std::cout make fifo success! std::endl;}bool OpenPipeFWrite(){OpenFifo(gForWrite);if (_fd 0){return false;}return true;}bool OpenPipeFRead(){OpenFifo(gForRead);if (_fd 0){return false;}return true;}int Wait(){int code 0;ssize_t n ::read(_fd, code, sizeof(code));if (n sizeof(code)) //正常读到信号{return 0;}else if(n 0) //对方退出了管道文件关闭{return 1;}else //其他错误{return 2;}}void Singal(){int code 1;::write(_fd, code, sizeof(code));}~Fifo(){if (_fd 0)::close(_fd);int n unlink(gpipeFile.c_str());if (n 0){std::cerr unlink error std::endl;return;}std::cout unlink fifo success! std::endl;}private:int _fd;
};Fifo gpipe;
ShareMemory.hpp
#pragma once#include iostream
#include string
#include cstdio
#include unistd.h
#include sys/types.h
#include sys/ipc.h
#include sys/shm.hconst std::string gpath /home/atlanteep/Linux_learn;
int gprojId 0x666;
int gshmsize 4096;
mode_t gmode 0600; // 拥有者读写权限全开std::string ToHex(key_t k)
{char buffer[64];snprintf(buffer, sizeof(buffer), %x, k);return buffer;
}class ShareMemory
{
private:int CreatShmHelper(int shmflg){_k ::ftok(gpath.c_str(), gprojId);if (_k 0){std::cerr ftok error std::endl;return -1;}// 创建共享内存_shmid ::shmget(_k, gshmsize, shmflg);if (_shmid 0){std::cerr shm get error std::endl;return -2;}std::cout shmid: _shmid std::endl;return _shmid;}public:ShareMemory() : _shmid(-1), _k(0), _addr(nullptr){}~ShareMemory() {}void CreatShm(){if (_shmid -1)CreatShmHelper(IPC_CREAT | IPC_EXCL | gmode);}void GetShm(){CreatShmHelper(IPC_CREAT);}void AttachShm(){_addr ::shmat(_shmid, nullptr, 0);if ((long long)_addr -1){std::cout attach error! std::endl;}}void DetachShm(){if (_addr ! nullptr)::shmdt(_addr);}void Delete(){shmctl(_shmid, IPC_RMID, nullptr);}void *GetAddr(){return _addr;}private:key_t _k; // 关键码int _shmid;void *_addr; // 起始地址
};ShareMemory shm;struct data
{char status[32];char lasttime[64];char image[1024];
};
server.cc
#include iostream
#include string.h
#include ShareMemory.hpp
#include Time.hpp
#include Fifo.hppint main()
{// 创建共享内存shm.CreatShm();shm.AttachShm();// 打开管道gpipe.OpenPipeFRead();// 通讯struct data *image (struct data *)shm.GetAddr();while (true){//阻塞等待另一个进程写完内容的信号gpipe.Wait();printf(status: %s\n, image-status);printf(lasttime: %s\n, image-lasttime);printf(image: %s\n, image-image);strcpy(image-status, 过期);}// sleep(10);shm.DetachShm();shm.Delete();return 0;
}
client.cc
#include iostream
#include string.h
#include ShareMemory.hpp
#include Time.hpp
#include Fifo.hppint main()
{//先获取共享内存shm.GetShm();shm.AttachShm();//打开管道gpipe.OpenPipeFWrite();// 通讯struct data *image (struct data *)shm.GetAddr();int num 10;while(num--){strcpy(image-status, 最新);strcpy(image-lasttime, GetCurrTime().c_str());strcpy(image-image, atlanteep picture);//写好消息了用管道通知另一个进程gpipe.Singal();sleep(3);} sleep(10);shm.DetachShm();return 0;
} 2. 消息队列 system V 消息队列是内核提供的一种进程间通讯的方式。 消息队列是存在于操作系统中的其每个节点data的元素是由type标定发送进程text存储具体数据。 由进程A将要发送的信息传给消息队列由OS托管在一定时机由OS再将消息给到进程B反过来也是可以的消息队列支持双向通讯进程B将消息交给OS托管进而发送给进程A其中type就能够标定消息是由谁发出的进而得知要发送给谁。 消息队列的本质就是一个进程向另一个进程发送有类型数据块的方法。 man msgget 查看 msgget()函数创建消息队列 第一个参数key给OS看的唯一标识符同样用ftok()生成 第二个参数msgflg常用两种选项IPC_CREAT IPC_CREAT|IPC_EXCL含义与共享内存的这两个选项一摸一样。 返回值也是一样的给用户看的唯一标识符 man msgctl 查看 msgctl()函数消息队列控制常用删除消息队列 第一个参数给用户看的唯一标识符 第二个参数选择删除 IPC_RMID 第三个参数是输出型参数获取消息队列的各种属性 ipcrm -m (msqid) 指令层面删除消息队列 ipcs -q 单独查看消息队列 到这里我们发现消息队列的各种接口和选项都与共享内存有极大的相似性这是因为它们都在system V标准下的产物这就是标准的意义。 当然它们不可能完全一样还有一些个性化的接口比如消息队列的发送和接收消息接口与共享内存的挂接和去关联接口 收发消息操作要求用户层自定义一个msgbuf结构体mtype进程标志mtext数据可以随意定制大小用于保存消息数据 msgsnd()发送消息接口 第一个参数msqid给用户的唯一标识符 第二个参数msgp就是用户自定义的msgbuf 第三个参数msgsz用户自定义的消息大小就是mtext[ ]数组大小 第四个参数msgflg不管设置成0 msgrcv()接收消息接口 跟msgsnd的接口几乎一样就多了个msgtyp收哪个进程的消息 消息队列资源必须手动删除它不会随进程结束自动清除其实所有system V IPC资源的声明周期都随内核。 3. 信号量 semaphore semget() 获取信号量 semctl() 删除信号量 因为同在system V标准下信号量的接口也和共享内存与消息队列很像。 ipcs -s单独查看信号量 ipcrm -s (semid)命令删除某个信号量集 下面我们重述一下各概念 多个执行流能看到同一份资源则称这份资源为共享资源 被保护起来的共享资源叫临界资源 保护的方式常用同步和互斥。任何时刻只允许一个执行流访问资源叫互斥多个执行流访问临界资源的时候具有一定的顺序性叫做同步。 涉及到访问临界资源的代码叫临界区相反不涉及临界资源的代码叫非临界区 所谓对共享内存进行保护本质上是对访问共享资源的代码进行保护。 信号量的本质是一个计数器 之前我们对共享内存的操作都是整体使用开出一大块空间之后写进程在完全写完之后读进程才能进来读。但是我们还可以将共享内存不是整体使用比如一大块空间分成50个小区域写进程写到第四块小区域的时候读进程从第一块小区域开始读这样也能保证数据的完性。 通过我们上述的操作完整了多个进程具有一定并发性的访问共享资源。 但是这么做也有弊端50个小区域如果同时来了100个进程那一定就会造成进程之间访问小区域的冲突也就是如果资源不是整体使用我们要避免过量的进程进入临界资源操作可以简化成用一个计数器变量 num 50来标定剩余小区域来一个进程使用区域就 num-- 出一个进程就num如果num 0说明小区域都在被占用那就拒绝再新来的进程进入共享资源。 事实上进程没必要进入共享资源之后再对计数器进行操作而是让进程可以预定一个区域提前将计数器-- 即使后面进程不来这个区域也要给进程留着。那这个可以预定的计数器就叫信号量。 信号量是可以对资源进行预定的计数器。 当我们把资源当作整体来用时信号量要么为0要么为1这种情况叫二元信号量也是后面所谓的锁。 我们上面的例子是有问题的首先多个进程之间无法看到同一份信号量那如果我们把信号量弄到共享内存中去让各个进程去修改不就行了吗确实但此时又出现了一个问题信号量变成了共享资源那共享资源就需要保护此时套娃开启了。 计数器变成公共资源后如果要保证自己的安全就要让自增自减操作具有原子性原本的自增自减操作虽然在C中是一条语句但是在汇编中是多条所以具有安全风险。 将自减操作变成原子性后叫P操作将自增操作具有原子性后叫V操作。 3.1 信号量操作 可能公共资源不止一大块也就是说信号量是以信号量集的方式申请的 此时我们回过头看信号量申请函数第二个参数nsems就是信号量集里要几个信号量。 信号量控制函数中第二个参数semnum就是信号量集的某一个信号量下标 man semop查看 第一个参数semid给用户看的唯一标识符 第二个参数sops是sembuf结构体的数组也是一个需要自己定义的结构体其成员如下 结构体中sem_num表示信号量对应下标sem_op -1为自减 1为自增sem_flg 我们不管设置为0 这个结构体让我们自定义提供信号量操作方案信号量集中可能存在多个信号量那我们也能提供对应的多个操作方案合并成一个大数组 第三个擦书nsops就是数组的元素个数。 由此sem支持了同时对多个信号量进行PV操作 4. IPC原理 至此system V的三种IPC方案全部讲完那它与之前的管道有什么不同呢 无论是共享内存消息队列还是通讯都有对应的模块属性那共享内存的属性举例 获取共享内存属性 IPC_STAT 选项 属性消息中第一个是创建共享内存时的时间戳第二个是当前进程pid第三个访问时间第四个挂接进程数最后一个就是key值 IPC资源一定是全局的资源它需要被所有进程都看到在操作系统层面为我们维护了一个struct ipc_ids 这给结构体中有一个entries指针entriens指向ipc_id_ary结构体这个结构体中有一个柔性数组(变长数组)system V的IPC方案的每一个属性结构体的第一个成员都是struct ipc_perm结构体刚才的柔性数组就指向这个结构体。 这个变长数组的下标就是我们在***get()比如shmget()创建共享内存时拿到的所谓给用户看的唯一标识符shmid没错变长数组的下标就是***mid 事实上当我们用柔性数组的元素指向ipc_perm结构体的时候因为这个结构体是在各个方案的属性的首个成员其地址就是某方案本身属性的起始也就是说我们可以通过强转ipc_perm结构体地址的方式拿到整个方案的所有属性。 这种通过一个指针指向结构体首成员地址从而可以访问部分成员和整体成员的现象在C中有一个更响亮的名字 多态 ipc_perm结构就是基类shmid_ds就是子类。 最后一个问题ipc_perm结构怎么知道自己目前所在哪个方案的属性结构体中其实在操作系统中ipc_ids有3个分别是sem_ids 、shm_ids 、msg_ids 这三个结构体最终指向同一个荣幸数组表就好了。