Linux 进程同步实战入门(共享内存、信号量)

  为了共同完成某项任务,不同进程间需要有一种协作的方式。其中最简单的一种,就是两个进程共享一块物理内存区域。多个进程都可以在其中读写数据,这样就完成了数据从一个进程传送到另一个进程的功能。但单纯使用共享内存还不够,当多个进程并发执行,一同访问共享数据区域,就可能产生数据错误。(关于并发和竞争条件,本文不详细展开,读者可以查阅其他资料)**所以,我们还需要一种同步这些进程行为的方式。**本文我们将采用信号量实现进程同步。

生产者-消费者问题

  让我们来考虑这种场景:有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: 获取的shmid
  • cmd: 对该共享内存段执行的命令
    • 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: 信号量在数组中的索引号,只有一个填0
  • cmd: 信号量命令(部分主要命令)
    • 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:执行操作的设置,一般为0
      • SEM_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_Keyglibc实现代码如下:

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.
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值