为了共同完成某项任务,不同进程间需要有一种协作的方式。其中最简单的一种,就是两个进程共享一块物理内存区域。多个进程都可以在其中读写数据,这样就完成了数据从一个进程传送到另一个进程的功能。但单纯使用共享内存还不够,当多个进程并发执行,一同访问共享数据区域,就可能产生数据错误。(关于并发和竞争条件,本文不详细展开,读者可以查阅其他资料)**所以,我们还需要一种同步这些进程行为的方式。**本文我们将采用信号量实现进程同步。
生产者-消费者问题
让我们来考虑这种场景:有n台电脑,m台打印机。n台电脑分别在运行需要打印机输出的程序,而m台打印机都可以胜任。**它们共享一个长度为l的缓冲区,**缓冲区可以循环使用。
这就是一个实例化的生产者-消费者问题。产生数据的电脑是生产者,而将数据打印出来消耗的打印机是消费者。在下面的例子中,我们将通过共享内存和信号量实现这样一个问题。
共享内存
共享内存(Shared Memory
),是进程通信(IPC, Interprocess Communication)
中最简单的一种方式。它允许多个进程访问同一块内存空间。共享内存的操作API如下:
创建共享内存 shmget()
原型如下:
/* Get shared memory segment. */
extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW;
该函数将会创建一块共享内存,并将其标识符返回(失败返回-1)
Parameters:
-
key: IPC通信键值,功能类似于组ID,用于标识一组相互通信的进程。详见第4节
ftok()
-
size: 共享内存大小,同
malloc
-
shmflg: 共享内存标志位,包含两个部分:
-
IPC控制指令,用于控制
shmget()
函数行为,位于bits/ipc.h
下,不可直接引用,需引用sys/ipc.h
/* Mode bits for `msgget', `semget', and `shmget'. */ #define IPC_CREAT 01000 /* Create key if key does not exist. */ #define IPC_EXCL 02000 /* Fail if key exists. */ #define IPC_NOWAIT 04000 /* Return error on wait. */
- 对于创建者,我们一般采用
IPC_CREAT
,如果不存在共享内存块对应的Key,就创建一个。 - 对于使用者(Client),不需要加控制指令,直接跟后面的权限位即可
- 对于创建者,我们一般采用
-
内存权限位:同文件的权限掩码,一般以3位八进制表示,从高到低分别为
rwx
,我们一般使用的掩码为0666
,其中0为八进制前缀。
-
常规用法:
int shmid = shmget(key, size, IPC_CREAT | 0666);
注意:一个IPC_Key只能创建一个共享内存段,当创建完成后,再使用这个Key创建,会直接返回Key对应的段,不会新创建(与size无关)
映射共享内存 shmat()
刚被创建完,或是刚获取到的共享内存标识符不能被直接使用,需要使用shmat
(shared memory attach)映射到进程自己的内存空间中。
/* Attach shared memory segment. */
extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg) __THROW;
-
shmid
: 通过shmget获取的shmid -
shmaddr
: 如果指定,则将内存映射到指定位置;否则,系统将自动分配合适地址 -
shmflg
:映射共享内存的标志参数,见下,一般不特殊指定,填0/* Flags for `shmat'. */ #define SHM_RDONLY 010000 /* attach read-only else read-write */ #define SHM_RND 020000 /* round attach address to SHMLBA */ #define SHM_REMAP 040000 /* take-over region on attach */ #define SHM_EXEC 0100000 /* execution access */
函数返回映射的目标地址指针,失败返回-1
解除映射 shmdt()
/* Detach shared memory segment. */
extern int shmdt (const void *__shmaddr) __THROW;
- 解除共享内存的映射,注意不会删除共享内存段,只是解除映射
共享内存控制 shmctl()
/* Shared memory control operation. */
extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW;
shmid
: 获取的shmidcmd
: 对该共享内存段执行的命令IPC_STAT
:将共享内存段信息复制到buf
IPC_SET
: 将buf设置的共享内存段信息设置到该段IPC_RMID
:将该段标记为删除,最常用,详见注意事项IPC_LOCK
:锁定该段,该段将不会被交换出内存,必须由root
执行
buf
:被部分命令使用,用于存放内存段控制信息,IPC_RMID
不需要,设置为NULL
即可
注意:删除共享内存段不会直接删除该段,而是将其标记为SHM_DEST
(Destroy on last detach)。当没有任何一个进程映射该内存段,它会自动删除。
信号量
信号量的基本知识,本文不做展开,有兴趣的读者请自行查阅资料。
信号量分类
目前Linux有两套信号信号量方案,一套是基于POSIX兼容层标准的信号量,位于semahphore.h
中,另一类则与上面的共享内存相同,采用SystemV
的方案。
- POSIX方案:
- 使用方便,其中无名信号量基于内存空间,有名信号量基于文件系统
- 一般适用于父子进程,无名信号量使用方便
- 有名信号量基于文件系统,效率较低,使用不多
- SystemV方案:
- 使用较为复杂(较POSIX而言)
- 基于内核IPC设施,信号量存放在内核中,具有持续性
所以,我们与上面的Shared Memory保持一致,采用SystemV的方案。
创建/获得信号量 semget()
信号量和共享内存同属SystemV IPC设施,接口基本一样,具有较好的通用性
/* Get semaphore. */
extern int semget (key_t __key, int __nsems, int __semflg) __THROW;
key
: IPC_key, 也可以通过ftok
生成,可以和shared memory重复nsems
: 信号数。semget()函数初始化一个信号量数组,里面可以有多个信号量,方便使用semflg
:类似于共享内存的flag,参数值同共享内存
函数返回sim_id
,即信号量组标识符。
常用语句:
int sem_id = semget(key, num, IPC_CREAT | 0666);
信号量控制 semctl()
此函数可以对信号量进行管理,包括删除,初始化等。
/* Semaphore control operation. */
extern int semctl (int __semid, int __semnum, int __cmd, ...) __THROW;
semid
: 信号量组标识符,通过semget()获得sumnum
: 信号量在数组中的索引号,只有一个填0cmd
: 信号量命令(部分主要命令)GETVAL
:返回值为对应信号量的值SETVAL
:通过semun
共用体设置信号的值,semun
位于第四个参数IPC_RMID
: 删除信号量组
注意:
-
部分命令(
SETVAL
等)需要加第四个参数,类型为共用体semun
union semun { int val; // <= value for SETVAL struct semid_ds *buf; // <= buffer for IPC_STAT & IPC_SET unsigned short int *array; // <= array for GETALL & SETALL struct seminfo *__buf; // <= buffer for IPC_INFO };
- 此部分在现行glibc中没有直接定义,需要使用者自行定义
- 对于
SETVUL
,将val
设置为对应值即可
信号量初始化示例:
int initSemaphores(void)
{
union semun sem_union;
int sem_id = getSemaphores();
// initialize semaphores
int init_val[] = {1, 0, BUFFER_SLOT_NUM, CONSUMER_NUM + PRODUCER_NUM};
for (int i = 0; i < SEM_COUNT; i++)
{
sem_union.val = init_val[i];
if (semctl(sem_id, i, SETVAL, sem_union) == -1)
perror("semaphore init");
}
return sem_id;
}
- 部分宏为自行定义,读者可不必理会
信号量操作 semop()
此函数对信号量进行常规操作:
/* Operate on semaphore. */
extern int semop (int __semid, struct sembuf *__sops, size_t __nsops) __THROW;
-
semid
: 信号量组标识符,通过semget()获得 -
sops
: 对信号量进行的操作,类型为struct sembuf
/* Structure used for argument to `semop' to describe operations. */ struct sembuf { unsigned short int sem_num; /* semaphore number */ short int sem_op; /* semaphore operation */ short int sem_flg; /* operation flag */ };
sem_num
:操作的信号量索引sem_op
:对信号量进行的操作+1
: 信号量+1,即V操作-1
: 信号量-1,即P操作0
:wait-for-zero
操作:进程挂起等待,直到信号量为0,所有对该信号量进行此操作的线程全部被唤醒
sem_flg
:执行操作的设置,一般为0SEM_UNDO
: 在进程退出时,将对信号量的操作还原,即使信号量被锁定也不会阻塞
-
nsops
:对信号量操作的个数,一般为1
信号量操作示例:
enum Operation {
op_P = -1,
op_w4z,
op_V
};
void sem_PV(int sem_id, int semnum, int op)
{
struct sembuf mybuf;
mybuf.sem_flg = 0;
mybuf.sem_num = semnum;
mybuf.sem_op = op;
if (semop(sem_id, &mybuf, 1) == -1)
perror("semop");
}
相关知识
ftok() 与 IPC_Key
IPC_Key
,类似于组ID,更通俗一点,当我们持有同一个组ID时,相当于我们是一组内的,可以互相通信,这里的互相通信,就是访问同一块共享内存。那么,如何得到这个ID?C标准库为我们提供了这样一个函数ftok()
。利用一个存在的目录名和一个自定义ID,生成一个特殊的IPC_Key
/* Generates key for System V style IPC. */
extern key_t ftok (const char *__pathname, int __proj_id) __THROW;
pathname
:目录名称,注意必须存在,否则返回错误proj_id
:一个自定义的序号,所有需要共享此内存块的都使用它
原理上,它调用stat
系统调用,获取pathname
对应的文件inode
,拼接上proj_id
后,生成整型的IPC_Key
。glibc
实现代码如下:
ftok (const char *pathname, int proj_id)
{
struct stat64 st;
key_t key;
if (__xstat64 (_STAT_VER, pathname, &st) < 0)
return (key_t) -1;
key = ((st.st_ino & 0xffff) | ((st.st_dev & 0xff) << 16)
| ((proj_id & 0xff) << 24));
return key;
}
但是,这种方法仍可能产生Key碰撞,别的进程正在使用的ID与当前生成ID相同,导致访问同一块内存。如果在父子进程中,还可以使用一个特殊的key——IPC_PRIVATE
。
IPC_PRIVATE
这是一个特殊的IPC_Key
,完全无关的进程不能通过这个Key访问同一块共享内存。它是通过操作返回的shm对象标识符来查找的。父进程在创建子进程前获得shm对象标识符,父子进程通过相同的内存空间映射共享这个标识符,进而进行链接。IPC_PRIVATE
相当于创建了一个匿名的共享内存块,只能通过标识符来调用。
perror()
此函数用于打印错误信息,使用时需要stdio.h
, errno.h
。当函数发生错误返回时,perror根据错误号输出错误内容,方便排错。
extern void perror (const char *__s);
一般用法:
perror("Your Description");
- 输出时,会输出
Your Description: Error Description
.