我们的目的是,实现进程间传递文件描述符,是指 A进程打开文件fileA,获得文件描述符为fdA,现在 A进程要通过某种方法,传递fdA,使得另一个进程B,获得一个新的文件描述符fdB,这个fdB在进程B中的作用,跟fdA在进程A中的作用一样。即在 fdB上的操作,即是对fileA的操作。
可能很多人想到的是用管道pipe来实现,但是却没想象的那么容易。
1.了解fork函数
首先需要知道,在调用 fork()
函数之前打开的文件,其文件描述符会被复制到子进程中。这是因为在 fork()
函数执行时,操作系统会创建子进程,而子进程将复制父进程的地址空间,包括文件描述符表。因此,父进程打开的文件描述符会被子进程继承并复制到子进程的文件描述符表中。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
int main() {
int file_fd;
pid_t pid;
char buf[1024];
// 打开文件
file_fd = open("file.txt", O_RDONLY);
if (file_fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程
ssize_t num_read;
// 使用父进程打开的文件描述符操作文件
while ((num_read = read(file_fd, buf, sizeof(buf))) > 0) {
write(STDOUT_FILENO, buf, num_read);
}
if (num_read == -1) {
perror("read");
exit(EXIT_FAILURE);
}
close(file_fd); // 关闭文件描述符
_exit(EXIT_SUCCESS); // 退出子进程
} else {
// 父进程
int status;
// 等待子进程结束
wait(&status);
// 关闭文件描述符
close(file_fd);
if (WIFEXITED(status)) {
printf("子进程正常退出,退出状态码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程异常终止,信号编号:%d\n", WTERMSIG(status));
}
}
return 0;
}
其次,在父进程调用 fork()
函数之后打开的文件,子进程不会自动继承这些新打开的文件描述符。也就是说,父进程在 fork()
之后打开的文件描述符,只在父进程的文件描述符表中有效,子进程不会获得这些文件描述符。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
int main() {
int file_fd;
pid_t pid;
char buf[1024];
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程
printf("子进程开始...\n");
// 尝试读取父进程在fork之后打开的文件描述符(这里没有实际的文件描述符,所以操作会失败)
ssize_t num_read = read(3, buf, sizeof(buf)); // 假设文件描述符3是父进程在fork之后打开的
if (num_read == -1) {
perror("子进程无法读取文件(因为没有继承父进程在fork之后打开的文件描述符)");
} else {
write(STDOUT_FILENO, buf, num_read);
}
_exit(EXIT_SUCCESS); // 退出子进程
} else {
// 父进程
int status;
// 在fork之后打开文件
file_fd = open("file.txt", O_RDONLY);
if (file_fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
printf("父进程打开的文件描述符: %d\n", file_fd);
// 等待子进程结束
wait(&status);
// 关闭文件描述符
close(file_fd);
if (WIFEXITED(status)) {
printf("子进程正常退出,退出状态码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程异常终止,信号编号:%d\n", WTERMSIG(status));
}
}
return 0;
}
结论:
- 在
fork()
之前打开的文件描述符,子进程会继承并可以使用。 - 在
fork()
之后打开的文件描述符,子进程不会继承,无法直接使用这些文件描述符。
2.使用管道pipe传递文件描述符的问题
下面是使用管道pipe传递文件描述符的示例代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#define BUF_SIZE 1024
int main() {
int pipe_fd[2]; // 管道的文件描述符数组
int file_fd; // 文件的文件描述符
pid_t pid;
char buf[BUF_SIZE];
// 创建管道
if (pipe(pipe_fd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程
close(pipe_fd[1]); // 关闭子进程不需要的写入端
// 从管道中读取父进程传递的文件描述符
int received_fd;
read(pipe_fd[0], &received_fd, sizeof(int));
printf("received_fd:%d\n", received_fd);
// 使用接收到的文件描述符操作文件A
ssize_t num_read;
while ((num_read = read(received_fd, buf, BUF_SIZE)) > 0) {
write(STDOUT_FILENO, buf, num_read); // 在子进程中输出文件内容
}
if (num_read == -1) {
perror("read");
exit(EXIT_FAILURE);
}
close(received_fd); // 关闭文件描述符
close(pipe_fd[0]); // 关闭管道读取端
_exit(EXIT_SUCCESS); // 退出子进程
} else {
// 父进程
close(pipe_fd[0]); // 关闭父进程不需要的读取端
// 打开文件A并获取文件描述符
file_fd = open("fileA.txt", O_RDONLY);
if (file_fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
printf("file_fd:%d\n", file_fd);
// 将文件描述符写入管道,传递给子进程
if (write(pipe_fd[1], &file_fd, sizeof(int)) != sizeof(int)) {
perror("write");
exit(EXIT_FAILURE);
}
close(file_fd); // 关闭父进程中的文件描述符
close(pipe_fd[1]); // 关闭管道写入端
wait(NULL); // 等待子进程结束
exit(EXIT_SUCCESS); // 退出父进程
}
}
失败的原因:在代码中,父进程使用 write(pipe_fd[1], &file_fd, sizeof(int))
将 file_fd
写入管道的写入端。这一步骤本质上是将一个整数(文件描述符)写入到管道中。在Unix-like系统中,文件描述符是进程特定的,它们不能简单地通过整数值在不同的进程之间传递。直接通过管道传递文件描述符的整数值是无效的,因为文件描述符是相对于进程的。
要正确地传递文件描述符,必须使用sendmsg
和recvmsg
系统调用,以及SCM_RIGHTS
控制消息。这些系统调用允许在进程间传递文件描述符,而不仅仅是它们的整数值。
3.相关操作函数
3.1socketpair
函数
socketpair
函数用于在本地创建一对连接的套接字,通常用于同一主机上的进程间通信。它创建了一个双向通道,使得两个相关联的套接字可以在两个进程之间传递数据,实现全双工通信。socketpair
通常用于需要在同一台主机上的不同进程之间进行高效通信的场景。例如,父子进程间或者同一程序的不同线程之间。
cpp
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
参数详解
domain:指定套接字的协议族,通常为 AF_UNIX(Unix 域套接字),用于本地进程间通信。
type:指定套接字的类型,常见的有:
SOCK_STREAM:提供面向连接的、可靠的数据传输服务(如 TCP)。
SOCK_DGRAM:提供无连接、不可靠的数据传输服务(如 UDP)。
protocol:指定套接字使用的协议,一般为 0,表示使用默认协议。
sv[2]:一个整型数组,用于存放创建的套接字对的文件描述符。在调用 socketpair 函数后,sv[0] 和 sv[1] 分别包含这两个相关联的套接字的文件描述符。
返回值:
成功时返回 0。
失败时返回 -1,并设置 errno 指示错误的类型。
3.2 sendmsg函数
sendmsg
函数用于向指定套接字发送消息,支持发送多块数据和控制消息(例如文件描述符)。
cpp
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
参数详解
sockfd:指定发送消息的套接字描述符。
msg:
指向 struct msghdr 结构体的指针,描述了要发送的消息的详细信息,包括数据块 (iovec 结构体数组) 和控制消息 (cmsghdr 结构体)。
flags:通常为 0,用于控制消息发送的附加选项。
返回值:
成功时返回发送的字节数。
失败时返回 -1,并设置 errno 指示错误的类型。
3.3 recvmsg函数
recvmsg
函数用于从指定套接字接收消息,支持接收多块数据和控制消息(例如文件描述符)。
cpp
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
参数详解
sockfd:指定接收消息的套接字描述符。
msg:
指向 struct msghdr 结构体的指针,用于存放接收到的消息的详细信息,包括数据块 (iovec 结构体数组) 和控制消息 (cmsghdr 结构体)。
flags:通常为 0,用于控制消息接收的附加选项。
返回值:
成功时返回发送的字节数。
失败时返回 -1,并设置 errno 指示错误的类型。
3.4 struct msghdr
结构体详解
cpp
struct msghdr {
void *msg_name; // 指向目标地址的指针
socklen_t msg_namelen; // 目标地址的长度
struct iovec *msg_iov; // 数据块的数组
size_t msg_iovlen; // 数据块数组的长度
void *msg_control; // 控制消息的指针
size_t msg_controllen; // 控制消息的长度
int msg_flags; // 消息的标志
};
结构体成员:
msg_name 和 msg_namelen:用于指定目标地址的信息,通常用于 UDP 套接字。
msg_iov 和 msg_iovlen:用于指定数据块的数组和数组长度,支持数据的分散/聚集操作。
msg_control 和 msg_controllen:用于处理控制消息(如文件描述符)的指针和长度。
msg_flags:用于指定消息的标志,例如 MSG_DONTWAIT 等。
struct iovec {
void *iov_base; // 指向数据缓冲区的指针
size_t iov_len; // 数据缓冲区的长度
};
成员解释:
iov_base:指向数据缓冲区的指针,即存放数据的内存地址。
iov_len:数据缓冲区的长度,即缓冲区中可以传输的数据的字节数。
-
工作原理:
sendmsg
函数通过msg
结构体描述要发送的数据块和控制消息。recvmsg
函数从套接字接收数据,并将接收到的数据填充到msg
结构体中指定的缓冲区中。
-
控制消息传递:
sendmsg
和recvmsg
支持通过msg_control
和msg_controllen
参数传递控制消息(cmsghdr
结构体),特别是用于传递文件描述符等特殊信息。
-
应用场景:
- 进程间通信(IPC):
sendmsg
和recvmsg
可以在进程间传递复杂的数据结构和文件描述符,适用于需要高效数据传输和资源共享的场景。 - 网络编程:在网络编程中,这两个函数用于向套接字发送数据和从套接字接收数据,支持分散/聚集操作和控制消息传递。
- 进程间通信(IPC):
作用:
struct iovec
结构体主要用于描述msg_iov
参数,即struct msghdr
结构体中的数据块数组。它允许用户指定多个数据块的位置和长度,从而实现数据的分散(scatter)和聚集(gather)操作。
3.5 writev 和 readv 函数
操作 struct iovec
结构体的系统调用主要用于实现数据的分散读(scatter)和聚集写(gather)操作,这些操作通常用于高效地传输多个数据块。
功能:
writev
函数将多个数据块从iov
指定的缓冲区中写入到文件描述符fd
所指定的文件中。readv
函数从文件描述符fd
所指定的文件中读取数据,并存储到iov
指定的多个缓冲区中。
cpp
#include <sys/uio.h>
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
参数:
fd:文件描述符,表示要读写的文件。
iov:指向 struct iovec 结构体数组的指针,描述要读写的数据块及其位置。
iovcnt:iov 数组中元素的个数,即要读写的数据块的数量。
返回值:
成功时返回读写的字节数。
失败时返回 -1,并设置 errno 指示错误的类型。
-
工作原理:
writev
函数将iov
数组中描述的多个数据块依次写入到文件描述符fd
指定的文件中。readv
函数从文件描述符fd
指定的文件中读取数据,并将数据依次存储到iov
数组中指定的多个缓冲区中。
-
数据传输:
writev
和readv
函数支持数据的分散(scatter)和聚集(gather)操作,可以高效地处理多个非连续数据块的读写。
-
应用场景:
- 在网络编程中,
writev
和readv
可以用于同时发送或接收多个数据块,提高数据传输的效率。 - 在文件操作中,对于需要同时读写多个缓冲区数据的场景,如日志文件写入等,也可以使用这两个函数。
- 在网络编程中,
4.示例代码
将父进程中得到文件描述符传递给子进程,子进程得到该文件描述符同样可以操作该文件,实现文件描述符在进程间真正的传递。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/uio.h>
void send_fd(int socket, int fd_to_send) {
struct msghdr msg = {0}; // 定义消息头结构体并初始化
struct iovec iov[1]; // 定义数据块结构体数组
struct cmsghdr *cmsg; // 定义控制消息头指针
char control[CMSG_SPACE(sizeof(int))]; // 控制信息缓冲区
char dummy = '*'; // 用于填充数据块的虚拟字符
iov[0].iov_base = &dummy; // 设置数据块的基址为虚拟字符的地址
iov[0].iov_len = 1; // 设置数据块的长度为1字节
msg.msg_iov = iov; // 设置消息头的数据块数组
msg.msg_iovlen = 1; // 设置数据块数组的长度
msg.msg_control = control; // 设置消息头的控制信息
msg.msg_controllen = sizeof(control); // 设置控制信息的长度
cmsg = CMSG_FIRSTHDR(&msg); // 获取第一个控制消息头
cmsg->cmsg_level = SOL_SOCKET; // 设置控制消息级别为套接字级别
cmsg->cmsg_type = SCM_RIGHTS; // 设置控制消息类型为传递文件描述符
cmsg->cmsg_len = CMSG_LEN(sizeof(int)); // 设置控制消息长度
*((int *) CMSG_DATA(cmsg)) = fd_to_send; // 将待发送的文件描述符复制到控制消息的数据部分
if (sendmsg(socket, &msg, 0) == -1) { // 发送消息
perror("sendmsg");
}
}
int recv_fd(int socket) {
struct msghdr msg = {0}; // 定义消息头结构体并初始化
struct iovec iov[1]; // 定义数据块结构体数组
struct cmsghdr *cmsg; // 定义控制消息头指针
char control[CMSG_SPACE(sizeof(int))]; // 控制信息缓冲区
char dummy; // 用于填充数据块的虚拟字符
iov[0].iov_base = &dummy; // 设置数据块的基址为虚拟字符的地址
iov[0].iov_len = 1; // 设置数据块的长度为1字节
msg.msg_iov = iov; // 设置消息头的数据块数组
msg.msg_iovlen = 1; // 设置数据块数组的长度
msg.msg_control = control; // 设置消息头的控制信息
msg.msg_controllen = sizeof(control); // 设置控制信息的长度
if (recvmsg(socket, &msg, 0) == -1) { // 接收消息
perror("recvmsg");
return -1;
}
cmsg = CMSG_FIRSTHDR(&msg); // 获取第一个控制消息头
unsigned char *data = CMSG_DATA(cmsg); // 获取控制消息的数据部分
int fd = *((int *) data); // 提取文件描述符
return fd; // 返回接收到的文件描述符
}
int main() {
int socket_pair[2]; // 套接字对
int file_fd; // 文件的文件描述符
pid_t pid;
char buf[1024];
// 创建套接字对
if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1) {
perror("socketpair");
exit(EXIT_FAILURE);
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程
close(socket_pair[1]); // 关闭子进程不需要的写入端
// 从套接字中读取父进程传递的文件描述符
int received_fd = recv_fd(socket_pair[0]);
// 使用接收到的文件描述符操作文件A
ssize_t num_read;
while ((num_read = read(received_fd, buf, sizeof(buf))) > 0) {
write(STDOUT_FILENO, buf, num_read); // 在子进程中输出文件内容
}
if (num_read == -1) {
perror("read");
exit(EXIT_FAILURE);
}
close(received_fd); // 关闭文件描述符
close(socket_pair[0]); // 关闭套接字读取端
_exit(EXIT_SUCCESS); // 退出子进程
} else {
// 父进程
close(socket_pair[0]); // 关闭父进程不需要的读取端
// 在fork后打开文件并获取文件描述符
file_fd = open("fileA.txt", O_RDONLY);
if (file_fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 将文件描述符通过套接字传递给子进程
send_fd(socket_pair[1], file_fd);
close(file_fd); // 关闭父进程中的文件描述符
close(socket_pair[1]); // 关闭套接字写入端
wait(NULL); // 等待子进程结束
exit(EXIT_SUCCESS); // 退出父进程
}
return 0;
}