一.进程间通信概述
1.1概念
进程间通信(Inter-Process Communication,简称 IPC)是指不同进程之间进行数据交换和信息传递的机制和技术。在现代操作系统中,同时运行着多个进程,它们可能需要相互协作、共享数据或进行通信来完成特定任务。
进程间通信允许进程在同一台计算机上或不同计算机上进行交流和协作。通过进程间通信,进程可以通过共享数据、消息传递、信号量、管道等方式进行相互沟通和协作。
常见的进程间通信机制包括以下几种:
- 无名管道(Pipe):管道是一种单向通信机制,可以通过创建一个内核缓冲区来实现父子进程间的通信。
- 有名管道(Named Pipe):类似于管道,但允许无关的进程进行通信,且可以通过文件系统进行通信。
- 信号(Semaphore):信号用于控制对共享资源的并发访问,在多个进程之间进行同步。
- 共享内存(Shared Memory):共享内存是一种将一块内存区域映射到多个进程的地址空间,从而实现进程间的数据共享。
- 消息队列(Message Queue):消息队列是一种通过消息传递方式进行进程间通信的机制,允许多个进程发送和接收消息。
- 信号灯集(Signal):信号是一种用于进程间通知和处理异步事件的机制,一个进程可以发送信号给另一个进程来触发相应的处理动作。
- 套接字(Socket):套接字可以用于在网络上不同计算机的进程之间进行通信,实现分布式进程间通信。
选择适当的进程间通信机制取决于应用程序的需求和环境。不同的机制具有不同的特性、效率和复杂性。正确使用进程间通信可以实现并发处理、资源共享和协作,从而提高应用程序的效率和可靠性。
1.2功能
进程之间进行通信的主要原因有以下几个:
合作和协同工作:在复杂的应用程序中,不同的进程可能需要相互协作和共同完成特定任务。通过进程间通信,进程可以共享信息、传递消息、协调操作等,从而实现合作和协同工作。
数据共享:多个进程可能需要共享数据,以实现信息共享和统一的数据视图。进程间通信可以使得多个进程能够访问和修改相同的数据,从而实现数据共享。
任务分解和并行处理:将一个大型任务分解为多个子任务,并通过多个进程并行处理,可以提高系统的处理能力和效率。进程间通信可以用于分发任务、传递结果等,实现任务分解和并行处理。
资源共享与管理:不同进程之间可能需要共享系统资源,如共享内存、文件、设备等。进程间通信可以协调资源的分配和管理,确保资源的正确使用和避免冲突。
事件通知和处理:进程间通信可以用于事件的通知和处理。一个进程可以向另一个进程发送消息或信号,通知某个事件的发生,从而触发相应的处理动作。
通过进程间通信,不同进程之间可以实现信息交换、资源共享、任务协调和事件处理,从而提高系统的效率、可靠性、并发性和扩展性。进程间通信是构建复杂应用程序和多进程系统的重要基础。
二.无名管道
2.1特点
无名管道(Unnamed Pipe)是一种最简单的进程间通信机制,用于在具有父子关系的进程之间进行单向通信。无名管道是一个字节流,用于在一个进程中将输出连接到另一个进程的输入。以下是无名管道的一些详细介绍:
单向通信:无名管道是单向的,只能从管道的一端读取数据,从另一端写入数据。一般来说,管道具有读取端(读取管道的数据)和写入端(向管道写入数据)。
父子进程间通信:无名管道通常用于具有父子关系的进程间通信。当一个进程创建一个子进程时,子进程会继承父进程的文件描述符(包括管道),从而可以使用管道进行通信。
匿名性:无名管道不需要特定的名称或标识符来标识管道,所以被称为“无名”。因此,无名管道只能在具有共同祖先的进程之间使用。
FIFO(先进先出)效果:无名管道保持了一个基本的先进先出的数据传输顺序。在写入管道时,先写入的数据先被读取。
管道大小限制:管道具有固定的缓冲区大小,通常比较小。如果管道的写入端向管道写入数据超过缓冲区大小,写入操作会被阻塞,直到有足够的空间来写入数据。
关闭和资源管理:在使用完无名管道后,应当适时地关闭管道。关闭管道后,无法再使用它进行通信。
无名管道提供了一种简单而有效的进程间通信机制。然而,由于只支持单向通信且限制比较多,无名管道适用于特定的通信需求,如父子进程之间的简单数据交换。当需要双向通信、多个进程间通信或非相关进程间通信时,应该选择其他更复杂但适用的进程间通信机制。
2.2注意事项
注意事项:
1.当管道中无数据时,读操作会阻塞;管道中无数据,将写端关闭,读操作会立即返回
2.管道中装满(管道大小64K)数据写阻塞,一旦
3.有4k空间,写继续,直到写满为止
3.只有在管道的读端存在时,向管道中写入数据才有意义。否则,会导致管道破裂,向管道中写入数据的进程将收到内核传来的SIGPIPE信号 (通常Broken pipe错误)。
2.3编程实战
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int pipefd[2]; // 用于存储无名管道的文件描述符
if (pipe(pipefd) == -1) {
perror("无名管道创建失败");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid > 0) {
// 父进程
close(pipefd[0]); // 关闭读取端
char message[] = "Hello, 子进程!";
write(pipefd[1], message, sizeof(message)); // 写入数据到管道
close(pipefd[1]); // 关闭写入端
} else if (pid == 0) {
// 子进程
close(pipefd[1]); // 关闭写入端
char buffer[512];
ssize_t numRead = read(pipefd[0], buffer, sizeof(buffer)); // 从管道读取数据
printf("子进程接收到的数据:%.*s\n", (int)numRead, buffer);
close(pipefd[0]); // 关闭读取端
} else {
perror("进程创建失败");
exit(EXIT_FAILURE);
}
return 0;
}
这段代码使用 <unistd.h>
中的 pipe()
函数创建了一个无名管道。接着,使用 fork()
函数创建了一个子进程。
在父进程中,关闭了管道的读取端,然后使用 write()
函数向管道写入数据。最后,关闭了管道的写入端。
在子进程中,关闭了管道的写入端,然后使用 read()
函数从管道中读取数据到 buffer
缓冲区,并打印出来。最后,关闭了管道的读取端。
注意,在实际应用中,你可能需要添加错误检查和更完善的逻辑来处理数据的读取和写入。
三.有名管道
3.1有名管道特点
有名管道(Named Pipes)是一种在文件系统中有名字的特殊文件,用于进程间通信。以下是有名管道的几个特点:
有名:与无名管道不同,有名管道在文件系统中具有唯一的路径名,可以通过路径名进行访问和引用。
持久性:有名管道是持久的,与进程无关。即使创建它的进程退出,有名管道仍然存在于文件系统中,可以由其他进程打开和使用。
双向通信:有名管道可以支持双向通信,其中一个进程可以从管道中读取数据,而另一个进程可以向管道中写入数据。
阻塞和非阻塞:默认情况下,对有名管道的读取和写入操作是阻塞的,即如果没有数据可读或管道已满,进程将被阻塞。但是,可以通过设置文件描述符为非阻塞模式来实现非阻塞的读写操作。
管道容量有限:有名管道的容量是有限的,取决于系统的设置。如果管道已满,进程写入操作可能会被阻塞。
进程间独立:有名管道不会自动提供同步和互斥机制。进程需要自己协调读写操作,以避免数据竞争和同步问题。
由于有名管道在文件系统中具有唯一路径名,使得它们非常适合需要持久性进程间通信的应用程序,例如客户端和服务器应用程序之间的通信。
补充
1.有名管道可以使互不相关的两个进程互相通信。
2.有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。
3.进程通过文件IO来操作有名管道
4.有名管道遵循先进先出规则,不支持lseek()操作
5.半双工通信
3.2有名管道注意事项
在使用有名管道(Named Pipes)进行进程间通信时,以下是一些需要注意的事项:
创建和删除:在使用有名管道之前,必须通过调用
mkfifo()
函数来创建管道。使用rm
命令或unlink()
函数来删除有名管道。确保正确处理有名管道的创建和删除,以避免潜在的资源占用和权限问题。文件权限:有名管道在文件系统中以特殊文件的形式存在,因此需要正确设置文件权限以控制进程对管道的访问。确保正确地为有名管道设置合适的权限,以保护管道的安全和保密性。
同步和阻塞:有名管道的读取和写入操作默认是阻塞的,即进程在读取或写入时会被阻塞,直到操作完成。因此,需要注意防止进程陷入死锁和无限阻塞情况。可以使用非阻塞模式设置文件描述符,或使用多线程或多进程机制来处理并发读写操作。
缓冲区大小:有名管道的容量是有限的,取决于系统的设置。如果管道已满,写入操作可能被阻塞。因此,在实现进程间通信时,需要适当设置和管理缓冲区大小,以避免数据丢失和性能问题。
错误处理:使用有名管道时,需要适当处理错误和异常情况,并进行错误检查。例如,在打开管道文件、读写数据和处理管道关闭等情况下,需要检查系统调用或库函数的返回值,以确保操作正确完成。
进程间同步:有名管道本身不提供进程间同步和互斥机制。如果多个进程同时读取和写入管道,可能会导致数据竞争和不确定行为。因此,需要使用其他同步机制(如信号量、互斥锁等)来确保进程之间的正确协调和数据一致性。
关闭管道:及时关闭有名管道文件描述符,以及在不使用管道时适当地删除管道。这样可以释放系统资源,并避免潜在的问题和错误。
总之,在使用有名管道进行进程间通信时,需要考虑文件权限、同步和阻塞、缓冲区大小、错误处理等方面的因素,以确保通信的正确性、可靠性和安全性。
补充
1.以只写方式打开有名管道,写阻塞(open),直到另一个进程将读打开
2.以只读方式打开有名管道,读阻塞(open),直到另一个进程将写打开
3.以可读可写方式打开有名管道,管道中无数据,读阻塞,当管道中写满数据,写阻塞
3.3mkfifo函数
mkfifo()
是一个系统调用函数,用于创建有名管道(Named Pipes)。以下是该函数的原型和用法:
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数说明:
pathname
:要创建的有名管道的路径名。mode
:创建的有名管道的权限(权限掩码)。使用chmod
命令中的权限表示法,如0666
。
返回值:
- 成功时,返回0。
- 失败时,返回-1,并设置
errno
来指示具体错误。
示例用法:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
int main() {
const char *pathname = "/tmp/my_fifo";
int result = mkfifo(pathname, 0666);
if (result == 0) {
printf("有名管道创建成功\n");
} else {
perror("有名管道创建失败");
exit(EXIT_FAILURE);
}
return 0;
}
在上述示例中,mkfifo()
函数将创建一个 /tmp/my_fifo
的有名管道。如果创建成功,将打印一条成功消息;否则,将打印错误信息。
请确保确保创建的有名管道路径名的有效性和权限设置的正确性,并根据需要进行错误检查处理。
3.4编程实战
好的,以下是一个使用有名管道进行进程间通信的简单例子:
发送消息的进程(writer.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
const char *fifoPath = "/tmp/myfifo";
int fd;
// 创建有名管道
mkfifo(fifoPath, 0666);
// 打开管道以进行写操作
fd = open(fifoPath, O_WRONLY);
// 发送消息到管道
const char *message = "Hello, reader!";
write(fd, message, strlen(message) + 1);
close(fd);
return 0;
}
接收消息的进程(reader.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
const char *fifoPath = "/tmp/myfifo";
int fd;
char buffer[256];
// 打开管道以进行读操作
fd = open(fifoPath, O_RDONLY);
// 从管道读取消息
read(fd, buffer, sizeof(buffer));
printf("接收到的消息: %s\n", buffer);
close(fd);
return 0;
}
在上述例子中,发送消息的进程使用 mkfifo()
函数创建了一个有名管道,并使用 open()
函数以只写模式打开了管道。然后,使用 write()
函数将消息写入管道中。
接收消息的进程同样使用 open()
函数以只读模式打开了相同的管道,并使用 read()
函数从管道中读取消息。然后,打印出接收到的消息。
请确保在运行这两个进程之前,在命令行中先编译它们:
gcc writer.c -o writer
gcc reader.c -o reader
然后,可以分别运行 ./writer
和 ./reader
来观察进程间通过有名管道进行的简单消息通信。
四.信号
4.1信号特点
信号(Signal)在操作系统中是一种进程间通信机制,用于向进程发送异步通知。以下是信号的几个特点:
异步通知:信号是异步发送给进程的,即进程在接收到信号时会中断当前的执行流程,转而去处理信号的处理函数。这个特点使得信号非常适合处理一些突发事件或异步事件。
中断处理:当进程接收到信号时,会立即中断当前的执行,并跳转到预先设置好的信号处理函数(Signal Handler)去执行特定的操作。可以使用系统提供的函数(如
signal()
或sigaction()
)来注册信号处理函数。非透明性:信号的传达是通过内核来实现的,对进程本身来说是不可见的。进程无法得知信号是由谁发送的,也无法直接向特定进程发送信号,而只能向整个进程组或进程所属的进程组发送信号。
有限数量:每个操作系统都规定了一定的信号数量,不同的信号具有不同的含义和用途。例如,SIGINT 用于终止进程,SIGSEGV 用于表示段错误等。可以使用
kill
命令或相关函数来发送信号。默认处理行为:每个信号都有一个默认的处理行为,例如终止进程、忽略信号或产生核心转储(Core Dump)。然而,可以使用信号处理函数来自定义处理行为,从而实现对信号的控制。
无法排队:如果同一种信号在进程尚未处理完毕时再次发送,通常情况下,只有一个信号会被接收到。这是因为信号无法排队,进程只会接收到一次同一类型的信号。
可靠性限制:在信号的传递过程中,可能会存在信号丢失或信号合并等问题。一些信号在多次发送时可能只会被接收到一次。这使得信号在某些场景下的可靠性受到一定的限制。
需要注意,不同的操作系统可能支持不同的信号,并且有不同的信号编号和默认处理行为。因此,在使用信号时应仔细了解操作系统的信号机制和规范。
总的来说,信号提供了一种简单而有效的进程间通信机制,可以用于处理异步事件和相应外部通知。但是,使用信号时需要注意处理函数的可重入性、信号处理函数的执行上下文切换以及信号的可靠性等问题。
4.2信号注意事项
在处理信号时,以下是一些需要注意的事项:
可重入性:信号处理函数在接收到信号时会被异步调用,因此需要保证信号处理函数的可重入性。避免在信号处理函数中使用不可重入的函数,以及全局变量的不安全访问等操作。推荐使用线程安全的函数和数据结构。
非阻塞原则:信号处理函数应该尽可能地短小和简单,避免执行耗时的操作,以减少信号堆积和处理延迟。长时间的信号处理函数可能会导致其他信号被阻塞或丢失。
信号屏蔽:在需要临时关闭某个信号的处理时,可以使用信号屏蔽操作。通过调用
sigprocmask()
函数可临时阻塞或解除阻塞某个信号,确保在特定的代码区域内不会收到特定的信号。可靠信号处理:为了保证信号的可靠性,可以使用
sigaction()
函数来注册信号处理函数。sigaction()
允许对信号的处理方式进行更加精确的控制,并提供了一些可靠性增强的选项,可以避免信号的丢失和合并。重新注册处理函数:某些信号在接收到后会将默认处理行为重置回去,例如终止进程的信号。如果要持续地处理这些信号,需要在信号处理函数中重新注册处理函数,以确保持续接收和处理该信号。
对可中断的系统调用的处理:在一些系统调用(如
read()
、write()
和sleep()
等)中,当进程接收到信号时,这些系统调用可能会被中断,返回错误(例如EINTR
)。需要在合适的地方对这些可中断的系统调用进行重试。与其他异步机制的交互:注意信号处理函数与其他异步事件机制(如线程、定时器、异步IO等)之间的交互。需要合理地进行同步和互斥操作,以避免资源竞争和不确定行为。
信号的处理次数:注意同一种信号在进程中的处理次数限制。有些信号在多次触发时可能只能被处理一次,这可能导致其中某些信号被丢弃。了解并考虑系统对信号处理次数的限制。
不可移植性:不同的操作系统可能具有不同的信号机制和限制。因此,在处理信号时要注意确保代码的可移植性,并遵循操作系统的信号规范。
以上是一些在处理信号时需要注意的事项。合理处理信号可以帮助应用程序与操作系统之间进行有效的通信和响应外部事件。然而,信号处理也是一个复杂的主题,需要仔细考虑各种因素和风险。
4.3信号响应方式
在处理信号时,有三种主要的信号响应方式:
默认响应(Default Action):每个信号都有一个默认的处理行为。例如,
SIGINT
的默认行为是终止进程,SIGTERM
的默认行为是终止进程,SIGKILL
的默认行为是无法被阻塞和处理的直接终止进程等。对于大多数信号,默认行为是终止或终止并生成核心转储。可以使用man
命令来查看特定信号的默认行为。忽略信号(Ignore Signal):可以通过设置信号处理函数为
SIG_IGN
来忽略某个特定的信号。忽略信号意味着对该信号的接收和处理完全忽略,不会触发任何操作。例如,可以忽略SIGINT
信号以防止进程被键盘中断(Ctrl+C)终止。自定义信号处理函数(Signal Handling Function):可以为一个信号指定一个自定义的信号处理函数(Signal Handler)。信号处理函数是一个用户定义的函数,用于处理接收到的信号。可以使用
signal()
或sigaction()
函数来注册信号处理函数。在接收到信号时,进程会中断当前执行,并跳转到相应的信号处理函数进行操作。在处理函数中可以执行一些自定义的操作,如记录日志、改变标志位、发送信号给其他进程等。
要选择适当的信号响应方式,需要根据实际需求和特定的业务逻辑来决定。例如,对于某个关键的信号,可以选择自定义信号处理函数来执行特定任务,而对于其他一些信号可以选择忽略或采用默认行为。
需要注意的是,使用自定义信号处理函数时应保持函数的简洁和高效,避免耗时和不可重入的操作,以及需要合理处理信号处理函数与其他并发机制(如线程)之间的同步和互斥。
另外,还应注意某些无法被阻塞或处理的信号,如 SIGKILL
和 SIGSTOP
,不能被忽略或自定义信号处理函数。这些信号的默认行为是直接终止进程或暂停进程,无法被拦截或处理。
4.4怎么产生信号
在操作系统中,有几种常见的方法可以产生信号:
来自键盘或终端:键盘上的某些组合键,如Ctrl+C(SIGINT)、Ctrl+Z(SIGTSTP)等,可以通过终端或控制台产生相应的信号。例如,使用Ctrl+C可以发送SIGINT信号,这通常用于终止当前的运行进程。
使用 kill 命令:可以使用kill命令(或其它类似的命令)向指定的进程发送信号。kill命令的语法是
kill [-<信号>] <进程ID>
。其中,信号可以是信号名称(如INT、TERM、HUP等),也可以是信号编号(如2、9、15等),进程ID是要发送信号的目标进程的ID。硬件异常:当出现硬件异常(如除零错误,非法指令访问等)时,操作系统会向产生异常的进程发送相应的信号。这些信号通常用于指示进程发生了错误或异常情况,并让进程或操作系统采取适当的措施。
软件异常:有些特殊的函数调用,如abort()、raise()等,可以在程序中主动产生信号。例如,使用raise()函数可以向当前进程发送指定的信号。
定时器:操作系统提供了一些定时器功能,可以用来在特定时间间隔内发送指定信号。最常见的是使用timer_create()和timer_settime()函数来创建和设置定时器,并使用SIGALRM信号来触发定时器的信号。
需要注意的是,生成信号的能力通常受到用户的权限和操作系统对信号的限制。具体可生成的信号类型和权限可能因操作系统和用户的不同而有所不同。此外,特定信号的具体处理方式也由接收进程的信号处理机制决定。
4.5常用信号
以下是一些常见的信号及其默认编号及含义:
SIGINT (2):键盘中断信号。通常由用户按下Ctrl+C键产生,用于终止正在运行的进程。
SIGTERM (15):终止信号。用于请求进程正常终止,给进程一个机会进行清理和保存状态。
SIGKILL (9):强制终止信号。无法被阻塞、处理或忽略,用于强制终止进程。
SIGSTOP (19):暂停信号。用于暂停进程的执行,进程将被挂起直到收到继续执行的信号。
SIGCONT (18):继续信号。用于恢复之前暂停的进程的执行。
SIGHUP (1):终端挂起或控制进程断开的信号。用于指示终端会话已经断开,常用于在进程重启时重新加载配置文件。
SIGSEGV (11):段错误信号。通常表示进程访问了无效的内存地址。
SIGILL (4):非法指令信号。表示进程执行了非法、未定义或不协调的指令。
SIGFPE (8):浮点异常信号。表示进程执行了一个浮点运算异常,如除零错误或溢出。
SIGUSR1 (10) 和 SIGUSR2 (12):用户自定义信号。可以由应用程序自定义使用。
SIGPIPE (13):管道破裂信号。当进程写入已关闭的管道时会产生。
SIGALRM (14):定时器信号。通常由操作系统的定时器触发,可用于定时操作或超时处理。
这些信号仅是一部分常用的信号,不同的操作系统和环境中可能还有其他系统特定的信号。可以使用命令 kill -l
查看系统支持的所有信号及其编号。另外,还可以通过自定义信号处理函数来处理这些信号,以满足特定应用程序的需求。
4.6函数接口
在 C 语言中,有一些常用的函数接口可用于处理信号。以下是一些常见的信号相关函数接口:
signal() 函数:
void (*signal(int signum, void (*handler)(int)))(int)
该函数用于注册信号处理函数。它接受两个参数:signum 表示要处理的信号编号,handler 为信号处理函数的指针。signal() 函数会返回先前的信号处理函数指针。通过传递SIG_IGN
(忽略信号)或SIG_DFL
(默认处理)作为 handler,可以忽略或使用默认行为处理信号。需要注意的是,signal() 函数在不同的操作系统中可能会有不同的行为和使用限制。sigaction() 函数:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
该函数提供了更为高级和可靠的信号处理方式,相较于signal()函数,具有更多的选项和功能。它接受三个参数:signum 表示要处理的信号编号,act 为新的信号处理动作结构体指针,oldact 可用于保存旧的信号处理动作设置。通过设置 act 结构体的成员可以注册自定义的信号处理函数,以及指定一些信号处理的标志和选项。kill() 函数:
int kill(pid_t pid, int signum)
该函数用于向指定进程发送信号。它接受两个参数:pid 为目标进程的 ID,signum 表示要发送的信号编号。通过向进程发送不同的信号可以触发相应的处理动作。如果 pid 为正值且具有合法进程 ID,则向该进程发送信号;如果 pid 为0,则信号将发送到与调用进程属于同一进程组的所有进程;如果 pid 为-1,则信号将发送给与调用进程具有相同用户 ID 的所有进程。sigprocmask() 函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
该函数用于设置进程的信号屏蔽字,即阻塞或解除阻塞指定的信号集合。how 参数表示对信号屏蔽字的修改方式,可以是 SIG_BLOCK、SIG_UNBLOCK 或 SIG_SETMASK。通过指定 set 参数来设置要更改的信号集合。oldset 参数用于保存旧的信号屏蔽字。sigpending() 函数:
int sigpending(sigset_t *set)
该函数用于获取进程当前挂起(未决)的信号。如果有挂起的信号,则将其保存到 set 指向的信号集中。可以通过该函数了解当前未被处理的信号。
除了以上提到的函数,还有一些其他的信号相关函数接口,在不同的操作系统和平台上可能存在差异。因此,在编写处理信号的代码时,应该查阅相关的文档和手册,确保在目标平台上正确使用合适的函数接口。
4.7信号屏蔽函数
信号集屏蔽函数用于设置和操作信号集的屏蔽状态,以控制哪些信号会被阻塞。在 C 语言中,常用的信号集屏蔽函数有以下几个:
sigemptyset():
int sigemptyset(sigset_t *set)
该函数用于清空信号集,将所有信号都从集合中移除。传递一个信号集指针给 set 参数,函数将设置该指针所指向的信号集为空集。sigfillset():
int sigfillset(sigset_t *set)
该函数用于将所有信号都添加到信号集中。传递一个信号集指针给 set 参数,函数将设置该指针所指向的信号集为包含所有信号的集合。sigaddset():
int sigaddset(sigset_t *set, int signum)
该函数用于将特定信号添加到信号集中。传递一个信号集指针给 set 参数,signum 参数表示要添加的信号编号。函数将该信号添加到信号集中。sigdelset():
int sigdelset(sigset_t *set, int signum)
该函数用于从信号集中删除指定的信号。传递一个信号集指针给 set 参数,signum 参数表示要删除的信号编号。函数将从信号集中删除指定的信号。sigismember():
int sigismember(const sigset_t *set, int signum)
该函数用于检查给定信号是否在信号集中。传递一个信号集指针给 set 参数,signum 参数表示要检查的信号编号。如果指定的信号在信号集中,则返回非零值;否则返回 0。sigprocmask():
sigprocmask
是用于设置或修改进程的信号屏蔽字(signal mask)的函数。它可以阻塞或解除阻塞指定的信号集。
函数原型如下:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how
参数指定了对信号屏蔽字的修改方式,可以取以下三个值:
SIG_BLOCK
: 将set
中的信号添加到当前信号屏蔽字中。SIG_UNBLOCK
: 将set
中的信号从当前信号屏蔽字中解除阻塞。SIG_SETMASK
: 将当前信号屏蔽字替换为set
。
set
参数是一个指向信号集的指针,表示要添加或解除阻塞的信号集。
oldset
参数(可选)是一个用来保存之前信号屏蔽字的信号集指针。如果不为 NULL
,则 oldset
指向的信号集会被填充为函数调用之前的信号屏蔽字。
调用 sigprocmask
函数后,进程的信号屏蔽字会根据 how
参数对 set
中指定的信号进行修改。
示例用法:
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t newmask, oldmask;
// 设置屏蔽信号集包含 SIGINT 和 SIGQUIT
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
sigaddset(&newmask, SIGQUIT);
// 阻塞 newmask 中的信号
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
// 此处可以进行一些需要屏蔽信号的操作
// 恢复原来的信号屏蔽字
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return 0;
}
以上示例中,通过 sigprocmask
函数将 SIGINT
和 SIGQUIT
信号添加到当前进程的信号屏蔽字中,实现对这两个信号的阻塞。在执行需要阻塞信号的操作后,通过再次调用 sigprocmask
恢复原来的信号屏蔽字,解除对这两个信号的阻塞。
需要注意的是,sigprocmask
函数仅对当前进程的信号屏蔽字进行修改,并不影响其他进程的信号处理。
这些函数接口都是针对 <signal.h>
头文件中定义的 sigset_t
类型的信号集进行操作。它们一般用于配合其他信号处理函数或系统调用(如 sigprocmask())来设置或查询进程的信号屏蔽状态。需要注意的是,这些函数在不同的操作系统和平台上可能会有细微的差异,因此在使用时应查阅相关的文档和官方手册来了解具体的使用方式和兼容性。
4.8未处理信号集
实时信号和非实时信号在被屏蔽时的行为有所不同。
非实时信号(标准信号):
当一个非实时信号(编号为1~31)被屏蔽时,如果有多个该信号同时到达,只会为其保留一个未决(待处理)的信号。也就是说,非实时信号在被屏蔽时不会排队,只会保留一个待处理的信号。此时,如果该信号被解除屏蔽,只会处理一次。实时信号(实时时钟信号):
实时信号(编号为34~64)在被屏蔽时会排队保留,它们不会被合并或丢弃。如果多个相同的实时信号同时到达,并且被屏蔽,这些信号将按照到达的顺序排队,依次保留在未决(待处理)的信号集中。当该信号被解除屏蔽时,在信号处理函数中会按照队列顺序处理这些信号,一个接一个地处理每个实时信号。
需要注意的是,如果实时信号的队列溢出,而处理函数尚未处理完所有排队的实时信号时,新到达的实时信号将被丢弃。因此,在实时信号的处理函数中,应当尽早处理队列中的实时信号,以避免队列溢出。
总结起来,如果非实时信号被屏蔽,只会保留一个未处理的实例。而实时信号在被屏蔽时会排队保留,并在解除屏蔽后按照到达的顺序进行处理。
五.消息队列
5.1消息队列特点
Linux 中的消息队列具有以下特点:
基于内核:Linux 的消息队列是由内核提供的。它由内核维护和管理,可以在用户空间中进行操作和访问。
进程间通信:Linux 的消息队列是一种用于进程间通信的机制。它允许不同的进程之间通过发送和接收消息来实现数据的传递和共享。
高效性:Linux 的消息队列在内核中实现,因此具有较高的性能和效率。它是一种基于内存的通信方式,消息的传递速度较快。
异步通信:Linux 的消息队列支持异步通信。发送方可以将消息发送到消息队列中后立即返回,而不需要等待接收方的响应。
支持多种消息类型:Linux 的消息队列可以支持不同类型的消息。每个消息由消息类型和消息数据组成,接收方可以根据消息类型来进行区分和处理。
消息持久化:Linux 的消息队列可以通过设置相应的标志来实现消息的持久化。这意味着即使接收方在消息到达之前不可用,消息仍然会被保存在队列中,直到接收方准备好处理它。
需要注意的是,Linux 的消息队列仅仅是 IPC(Inter-Process Communication,进程间通信)机制之一,还有其他的方式,如管道、共享内存等。选择合适的通信机制需要根据具体的需求和应用场景进行评估。
补充
独立于进程
没有文件名和文件描述符
IPC对象具有key和ID
5.2编程步骤
使用消息队列进行编程通常包含以下步骤:
创建消息队列:首先,需要创建一个消息队列来存储和传递消息。具体的创建方式和语法取决于所用的编程语言和消息队列实现库。
发送消息:在发送消息之前,需要确定消息的格式和内容。然后,将消息发送到消息队列中,等待接收方来获取并处理该消息。
接收消息:接收方需要在消息队列中注册并等待接收消息。一旦有消息到达队列,接收方就可以从队列中获取消息并进行相应的处理。
处理消息:接收方获取到消息后,进行相应的处理逻辑。根据消息的内容和类型,可以进行业务逻辑的处理、数据操作等操作。
确认消息:在一些消息队列系统中,接收方需要确认已经成功接收和处理了消息。这样可以确保消息在发送和处理过程中的可靠性传递,避免丢失或重复处理。
销毁消息队列:当不再需要使用消息队列时,需要进行手动销毁或关闭消息队列,释放相关的资源。
需要注意的是,具体的编程步骤可能会因不同的编程语言、消息队列实现库和应用场景而有所差异。此外,还需了解所用消息队列的具体特性和相关的 API 文档,以便正确地使用和操作消息队列。
5.3函数接口
1.ftok()
ftok
函数是 POSIX 标准中定义的一个函数,用于将给定的路径名和项目标识符生成一个唯一的键值,通常用于创建和关联 System V 消息队列、共享内存和信号量等 IPC 机制。
ftok
函数原型如下:
key_t ftok(const char *pathname, int proj_id);
pathname
:一个存在的文件的路径名。通常使用一个特定的文件作为路径名,以表明某个特定 IPC 对象和项目相关联。proj_id
:一个非零的整数,用于区分不同的项目。需要保证在同一个pathname
下,不同的proj_id
生成的键值是唯一的。
ftok
函数返回一个 key_t
类型的键值。该键值在后续的 msgget
、shmget
、semget
等函数中用于标识和关联IPC对象。
需要注意的是,ftok
函数的键值生成是基于文件的 i-node 号和 proj_id
进行计算的。因此,当使用 ftok
生成键值时,需要确保 pathname
所指向的文件是存在的且不会轻易改变,否则可能导致生成的键值发生变化。
以下是一个示例使用 ftok
函数的代码片段:
#include <sys/types.h>
#include <sys/ipc.h>
int main() {
// 生成一个键值
key_t key = ftok("/tmp/myfile", 'A');
if (key == -1) {
perror("ftok");
return 1;
}
// 使用生成的键值进行其他操作,比如创建消息队列、共享内存、信号量等
return 0;
}
在这个示例中,我们将路径名设为 “/tmp/myfile”,proj_id
设置为 ‘A’,并使用 ftok
生成一个键值,然后可以将这个键值传递给其他 IPC 相关的函数使用。
总之,ftok
函数用于将路径名和项目标识符转换为一个唯一的键值,用于标识和关联 System V 消息队列、共享内存和信号量等 IPC 机制。
2.msgget()
msgget
函数用于创建消息队列或获取已存在的消息队列的标识符。
函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:
key
:一个键值,通常使用ftok
函数生成。该键值用于唯一标识一个消息队列。msgflg
:用于指定消息队列的权限和选项,可以通过按位或操作符|
来设置多个选项。常用的选项包括:IPC_CREAT
:如果没有与给定key
相关联的消息队列,则创建一个新的消息队列。IPC_EXCL
:与IPC_CREAT
一起使用时,如果与给定key
相关联的消息队列已经存在,则报错。
函数返回值:
- 成功时,返回消息队列的标识符(非负整数)。
- 失败时,返回 -1,并设置全局变量
errno
来指示错误原因。
以下是一个示例使用 msgget
的代码片段:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
key_t key = ftok("/tmp/myfile", 'A'); // 生成键值
if (key == -1) {
perror("ftok");
return 1;
}
int msgflg = IPC_CREAT | 0666; // 创建消息队列并设置权限
// 创建或获取消息队列
int msqid = msgget(key, msgflg);
if (msqid == -1) {
perror("msgget");
return 1;
}
printf("成功创建或获取消息队列,标识符为: %d\n", msqid);
return 0;
}
在这个示例中,我们调用 msgget
函数创建或获取一个消息队列,首先使用 ftok
生成键值,然后设置创建选项为 IPC_CREAT | 0666
来创建一个新的消息队列并设置权限。如果消息队列创建成功,msgget
函数将返回一个非负整数作为消息队列的标识符 msqid
。如果创建失败,调用 perror
函数打印错误信息,并返回非零值。
需要注意的是,如果一个进程已经创建了一个与给定键值相关联的消息队列,其他进程可以通过相同的键值来获取该消息队列的标识符,从而实现进程间的通信。
总之,msgget
函数用于创建或获取消息队列标识符,用于进程间的消息通信。
3.msgsnd()
msgsnd
函数用于向消息队列发送消息。
函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
msqid
:消息队列的标识符,由msgget
函数返回。msgp
:指向要发送的消息的指针,通常是一个结构体指针,结构体定义应与消息队列中消息的结构匹配。msgsz
:要发送的消息的大小(以字节为单位)。msgflg
:发送消息的选项,可以通过按位或操作符|
来设置多个选项,常用选项有:IPC_NOWAIT
:若消息队列已满,即时返回错误,而不是等待队列有空闲空间。MSG_NOERROR
:如果消息长度超过消息队列的最大字节数,则截断消息而不报错。
函数返回值:
- 成功时,返回 0。
- 失败时,返回 -1,并设置全局变量
errno
来指示错误原因。
以下是一个示例使用 msgsnd
的代码片段:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct message {
long mtype;
char mtext[100];
};
int main() {
key_t key = ftok("/tmp/myfile", 'A'); // 生成键值
if (key == -1) {
perror("ftok");
return 1;
}
int msgflg = IPC_CREAT | 0666; // 创建消息队列并设置权限
// 创建或获取消息队列
int msqid = msgget(key, msgflg);
if (msqid == -1) {
perror("msgget");
return 1;
}
// 准备要发送的消息
struct message msg;
msg.mtype = 1; // 消息类型,可以根据需要自定义
strncpy(msg.mtext, "Hello, message queue!", sizeof(msg.mtext));
// 发送消息
int result = msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
if (result == -1) {
perror("msgsnd");
return 1;
}
printf("成功发送消息到消息队列\n");
return 0;
}
在这个示例中,我们调用 msgsnd
函数向消息队列发送消息。首先使用 ftok
生成键值,然后创建或获取了一个消息队列,并将准备好的消息(定义为一个结构体)发送到消息队列中。如果发送成功,msgsnd
函数将返回 0;如果发送失败,返回值为 -1,并通过调用 perror
函数打印错误信息。
总之,msgsnd
函数用于向消息队列发送消息,需要指定消息队列的标识符、要发送的消息内容和选项。
4.msgrcv()
msgrcv
函数用于从消息队列接收消息。
函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
msqid
:消息队列的标识符,由msgget
函数返回。msgp
:指向接收消息的缓冲区的指针,通常是一个结构体指针,结构体定义应与消息队列中消息的结构匹配。msgsz
:接收缓冲区的大小(以字节为单位)。msgtyp
:指定要接收的消息类型。设置为 0 表示接收队列中的第一条消息,设置为正数表示接收指定类型的消息,设置为负数表示接收小于或等于指定类型的最小数值的消息。msgflg
:接收消息的选项,可以通过按位或操作符|
来设置多个选项,常用选项有:IPC_NOWAIT
:若队列为空,即时返回错误,而不是等待队列有可用消息。
函数返回值:
- 成功时,返回实际接收到的消息的长度(以字节为单位)。
- 失败时,返回 -1,并设置全局变量
errno
来指示错误原因。
以下是一个示例使用 msgrcv
的代码片段:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct message {
long mtype;
char mtext[100];
};
int main() {
key_t key = ftok("/tmp/myfile", 'A'); // 生成键值
if (key == -1) {
perror("ftok");
return 1;
}
int msgflg = IPC_CREAT | 0666; // 创建消息队列并设置权限
// 创建或获取消息队列
int msqid = msgget(key, msgflg);
if (msqid == -1) {
perror("msgget");
return 1;
}
// 准备接收消息的缓冲区
struct message received_msg;
// 接收消息
ssize_t result = msgrcv(msqid, &received_msg, sizeof(received_msg.mtext), 1, 0);
if (result == -1) {
perror("msgrcv");
return 1;
}
printf("成功接收消息:%s\n", received_msg.mtext);
return 0;
}
在这个示例中,我们调用 msgrcv
函数从消息队列接收消息。首先使用 ftok
生成键值,然后创建或获取一个消息队列,并准备一个缓冲区用于接收消息。通过调用 msgrcv
函数,指定要接收的消息类型为 1,然后将接收到的消息存储到接收缓冲区中。如果接收成功,msgrcv
函数将返回接收到的消息的长度(以字节为单位);如果接收失败,返回值为 -1,并通过调用 perror
函数打印错误信息。
总之,msgrcv
函数用于从消息队列中接收消息,需要指定消息队列的标识符、接收缓冲区的大小、要接收的消息类型和选项。
5.msgctl()
msgctl
函数用于控制消息队列,包括对消息队列的删除、获取与修改等操作。
函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
msqid
:消息队列的标识符,由msgget
函数返回。cmd
:控制操作的命令,可以是以下值之一:IPC_RMID
:私有的标识符被删除,该消息队列将被销毁。IPC_SET
:设置消息队列的属性,需要提供buf
参数指向的msqid_ds
结构体,用于存储需要修改的属性。IPC_STAT
:获取消息队列的属性,buf
参数指向的msqid_ds
结构体将被填充为消息队列的当前属性值。
buf
:指向msqid_ds
结构体的指针,用于存储或获取消息队列的属性。
函数返回值:
- 成功时,返回 0。
- 失败时,返回 -1,并设置全局变量
errno
来指示错误原因。
以下是一个示例使用 msgctl
的代码片段:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
key_t key = ftok("/tmp/myfile", 'A'); // 生成键值
if (key == -1) {
perror("ftok");
return 1;
}
int msgflg = IPC_CREAT | 0666; // 创建消息队列并设置权限
// 创建或获取消息队列
int msqid = msgget(key, msgflg);
if (msqid == -1) {
perror("msgget");
return 1;
}
// 删除消息队列
int result = msgctl(msqid, IPC_RMID, NULL);
if (result == -1) {
perror("msgctl");
return 1;
}
printf("成功删除消息队列\n");
return 0;
}
在这个示例中,我们调用 msgctl
函数删除一个消息队列。首先使用 ftok
生成键值,然后创建或获取一个消息队列,并指定要删除操作的命令为 IPC_RMID
,通过调用 msgctl
函数来删除消息队列。如果删除成功,msgctl
函数将返回 0;如果删除失败,返回值为 -1,并通过调用 perror
函数打印错误信息。
总之,msgctl
函数用于对消息队列进行控制操作,可以删除消息队列、获取消息队列属性或修改消息队列属性。需要提供消息队列的标识符、控制操作的命令以及可选的参数 buf
来存储或获取属性。
5.4编程实战
msgsend.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
struct msgsend
{
long type; //第一个必须是long类型
char text[1024];
};
int main(int argc, char const *argv[])
{
//1.ftok产生唯一key值
key_t key = ftok("/home/hq/demo/进程/a.txt", 'A');
if (key < 0)
{
perror("key is err");
return 0;
}
//2.创建或打开消息队列
int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
//等于0报错证明已经创建过相同key的消息队列
if (msgid <= 0)
{
if (errno == 17)
{
msgid = msgget(key, 0666);
}
else
{
perror("msgget is err");
exit(-1);
}
}
//3.创建结构体(第一个必须是long类型)
struct msgsend send;
send.type = getpid();
int ret;
while (1)
{
//从终端输入
fgets(send.text, sizeof(send.text), stdin);
ret = msgsnd(msgid, &send, sizeof(send) - sizeof(long), 0);
if (ret < 0)
{
perror("msgsend is err");
exit(-1);
}
if(strcmp("quit\n",send.text)==0)
exit(0);
}
return 0;
}
msgrcv.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
struct msgrcv
{
long type; //第一个必须是long类型
char text[1024];
};
int main(int argc, char const *argv[])
{
//1.ftok产生唯一key值
key_t key = ftok("/home/hq/demo/进程/a.txt", 'A');
if (key < 0)
{
perror("key is err");
return 0;
}
//2.创建或打开消息队列
int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
//等于0报错证明已经创建过相同key的消息队列
if (msgid <= 0)
{
if (errno == 17)
{
msgid = msgget(key, 0666);
}
else
{
perror("msgget is err");
exit(-1);
}
}
struct msgrcv rcv;
//rcv.type = getpid();
int ret;
while (1)
{
ret = msgrcv(msgid, &rcv, sizeof(rcv) - sizeof(long), 0, 0);
if (ret < 0)
{
perror("msgrcv is err");
exit(-1);
}
if (strcmp(rcv.text, "quit\n") == 0)
break;
printf("%s", rcv.text);
}
ret = msgctl(msgid, IPC_RMID, NULL);
if (ret < 0)
{
perror("msgctl is err");
}
return 0;
}
六.信号量
6.1信号量概念
信号量是一种同步机制,用于控制对共享资源的访问。它是由计算机科学家 Edsger Dijkstra 在 1965 年提出的。
信号量可以理解为一个计数器,它可以表示可用资源的数量,可以是整数类型。信号量有两个主要操作:P(等待)和 V(释放)。
P(等待)操作:当一个进程或线程需要访问共享资源时,它会尝试执行 P 操作。如果信号量的值大于 0,则进程可以继续访问资源,并将信号量的值减 1;如果信号量的值等于 0,则进程会被阻塞,直到信号量的值变为正数。
V(释放)操作:当一个进程或线程完成对共享资源的访问时,它会执行 V 操作,将信号量的值加 1。如果有其他等待进程被阻塞,它们中的一个将被唤醒并获得对资源的访问权限。
信号量主要用于避免多个进程或线程同时访问共享资源导致的竞态条件。通过合理地控制信号量的值,可以实现对共享资源的互斥访问和同步操作。
此外,信号量还可以用于解决生产者-消费者问题、读者-写者问题等并发编程中的同步和互斥需求。
需要注意的是,信号量的正确使用需要仔细设计和管理,以避免死锁和竞态条件的发生。因此,在使用信号量时,应该了解其概念,并根据具体的应用场景进行合理的使用和配置。
6.2信号量编程步骤
使用信号量进行编程时,可以按照以下步骤进行:
导入信号量库:根据所使用的编程语言,首先需要导入相关的信号量库或模块。
创建信号量:使用相应的函数或方法创建一个信号量对象,并指定初始值。初始值表示可用资源的数量。
P(等待)操作:当一个进程或线程需要使用共享资源时,执行 P 操作,尝试获取信号量。具体的实现细节可能因编程语言不同而有所差异,通常使用对应的函数或方法。
判断信号量值:在执行 P 操作后,判断信号量的值。如果信号量的值大于 0,则获得了对共享资源的访问权限;如果信号量的值等于 0,则进程或线程会被阻塞,直到信号量的值变为正数。
访问共享资源:在获得对共享资源的访问权限后,执行需要的操作,可以是对共享资源的读取或修改。
V(释放)操作:完成对共享资源的访问后,执行 V 操作,释放信号量。这将增加信号量的值。
以上是使用信号量的基本编程步骤。需要注意的是,对于不同的编程语言和库,具体的函数和方法名称可能会有所差异,因此请参考相应的文档和资源以确保正确使用信号量机制。
此外,在使用信号量时,还应注意避免出现死锁和竞态条件等并发编程中的常见问题。合理地设计和管理信号量的使用,以确保资源访问的互斥性和同步性,是编程中的重要考虑因素。
6.3函数接口
1.ftok
ftok
函数是一个用于生成 System V IPC 键值 (key) 的函数,它可以将一个已存在的文件的路径名(或其他非零字符串)和一个整数项目标识符(如进程 ID)组合生成一个唯一的键值。
函数原型如下:
key_t ftok(const char *pathname, int proj_id);
pathname
:一个已存在的文件路径名,用于生成 IPC 键值;文件必须对调用进程可读。proj_id
:一个非零的整数项目标识符,可以是任意值,用来区分不同的 IPC 对象。
ftok
函数根据给定的文件路径名和项目标识符计算出一个唯一的键值(key),该键值可以用于创建或访问 System V 信号量、共享内存和消息队列等 IPC 对象。
需要注意的是,使用 ftok
函数生成的键值并不保证在不同的系统上相同,因此,如果需要跨平台的 IPC 通信,建议使用其他方式生成键值。
示例用法:
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
int main() {
key_t key = ftok("/path/to/file", 'A');
if (key == -1) {
perror("ftok");
return -1;
}
printf("Generated key: %x\n", key);
return 0;
}
以上示例中,通过给定文件路径 /path/to/file
和项目标识符 'A'
,使用 ftok
函数生成了一个对应的键值。生成的键值将会以 16 进制形式打印出来。
2.semget
semget()
函数用于创建或访问一个 System V 信号量集。它返回一个与指定键值关联的信号量集标识符(semid)。如果已存在具有相同键值的信号量集,则返回其对应的标识符。如果不存在,则根据给定的键值创建一个新的信号量集。
函数原型如下:
int semget(key_t key, int nsems, int semflg);
key
:用于标识信号量集的键值。通常可以使用ftok
函数生成。nsems
:指定将创建或访问的信号量集中的信号量数量。semflg
:标志参数,用于指定信号量集的创建和访问方式。
返回值:
- 成功:返回一个大于等于 0 的信号量集标识符(semid)。
- 失败:返回 -1,并设置 errno 来指示错误。
使用 semget()
函数创建新的信号量集时,需要满足以下条件:
- 要创建的信号量集不存在具有相同键值的信号量集。
key
为 IPC_PRIVATE,表示创建一个私有的信号量集。
常用的 semflg
标志参数如下:
IPC_CREAT
:如果与给定键值关联的信号量集不存在,则创建一个新的信号量集。IPC_EXCL
:与IPC_CREAT
参数一起使用,如果该信号量集已经存在,则创建失败。
示例用法:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
int main() {
int semid;
key_t key = ftok("/path/to/file", 'A');
if (key == -1) {
perror("ftok");
return -1;
}
semid = semget(key, 3, IPC_CREAT | IPC_EXCL | 0644);
if (semid == -1) {
perror("semget");
return -1;
}
printf("Semaphore ID: %d\n", semid);
return 0;
}
以上示例中,通过 ftok
函数生成一个键值,然后使用 semget
函数创建一个具有 3 个信号量的新信号量集。如果已存在具有相同键值的信号量集,创建会失败。最后,打印生成的信号量集标识符(semid)。
3.semctl
semctl()
函数用于对 System V 信号量集进行控制操作,如获取信号量的状态、设置信号量的值、删除信号量集等。它可以实现对单个信号量或整个信号量集的操作。
函数原型如下:
int semctl(int semid, int semnum, int cmd, ...);
semid
:目标信号量集的标识符。semnum
:用于指定要操作的具体信号量的索引。对于整个信号量集的操作,此参数一般设置为 0。cmd
:指定要执行的控制操作。
semctl()
函数的第四个参数为可选参数,根据不同的 cmd
命令,它的类型和用法可能不同。常用的 cmd
控制命令如下:
IPC_STAT
:获取信号量集的状态信息,将结果存储在semun
结构体中。SETVAL
:设置单个信号量的值。第四个参数为一个新的值,可以是整数或结构体semun
。GETVAL
:获取单个信号量的值。IPC_RMID
:删除整个信号量集。
示例用法:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <errno.h>
int main() {
key_t key = ftok("/path/to/file", 'A');
if (key == -1) {
perror("ftok");
return -1;
}
int semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0644);
if (semid == -1) {
perror("semget");
return -1;
}
// 设置信号量的值
union semun arg;
arg.val = 5;
int result = semctl(semid, 0, SETVAL, arg);
if (result == -1) {
perror("semctl");
return -1;
}
// 获取信号量的值
int val = semctl(semid, 0, GETVAL);
if (val == -1) {
perror("semctl");
return -1;
}
printf("Semaphore value: %d\n", val);
// 删除信号量集
result = semctl(semid, 0, IPC_RMID);
if (result == -1) {
perror("semctl");
return -1;
}
return 0;
}
以上示例中,通过 ftok
函数生成一个键值,然后使用 semget
函数创建一个具有一个信号量的新信号量集。接着,使用 semctl
函数分别设置该信号量的值为 5,并获取其值并打印。最后,使用 semctl
函数将删除信号量集。
需要注意的是,semctl
函数的错误处理需要通过返回值判断并使用 perror
函数打印错误信息。另外,对于复杂的操作,可能需要使用结构体 semid_ds
和 semun
,详细使用方法需要参考相关文档和手册。
4.semop
semop()
函数用于执行一系列的信号量操作,即对 System V 信号量集进行 P(等待)和 V(释放)操作,实现进程间的互斥和同步。
函数原型如下:
int semop(int semid, struct sembuf *sops, size_t nsops);
semid
:目标信号量集的标识符。sops
:指向sembuf
结构体数组的指针,每个结构体表示一个信号量操作。nsops
:指定要执行的信号量操作的数量。
sembuf
结构体定义如下:
struct sembuf {
unsigned short sem_num; // 信号量的索引
short sem_op; // 信号量操作
short sem_flg; // 操作标志
};
sem_num
:要操作的信号量的索引。sem_op
:表示信号量的操作,负值表示 P(等待)操作,正值表示 V(释放)操作,0 表示等待信号量值为 0。sem_flg
:操作标志,控制操作行为。常用的标志包括 IPC_NOWAIT(非阻塞)和 SEM_UNDO(系统恢复时撤消操作)等。
示例用法:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <errno.h>
int main() {
key_t key = ftok("/path/to/file", 'A');
if (key == -1) {
perror("ftok");
return -1;
}
int semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0644);
if (semid == -1) {
perror("semget");
return -1;
}
// 设置信号量的初始值为 1
union semun arg;
arg.val = 1;
int result = semctl(semid, 0, SETVAL, arg);
if (result == -1) {
perror("semctl");
return -1;
}
// 等待信号量的值为 0
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = 0;
sop.sem_flg = 0;
result = semop(semid, &sop, 1);
if (result == -1) {
perror("semop");
return -1;
}
// 对信号量做 P 操作
sop.sem_op = -1;
result = semop(semid, &sop, 1);
if (result == -1) {
perror("semop");
return -1;
}
printf("Semaphore operation complete.\n");
// 对信号量做 V 操作
sop.sem_op = 1;
result = semop(semid, &sop, 1);
if (result == -1) {
perror("semop");
return -1;
}
// 删除信号量集
result = semctl(semid, 0, IPC_RMID);
if (result == -1) {
perror("semctl");
return -1;
}
return 0;
}
以上示例中,通过 ftok
函数生成一个键值,然后使用 semget
函数创建一个具有一个信号量的新信号量集。接着,使用 semctl
函数设置该信号量的初始值为 1。然后,使用 semop
函数对信号量进行操作,先等待信号量的值为 0,再执行 P(等待)操作,最后执行 V(释放)操作。最后,通过 semctl
函数删除信号量集。
需要注意的是,semop
函数的错误处理需要通过返回值判断并使用 perror
函数打印错误信息。另外,对于复杂的操作,可能需要使用多个 sembuf
结构体来执行一系列的信号量操作。详细使用方法需要参考相关文档和手册。
6.4编程实战
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdlib.h>
union semun {
int val;
struct semid_ds *buf;
};
void mysemop(int semid, int num, int op)
{
struct sembuf buf;
buf.sem_flg = 0; //堵塞
buf.sem_num = num; //信号灯的编号
buf.sem_op = op; //pv操作
semop(semid, &buf, 1);
}
int main(int argc, char const *argv[])
{
//1.产生一个唯一的key值
key_t key = ftok("/home/hq/demo/进程/a.txt", 'B');
if (key < 0)
{
perror("ftok is err");
exit(0);
}
//2.创建或打开信号灯集
int semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
if (semid < 0)
{
if (errno == 17)
semget(key, 2, 0666);
else
{
perror("semget is err");
exit(0);
}
}
//3.初始化信号灯集
union semun sem;
sem.val = 0;
semctl(semid, 0, SETVAL, sem);
sem.val = 10;
semctl(semid, 1, SETVAL, sem);
//3.pv操作
int i = 6;
while (i--)
{
mysemop(semid, 1, -1);
printf("%d\n", semctl(semid, 1, GETVAL));
}
//4.删除信号灯集
semctl(semid, 0, IPC_RMID);
return 0;
}
七.共享内存
7.1共享内存特点
共享内存是在多个进程之间共享和访问相同的内存区域的一种机制。以下是共享内存的几个特点:
快速:共享内存是一种高效的进程间通信方式,因为它直接在进程之间共享内存区域,不需要复制数据,避免了数据的拷贝开销,提高了访问数据的速度。
高容量:共享内存可以承载大量的数据,适用于需要共享大量数据的场景。
实时性:由于共享内存是直接在进程之间共享数据,进程可以实时地读取和修改共享内存中的数据,实现了实时更新和同步。
灵活性:共享内存可以作为一个共享的缓冲区,使进程可以通过读写共享内存来进行进程间的协作。进程可以根据自己的需要自由地访问和操作共享内存中的数据。
并发性:多个进程可以同时对共享内存进行读写操作,因此共享内存适用于需要并发读写的场景。
同步与互斥:由于共享内存可以被多个进程同时访问,因此需要使用信号量等同步机制来保证数据的一致性和避免竞态条件的发生。
尽管共享内存提供了高效的数据共享方式,但它也需要仔细的设计和管理,避免数据竞争和脏数据的问题。在使用共享内存时,需要正确地实现同步机制,保证多个进程之间对共享内存的访问正确和安全。
7.2共享内编程步骤
编写共享内存程序涉及以下步骤:
创建共享内存:
- 使用
shmget
函数创建一个共享内存段,指定共享内存的大小和权限等参数。该函数返回一个标识符,用于标识共享内存段。
- 使用
连接共享内存:
- 使用
shmat
函数将共享内存段连接到当前进程的地址空间。该函数返回共享内存段的首地址。
- 使用
访问和操作共享内存:
- 使用共享内存的地址,可以直接在程序中访问和操作共享内存中的数据。
分离共享内存:
- 使用
shmdt
函数将共享内存段与当前进程分离。分离后,进程将不能再访问共享内存中的数据。
- 使用
控制共享内存:
- 使用
shmctl
函数对共享内存进行控制,如删除共享内存段。
- 使用
下面是一个简单的共享内存程序示例,演示了基本的步骤:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
key_t key = ftok("/path/to/file", 'A');
if (key == -1) {
perror("ftok");
return -1;
}
int shmid = shmget(key, sizeof(int), IPC_CREAT | IPC_EXCL | 0644);
if (shmid == -1) {
perror("shmget");
return -1;
}
int *shared_memory = (int *)shmat(shmid, NULL, 0);
if (shared_memory == (void *)-1) {
perror("shmat");
return -1;
}
// 在共享内存中写入数据
*shared_memory = 12345;
// 从共享内存中读取数据
printf("Shared memory value: %d\n", *shared_memory);
// 分离共享内存
if (shmdt(shared_memory) == -1) {
perror("shmdt");
return -1;
}
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
return -1;
}
return 0;
}
在以上示例中,我们首先使用 ftok
函数生成一个唯一的键值,然后使用 shmget
函数创建了一个共享内存段。使用 shmat
函数将该共享内存段连接到当前进程的地址空间。在共享内存中进行读写操作后,使用 shmdt
函数将共享内存与进程分离,并使用 shmctl
函数删除共享内存段。
请注意,这只是一个简单的示例,实际的共享内存程序可能需要更复杂的同步机制来避免竞态条件和保证数据的一致性。
7.3共享内存函数接口
1.shmget
shmget
是一个系统调用(syscall),用于创建或获取一个共享内存段。
函数原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明:
key
:共享内存段的键值,可以通过ftok
函数生成。多个进程可以通过相同的键值来获取到同一个共享内存段。size
:共享内存段的大小,以字节为单位。shmflg
:标志位,指定创建共享内存的权限和操作方式,可以是以下值的按位或(|
)组合:IPC_CREAT
:如果共享内存不存在,则创建一个新的共享内存段。IPC_EXCL
:与IPC_CREAT
同时使用,用于确保只有当前调用的进程可以创建共享内存段。- 权限标志:用于指定共享内存的权限,类似于文件权限,例如
0666
表示读写权限。
返回值:
- 若调用成功,返回共享内存段的标识符(非负整数),用于之后的操作。
- 若出现错误,返回 -1,并设置
errno
全局变量以指示错误。
使用 shmget
函数后,可以使用其他函数如 shmat
、shmdt
、shmctl
来创建、连接和操作共享内存段。
以下是一个示例代码,展示了如何使用 shmget
创建一个共享内存段:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
key_t key = ftok("/path/to/file", 'A');
if (key == -1) {
perror("ftok");
return -1;
}
size_t size = 1024; // 共享内存段的大小为1024字节
int shmflg = IPC_CREAT | 0666; // 创建共享内存段,并设置读写权限
int shmid = shmget(key, size, shmflg);
if (shmid == -1) {
perror("shmget");
return -1;
}
printf("Shared memory ID: %d\n", shmid);
return 0;
}
以上示例中,我们使用 ftok
函数生成了一个唯一的键值,然后使用 shmget
函数创建了一个大小为1024字节的共享内存段,并设置了读写权限。注意在创建共享内存段时,需要使用 IPC_CREAT
标志来表示创建操作。
2.shmat
shmat
是一个系统调用(syscall),用于将共享内存段连接到当前进程的地址空间,使得进程可以访问和操作共享内存中的数据。
函数原型如下:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
shmid
:共享内存段的标识符,即创建或获取共享内存段时shmget
返回的值。shmaddr
:指定连接的地址,一般为NULL
。如果设置为NULL
,系统会选择一个可用的地址,如果设置为一个非空指针,表示使用指定的地址。shmflg
:标志位,用于指定连接共享内存的选项,一般使用0
。
返回值:
- 若调用成功,返回值为共享内存段的首地址(
void *
类型)。 - 若出现错误,返回值为
(void *) -1
,并设置errno
全局变量以指示错误。
在使用 shmat
函数之前,必须先调用 shmget
函数创建或获取共享内存段。
以下是一个示例代码,展示了如何使用 shmat
连接共享内存段:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
key_t key = ftok("/path/to/file", 'A');
if (key == -1) {
perror("ftok");
return -1;
}
int shmid = shmget(key, 1024, IPC_CREAT | 0666); // 获取共享内存段的标识符
if (shmid == -1) {
perror("shmget");
return -1;
}
void *shared_memory = shmat(shmid, NULL, 0);
if (shared_memory == (void *)-1) {
perror("shmat");
return -1;
}
printf("Shared memory attached at address: %p\n", shared_memory);
// 访问共享内存
// 分离共享内存
if (shmdt(shared_memory) == -1) {
perror("shmdt");
return -1;
}
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
return -1;
}
return 0;
}
在以上示例中,我们首先使用 ftok
函数生成了一个唯一的键值,然后使用 shmget
函数创建了一个大小为1024字节的共享内存段,并设置了读写权限。然后,我们使用 shmat
函数将共享内存段连接到当前进程的地址空间。连接成功后,可以使用 shared_memory
变量访问和操作共享内存中的数据。当结束对共享内存的操作后,需要使用 shmdt
函数将共享内存从进程中分离。
3.shmdt
shmdt
是一个系统调用(syscall),用于将共享内存段从当前进程的地址空间中分离。
函数原型如下:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数说明:
shmaddr
:要分离的共享内存段的首地址。
返回值:
- 若调用成功,返回值为
0
。 - 若出现错误,返回值为
-1
,并设置errno
全局变量以指示错误。
在调用 shmdt
函数之后,进程将不再能够访问和操作共享内存段中的数据。
以下是一个示例代码,展示了如何使用 shmdt
分离共享内存段:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
key_t key = ftok("/path/to/file", 'A');
if (key == -1) {
perror("ftok");
return -1;
}
int shmid = shmget(key, 1024, IPC_CREAT | 0666); // 获取共享内存段的标识符
if (shmid == -1) {
perror("shmget");
return -1;
}
void *shared_memory = shmat(shmid, NULL, 0); // 将共享内存段连接到当前进程
if (shared_memory == (void *)-1) {
perror("shmat");
return -1;
}
printf("Shared memory attached at address: %p\n", shared_memory);
// 操作共享内存
if (shmdt(shared_memory) == -1) { // 分离共享内存段
perror("shmdt");
return -1;
}
// 其他操作
return 0;
}
在以上示例中,我们使用 ftok
函数生成了一个唯一的键值,然后使用 shmget
函数创建了一个大小为1024字节的共享内存段,并设置了读写权限。然后,我们使用 shmat
函数将共享内存段连接到当前进程的地址空间,并将连接得到的共享内存首地址存储在 shared_memory
变量中。在进行共享内存操作后,我们使用 shmdt
函数将共享内存段从当前进程中分离。
4.shmctl
shmctl
是一个系统调用(syscall),用于对共享内存段进行控制操作,如获取共享内存的状态信息、修改共享内存的权限等。
函数原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
shmid
:共享内存段的标识符,即创建或获取共享内存段时shmget
返回的值。cmd
:控制命令,用于指定要执行的操作。常见的命令值包括:IPC_STAT
:获取共享内存的状态信息,并存储在buf
参数指向的struct shmid_ds
结构体中。IPC_SET
:设置共享内存的状态信息,需要将新的状态信息存储在buf
参数指向的struct shmid_ds
结构体中。IPC_RMID
:删除共享内存段。
buf
:对于某些命令,需要传递一个指向struct shmid_ds
结构体的指针,用于传递或接收共享内存的状态信息。
返回值:
- 若调用成功,返回值取决于具体的操作,一般为
0
。 - 若出现错误,返回值为
-1
,并设置errno
全局变量以指示错误。
以下是一个示例代码,展示了如何使用 shmctl
控制共享内存段:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
key_t key = ftok("/path/to/file", 'A');
if (key == -1) {
perror("ftok");
return -1;
}
int shmid = shmget(key, 1024, IPC_CREAT | 0666); // 获取共享内存段的标识符
if (shmid == -1) {
perror("shmget");
return -1;
}
struct shmid_ds shm_info;
// 获取共享内存段的状态信息
if (shmctl(shmid, IPC_STAT, &shm_info) == -1) {
perror("shmctl");
return -1;
}
printf("Shared memory mode: %o\n", shm_info.shm_perm.mode);
printf("Shared memory size: %lu bytes\n", shm_info.shm_segsz);
// 修改共享内存段的权限
shm_info.shm_perm.mode = 0666;
if (shmctl(shmid, IPC_SET, &shm_info) == -1) {
perror("shmctl");
return -1;
}
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
return -1;
}
return 0;
}
在以上示例中,我们使用 ftok
函数生成了一个唯一的键值,然后使用 shmget
函数创建了一个大小为1024字节的共享内存段,并设置了读写权限。接下来,我们使用 shmctl
函数来获取共享内存段的状态信息,并打印出其中的一些属性,比如权限和大小。然后,我们修改了共享内存段的权限,将其设置为0666。最后,我们使用 shmctl
函数将共享内存段删除。
7.4编程实战
send.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <string.h>
union semun {
int val;
struct semid_ds *buf;
};
void mysemop(int semid, int num, int op)
{
struct sembuf buf;
buf.sem_flg = 0; //堵塞
buf.sem_num = num; //信号灯的编号
buf.sem_op = op; //pv操作
semop(semid, &buf, 1);
}
void mysemctl(int semid, int num, int val)
{
union semun sem;
sem.val = val;
semctl(semid, num, SETVAL, sem);
}
int main(int argc, char const *argv[])
{
//1.产生一个唯一的key值
key_t key = ftok("/home/hq/demo/进程/a.txt", 'B');
if (key < 0)
{
perror("ftok is err");
exit(0);
}
//2.创建共享内存
int shmid = shmget(key, 1024, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
if (errno == 17)
{
shmid = shmget(key, 1024, 0666);
}
else
{
perror("shmget is err");
exit(0);
}
}
//3.共享内存映射到私有地址,0可读可写
char *add = shmat(shmid, NULL, 0);
if (add == (char *)(-1))
{
perror("shmat is err");
exit(0);
}
//4.创建信号灯集
int semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
if (errno == 17)
semid=semget(key, 1, 0666);
else
{
perror("semget is err");
exit(0);
}
}
else
{
//5.初始化信号灯集
mysemctl(semid, 0, 0);
}
//6.循环写入共享内存
while (1)
{
printf("%d\n", semctl(semid, 0, GETVAL));
fgets(add, 1024, stdin);
mysemop(semid, 0, 1);
if (strcmp(add, "quit\n") == 0)
break;
}
//7.取消映射
shmdt(add);
return 0;
}
recv.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <string.h>
union semun {
int val;
struct semid_ds *buf;
};
void mysemop(int semid, int num, int op)
{
struct sembuf buf;
buf.sem_flg = 0; //堵塞
buf.sem_num = num; //信号灯的编号
buf.sem_op = op; //pv操作
semop(semid, &buf, 1);
}
void mysemctl(int semid, int num, int val)
{
union semun sem;
sem.val = val;
semctl(semid, num, SETVAL, sem);
}
int main(int argc, char const *argv[])
{
//1.产生一个唯一的key值
key_t key = ftok("/home/hq/demo/进程/a.txt", 'B');
if (key < 0)
{
perror("ftok is err");
exit(0);
}
//2.创建共享内存
int shmid = shmget(key, 1024, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
if (errno == 17)
{
shmid = shmget(key, 1024, 0666);
}
else
{
perror("shmget is err");
exit(0);
}
}
//3.共享内存映射到私有地址,0可读可写
char *add = shmat(shmid, NULL, 0);
if (add == (char *)(-1))
{
perror("shmat is err");
exit(0);
}
//4.创建信号灯集
int semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
if (errno == 17)
semid=semget(key, 1, 0666);
else
{
perror("semget is err");
exit(0);
}
}
else
{
//5.初始化信号灯集
mysemctl(semid, 0, 0);
}
//6.循环读出
while (1)
{
mysemop(semid, 0, -1);
printf("%d\n", semctl(semid, 0, GETVAL));
if (strcmp("quit\n", add) == 0)
break;
printf("%s", add);
}
//7.取消映射
shmdt(add);
//8.删除共享内存和信号灯集
shmctl(shmid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
return 0;
}