System V 信号量
SystemV不是用来在进程间传输数据的。相反,它是用来同步进程的动作的。
信号量的一个常见用途是同步对一块共享内存的访问以防止出现一个进程在访问共享内存的同时另一个进程更新这块内存的情况
一个信号量是由一个由内核维护的整数,其值被限制为大于等于0
使用System V的常规步骤如下:
- 使用semget()创建或打开一个信号量集
- 使用semctl() SETVAL或SETVAL操作初始化集合中的信号量(只有一个进程需要完成这个任务)
- 使用semop()操作信号量的值。使用信号量的进程通常会使用这些操作来表示一种共享资源的获取和释放
- 当所有进程都不再需要使用信号量集之后使用semctl() IPC_RMID操作删除这个集合
大多数操作系统都为应用程序提供了一些信号量原语。但System V信号量表现出看不同寻常的复杂性,因为它们的分配是以被称为信号量集的组为单位进行的。
在使用semget()系统调用创建集合的时候需要制定集合中的信号量数量。虽然在同一时刻通常只操作一个信号量,但通过semop()系统调用可以原子的在同一个集合中的多个信号量之上执行一组操作
//semun.h
#ifndef SEMUN_H
#define SEMUN_H
#include <sys/types.h>
#include <sys/sem.h>
union semun{
int val;
struct semid_ds* buf;
unsigned short* array;
#if defined(__linux__)
struct seminfo* __buf;
#endif
};
#endif
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/stat.h>
#include <stdio.h>
#include "semun.h"
int main(int argc, char*argv[]){
int semid;
if(argc<2){
printf("arg err\n");
return 0;
}
if(argc==2){
union semun arg;
semid = semget(IPC_PRIVATE,1,S_IRUSR | S_IWUSR);
if(semid==-1){
printf("semid\n");
return 0;
}
arg.val = atoi(argv[1]);
if(semctl(semid,/*semnum= */ 0, SETVAL, arg )==-1){
printf("err\n");
return 0;
}
printf("Semaphore ID = %d\n", semid);
}
else{
struct sembuf sop;
semid = atoi(argv[1]);
sp[.sem_num = 0;
sop.sem_op = atoi(argv[2]);
sop.sem_flg = 0;
printf("%ld: about to semop\n", (long)getpid());
if(semop(semid, &sop, 1)==-1){
printf("err\n");
return 0;
}
printf("%ld: semop complated\n", (long)getpid());
}
return 0;
}
创建或打开一个信号量集
semget()系统调用创建一个信号量集或获取一个既有集合的标识符
#include <sys/types.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
//如果是创建一个新信号量集,那么nsems会指定集合中信号量的数量,并且其值必须大于0.如果使用semget()来获取一个既有集的标识符,那么nsems必须要小于或等于集合的大小
//semflg:位掩码
信号量控制操作
#include <sys/types.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd,...);
常规控制操作
获取和初始化信号量值
获取单个信号量的信息
struct semid_ds{
struct ipc_perm sem_perm;
time_t sem_otime;
time_t sem_ctime;
unsigned long sem_nsems;
};
System V共享内存
共享内存允许两个或多个进程共享物理内存的同一块区域(段)
由于一个共享内存段会成为一个进程用户空内存的一部分,因此这种IPC机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他共享同一个段的进程可用
共享内存这种IPC机制不由内核控制意味着通常需要通过某些同步方法是的进程不会出现同时访问共享内存的情况 为了使用一个共享内存段通常需要执行下面的步骤:
- 调用shmget()创建一个新共享内存段获取的一个既有共享内存段的标识符
- 使用shmat()来附上共享内存段,即使该段称为调用进程的虚拟内存的一部分
- 此刻在程序中可以像对待其它可用内存那样对待这个共享内存段,为引用这块共享内存,程序需要使用shmat()调用返回的addr值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针
- 调用shmdt()来分离共享内存段。进程终止时,这一步会自动完成
- 调用shmctl()来删除共享内存段,只有当当前所有附加内存段的进程都与之分离之后内存段才会被销毁
创建或打开一个共享内存段
新创建的内存段的内容会被初始化为0
#include <sys/types.h>
#include <sys/shm.h>
int shmget(keyy_t key, size_t size, int shmflg);
当时有shmget()创建一个新共享内存段时,size是一个正整数,它表示需要分配的字节数。内核是以系统分页大小的整数倍来分配共享内存的,因此实际上size会被提升到最近的系统分页大小的整数倍
shmflg参数执行的任务与其他IPC get调用中执行的任务一样,即指定施加于新共享内存段上的权限或需检查的既有内存段的权限
使用共享内存
shmat()系统调用将shmid标识的共享内存段附加到调用程序进程的虚拟地址空间中
#include <sys/types.h>
#include <sys/shm.h>
void*shmat(int shmid, const void *shmaddr, int shmflg);
shmaddr参数和shmflg位掩码参数中SHM_RND位的设置控制着段是如何被附加上去的
shmat()的函数结果是返回附加共享内存段的地址。开发人员可以像对待普通C指针那样对待这个值,段与进程的虚拟内存的其他部分看其他毫无差异。
通常会将shmat()的返回值赋给一个指向某个程序员定义的结构的指针以便在该段上设定该结构
一个进程要附加一个共享内存段就需要在该段上具备读写权限,除非知道你过来SHM_RDONLY标记,那样的话就只需要具备读权限
当一个进程不再需要访问一个共享内存段时就可以调用shmdt()来将该段分离出其虚拟地址空间
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void* shmaddr);
通过fork()创建的子进程会继承其父进程附加的共享内存段。因此,共享内存位父进程和子进程之间的通信提供了一种简单的IPC方式
在一个exec()中,所有附加的共享内存段都会被分离。进程中州,共享内存段也会自动被分离
通过共享内存传输数据
通过共享内存传输数据
程序使用二元信号量协议中的一个System V信号量来确保:
- 一次只有一个进程访问共享内存段
- 进程交替的访问段
写者对这两个信号量进行了初始化,这样他就成为两个程序中第一个能够访问共享内存段的程序了,即写者的信号量初始时时可用的,而读者的信号量初始时是正在被使用中的 code: github.com/1529046970/…
linux--内存
共享内存在虚拟内存中的位置
为了给堆和栈的增长腾出空间,附加共享内存段的虚拟地址从0x40000000开始。内存映射和共享库也是放在这个区域中(共享内存映射和内存段默认被放置的位置可能会有不同,这一开宇内核版本和进程的RLIMIT_STACK资源限制的设置)
linux通过/proc/PID/maps文件能够看到一个程序映射的共享内存段和共享库的位置
# cat /proc/10903/maps
00400000-00401000 r-xp 00000000 fd:01 805828 /home/code/ipc/svshm/writer #与共享内存相关 对应程序文本段和数据段
00601000-00602000 r--p 00001000 fd:01 805828 /home/code/ipc/svshm/writer
00602000-00603000 rw-p 00002000 fd:01 805828 /home/code/ipc/svshm/writer #保存一个程序所使用的字符串常量的只读分页
7f13a0961000-7f13a0b25000 r-xp 00000000 fd:01 1050289 /usr/lib64/libc-2.17.so
7f13a0b25000-7f13a0d24000 ---p 001c4000 fd:01 1050289 /usr/lib64/libc-2.17.so
7f13a0d24000-7f13a0d28000 r--p 001c3000 fd:01 1050289 /usr/lib64/libc-2.17.so
7f13a0d28000-7f13a0d2a000 rw-p 001c7000 fd:01 1050289 /usr/lib64/libc-2.17.so
7f13a0d2a000-7f13a0d2f000 rw-p 00000000 00:00 0
7f13a0d2f000-7f13a0d51000 r-xp 00000000 fd:01 1066085 /usr/lib64/ld-2.17.so
7f13a0f44000-7f13a0f47000 rw-p 00000000 00:00 0
7f13a0f4e000-7f13a0f4f000 rw-s 00000000 00:04 2 /SYSV00001234 (deleted) #被附加的SystemV共享内存段相关
7f13a0f4f000-7f13a0f50000 rw-p 00000000 00:00 0
7f13a0f50000-7f13a0f51000 r--p 00021000 fd:01 1066085 /usr/lib64/ld-2.17.so #动态链接器
7f13a0f51000-7f13a0f52000 rw-p 00022000 fd:01 1066085 /usr/lib64/ld-2.17.so
7f13a0f52000-7f13a0f53000 rw-p 00000000 00:00 0
7ffc63315000-7ffc63336000 rw-p 00000000 00:00 0 [stack]
7ffc633f9000-7ffc633fb000 r-xp 00000000 00:00 0 [vdso] #linux-gate虚拟动态共享对象
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
- 第一列:内存段被映射到虚拟地址反问
- 第二列:内存段的保护位和标记位,最后一个字母表示内存段的映射标记,取指要么私有(p)要么共享(s)
- 第三列:段在对应映射文件中的十六进制偏移量(对system V共享内存来讲,偏移量总是0)
- 第四列:相应的映射文件所位于的设备的设备号(主要和次要ID)
- 第五列:映射文件的i-node或System V共享内存段的标识符
- 第六列:与这个内存段相关联的文件名或其他标识标签
在共享内存中存储指针
每个进程都可能会用到不同的共享库和内存映射,并且可能会附加不同的共享内存段集。因此如果遵循推荐的做法,让内核来选择将共享内存段附加到何处,那么一个段在各个进程中可能会被附加到不同的地址上。所以在共享内存段中存储指向段中其他地址的引用时应该使用(相对)偏移量,而不是(绝对)指针
共享内存控制操作
shmctl()系统调用在shmid标识的共享内存段上执行一组控制操作
#include <sys/types.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
内存映射
mmap()在调用进程虚拟地址空间创建一个新内存映射:
- 文件映射: 将一个文件的一部分直接映射到调用进程的虚拟内存中。映射的分页会在需要的时候从文件中自动加载。这种映射称为基于文件的映射或内存映射文件
- 匿名映射:没有对应的文件。这种映射的分页会被初始化为0
一个进程的映射中的内存可以与其他进程中的映射共享(即各个进程的分页表条目指向RAM中相同的分页)这种行为会在两种情况下发生:
- 当两个进程映射了一个文件的同一个区域时它们会共享物理内存的相同分页
- 通过fork()创建的子进程会继承父进程的映射的副本,并且这些映射所引用的物理内存分页与父进程中相应映射所引用的分页相同
映射类型
变更的可见性 文件 匿名
私有 根据文件内容初始化内存 内存分配
共享 内存映射I/O:进程间共享内存(IPC) 进程间共享内存
四种不同的内存映射的创建和使用:
- 私有文件映射: 映射的内存被初始化为一个文件区域中的内容。多个映射同一个文件的进程初始时会共享同样的内存物理分页。但系统使用写实复制技术使得一个进程对映射所作出的变更对其他进程不可见
这种映射的主要用途是使用一个文件的内容来初始化一块内存区域。一些常见的例子包括根据二进制可执行文件或共享库文件的相应部分来初始化一个进程的文本和数据段
- 私有匿名映射: 每次调用mmap()创建一个私有匿名映射时都会产生一个新映射,该映射与同一(或不同)进程创建的其他匿名映射是不同的(即不会共享物理分页)。
私有匿名的主要用途是为一个进程分配新(用零填充)内存(如在分配大块内存时malloc()会为此而使用mmap)
- 共享文件映射:所有映射一个文件的同一区域的进程会共享同样的内存物理分页,这些分页的内容将被初始化为该我呢间区域。对映射内容的修改直接在文件中进行。 两个用途:
- 第一:允许内存映射IO:这表示一个文件会被加载到进程的虚拟内存中的一个区域中并且对该块内容的变更会自动被写入到这个文件中。因此,内存映射IO为使用read()/write()来执行文件IO这种做法提供了一种替代方案
- 第二:允许无关进程共享一块内容,以便以一种类似System V共享内存段的方式来执行IPC
- 共享匿名映射: 与私有匿名映射一样,每次调用mmap()创建一个共享匿名映射时都会产生一个新的、与其他映射不共享分页的映射。差别在于映射的页不会被写时复制。
这意味着当一个子进程在fork()之后继承映射时,父进程和子进程共享同样的RAM分页,并且一个进程对映射内容所做出的变更会对其他进程可见
mmap()系统调用在调用进程虚拟地址空间创建一个新映射。成功会返回新映射的起始地址
文件映射
创建一个文件映射需要执行下面的步骤:
- 获取文件的一个描述符,通常通过调用Open()来完成
- 将文件描述符作为fd参数传入mmap()调用
执行上述步骤后mmap()会将打开的文件的内容映射到调用进程的地址空间中,一旦mmap()被调用之后就能关闭文件描述符了,而不会对映射产生任何影响
私有文件映射
用途:
- 允许多个执行同一个程序或使用同一个共享库的进程共享同样的(只读的)文本段,他是从底层可执行文件或库文件的相应部分映射而来
- 映射一个可执行文件爱你或共享库的初始化数据段。这种映射会被处理成私有映射是的对映射数据段内容的变更不会发生在底层文件上 mmap()的这两种用法通常对程序是不可见的,因为这些映射是由程序加载器和动态链接器创建的
共享文件映射
当多个进程创建了同一个文件区域的共享映射时,他们会共享同样的内存物理分页
同步映射区
内核会自动将发生在MAP_SHARED映射内容上的变更写入到底层文件中,但在默认情况下,内核不保证这种同步操作会在何时发生
msync()系统调用让应用程序能够显示的控制何时完成共享映射与映射文件之间的同步。同步一个映射与底层文件在多种情况下都是非常有用的
如:为确保数据完整性,一个数据库应用程序可能会调用msync()强制将数据写入到磁盘。
调用msync()还允许一个应用程序确保在可写入映射上发生的更新会对该文件上执行read()的其他进程可见
Linux提供一个所谓的同一虚拟内存系统。这表示内存映射和高速缓冲区块会尽可能得共享同样的物理内存分页。因此通过映射获取的文件视图与通过IO系统调用获得的文件视图总是一致的,msync()的唯一用途就是强制将一个映射区域中的内容写入到磁盘