介绍进程间通信的几种方式:管道、消息队列、信号量、共享内存
注:标题中显示的函数数字表示该函数在man手册中所在章节(第2章的是系统调用函数,第3章的是标准函数)
管道
管道就是顺序存储的循环队列。管道只有凑齐读写双方才算建立成功。
内核提供,单工,自同步机制(迁就较慢的一方)。
匿名管道|pipe(2)
函数背景:在UNIX系统中,进程间通信(IPC)是一个核心功能。为了让一个进程可以将数据发送到另一个进程,UNIX提供了一个叫做管道(pipe)的原始IPC机制。pipe() 函数用于创建这样的匿名管道。
PIPE(2)
NAME
pipe, pipe2 - create pipe
SYNOPSIS
#include <unistd.h>
/* On Alpha, IA-64, MIPS, SuperH, and SPARC/SPARC64; see NOTES */
struct fd_pair {
long fd[2];
};
struct fd_pair pipe();
/* On all other architectures */
int pipe(int pipefd[2]);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int pipe2(int pipefd[2], int flags);
DESCRIPTION
pipe() creates a pipe, a unidirectional data channel that can be used for interprocess communication. The array pipefd is used to return two file descriptors referring to the ends of the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe. Data written to the write end of the pipe is buffered by the kernel until it is read from the read end of the pipe. For further details, see pipe(7).
If flags is 0, then pipe2() is the same as pipe(). The following values can be bitwise ORed in flags to obtain different behavior:
O_CLOEXEC : Set the close-on-exec (FD_CLOEXEC) flag on the two new file descriptors. See the description of the same flag in open(2) for reasons why this may be useful.
O_DIRECT (since Linux 3.4) : Create a pipe that performs I/O in “packet” mode. Each write(2) to the pipe is dealt with as a separate packet, and read(2)s from the pipe will read one packet at a time. Note the following points:
- Writes of greater than PIPE_BUF bytes (see pipe(7)) will be split into multiple packets. The constant PIPE_BUF is defined in <limits.h>.
- If a read(2) specifies a buffer size that is smaller than the next packet, then the requested number of bytes are read, and the excess bytes in the packet are discarded. Specifying a buffer size of PIPE_BUF will be sufficient to read the largest possible packets (see the previous point).
- Zero-length packets are not supported. (A read(2) that specifies a buffer size of zero is a no-op, and returns 0.)
Older kernels that do not support this flag will indicate this via an EINVAL error.
Since Linux 4.5, it is possible to change the O_DIRECT setting of a pipe file descriptor using fcntl(2).
O_NONBLOCK : Set the O_NONBLOCK file status flag on the open file descriptions referred to by the new file descriptors. Using this flag saves extra calls to fcntl(2) to achieve the same result.
RETURN VALUE
On success, zero is returned. On error, -1 is returned, errno is set appropriately, and pipefd is left unchanged.
On Linux (and other systems), pipe() does not modify pipefd on failure. A requirement standardizing this behavior was added in POSIX.1-2016. The Linux-specific pipe2() system call likewise does not modify pipefd on failure.
函数功能:pipe()函数用于创建一个匿名管道,允许有亲缘关系的进程之间进行通信。数据写入管道的一端,可以从另一端读出。
函数原型如下:
1 |
|
参数说明:
pipefd[0]
:读端的文件描述符。pipefd[1]
:写端的文件描述符。
函数返回值:
- 成功时,返回0。
- 失败时,返回-1,并设置
errno
为相应的错误。
注意事项:
- 当不再需要管道时,应关闭相关的文件描述符。
- 管道的读端和写端都应该是单独使用的。例如,在父进程中写,子进程中读。
- 如果写端被所有进程关闭,从读端读取数据将返回0(表示文件结束)。
- 如果读端被所有进程关闭,写入数据到写端将导致发送信号
SIGPIPE
至进程,并导致进程退出,除非进程捕捉或忽略该信号。 - 管道是半双工的。如果需要全双工通信(即双方都可以读写),可以考虑使用其他IPC机制,如套接字。
举个栗子:
产生一个匿名管道,用于具有亲缘关系的父子间进程通信,父进程写管道,子进程读管道:
1 |
|
命名管道|mkfifo(3)
函数背景:在Unix-like系统中,FIFO (First In First Out),也被称为命名管道(named pipe),是一种特殊的文件类型。它提供了一种在不相关的进程之间进行通信的机制,和匿名管道(由pipe() 函数创建)相似,但其作为一个真实的文件存在于文件系统中,并且拥有一个路径名。为了创建这样的命名管道,我们使用 mkfifo() 函数。
MKFIFO(3)
NAME
mkfifo - make a FIFO special file (a named pipe)
SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
DESCRIPTION
mkfifo() makes a FIFO special file with name pathname. mode specifies the FIFO’s permissions. It is modified by the process’s umask in the usual way: the permissions of the created file are (mode & ~umask).
A FIFO special file is similar to a pipe, except that it is created in a different way. Instead of being an anonymous communications channel, a FIFO special file is entered into the filesystem by calling mkfifo().
Once you have created a FIFO special file in this way, any process can open it for reading or writing, in the same way as an ordinary file. However, it has to be open at both ends simultaneously before you can proceed to do any input or output operations on it. Opening a FIFO for reading normally blocks until some other process opens the same FIFO for writing, and vice versa. See fifo(7) for nonblocking handling of FIFO special files.
RETURN VALUE
On success mkfifo() returns 0. In the case of an error, -1 is returned (in which case, errno is set appropriately).
mkfifo() 函数用于创建一个新的FIFO文件,即命名管道。其他进程可以打开这个文件进行读或写操作,从而实现进程间通信。
函数原型如下:
1 |
|
参数说明:
pathname
:FIFO文件的路径名。mode
:指定新文件的权限。常见的模式有S_IRUSR
,S_IWUSR
等,通常与umask函数的设置相结合。
函数返回值:
- 成功时,返回0。
- 失败时,返回-1,并设置
errno
为相应的错误。
注意事项:
- 创建FIFO后,可以使用普通的文件I/O函数(如open(),read(),write()等)来进行读写操作。
- 在打开FIFO进行读或写操作时,可能会阻塞,直到另一端的进程也打开FIFO。例如,一个进程打开FIFO进行读操作可能会阻塞,直到另一个进程打开同一FIFO进行写操作。
- 与普通的无名管道(由 pipe() 函数创建)不同,命名管道的两个端点可以在完全不相关的进程之间打开和关闭,而不仅仅是在父子进程之间。
- 当不再需要FIFO时,可以使用 unlink() 或 remove() 函数来删除它。
- 除了通过 mkfifo() ,命令行工具
mkfifo
也可以用于创建命名管道。
管道只有凑齐读写双方才算建立成功:date > mypipe
和 cat mypipe
单独执行时都会阻塞住。
XSI-IPC
XSI IPC 是 Unix System V 的一个子集,是一套跨多种 Unix 系统版本都支持的进程间通信 (IPC) 机制。这套机制提供了三种基本的 IPC 形式:消息队列(Message Queues)、信号量(Semaphores)和共享内存(Shared Memory)。
消息队列(Message Queues)
功能:消息队列允许进程发送和接收具有类型的消息。这些消息排队,按照发送顺序或其类型被接收。
主要函数:
msgget()
:获取或创建一个消息队列。msgsnd()
:向消息队列中发送消息。msgrcv()
:从消息队列中接收消息。msgctl()
:控制消息队列的各种属性。
信号量(Semaphores)
功能:信号量主要用于同步和互斥,用于控制多个进程对共享资源的访问。它常用于解决竞态条件和确保在多进程环境中资源的安全访问。
主要函数:
semget()
:获取或创建一组信号量。semop()
:对一组信号量进行操作,如增加、减少或等待。semctl()
:控制信号量的各种属性。
共享内存(Shared Memory)
功能:共享内存允许多个进程共享一段内存区域。这是一种非常快速的 IPC 机制,因为数据不需要在进程之间复制。但是,通常需要使用信号量来同步对共享内存的访问。
主要函数:
shmget()
:获取或创建一个共享内存段。shmat()
:将共享内存段附加到进程的地址空间。shmdt()
:分离共享内存段。shmctl()
:控制共享内存段的各种属性。
xxxget():用于创建
xxxop():用于初始化等操作
xxxctl():用于控制,销毁等操作
xxx:msg、sem、shm
注意事项:
- 对于所有的 XSI IPC 机制,都需要进行适当的清理,例如删除消息队列、信号量或共享内存段,以避免资源泄漏。
- 虽然共享内存提供了一种高效的通信方式,但由于它允许多个进程直接访问相同的内存区域,所以必须小心同步。
- XSI IPC 资源有全局限制,如系统中可以有的最大消息队列数、信号量数和共享内存段数。
ftok(3)
函数背景:在UNIX系统中,许多进程间通信(IPC)机制(如消息队列、信号量和共享内存)需要一个唯一的键(key)来区分不同的IPC对象。这通常通过硬编码一个固定的键来实现,但这种方法可能会导致键的冲突。ftok() 函数为我们提供了一种生成基于文件路径的、相对唯一的IPC键的方法。
注:如果通信双方具有亲缘关系,就不需要进行key值的获取,直接创建匿名IPC就行,如果没有亲缘关系就需要创建key值。key值是为了让通信的双方拿到同一个通信机制。
FTOK(3)
NAME
ftok - convert a pathname and a project identifier to a System V IPC key
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
DESCRIPTION
The ftok() function uses the identity of the file named by the given pathname (which must refer to an existing, accessible file) and the least significant 8 bits of proj_id (which must be nonzero) to generate a key_t type System V IPC key, suitable for use with msgget(2), semget(2), or shmget(2).
The resulting value is the same for all pathnames that name the same file, when the same value of proj_id is used. The value returned should be different when the (simultaneously existing) files or the project IDs differ.
RETURN VALUE
On success, the generated key_t value is returned. On failure -1 is returned, with errno indicating the error as for the stat(2) system call.
ftok() 函数用于根据一个路径名和一个整型数生成一个唯一的 key 值,用于 System V IPC(Inter-process Communication,进程间通信)机制中的消息队列、共享内存和信号量的标识符生成。函数原型如下:
1 |
|
参数说明:
pathname
:指定用于生成 key 值的路径名,可以是任何一个已存在文件的路径名;proj_id
:指定与 pathname 同级的整型 ID,范围为 0~255。在消息队列和共享内存中,一个 key 可以对应多个消息队列或共享内存区域,因此需要通过 proj_id 来进一步区分。
函数返回值:
- 成功时,返回生成的IPC键。
- 失败时,返回-1,并设置
errno
为相应的错误。
注意事项:
- 文件必须存在且可访问。如果文件不存在或者当前进程没有访问权限(注:ftok()需要获取文件的inode号),则ftok()函数将失败。
- proj_id必须是非零的整数。如果proj_id为0,则ftok()函数将失败。
- 文件路径名的长度不能超过系统限制。通常情况下,文件路径名的长度不能超过255个字符。
- 使用ftok()函数生成的键并不保证是唯一的。在不同的系统中,可能存在相同的文件路径和proj_id,这样就会导致生成相同的键。但在实践中生成相同 key 值的概率很小。而且如果多个进程或线程要使用同一个 System V IPC 对象,必须使用同一个 key 值。
它虽然有一些限制,但是在合适的情况下,使用ftok()函数可以简化进程间通信的实现。
1 |
|
消息队列
主动端:先发包的一方;
被动端:先收包的一方(先运行);
函数背景:在Unix-like系统中,消息队列是进程间通信(IPC)的一种机制,允许进程发送和接收消息。这些消息可以根据发送顺序或它们的类型进行排列。为了创建或获取访问一个消息队列,我们使用 msgget() 函数。
msgget(2)
MSGGET(2)
NAME
msgget - get a System V message queue identifier
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
DESCRIPTION
The msgget() system call returns the System V message queue identifier associated with the value of the key argument. It may be used either to obtain the identifier of a previously created message queue (when msgflg is zero and key does not have the value IPC_PRIVATE), or to create a new set.
A new message queue is created if key has the value IPC_PRIVATE or key isn’t IPC_PRIVATE, no message queue with the given key key exists, and IPC_CREAT is specified in msgflg.
If msgflg specifies both IPC_CREAT and IPC_EXCL and a message queue already exists for key, then msgget() fails with errno set to EEXIST. (This is analogous to the effect of the combination O_CREAT | O_EXCL for open(2).)
Upon creation, the least significant bits of the argument msgflg define the permissions of the message queue. These permission bits have the same format and semantics as the permissions specified for the mode argument of open(2). (The execute permissions are not used.)
If a new message queue is created, then its associated data structure msqid_ds (see msgctl(2)) is initialized as follows:
msg_perm.cuid and msg_perm.uid are set to the effective user ID of the calling process.
msg_perm.cgid and msg_perm.gid are set to the effective group ID of the calling process.
The least significant 9 bits of msg_perm.mode are set to the least significant 9 bits of msgflg.
msg_qnum, msg_lspid, msg_lrpid, msg_stime, and msg_rtime are set to 0.
msg_ctime is set to the current time.
msg_qbytes is set to the system limit MSGMNB.
If the message queue already exists the permissions are verified, and a check is made to see if it is marked for destruction.
RETURN VALUE
If successful, the return value will be the message queue identifier (a nonnegative integer), otherwise -1 with errno indicating the error.
msgget() 函数用于获取一个新的消息队列或访问一个已经存在的消息队列。
函数原型如下:
1 |
|
参数说明:
key
:这是消息队列的键,它可以是一个明确的值,或者使用IPC_PRIVATE
创建一个新的私有消息队列。msgflg
:它是一个标志位集合,常用的标志包括:IPC_CREAT
:如果消息队列不存在,则创建它。IPC_EXCL
:与IPC_CREAT
一起使用,确保创建一个新的消息队列。如果消息队列已经存在,则失败。- 权限位:如
S_IRUSR
,S_IWUSR
等,指定谁可以对消息队列执行读/写操作。
函数返回值:
- 成功时,返回消息队列的标识符(一个非负整数)。
- 失败时,返回-1,并设置
errno
为相应的错误。
注意事项:
- 为了避免资源泄漏和其他进程无法访问消息队列,当不再需要消息队列时,应该使用 msgctl() 函数删除它。
- 在高并发的环境中,可能需要处理由于消息队列满或其他原因导致的发送/接收消息失败的情况。
- 使用
IPC_PRIVATE
作为键可以确保创建一个新的私有消息队列,但这样的消息队列只能在父子进程之间共享。 - 在使用 ftok() 生成键时,确保指定的文件存在并且对于生成的所有键都是唯一的。
举个栗子:
1 | // msgget.c |
编译运行结果如下:
1 | $ make msgget |
msgop(2)
MSGOP(2)
NAME
msgrcv, msgsnd - System V message queue operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
DESCRIPTION
The msgsnd() and msgrcv() system calls are used to send messages to, and receive messages from, a System V message queue. The calling process must have write permission on the message queue in order to send a message, and read permission to receive a message.
The msgp argument is a pointer to a caller-defined structure of the following general form:
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
The mtext field is an array (or other structure) whose size is specified by msgsz, a non‐ negative integer value. Messages of zero length (i.e., no mtext field) are permitted.
The mtype field must have a strictly positive integer value. This value can be used by the receiving process for message selection (see the description of msgrcv() below).
msgsnd()
The msgsnd() system call appends a copy of the message pointed to by msgp to the message queue whose identifier is specified by msqid.
If sufficient space is available in the queue, msgsnd() succeeds immediately. The queue capacity is governed by the msg_qbytes field in the associated data structure for the message queue. During queue creation this field is initialized to MSGMNB bytes, but this limit can be modified using msgctl(2). A message queue is considered to be full if either of the following conditions is true:
Adding a new message to the queue would cause the total number of bytes in the queue to exceed the queue’s maximum size (the msg_qbytes field).
Adding another message to the queue would cause the total number of messages in the queue to exceed the queue’s maximum size (the msg_qbytes field). This check is necessary to prevent an unlimited number of zero-length messages being placed on the queue. Although such messages contain no data, they nevertheless consume (locked) kernel memory.
If insufficient space is available in the queue, then the default behavior of msgsnd() is to block until space becomes available. If IPC_NOWAIT is specified in msgflg, then the call instead fails with the error EAGAIN.
A blocked msgsnd() call may also fail if:
the queue is removed, in which case the system call fails with errno set to EIDRM; or
a signal is caught, in which case the system call fails with errno set to EINTR;see signal(7). (msgsnd() is never automatically restarted after being interrupted by a signal handler, regardless of the setting of the SA_RESTART flag when establishing a signal handler.)
Upon successful completion the message queue data structure is updated as follows:
msg_lspid is set to the process ID of the calling process.
msg_qnum is incremented by 1.
msg_stime is set to the current time.
msgrcv()
The msgrcv() system call removes a message from the queue specified by msqid and places it in the buffer pointed to by msgp.
The argument msgsz specifies the maximum size in bytes for the member mtext of the structure pointed to by the msgp argument. If the message text has length greater than msgsz, then the behavior depends on whether MSG_NOERROR is specified in msgflg. If MSG_NOERROR is specified, then the message text will be truncated (and the truncated part will be lost); if MSG_NOERROR is not specified, then the message isn’t removed from the queue and the system call fails returning -1 with errno set to E2BIG.
Unless MSG_COPY is specified in msgflg (see below), the msgtyp argument specifies the type of message requested, as follows:
If msgtyp is 0, then the first message in the queue is read.
If msgtyp is greater than 0, then the first message in the queue of type msgtyp is read, unless MSG_EXCEPT was specified in msgflg, in which case the first message in the queue of type not equal to msgtyp will be read.
If msgtyp is less than 0, then the first message in the queue with the lowest type less than or equal to the absolute value of msgtyp will be read.
The msgflg argument is a bit mask constructed by ORing together zero or more of the following flags:
IPC_NOWAIT
Return immediately if no message of the requested type is in the queue. The system call fails with errno set to ENOMSG.
MSG_COPY (since Linux 3.8)
Nondestructively fetch a copy of the message at the ordinal position in the queue specified by msgtyp (messages are considered to be numbered starting at 0).
This flag must be specified in conjunction with IPC_NOWAIT, with the result that, if there is no message available at the given position, the call fails immediately with the error ENOMSG. Because they alter the meaning of msgtyp in orthogonal ways, MSG_COPY and MSG_EXCEPT may not both be specified in msgflg.
The MSG_COPY flag was added for the implementation of the kernel checkpoint-restore facility and is available only if the kernel was built with the CONFIG_CHECKPOINT_RESTORE option.
MSG_EXCEPT Used with msgtyp greater than 0 to read the first message in the queue with message type that differs from msgtyp.
MSG_NOERROR To truncate the message text if longer than msgsz bytes.
If no message of the requested type is available and IPC_NOWAIT isn’t specified in msgflg, the calling process is blocked until one of the following conditions occurs:
A message of the desired type is placed in the queue.
The message queue is removed from the system. In this case, the system call fails with errno set to EIDRM.
The calling process catches a signal. In this case, the system call fails with errno set to EINTR. (msgrcv() is never automatically restarted after being interrupted by a signal handler, regardless of the setting of the SA_RESTART flag when establishing a signal handler.)
Upon successful completion the message queue data structure is updated as follows:
msg_lrpid is set to the process ID of the calling process.
msg_qnum is decremented by 1.
msg_rtime is set to the current time.
RETURN VALUE
On failure both functions return -1 with errno indicating the error, otherwise msgsnd() returns 0 and msgrcv() returns the number of bytes actually copied into the mtext array.
msgsnd(2)
msgsnd()函数用于将消息发送到消息队列。函数原型如下:
1 |
|
参数说明:
msqid
:消息队列标识符,通常由 msgget() 返回。msgp
:指向要发送消息的指针。msgsz
:消息的大小,不包括消息类型。msgflg
:标志位集合。例如,IPC_NOWAIT
用于设置非阻塞发送。
函数返回值:
- 成功时,返回0。
- 失败时,返回-1,并设置
errno
为相应的错误。
msgrcv(2)
msgrcv() 函数用于从消息队列中接收消息。函数原型如下:
1 |
|
参数说明:
msqid
:消息队列标识符。msgp
:指向存放接收消息的缓冲区的指针。msgsz
:缓冲区的大小,不包括消息类型。msgtyp
:要接收消息的类型。如果为0,则接收第一个可用的消息。msgflg
:标志位集合。例如,IPC_NOWAIT
用于设置非阻塞接收。
函数返回值:
- 成功时,返回接收到的消息的大小。
- 失败时,返回-1,并设置
errno
为相应的错误。
消息队列是双工的。消息队列这个机制严格来讲不是队列,问题就体现在 long msgtyp
这个参数上,该参数字面意义上是“接收的消息类型”。是否要挑消息来收,比如说当前消息队列中有10个包,msgtyp在正常范围内指的是要收取哪个包,相当于是包的编号,比如说10个包要收第3个,其它包还按当前顺序在队列中排队,而我们之前提到的队列、管道是没有这个特点的,遵循严格意义上的先进先出。
举个栗子:
1 | // msgop.c |
编译运行结果如下:
1 | $ make msgop |
msgctl(2)
函数背景:在Unix-like系统中,为了维护消息队列的生命周期和属性,需要有一个机制。这就是 msgctl() 函数的作用。它允许我们查询、设置或销毁消息队列。函数原型如下:
1 |
|
参数说明:
msqid
:消息队列标识符,通常由 msgget() 返回。cmd
:要执行的操作命令。常见的命令有:IPC_STAT
: 用buf
获取消息队列的当前属性。IPC_SET
: 设置消息队列的属性为buf
所指定的值。IPC_RMID
: 删除消息队列。
buf
:一个指向msqid_ds
结构的指针,该结构保存消息队列的属性。
函数返回值:
- 成功时,返回0。
- 失败时,返回-1,并设置
errno
为相应的错误。
举个栗子:
1 | // msgctl.c |
编译运行结果如下:
1 | $ make msgctl |
信号量
在多任务环境中,多个进程可能会尝试同时访问某一共享资源,如共享内存、文件或其他共享数据结构。这种并发访问可能导致数据不一致或其他不可预测的结果。为了防止这种情况,需要一种机制来同步进程的行为,确保任何时候只有一个进程能访问该资源。这就是信号量的主要用途。
信号量功能:信号量基本上是一个整数值,它有三个基本操作:
- 初始化 (P):设置信号量的初始值。
- 等待 (V):如果信号量的值大于零,就将其减少。否则,进程会睡眠,直到信号量大于零。
- 释放 (S):增加信号量的值。如果有其他进程在等待该信号量,它可能会被唤醒。
通过这些基本操作,信号量提供了一种限制对共享资源并发访问的方式。
在UNIX和Linux系统中,常见的信号量API包括 semget(), semop() 和 semctl() 等。
- semget():获取一个信号量集。
- semop():在一个或多个信号量上执行操作。
- semctl():直接控制信号量信息。
注意事项:
- 信号量通常用于短期锁定,如果需要长时间锁定资源,考虑使用其他同步机制。
- 尝试避免死锁,即两个或多个进程无限期地等待一个资源集,而这个资源集中的每个资源都被该进程集中的一个进程所持有。
- 考虑使用更高级的同步原语,如互斥锁或条件变量,这可能更适合某些场景。
- 当不再需要信号量时,使用 semctl() 与 IPC_RMID 命令确保删除它,以释放系统资源。
semget(2)
函数背景:在多任务环境中,需要有一种方式来保证多个进程不会同时访问某个共享资源,以防止可能的数据冲突或不一致。这正是信号量的目的,为了能够访问这些信号量并对其进行操作,semget() 函数被引入,它允许使用者创建新的信号量集或获取现有的信号量集。
SEMGET(2)
NAME
semget - get a System V semaphore set identifier
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
DESCRIPTION
The semget() system call returns the System V semaphore set identifier associated with the argument key. It may be used either to obtain the identifier of a previously created semaphore set (when semflg is zero and key does not have the value IPC_PRIVATE), or to create a new set.
A new set of nsems semaphores is created if key has the value IPC_PRIVATE or if no existing semaphore set is associated with key and IPC_CREAT is specified in semflg.
If semflg specifies both IPC_CREAT and IPC_EXCL and a semaphore set already exists for key, then semget() fails with errno set to EEXIST. (This is analogous to the effect of the combination O_CREAT | O_EXCL for open(2).)
Upon creation, the least significant 9 bits of the argument semflg define the permissions (for owner, group and others) for the semaphore set. These bits have the same format, and the same meaning, as the mode argument of open(2) (though the execute permissions are not meaningful for semaphores, and write permissions mean permission to alter semaphore values).
When creating a new semaphore set, semget() initializes the set’s associated data structure, semid_ds (see semctl(2)), as follows:
sem_perm.cuid and sem_perm.uid are set to the effective user ID of the calling process.
sem_perm.cgid and sem_perm.gid are set to the effective group ID of the calling process.
The least significant 9 bits of sem_perm.mode are set to the least significant 9 bits of semflg.
sem_nsems is set to the value of nsems.
sem_otime is set to 0.
sem_ctime is set to the current time.
The argument nsems can be 0 (a don’t care) when a semaphore set is not being created. Otherwise, nsems must be greater than 0 and less than or equal to the maximum number of semaphores per semaphore set (SEMMSL).
If the semaphore set already exists, the permissions are verified.
RETURN VALUE
If successful, the return value will be the semaphore set identifier (a nonnegative integer), otherwise, -1 is returned, with errno indicating the error.
semget() 函数用于获取一个已存在的信号量集的标识符或创建一个新的信号量集。
函数原型如下:
1 |
|
参数说明:
key
:用于标识信号量集的键。可以使用 ftok() 函数来生成这个键。nsems
:指定信号量集中的信号量数。如果只是获取现有的信号量集,此参数可以为0。semflg
:定义了一些标志和权限。常用的标志有:IPC_CREAT
:如果指定的信号量集不存在,则创建它。IPC_EXCL
:与IPC_CREAT
一同使用,确保创建一个全新的信号量集。如果信号量集已存在,则返回错误。
函数返回值:
- 成功时,返回一个非负整数,这是信号量集的标识符;
- 失败时,返回-1,并设置
errno
为相应的错误代码。
semop(2)
SEMOP(2)
NAME
semop, semtimedop - System V semaphore operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops, const struct timespec *timeout);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
semtimedop(): _GNU_SOURCE
DESCRIPTION
Each semaphore in a System V semaphore set has the following associated values:
unsigned short semval; /* semaphore value */
unsigned short semzcnt; /* # waiting for zero */
unsigned short semncnt; /* # waiting for increase */
pid_t sempid; /* PID of process that last
semop() performs operations on selected semaphores in the set indicated by semid. Each of the nsops elements in the array pointed to by sops is a structure that specifies an operation to be performed on a single semaphore. The elements of this structure are of type struct sembuf, containing the following members:
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
Flags recognized in sem_flg are IPC_NOWAIT and SEM_UNDO. If an operation specifies SEM_UNDO, it will be automatically undone when the process terminates.
The set of operations contained in sops is performed in array order, and atomically, that is, the operations are performed either as a complete unit, or not at all. The behavior of the system call if not all operations can be performed immediately depends on the presence of the IPC_NOWAIT flag in the individual sem_flg fields, as noted below.
Each operation is performed on the sem_num-th semaphore of the semaphore set, where the first semaphore of the set is numbered 0. There are three types of operation, distinguished by the value of sem_op.
If sem_op is a positive integer, the operation adds this value to the semaphore value (semval). Furthermore, if SEM_UNDO is specified for this operation, the system subtracts the value sem_op from the semaphore adjustment (semadj) value for this semaphore. This operation can always proceed—it never forces a thread to wait. The calling process must have alter permission on the semaphore set.
If sem_op is zero, the process must have read permission on the semaphore set. This is a “wait-for-zero” operation: if semval is zero, the operation can immediately proceed. Otherwise, if IPC_NOWAIT is specified in sem_flg, semop() fails with errno set to EAGAIN (and none of the operations in sops is performed). Otherwise, semzcnt (the count of threads waiting until this semaphore’s value becomes zero) is incremented by one and the thread sleeps until one of the following occurs:
• semval becomes 0, at which time the value of semzcnt is decremented.
• The semaphore set is removed: semop() fails, with errno set to EIDRM.
• The calling thread catches a signal: the value of semzcnt is decremented and semop() fails, with errno set to EINTR.
If sem_op is less than zero, the process must have alter permission on the semaphore set. If semval is greater than or equal to the absolute value of sem_op, the operation can proceed immediately: the absolute value of sem_op is subtracted from semval, and, if SEM_UNDO is specified for this operation, the system adds the absolute value of sem_op to the semaphore adjustment (semadj) value for this semaphore. If the absolute value of sem_op is greater than semval, and IPC_NOWAIT is specified in sem_flg, semop() fails, with errno set to EAGAIN (and none of the operations in sops is performed). Otherwise, semncnt (the counter of threads waiting for this semaphore’s value to increase) is incremented by one and the thread sleeps until one of the following occurs:
• semval becomes greater than or equal to the absolute value of sem_op: the operation now proceeds, as described above.
• The semaphore set is removed from the system: semop() fails, with errno set to EIDRM.
• The calling thread catches a signal: the value of semncnt is decremented and semop() fails, with errno set to EINTR.
On successful completion, the sempid value for each semaphore specified in the array pointed to by sops is set to the caller’s process ID. In addition, the sem_otime is set to the current time.
semtimedop()
semtimedop() behaves identically to semop() except that in those cases where the calling thread would sleep, the duration of that sleep is limited by the amount of elapsed time specified by the timespec structure whose address is passed in the timeout argument. (This sleep interval will be rounded up to the system clock granularity, and kernel scheduling delays mean that the interval may overrun by a small amount.) If the specified time limit has been reached, semtimedop() fails with errno set to EAGAIN (and none of the operations in sops is performed).
If the timeout argument is NULL, then semtimedop() behaves exactly like semop().
Note that if semtimedop() is interrupted by a signal, causing the call to fail with the error EINTR, the contents of timeout are left unchanged.
RETURN VALUE
If successful, semop() and semtimedop() return 0; otherwise they return -1 with errno indicating the error.
semop() 函数允许进程在一个或多个信号量上执行原子操作,在信号量上执行P操作(减少信号量值)和V操作(增加信号量值)。
函数原型如下:
1 |
|
参数说明如下:
semid
:信号量集标识符,由 semget() 返回。sops
:指向sembuf
结构数组的指针,描述要在信号量上执行的操作。nsops
:sops
数组中的元素数量。
sembuf
结构定义如下:
1 | struct sembuf { |
函数返回值:
- 成功时,返回0;
- 失败时,返回-1,并设置
errno
为相应的错误代码。
semctl(2)
函数背景:在多进程环境中,为了实现进程之间的同步和资源共享,需要一种方法来管理和控制信号量的状态和权限。semctl() 就是为这个目的而设计的。
SEMCTL(2)
NAME
semctl - System V semaphore control operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, …);
DESCRIPTION
semctl() performs the control operation specified by cmd on the System V semaphore set identified by semid, or on the semnum-th semaphore of that set. (The semaphores in a set are numbered starting at 0.)
RETURN VALUE
On failure, semctl() returns -1 with errno indicating the error.
Otherwise, the system call returns a nonnegative value depending on cmd as follows:
GETNCNT the value of semncnt.
GETPID the value of sempid.
GETVAL the value of semval.
GETZCNT the value of semzcnt.
IPC_INFO the index of the highest used entry in the kernel’s internal array recording information about all semaphore sets. (This information can be used with repeated SEM_STAT or SEM_STAT_ANY operations to obtain information about all semaphore sets on the system.)
SEM_INFO as for IPC_INFO.
SEM_STAT the identifier of the semaphore set whose index was given in semid.
SEM_STAT_ANY as for SEM_STAT.
All other cmd values return 0 on success.
semctl()函数允许用户设置信号量的值、获取其值、更改权限、删除信号量等。
函数原型如下:
1 |
|
参数说明:
semid
:信号量集标识符,由 semget() 返回。semnum
:信号量集中的信号量编号,用于指定在哪个特定信号量上执行操作。cmd
:指定要执行的操作。常见的命令有SETVAL
(设置信号量的值)、GETVAL
(获取信号量的值)、IPC_RMID
(删除信号量)等。...
:根据cmd
的值,此参数可能是必需的。例如,当使用SETVAL
时,需要提供要设置的值。
函数返回值:
- 成功时,根据
cmd
的值返回不同的结果,例如,对于GETVAL
,它返回信号量的当前值; - 失败时,返回-1,并设置
errno
为相应的错误代码。
函数使用示例:
(1)设置信号量的值:
1 | int value = 10; |
(2)获取信号量的值:
1 | int value; |
(3)删除信号量:
1 | if (semctl(semid, 0, IPC_RMID) == -1) { |
注意事项:
- 使用 semctl() 修改信号量或执行其他操作时应特别小心,确保不会意外地中断其他正在使用该信号量的进程。
- 在使用 semctl() 删除信号量之前,应确保没有其他进程正在或计划使用它。
- 当使用信号量进行进程间同步时,始终确保在程序结束时或在不再需要信号量时清理和删除它,以防止资源泄露。
- semctl() 的行为可能会因平台或操作系统的不同而略有变化,因此在移植代码时要小心。
举个栗子:
之前在介绍线程相关内容时,有如下案例:
使用20个线程分别对同一个文件进行如下操作:打开,读取数据,加1,覆盖写回去,关闭文件。
当时介绍了 “多线程+互斥量”(add_mutex.c)的方式,接下来使用 “多线程+信号量”(add_sem.c)的方式重构这个程序。
1 | // add_sem.c |
编译运行结果如下:
1 | $ cat /tmp/out |
共享内存
为了允许进程间快速交换大量数据而无需进行复制,UNIX系统引入了共享内存概念。共享内存创建一个物理内存的区域,可以被多个进程直接读取或写入,从而避免了昂贵的数据复制操作。
关键函数和结构:
shmget()
:获取共享内存标识符或创建一个新的共享内存段。shmat()
:将共享内存段附加到进程的地址空间。shmdt()
:分离共享内存段。shmctl()
:控制共享内存段的各种属性。struct shmid_ds
:描述共享内存段的数据结构,经常与shmctl()
结合使用。
shmget(2)
SHMGET(2)
NAME
shmget - allocates a System V shared memory segment
SYNOPSIS
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
DESCRIPTION
shmget() returns the identifier of the System V shared memory segment associated with the value of the argument key. It may be used either to obtain the identifier of a previously created shared memory segment (when shmflg is zero and key does not have the value IPC_PRIVATE), or to create a new set.
A new shared memory segment, with size equal to the value of size rounded up to a multiple of PAGE_SIZE, is created if key has the value IPC_PRIVATE or key isn’t IPC_PRIVATE, no shared memory segment corresponding to key exists, and IPC_CREAT is specified in shmflg.
If shmflg specifies both IPC_CREAT and IPC_EXCL and a shared memory segment already exists for key, then shmget() fails with errno set to EEXIST. (This is analogous to the effect of the combination O_CREAT | O_EXCL for open(2).)
The value shmflg is composed of:
IPC_CREAT Create a new segment. If this flag is not used, then shmget() will find the segment associated with key and check to see if the user has permission to access the segment.
IPC_EXCL This flag is used with IPC_CREAT to ensure that this call creates the segment. If the segment already exists, the call fails.
……
RETURN VALUE
On success, a valid shared memory identifier is returned. On error, -1 is returned, and errno is set to indicate the error.
shmget() 函数用于创建一个新的共享内存段或获取现有的共享内存段的标识符。
函数原型如下:
1 |
|
参数说明:
key
:一个关键码,用于唯一标识一个共享内存段。通常,此关键码是使用 ftok() 函数生成的。size
:要创建的共享内存段的大小(以字节为单位)。此参数仅在创建新的共享内存段时使用。shmflg
:标志,定义了共享内存的权限和其他属性。常见的标志包括IPC_CREAT
(用于创建新的共享内存段)和IPC_EXCL
(与IPC_CREAT
一起使用,确保不会意外地打开现有的段)。权限标志与文件权限相似,如0666
等。
函数返回值:
- 成功时,返回共享内存段的标识符 (shmID);
- 失败时,返回-1,并设置
errno
为相应的错误代码。
shmop(2)
NAME
shmat, shmdt - System V shared memory operations
SYNOPSIS
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
DESCRIPTION
shmat()
shmat() attaches the System V shared memory segment identified by shmid to the address space of the calling process. The attaching address is specified by shmaddr with one of the following criteria:
If shmaddr is NULL, the system chooses a suitable (unused) page-aligned address to attach the segment.
If shmaddr isn’t NULL and SHM_RND is specified in shmflg, the attach occurs at the address equal to shmaddr rounded down to the nearest multiple of SHMLBA.
Otherwise, shmaddr must be a page-aligned address at which the attach occurs.
In addition to SHM_RND, the following flags may be specified in the shmflg bit-mask argument:
SHM_EXEC (Linux-specific; since Linux 2.6.9) Allow the contents of the segment to be executed. The caller must have execute permission on the segment.
SHM_RDONLY Attach the segment for read-only access. The process must have read permission for the segment. If this flag is not specified, the segment is attached for read and write access, and the process must have read and write permission for the segment. There is no notion of a write-only shared memory segment.
SHM_REMAP (Linux-specific) This flag specifies that the mapping of the segment should replace any existing mapping in the range starting at shmaddr and continuing for the size of the segment. (Normally, an EINVAL error would result if a mapping already exists in this address range.) In this case, shmaddr must not be NULL.
The brk(2) value of the calling process is not altered by the attach. The segment will automatically be detached at process exit. The same segment may be attached as a read and as a read-write one, and more than once, in the process’s address space.
A successful shmat() call updates the members of the shmid_ds structure (see shmctl(2)) associated with the shared memory segment as follows:
shm_atime is set to the current time.
shm_lpid is set to the process-ID of the calling process.
shm_nattch is incremented by one.
shmdt()
shmdt() detaches the shared memory segment located at the address specified by shmaddr from the address space of the calling process. The to-be-detached segment must be currently attached with shmaddr equal to the value returned by the attaching shmat() call.
On a successful shmdt() call, the system updates the members of the shmid_ds structure associated with the shared memory segment as follows:
shm_dtime is set to the current time.
shm_lpid is set to the process-ID of the calling process.
shm_nattch is decremented by one. If it becomes 0 and the segment is marked for deletion, the segment is deleted.
RETURN VALUE
On success, shmat() returns the address of the attached shared memory segment; on error, (void *) -1 is returned, and errno is set to indicate the cause of the error.
On success, shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.
ERRORS
When shmat() fails, errno is set to one of the following:
EACCES The calling process does not have the required permissions for the requested attach type, and does not have the CAP_IPC_OWNER capability in the user namespace that governs its IPC namespace.
EIDRM shmid points to a removed identifier.
EINVAL Invalid shmid value, unaligned (i.e., not page-aligned and SHM_RND was not specified) or invalid shmaddr value, or can’t attach segment at shmaddr, or SHM_REMAP was specified and shmaddr was NULL.
ENOMEM Could not allocate memory for the descriptor or for the page tables.
When shmdt() fails, errno is set as follows:
EINVAL There is no shared memory segment attached at shmaddr; or, shmaddr is not aligned on a page boundary.
shmat(2)
函数背景:当进程需要访问共享内存段的数据时,它必须首先将该段“附加”到自己的地址空间。shmat() 正是执行这一操作的函数。附加操作使得共享内存段在进程的地址空间中成为一个连续的内存块,进程可以像访问其常规内存那样访问它。
shmat()函数功能为:
- 将指定的共享内存段附加到调用进程的地址空间;
- 提供对共享内存段的直接内存访问。
函数原型如下:
1 |
|
参数说明:
shmid
:是由 shmget() 返回的共享内存段标识符。shmaddr
:是一个指针,指示共享内存应该附加到进程地址空间的位置。通常,这个参数设置为NULL
,让系统选择合适的地址。shmflg
:这是一个标志集,它指定了附加的语义。最常用的标志是SHM_RDONLY
,用于以只读方式附加共享内存。
函数返回值:
- 成功时,返回指向共享内存段的指针。
- 失败时,返回
(void *) -1
并设置errno
为相应的错误代码。
注意事项:
- 附加次数:同一个共享内存段可以被多个进程多次附加。
- 地址选择:通常,建议让系统选择共享内存的附加地址,除非有特定的原因需要选择特定的地址。
- 分离:一旦完成对共享内存的访问,进程应使用 shmdt() 函数分离共享内存,尽管进程终止时会自动分离。
- 同步:当多个进程访问共享内存时,需要考虑同步,确保数据的一致性和完整性。
shmdt(2)
函数背景:当进程完成对共享内存段的访问后,为了保持资源管理的整洁,通常需要将其从进程的地址空间中分离出来。shmdt() 函数将先前通过 shmat() 附加到进程地址空间的共享内存段进行分离。
函数原型如下:
1 |
|
参数说明:
shmaddr
:是指向共享内存段开始的指针,这是先前由 shmat() 返回的。
函数返回值:
- 成功时,返回 0;
- 失败时,返回 -1,并设置
errno
为相应的错误代码。
举个栗子:
1 |
|
shmctl(2)
函数背景:在共享内存的生命周期中,可能需要执行各种管理和控制任务。例如我们可能需要更改共享内存的权限,获取其使用状态,或者当它不再需要时删除它。shmctl() 提供了这样的管理和控制能力。
函数功能:
- 获取共享内存的状态信息;
- 设置共享内存的权限或其他属性;
- 删除共享内存段。
1 |
|
参数说明:
shmid
:是由 shmget() 返回的共享内存段标识符。cmd
:指定要执行的操作,常见的命令有:IPC_STAT
:获取共享内存的状态。IPC_SET
:设置共享内存的属性。IPC_RMID
:删除共享内存段。
buf
:是一个指向shmid_ds
结构的指针,该结构包含有关共享内存段的信息。对于IPC_STAT
,此结构被填充;对于IPC_SET
,此结构提供新的属性值。
函数返回值:
- 成功时,返回 0;
- 失败时,返回 -1,并设置
errno
为相应的错误代码。
注意事项:
- 删除共享内存段:使用
IPC_RMID
命令删除共享内存段后,该段立即被标记为“待删除”,并在最后一个附加进程分离后实际删除。这意味着,即使已经调用了IPC_RMID
,只要仍有进程附加到共享内存,它就不会立即消失。 - 权限和所有权:使用
IPC_SET
更改共享内存段的属性时,需要确保具有相应的权限。例如,只有创建共享内存的用户或超级用户可以更改其权限。 - 状态信息:使用
IPC_STAT
可以获取共享内存段的多种信息,包括创建时间、最后一次附加和分离的时间、当前附加的进程数等。