学习在 Linux 中进程是如何与其他进程进行同步的。
本篇是 Linux 下进程间通信(IPC)系列的第一篇文章。这个系列将使用 C 语言代码示例来阐明以下 IPC 机制:
- 共享文件
- 共享内存(使用信号量)
- 管道(命名的或非命名的管道)
- 消息队列
- 套接字
- 信号
在聚焦上面提到的共享文件和共享内存这两个机制之前,这篇文章将带你回顾一些核心的概念。
核心概念
进程是运行着的程序,每个进程都有着它自己的地址空间,这些空间由进程被允许访问的内存地址组成。进程有一个或多个执行线程,而线程是一系列执行指令的集合:单线程进程就只有一个线程,而多线程的进程则有多个线程。一个进程中的线程共享各种资源,特别是地址空间。另外,一个进程中的线程可以直接通过共享内存来进行通信,尽管某些现代语言(例如 Go)鼓励一种更有序的方式,例如使用线程安全的通道。当然对于不同的进程,默认情况下,它们不能共享内存。
有多种方法启动之后要进行通信的进程,下面所举的例子中主要使用了下面的两种方法:
- 一个终端被用来启动一个进程,另外一个不同的终端被用来启动另一个。
- 在一个进程(父进程)中调用系统函数
fork ,以此生发另一个进程(子进程)。
第一个例子采用了上面使用终端的方法。这些代码示例的 ZIP 压缩包可以从我的网站下载到。
共享文件
程序员对文件访问应该都已经很熟识了,包括许多坑(不存在的文件、文件权限损坏等等),这些问题困扰着程序对文件的使用。尽管如此,共享文件可能是最为基础的 IPC 机制了。考虑一下下面这样一个相对简单的例子,其中一个进程(生产者 producer )创建和写入一个文件,然后另一个进程(消费者 consumer )从这个相同的文件中进行读取:
writes +-----------+ reads producer-------->| disk file |<-------consumer +-----------+
在使用这个 IPC 机制时最明显的挑战是竞争条件可能会发生:生产者和消费者可能恰好在同一时间访问该文件,从而使得输出结果不确定。为了避免竞争条件的发生,该文件在处于读或写状态时必须以某种方式处于被锁状态,从而阻止在写操作执行时和其他操作的冲突。在标准系统库中与锁相关的 API 可以被总结如下:
- 生产者应该在写入文件时获得一个文件的排斥锁。一个排斥锁最多被一个进程所拥有。这样就可以排除掉竞争条件的发生,因为在锁被释放之前没有其他的进程可以访问这个文件。
- 消费者应该在从文件中读取内容时得到至少一个共享锁。多个读取者可以同时保有一个共享锁,但是没有写入者可以获取到文件内容,甚至在当只有一个读取者保有一个共享锁时。
共享锁可以提升效率。假如一个进程只是读入一个文件的内容,而不去改变它的内容,就没有什么原因阻止其他进程来做同样的事。但如果需要写入内容,则很显然需要文件有排斥锁。
标准的 I/O 库中包含一个名为 fcntl 的实用函数,它可以被用来检查或者操作一个文件上的排斥锁和共享锁。该函数通过一个文件描述符(一个在进程中的非负整数值)来标记一个文件(在不同的进程中不同的文件描述符可能标记同一个物理文件)。对于文件的锁定, Linux 提供了名为 flock 的库函数,它是 fcntl 的一个精简包装。第一个例子中使用 fcntl 函数来暴露这些 API 细节。
示例 1. 生产者程序
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> -
#define FileName "data.dat" -
void report_and_exit(const char* msg) { [perror][4](msg); [exit][5](-1); /* EXIT_FAILURE */ } -
int main() { struct flock lock; lock.l_type = F_WRLCK; /* read/write (exclusive) lock */ lock.l_whence = SEEK_SET; /* base for seek offsets */ lock.l_start = 0; /* 1st byte in file */ lock.l_len = 0; /* 0 here means 'until EOF' */ lock.l_pid = getpid(); /* process id */ -
int fd; /* file descriptor to identify a file within a process */ if ((fd = open(FileName, O_RDONLY)) < 0) /* -1 signals an error */ report_and_exit("open to read failed..."); -
/* If the file is write-locked, we can't continue. */ fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */ if (lock.l_type != F_UNLCK) report_and_exit("file is still write locked..."); -
lock.l_type = F_RDLCK; /* prevents any writing during the reading */ if (fcntl(fd, F_SETLK, &lock) < 0) report_and_exit("can't get a read-only lock..."); -
/* Read the bytes (they happen to be ASCII codes) one at a time. */ int c; /* buffer for read bytes */ while (read(fd, &c, 1) > 0) /* 0 signals EOF */ write(STDOUT_FILENO, &c, 1); /* write one byte to the standard output */ -
/* Release the lock explicitly. */ lock.l_type = F_UNLCK; if (fcntl(fd, F_SETLK, &lock) < 0) report_and_exit("explicit unlocking failed..."); -
close(fd); return 0; }
上面生产者程序的主要步骤可以总结如下:
|