本文总结进程间通信的方法和各种方法的差异,以及注意事项。

1 Linux支持的进程间通信方式

IPC类型 进程关系
半双工管道(匿名) 主机内的进程
FIFO(命名管道) 主机内的进程
消息队列 主机内的进程
信号量 主机内的进程
共享内存 主机内的进程
socket(套接字) 主机内或者主机间的进程

2 管道

3 FIFO

4 消息队列

5 共享内存

共享内存和内存映射IO的不同之处在于,共享内存不需要关联文件,使用的是虚拟内存中的匿名段。

体现在用法上,就是共享内存不需要关联文件,通过shm_open()创建一个共享内存(虚拟的文件),所有的进程就可以使用mmap来进行共享了。

5.1 XSI共享内存

XSI共享内存相关接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
key_t ftok(const char *pathname, int proj_id);
int shmget(key_t key, size_t size, int shmflg);         // get or create a shared memory
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

void *shmat(int shmid, const void *shmaddr, int shmflg); // attach shared memory
int shmdt(const void *shmaddr);

void *shmat(int shmid, const void *shmaddr, int shmflg); // detach shard memory
int shmdt(const void *shmaddr);

XSI共享内存的用法见APUE第三版的15.9节。

5.2 POSIX共享内存

POSIX共享内存接口最早出现在Linux kernel 2.4和glibc 2.2版本。通过man 7 shm_overview可以查看详细说明。POSIX共享内存接口使用更方便,通用性更强。

POSIX共享内存相关接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <sys/mman.h>

int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);

void* mmap(void* addr, size_t length, int prot, int flag, int fd, off_t offset);
int munmap(void *addr, size_t length);

// Set the size of the shared memory object.  (A newly created shared memory object has a length of zero.)
int ftruncate(int fd, off_t length);
// Close the file descriptor allocated by shm_open(3) when it is no longer needed.
int close(int fd); 

链接时需要增加链接选项-lrt

  1. shm_open():shm_open接口和open接口作用类似。只不过shm_open是为了创建一个用于共享内存的文件而已。因此shm_open()创建的文件描述符同样用close()关闭。

    • name:为了具有更好的可移植性,最好将name命名为以/开头的字符串。除了开头的/外不包含其他字符串。而name的长度也要符合当前系统的文件名长度的要求。

    • oflag:为了确保创建一个新的共享内存,可以将oflag参数设置为O_CREAT | O_EXEL。这样当name已经存在时,就会返回错误,保证只有不存在才会创建。

  2. shm_unlink():shm_unlink()删除shm_open创建的共享内存。

而要实现不同的进程共同使用同一片共享内存,只需要其中约定某个进程创建一个共享内存,然后将该共享内存的名字提供出来。其他进程就可以使用mmap来使用这个共享内存了。

6 信号量

6.1 XSI信号量

SUS提供的信号量机制,提供了相当全面的功能。但是和POSIX信号量相比,使用更加复杂,而且性能也不如POSIX信号量。

因此linux环境中一般使用POSIX信号量。感兴趣的可以阅读APUE第三版15.8节了解它的使用细节。

XSI 信号量相关接口:

1
2
3
4
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
int semctl(int semid, int semnum, int cmd, ...);
int semop(int semid, struct sembuf *sops, size_t nsops);

6.2 POSIX信号量

信号量相关接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <semaphore.h>
 typedef union {
   char __size[__SIZEOF_SEM_T];
   long int __align;                                                                                                            
} sem_t;

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);

sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
int sem_unlink(const char *name);
int sem_close(sem_t *sem);

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);

int sem_getvalue(sem_t *sem, int *sval);  // 

POSIX信号量有两种:匿名型号量和命名信号量。它们的除了创建和销毁方式不同,其它没什么区别。

  • 匿名信号量只存在于内存中,只有可以访问对应内存的进程才可以使用。这意味着只有同一个进程中的线程可以使用或者已经映射相同内存内容到它们的地址空间的不同进程中的线程。
  • 命名信号量可以通过名字访问,因此知道它们的名字的进程都可以使用。

