进程之间共享数据的方式可以通过进程通信:
1、古老的通信方式:无名管道 有名管道 信号
2、IPC对象通信 :消息队列(用的相对少,这里不讨论)、共享内存(最高效)、 信号量集 3、socket通信:网络通信、线程信号
这里我们主要介绍下无名管道、有名管道、信号量集、socket通信在后序中会一次介绍。先说下管道相关的知识。
管道(pipe)是进程间通信(IPC)的一种机制,允许一个进程将数据传输到另一个进程。它具有以下特性和行为:
1. 半双工模式
半双工:管道是半双工的,只能单向传输数据,即数据只能从一个方向流动(从写端到读端)。如果需要双向通信,需要两个管道。
2. 特殊文件
特殊文件:管道是特殊文件,不支持定位操作。与普通文件不同,管道不支持 lseek 和 fseek,因为它们的读取是基于流的,不像普通文件那样可以随机访问。
3. 文件IO操作
文件IO:对管道的读写操作使用文件IO函数,例如 open、read、write 和 close。同样,使用标准库函数如 fgets 和 fread 也可以对管道进行操作,但需要配合文件描述符和流管理。
特性和行为:
**写阻塞:**如果管道的缓冲区(通常是 64KB)已满,进一步写入数据会阻塞,直到管道有足够的空间。这个行为保证了数据不会丢失。
**读阻塞:**如果管道为空,读操作会阻塞,直到管道中有数据可读。若管道关闭,读操作将返回 0,表示管道已经结束。
管道破裂:如果管道的读端被关闭,而写端仍存在,写操作会失败,通常返回 EPIPE 错误(Broken pipe)。也可以通过触发它关闭程序。
**正常结束:**如果管道的写端关闭且管道中没有数据,读操作将返回 0,表示管道已经关闭,没有更多数据可读。
gdb调试中可以使用 set folloe -fork(回车) -mode f child/parent选择进程进行调试
一、无名管道
无名管道(anonymous pipe)是一种用于在相关进程间进行简单数据通信的机制。它通过一对文件描述符提供了一个半双工的通信通道,即一个进程可以写入数据到管道的写端,另一个进程从管道的读端读取数据。以下是关于无名管道的特性:
**亲缘关系进程:**无名管道只能在具有亲缘关系的进程之间使用,通常是父子进程。创建无名管道的进程(通常是父进程)在 fork 调用之后,子进程会继承父进程的文件描述符,允许父子进程通过管道进行通信。
固定的读写端:无名管道有两个固定的文件描述符,一个用于读取(读端),关闭写的文件描述符pipefd[1] ;另一个用于写入(写端)、关闭读的文件描述符pipefd[0] 。读端和写端在 pipe 调用时被创建,且只能通过文件描述符 pipefd[0] 和 pipefd[1] 进行操作。也可以通过fdopen()转为文件流指针进行使用。
1、创建并打开管道
cpp
int pipe(int pipefd[2]);
功能:创建一个无名管道,并初始化管道的读端和写端。
参数:
pipefd[0]:指向管道的读端的文件描述符。
pipefd[1]:指向管道的写端的文件描述符。
返回值:成功返回 0;失败返回 -1,并设置 errno 以指示错误。
注意事项:**fork 之前创建,**无名管道的创建应在 fork 调用之前进行,以便父进程和子进程共享同一管道的文件描述符。
2、读写
cpp
ssize_t read(int fd, void *buf, size_t count);
**功能:**从管道的读端读取数据。
参数:
fd:管道的读端文件描述符(pipefd[0])。
buf:用于存储读取数据的缓冲区。
count:要读取的最大字节数。
**返回值:**成功时返回读取的字节数,失败时返回 -1。
cpp
ssize_t write(int fd, const void *buf, size_t count);
**功能:**向管道的写端写入数据。
参数:
fd:管道的写端文件描述符(pipefd[1])。
buf:包含要写入数据的缓冲区。
count:要写入的字节数。
**返回值:**成功时返回写入的字节数,失败时返回 -1。
3、关闭管道
cpp
int close(int fd);
功能:关闭管道的文件描述符。
二、有名管道
有名管道 (FIFO - First In, First Out) 是一种特殊类型的文件,它允许进程间通信。与匿名管道不同,有名管道在文件系统中可见,并且通过文件路径进行标识。它在多个不相关进程之间传递数据非常有用,任意进程都可以使用。
1、创建有名管道:使用 mkfifo 函数创建有名管道
cpp
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:pathname:指定有名管道的路径和名称。
mode:指定文件权限,通常为八进制表示,如 0666(读写权限)。
返回值:成功返回 0,失败返回 -1。
2、打开有名管道:使用 open 函数打开有名管道,注意半双工的特性。
cpp
int fd_read = open("./fifo", O_RDONLY); // 读端打开
int fd_write = open("./fifo", O_WRONLY); // 写端打开
**注意:**有名管道尽量不使用 O_RDWR 方式打开,因为这会违背管道的半双工特性;不能使用 O_CREAT 选项创建文件,应使用 mkfifo 函数。
3、读写管道:通过标准文件 I/O 函数进行读写操作、和文件I\O一样的操作步骤。
cpp
read(fd_read, buffer, sizeof(buffer));
write(fd_write, buffer, sizeof(buffer));
4、删除有名管道:使用 unlink 函数删除有名管道文件。
cpp
int unlink(const char *pathname);
pathname:要删除的有名管道路径。
返回值:成功返回 0,失败返回 -1。
有名管道的关键点:
同步问题、当读端关闭时,写操作会返回错误,通常是 SIGPIPE 信号;当写端关闭时,读操作返回 0。读写端必须同时存在,否则 open 函数会阻塞,直到另一端打开。亲缘关系进程中的使用、有名管道可以在 fork 之后的亲缘关系进程中使用。
三、信号通信
信号的响应方式分为以下几种:
Term(终止):默认操作是终止进程。
Ign(忽略):默认操作是忽略信号。
Core (生成核心转储文件):默认操作是终止进程并生成一个核心转储文件,用于调试。
例如:gdb a.out -c core 用于分析核心转储文件。
Stop (停止):默认操作是停止进程。
Cont (继续):默认操作是继续执行已停止的进程。
**1、信号发送:**通过 kill 命令或系统调用 kill() 函数可以向指定进程发送信号。
cpp
kill -9 1000 # 向进程 ID 为 1000 的进程发送 SIGKILL (9) 信号
- 使用 kill() 函数发送信号
cpp
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
参数:
pid:目标进程的进程 ID。
sig:要发送的信号编号(可以通过 kill -l 查看所有信号的编号和名称)。
返回值:成功返回 0,失败返回 -1。
3、自发信号:raise() 函数
cpp
int raise(int sig);
4、定时信号:alarm() 函数
cpp
unsigned int alarm(unsigned int seconds);
作用:在指定的时间之后,系统会自动向进程发送 SIGALRM 信号。常用于实现定时功能。
5、暂停进程:pause() 函数
cpp
int pause(void);
作用:使进程暂停执行,直到收到信号;信号的接收和处理。
每个进程对信号有三种默认响应方式:
默认处理:系统默认的处理方式,如终止进程。
忽略处理:忽略某些信号,如 SIGKILL (9) 和 SIGSTOP (19) 不可忽略。
自定义处理:通过信号捕获机制,自定义信号的处理逻辑。
6、信号处理函数 signal()
cpp
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数:signum:信号编号。
handler:信号处理函数,可以是以下三种宏之一:
SIG_DFL:默认处理。
SIG_IGN:忽略处理。
自定义处理函数:定义特定的处理逻辑。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void handle(int num)
{
pid_t pid = wait(NULL);
printf("wait pid :%d\n",pid);
}
int main(int argc, char *argv[])
{
signal(SIGCHLD,handle);
pid_t pid = fork();
if(pid>0)
{
while(1)
{
printf("father processing...\n");
sleep(1);
}
}
else if(0 == pid)
{
printf("child pid %d\n",getpid());
sleep(rand()%3);
printf("child will done %d\n",getpid());
exit(0);
}
else
{
perror("fork");
return 1;
}
return 0;
}