Linux 平台上在 Kernel 协调下完成进程之间的相互通信,有多种进程间通信 —— Inter Process Communication(IPC)方式。
1. IPC 分类
按照功能用途来看有三种基本的进程间通信类型,分别用户信息交换(Communication),同步(Synchronization)和信号(Signal)。另外,在基本的 IPC 通信机制之上还存在更为复杂和广泛应用的进程间通信机制,通常提供了丰富和更高层次的封装以便于应用开发,比如 Android 平台采用的 Binder 机制以及广泛应用的 D-Bus。
- Communication:如 pipe 和 memory mapping
- Synchronization:如 eventfd
- Signal:Linux 上的 signal 机制
- Others:Binder,D-Bus
下面分别来介绍各种类型的 IPC 机制。
2. Communication 类型的 IPC
Linux 平台上最为常见的是用于 Communication 的 IPC 机制,通常从狭义的角度来理解 IPC,可以将其认为是用于 Communication 的这一类,列举如下:
- Pipes
- FIFOs
- Pseudoterminals
- Sockets:分为 Streams 和 Datagram(以及 Seq)类型,主要的 UNIX 和 Internet domain 套接字
- POSIX message queues
- POSIX shared memory
- System V message queues
- System V shared memory
- Shared memory mappings:分为 File 以及 Anonymous 映射
- Cross-memory attach:proc_vm_readv 和 proc_vm_writev
对他们进行进一步的细分,首先可以分为 data transfer 用途和 shared memory,其中 data transfer 显而易见存在消息或者数据的流动传输,通过 Kernel 提供的机制将数据以中转的方式通常从一个进程到 Kernel 再到另一个进程;而 shared memory 则是通过进程之间共享存储区域的方式实现消息或数据的通信。Data transfer 又分为 byte stream,pseudo-terminal 和 message,广泛应用于各种场景。立体的划分如下图所示:
下面对常见的主要用于 Communication 的 IPC 机制进行分析。
2.1 Pipe
这里将 Pipe 和 FIFO 分开来讨论,首先介绍 Pipe,然后在其基础上再分析 FIFO。Pipe 最常见的应用方式:
$ ls | pr | lpr
这个管道应用实现了输入输出的重定向,ls列当前目录的输出被作为标准输入送到pr程序中,而pr的输出又被作为标准输入送到lpr程序中。Shell 负责在不同进程之间建立临时的管道。
Pipe 在内核中借助文件系统来实现,其源码在 fs/pipe.c 中。在 Kernel 初始化的过程中 fs_initcall 阶段将 pipe_fs_type 文件系统类型注册到文件系统中:
/*
* pipefs should _never_ be mounted by userland - too much of security hassle,
* no real gain from having the whole whorehouse mounted. So we don't need
* any operations on the root directory. However, we need a non-trivial
* d_name - pipe: will go nicely and kill the special-casing in procfs.
*/
static int pipefs_init_fs_context(struct fs_context *fc)
{
struct pseudo_fs_context *ctx = init_pseudo(fc, PIPEFS_MAGIC);
if (!ctx)
return -ENOMEM;
ctx->ops = &pipefs_ops;
ctx->dops = &pipefs_dentry_operations;
return 0;
}
static struct file_system_type pipe_fs_type = {
.name = "pipefs",
.init_fs_context = pipefs_init_fs_context,
.kill_sb = kill_anon_super,
};
static int __init init_pipe_fs(void)
{
int err = register_filesystem(&pipe_fs_type);
if (!err) {
pipe_mnt = kern_mount(&pipe_fs_type);
if (IS_ERR(pipe_mnt)) {
err = PTR_ERR(pipe_mnt);
unregister_filesystem(&pipe_fs_type);
}
}
return err;
}
fs_initcall(init_pipe_fs);
在 Kernel 中 Pipe 以单向(Unidirectional)字节流缓冲区的形式存在,在 VFS 中有与之关联的临时 inode 表示,通过两个 file 数据结构与其关联,分别用于读和写操作,在用户态分别对应到进程中的两个文件描述符,如下图所示:
通过 pipe 系统调用(glibc 等标准库提供相应的应用调用接口),获得两个文件系统描述符,其中 filedes[0] 用于读,filedes[1] 用于写:
int filedes[2];
pipe(filedes);
...
write(filedes[1], buf, count);
read(filedes[0], buf, count);
在父子进程之间通过 fork 可以将 pipe 进行共享以用于父子进程间的通信,在 fork 过程中父子进程之间共享文件描述符。而多余的打开文件描述符则需要在 fork 执行之后进行关闭:
int filedes[2];
pipe(filedes);
child_pid = fork();
if (child_pid == 0) {
close(filedes[1]);
/* Child now reads */
} else {
close(filedes[0]);
/* Parent now writes */
}
在 Linux 的 Manual 页有 sample:https://man7.org/tlpi/code/online/dist/pipes/simple_pipe.c.html
当写入进程对管道写时,字节被拷贝到共享数据页面中,当读取进程从管道中读时,字节从共享数据页面中拷贝出来。同时 Pipe 缓冲区有大小限制 PIPE_BUF,在 Linux 平台为 4096B,POSIX 标准要求最低 512B。Linux必须同步对管道的访问,它必须保证读者和写者以确定的步骤执行,并且可能出现缓冲区为空或者已满以及读者或写者不存在的情况,为此需要使用锁、等待队列和信号等同步机制。同步机制在文章后面进行介绍。
Pipe 支持 dup2 和 fcntl 操作,不能进行 lseek 操作。
2.2 FIFO
First-In-First-Out, 简称问 FIFO,中文用“命名管道”表述,也即 pipe with name in file system。它建立在 Pipe 基础之上,通过 mkfifo 创建,然后可以以文件的形式进行打开和读写操作。
相比 Pipe,FIFO 在相应的文件系统中存在对应的 inode 节点,相当于实际存在于对应的文件系统中,而实际的数据缓冲区域则存在于内存中。因此可以用于非关联进程之间的通信。
2.3 POSIX Message Queue
POSIX Message Queues 实现了 POSIX 标准的消息队列,是一种基于消息 Message 的通信方式,可以存在多个消息发送和读取者,每次读取一个消息进行处理,并且支持消息的优先级别和消息通知。
在用户空间有专门的 POSIX MQ APIs 支持消息操作:
- Queue management (analogous to files)
- mq_open(): open/create MQ, set attributes
- mq_close(): close MQ
- mq_unlink(): remove MQ pathname
- I/O
- mq_send(): send message
- mq_receive(): receive message
- Other
- mq_setattr(), mq_getattr(): set/get MQ attributes
- mq_notify(): request notification of msg arrival
通过 mq_open 可以打开或者创建新的 MQ,其中 name 为其对应的名字,在伪终端文件系统上可以看到 /somename:
int mqd = mq_open(name, flags [, mode, &attr]);
其中 flags 类似 open 操作:
- O_CREAT – create MQ if it doesn’t exist
- O_EXCL – create MQ exclusively
- O_RDONLY, O_WRONLY, O_RDWR – just like file open
- O_NONBLOCK – non-blocking I/O
通过 attributes 可以控制 MQ 的属性:
struct mq_attr {
long mq_flags; // MQ description flags, 0 or O_NONBLOCK, [mq_getattr(), mq_setattr()]
long mq_maxmsg; // Max. # of msgs on queue, [mq_open(), mq_getattr()]
long mq_msgsize; // Max. msg size (bytes), [mq_open(), mq_getattr()]
long mq_curmsgs; // # of msgs currently in queue, [mq_getattr()]
};
比如 MQ 的大小等。
Message Queue 在 Kernel 中也是以文件系统的形式存在,其实现在 ipc/mqueue.c 文件中,在 Kernel 启动的 device_initcall 阶段进行了初始化:
static const struct inode_operations mqueue_dir_inode_operations = {
.lookup = simple_lookup,
.create = mqueue_create,
.unlink = mqueue_unlink,
};
static const struct file_operations mqueue_file_operations = {
.flush = mqueue_flush_file,
.poll = mqueue_poll_file,
.read = mqueue_read_file,
.llseek = default_llseek,
};
static const struct super_operations mqueue_super_ops = {
.alloc_inode = mqueue_alloc_inode,
.free_inode = mqueue_free_inode,
.evict_inode = mqueue_evict_inode,
.statfs = simple_statfs,
};
static const struct fs_context_operations mqueue_fs_context_ops = {
.free = mqueue_fs_context_free,
.get_tree = mqueue_get_tree,
};
static struct file_system_type mqueue_fs_type = {
.name = "mqueue",
.init_fs_context = mqueue_init_fs_context,
.kill_sb = kill_litter_super,
.fs_flags = FS_USERNS_MOUNT,
};
int mq_init_ns(struct ipc_namespace *ns)
{
struct vfsmount *m;
ns->mq_queues_count = 0;
ns->mq_queues_max = DFLT_QUEUESMAX;
ns->mq_msg_max = DFLT_MSGMAX;
ns->mq_msgsize_max = DFLT_MSGSIZEMAX;
ns->mq_msg_default = DFLT_MSG;
ns->mq_msgsize_default = DFLT_MSGSIZE;
m = mq_create_mount(ns);
if (IS_ERR(m))
return PTR_ERR(m);
ns->mq_mnt = m;
return 0;
}
void mq_clear_sbinfo(struct ipc_namespace *ns)
{
ns->mq_mnt->mnt_sb->s_fs_info = NULL;
}
void mq_put_mnt(struct ipc_namespace *ns)
{
kern_unmount(ns->mq_mnt);
}
static int __init init_mqueue_fs(void)
{
int error;
mqueue_inode_cachep = kmem_cache_create("mqueue_inode_cache",
sizeof(struct mqueue_inode_info), 0,
SLAB_HWCACHE_ALIGN|SLAB_ACCOUNT, init_once);
if (mqueue_inode_cachep == NULL)
return -ENOMEM;
/* ignore failures - they are not fatal */
mq_sysctl_table = mq_register_sysctl_table();
error = register_filesystem(&mqueue_fs_type);
if (error)
goto out_sysctl;
spin_lock_init(&mq_lock);
error = mq_init_ns(&init_ipc_ns);
if (error)
goto out_filesystem;
return 0;
out_filesystem:
unregister_filesystem(&mqueue_fs_type);
out_sysctl:
if (mq_sysctl_table)
unregister_sysctl_table(mq_sysctl_table);
kmem_cache_destroy(mqueue_inode_cachep);
return error;
}
device_initcall(init_mqueue_fs);
关于 Message Queue 的详细用法可以参考 https://man7.org/tlpi/code/online/dist/pmsg/ 中的实例。
2.4 Shared Memory
共享内存的最大特点就是高效,通过减少数据在存储区域之间的拷贝来实现高性能的进程间通信操作,广泛应用于如多媒体等大量数据通信和处理的应用中。具体来讲,通常的数据传输通信中数据的经过了 user space ==> kernel ==> user space 处理流程,从而存在两次拷贝操作,在 Kernel 中 copy_from_user 和 copy_to_user 就是用来完成相应的数据拷贝操作的。而 Shared Memory 则只需要将数据拷贝到特定的内存中,并将其映射到不同的进程地址空间,以实现对数据的访问。
当然这样的高效率特性同样要求 Shared Memory 的使用者采取其他的机制来实现同步来避免错误。
Shared Memory 有如下三种类型:
- Shared Anonymous Mappings:用于关联进程之间
- Shared File Mappings:用于非关联进程之间,采用文件备份
- POSIX Shared Memory:用于非关联进程之间,且不采用传统的文件备份
三者采用相同的系统调用,实际使用则填写不同的参数:
void *mmap(void *daddr, size_t len, int prot, int flags, int fd, off_t offset);
使用方式:
void *addr = mmap(daddr, len, prot, flags, fd, offset);
各项参数的含义:
- daddr – choose where to place mapping;
- Best to use NULL, to let kernel choose
- len – size of mapping
- prot – memory protections (read, write, exec)
- flags – control behavior of call
- MAP_SHARED, MAP_ANONYMOUS
- fd – file descriptor for file mappings
- offset – starting offset for mapping from file
- addr – returns address used for mapping
其中 addr 作为返回参数相当于普通的 C 指针,其指向的内容的修改对所有的相关进程可见。
2.4.1 Shared Anonymous Mapping
用于关联进程,在 mmap 调用中 fd 和 offset 不会用到,即不会涉及文件信息,目的地址一般也不需要指定,直接写入 NULL,这样 Kernel 会帮忙查找对应的地址空间中可以匹配的地址进行映射并在成功后返回给 addr。需要特别注意指定 MAP_ANONYMOUS 标识。
addr = mmap(NULL, length,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
-1, 0);
pid = fork();
length 标识需要映射的地址长度,在关联进程吉间即共享了 addr:length 内存。
2.4.2 Shared File Mapping
又称为“文件映射”共享内存,用于非关联进程之间,以文件备份共享,所以涉及到文件操作。
int fd = open(...);
void *addr = mmap(..., fd, offset);
而内存内容从打开的文件初始化,存在 “memory-mapped I/O”,因此对于内存的更新同样会更新到文件,而不同的进程如果映射到相同文件的相同区域,则相互共享映射到的内存。
fd = open(pathname, O_RDWR);
addr = mmap(NULL, length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, 0);
...
close(fd); /* No longer need 'fd' */
2.4.3 POSIX Shared Memory
用于非关联进程之间内存共享,并不需要创建文件,所以省去了 I/O 相关的性能损耗,具有更高的效率。
POSIX SHM 有其对应的 APIs:
- Object management
- shm_open(): open/create SHM object
- mmap(): map SHM object
- shm_unlink(): remove SHM object pathname
- Operations on SHM object via fd returned by shm_open():
- fstat(): retrieve info (size, ownership, permissions)
- ftruncate(): change size
- fchown(): fchmod(): change ownership, permissions
SHM 的操作及其特性类似于 FIFO,存在对应的文件名可以在 tmpfs(在 /dev/shm)中查看到,并且其大小可以进行修改,O_TRUNC flag 位以及 ftruncate 系统调用。
SHM 在内核中的实现在 mm/shmem.c,以 shmem_fs_type 文件系统形式存在,在早期版本中为 tmpfs_fs_type 形式:
static struct file_system_type shmem_fs_type = {
.owner = THIS_MODULE,
.name = "tmpfs",
.init_fs_context = shmem_init_fs_context,
#ifdef CONFIG_TMPFS
.parameters = shmem_fs_parameters,
#endif
.kill_sb = kill_litter_super,
.fs_flags = FS_USERNS_MOUNT | FS_THP_SUPPORT,
};
int __init shmem_init(void)
{
int error;
shmem_init_inodecache();
error = register_filesystem(&shmem_fs_type);
if (error) {
pr_err("Could not register tmpfs\n");
goto out2;
}
shm_mnt = kern_mount(&shmem_fs_type);
if (IS_ERR(shm_mnt)) {
error = PTR_ERR(shm_mnt);
pr_err("Could not kern_mount tmpfs\n");
goto out1;
}
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
if (has_transparent_hugepage() && shmem_huge > SHMEM_HUGE_DENY)
SHMEM_SB(shm_mnt->mnt_sb)->huge = shmem_huge;
else
shmem_huge = 0; /* just in case it was patched */
#endif
return 0;
out1:
unregister_filesystem(&shmem_fs_type);
out2:
shmem_destroy_inodecache();
shm_mnt = ERR_PTR(error);
return error;
}
其初始化过程发生在 mnt_init 阶段:
void __init mnt_init(void)
{
...
shmem_init();
init_rootfs();
init_mount_tree();
}
调用过程为:
关于 SHM 的实例参考:https://man7.org/tlpi/code/online/dist/pshm
3. Synchronization 类型的 IPC
用于 Synchronization 的 IPC 机制主要完成较为同步过程,比如辅助 Communication 类型的 IPC(如 SHM)实现更为复杂的功能。列举如下:
- Eventfd
- Futexes
- Record locks
- File locks
- Mutexes
- Condition variables
- Barriers
- Read-write locks
- POSIX semaphores:分为 Named 和 Unnamed 类型
- System V semaphores
归类来讲分为 semaphore,eventfd,file lock,futex 和 thread-related 类型。相关归类如下图所示:
就同步本身来讲是为了实现对资源的访问同步,包括内存以及文件资源:
- Shared Memory - semaphores
- File - file locks
下面分别对主要的 Synchronization IPC 机制进行分析。
3.1 POSIX Semaphores
POSIX Semaphores 实质为在 Kernel 中维护的整数,获取过程中会对该值减一操作,如果小于零则 Kernel 会阻塞操作,因此可能会导致进程进入睡眠状态。POSIX Semaphore 可以分为两种,Named 和 Unnamed 类型,其中 Unnamed 类型嵌入在共享的内存中,而 Named 类型则为独立的命名实体。
3.1.1 Unnamed Semaphores
与 Named Semaphore 的接口不同,采用 sem_init 完成初始化,然后使用:
- sem_init(semp, pshared, value): initialize semaphore pointed to by semp to value
- sem_t *semp
- pshared: 0, thread sharing; != 0, process sharing
- sem_post(semp): add 1 to value
- sem_wait(semp): subtract 1 from value
- sem_destroy(semp): free semaphore, release resources back to system
用于读写进程之间的数据同步,通过 POSIX Shared Memory 来完成数据的发送来完成 SHM 的共享访问:
#define BUF_SIZE 1024
struct shmbuf { // Buffer in shared memory
sem_t wsem; // Writer semaphore
sem_t rsem; // Reader semaphore
int cnt; // Number of bytes used in 'buf'
char buf[BUF_SIZE]; // Data being transferred
}
上面的 shmbuf 定义了用于数据传输的结构,包括共享的存储内容和 POSIX Semaphore,下面是 Writer 的实例:
fd = shm_open(SHM_PATH, O_CREAT|O_EXCL|O_RDWR, OBJ_PERMS);
ftruncate(fd, sizeof(struct shmbuf));
shmp = mmap(NULL, sizeof(struct shmbuf),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
sem_init(&shmp->rsem, 1, 0);
sem_init(&shmp->wsem, 1, 1); // Writer gets first turn
for (xfrs = 0, bytes = 0; ; xfrs++, bytes += shmp->cnt) {
sem_wait(&shmp->wsem); // Wait for our turn
shmp->cnt = read(STDIN_FILENO, shmp->buf, BUF_SIZE);
sem_post(&shmp->rsem); // Give reader a turn
if (shmp->cnt == 0) // EOF on stdin?
break;
}
sem_wait(&shmp->wsem); // Wait for reader to finish
// Clean up
Reader 的实例如下:
fd = shm_open(SHM_PATH, O_RDWR, 0);
shmp = mmap(NULL, sizeof(struct shmbuf),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
for (xfrs = 0, bytes = 0; ; xfrs++) {
sem_wait(&shmp->rsem); // Wait for our turn */
if (shmp->cnt == 0) // Writer encountered EOF */
break;
bytes += shmp->cnt;
write(STDOUT_FILENO, shmp->buf, shmp->cnt) != shmp->cnt);
sem_post(&shmp->wsem); // Give writer a turn */
}
sem_post(&shmp->wsem); // Let writer know we're finished
打开共享内存地址,将对应的内容映射到 shmp 从而可以进行访问,以对应的数据结构访问其中的读写信号量,实现同步的功能
3.1.2 Named Semaphores
其处理过程涉及到 name 信息,为信号量的路径,以 tmpfs(最新 shmem_fs_type)形式存在,通过 sem_open 打开或创建,而 sem_unlink 来删除
3.2 Eventfd
Eventfd 是一种简化形式的文件系统实现,其在 Kernel 中的源码为 fs/eventfd.c,并没有实现具体的文件系统,只是定义了特定的一些系统调用,在系统调用内部安装了文件描述符信息以及对应的文件操作接口,实现 eventfd 特定的文件操作:
static const struct file_operations eventfd_fops = {
#ifdef CONFIG_PROC_FS
.show_fdinfo = eventfd_show_fdinfo,
#endif
.release = eventfd_release,
.poll = eventfd_poll,
.read_iter = eventfd_read,
.write = eventfd_write,
.llseek = noop_llseek,
};
系统调用的定义:
SYSCALL_DEFINE2(eventfd2, unsigned int, count, int, flags)
{
return do_eventfd(count, flags);
}
SYSCALL_DEFINE1(eventfd, unsigned int, count)
{
return do_eventfd(count, 0);
}
Eventfd 主要用于内核与用户态进程之间的事件通知,以实现资源的高效利用,在用户空间通过如下方式调用:
int eventfd(unsigned int initval, int flags);
内核维护对应的 64 位整形计数器,初始化为 initval 值。而 flags 有如下标识位:
- EFD_CLOEXEC:FD_CLOEXEC,简单说就是fork子进程时不继承,对于多线程的程序设上这个值不会有错的
- EFD_NONBLOCK:文件会被设置成O_NONBLOCK,一般要设置
- EFD_SEMAPHORE:(2.6.30以后支持)支持semophore语义的read,简单说就值递减1
对其进行 read 操作就是将对应的 counter 置零,如果是 semophore 则减去一,write 则设置 counter 的值,支持 epoll/poll/select 操作,如上面的 eventfd_fops 定义。
在 Android 等平台上 eventfd 被用来作为基础的通信机制,在其 Native 层实现上采用其作为 Message 的送达通知机制。
3.3 File Lock
File lock 参考 http://blog.hongxiaolong.com/posts/flock-and-lockf.html
3.4 Futex
Futex 为 File lock 的互斥类型。
4. Socket
Socket 较为特殊,特别列出来单独讨论。
5. Binder
Android 平台的 Binder 机制是非常重要且复杂的内容,这里也特别列出来单独讨论。
6. D-Bus
D-Bus 是目前最广泛应用于各大 Linux 常用平台的进程间通信机制,下面单独分析。