6.2.1 信号量通用接口

  1. sem_wait():该函数用来实现信号量的减1操作。也可以理解为申请1个资源。调用该函数时,若信号量为0就会被阻塞。直到成功将信号量减1或者被一个信号中断。

  2. sem_try_wait():该函数功能和sem_wait相同,都是用来对信号减1的。但是该函数在遇到信号量为0时不会被阻塞,而是返回-1并且将errno设置为EAGAIN。

  3. sem_timedwait():该函数也是用于对信号量进行减1操作的。不同之处在于,它可以设置一个超时时间,当遇到信号量为0时,等待一个指定时间后仍然未能对信号量减1,就返回-1并将errno设置为ETIMEDOUT。

  4. sem_post():该函数用来实现信号量的加1操作。也可以理解为释放1个资源。在调用sem_post()时,存在被信号量阻塞的进程,那么被阻塞的线程会被唤醒。而且被sem_post()加1的信号量会再次被sem_wait()或者sem_timedwait()函数减1。

  5. sem_getvalue():获取当前信号量的值。该函数的操作不是原子的,即该函数返回是这个值可能已经改变,若需要获取准确的值,需要自行保证获取value的原子性。

6.2.2 匿名信号量

如果信号量只在某个进程内部使用,使用匿名信号量更方便。

  1. sem_init():创建匿名信号量。

    • sem:sem用于保存新创建的匿名信号量的地址,需要事先定义好。

      若只在进程内部使用,需要对使用该信号量的线程均可见。例如全局变量或者在堆内存中申请内存的变量。

      若需要在两个进程之间使用,则变量需要在两个进程共享内存区域中。

    • pshared:pshared参数用于指定当前信号量的共享方式。若只在进程内使用,则将值设置为0;若在进程间使用,将值设置为非0值,例如1。

    • value:value用于设置信号量的初始值。

  2. sem_destroy():销毁匿名信号量。调用sem_destroy()后,不能再使用任何带有sem的函数了。

6.2.3 命名信号量

  1. sem_open():sem_open()可以用来创建一个新的命名信号量或者使用一个已经存在的命名信号量。

    • 创建一个新的信号量的方法:如果要确保创建一个新的信号量,可以将oflag设置为O_CREAT|O_EXCL。当指定name的信号量已经存在时,会返回sem_open失败。

    • 使用已经存在的信号量的方法:使用只包含name和flag参数的sem_open函数,而且oflag设置为0。

    • name:name参数用于指定信号量的名字。为了增加可移植性,建议名字以/开头。而且除了开头的/外,不能有其他的/存在。信号量的名字有最大长度限制,其长度不应该超过_POSIX_NAME_MAX

    • oflag:oflag用于控制创建信号量的行为,参见前面两条说明。

    • mode:mode为创建的信号量的文件访问权限,同open()函数的mode。

    • value:value为创建的信号量的初始值。

    • 返回值:sem_open()函数的返回值为sem_t类型的指针,作为只用这个变量对该信号量执行其他操作。

  2. sem_close():sem_close()用于释放进程申请的所有和该命名信号量相关的资源。

    • 如果进程在退出前,没有主动关闭命名信号量,那么内核会自动关闭该进程打开所有信号量。

    • 命名信号量关闭后,不会影响信号量的值。进程退出后由内核自动关闭的信号量也是一样的。

  3. sem_unlink():sem_unlink()删除命名信号量的名字。调用该函数后信号量的名字会被立刻删除。但是只有当所有的信号量的使用者均关闭了该信号量,信号量才会被销毁。

6.2.3 二进制信号量

二值信号量和计数信号量的差异在于初始化和使用信号量。如果信号量的值只有0和1,那它就是二值信号量。

当二值信号量为1时,表示它处于"解锁状态";当信号量的值为"0"时,表示他处于”加锁“状态。

7 sockek

socket相关的内容比较多,单独作为一篇文章介绍。