我正在开展一个个人项目,试图更好地理解 Unix 上的进程间通信。我有两个用 C 编译的二进制文件,我正在尝试使用 Unix 套接字将数据从一个进程传递到另一个进程。
我想让我的发送/接收函数尽可能通用,以便能够使用相同的消息结构传递任何类型的数据(int、char、复杂结构):
enum DataType
{
INT_TYPE,
FLOAT_TYPE,
CHAR_TYPE,
STRUCT_TYPE,
};
struct Message
{
int identifier;
enum DataType data_type;
void* data;
size_t data_length;
};
这是我想出的发送功能:
ssize_t Send_message(const int pSocket, struct Message pMessage)
{
// Send the message over the socket
ssize_t bytes_sent = send(pSocket, &pMessage, sizeof(struct Message), 0);
if (bytes_sent == -1)
{
perror("Error in ipc.c, Send_message: Error sending message");
return -1;
}
if (bytes_sent != sizeof(struct Message))
{
fprintf(stderr, "Error in ipc.c, Send_message: Incomplete message sent\n");
return -1;
}
if(pMessage.data_length > 0 && pMessage.data != NULL)
{
bytes_sent += send(pSocket, pMessage.data, pMessage.data_length, 0);
if (bytes_sent == -1)
{
perror("Error in ipc.c, Send_message: Error sending message");
return -1;
}
if (bytes_sent != pMessage.data_length + sizeof(struct Message))
{
fprintf(stderr, "Error in ipc.c, Send_message: Incomplete message sent\n");
return -1;
}
}
printf("\nSent message with Request Type : %d, Identifier :%d, Data Lenght : %d \n", pMessage.request_type, pMessage.identifier, pMessage.data_length);
return bytes_sent;
}
我认为尽可能通用的最佳方法是将我想要传递的数据转换为 void*,然后在接收端转换回正确的类型。发送过程示例:
struct Message response;
// ** Input here response.identifier
// ** Input here response.data_type
// ** Input here response.data_length
char *string_val = "HELLO WORLD";
int int_val = 42;
if(received_message.data_type == CHAR_TYPE)
{
response.data = (void*)string_val;
}
if(received_message.data_type == INT_TYPE)
{
response.data = (void*)&int_val ;
}
Send_message(pSocket, response);
这非常适合基本类型。但如果我想传递复杂的结构,比如:
typedef struct {
int subparam1;
float subparam2;
char * subparam3;
} SubConfiguration;
SubConfiguration subconf;
// ** Fill in the struct
response.data = (void*)&subconf;
Send_message(pSocket, response);
-- 编辑添加 Receive_message 来接收消息
ssize_t Receive_message(const int pSocket, struct Message *pMessage)
{
// Receive the message into the buffer
ssize_t bytes_received = recv(pSocket, pMessage, sizeof(struct Message), 0);
if (bytes_received != sizeof(struct Message))
{
perror("\n Error in ipc.c, Receive_message: Error receiving message");
return -1;
}
if(pMessage->data_length > 0 )
{
pMessage->data = malloc(pMessage->data_length);
bytes_received += recv(pSocket, pMessage->data, pMessage->data_length, 0);
if (bytes_received != pMessage->data_length + sizeof(struct Message))
{
perror("\n Error in ipc.c, Receive_message: Error receiving message");
return -1;
}
}
printf("\nReceived message with Request Type : %d, Identifier :%d, Data Lenght : %d \n", pMessage->request_type, pMessage->identifier, pMessage->data_length);
return bytes_received;
}
现在我在接收端得到的只是结构体的 int 和 float 值。我放入的 char* 无法访问。
我的问题是:是否可以做我想做的事情?我做错了什么?我开始考虑集成像 protobuf 这样的协议缓冲区来正确序列化和反序列化我的数据:在我的情况下有必要吗?
“现代”(这意味着基本上每个系统都不仅仅是过去 40 年左右开发的微控制器)系统确实具有虚拟内存。这意味着每个进程都有自己独立于其他进程的虚拟地址范围。
如果一个进程(让我们调用进程A)需要内存,进程A必须向内核请求内存(在unix上可以使用
mmap()
系统调用)。然后,内核(或稍后,如果使用延迟分配)为进程 A 保留物理内存。假设物理地址从 0x12345600 开始,但进程 A 可能无法使用指向地址 0x12345600 的指针来访问它,而是使用虚拟地址来访问它。是地址0xABCDEC00。 CPU 自动将进程 A 的虚拟地址 0xABCDEC00 转换为物理地址 0x12345600。
现在,当进程A将指向地址0xABCDEC00的指针发送给进程B时。当进程B想要访问0xABCDEC00时,要么没有进程B映射到该地址的物理地址,从而导致段错误。或者进程 B 确实在地址 0xABCDEC00 处映射了某些内容(其他),然后访问该地址而不是物理地址 0x12345600(导致不可预测的行为,这就是为什么在 C 中访问此地址会导致 UB)。
这就是为什么接收器中的
void* data;
要么指向任何地方,要么指向一些不相关的数据。这是行不通的。
也许您读过有关虚拟内存、地址转换和 MMU(内存管理单元)的内容。
如何避免这种情况:
您可以将数据写入套接字中。这意味着您想要传输的所有数据都包含在
write()
或 send()
调用中。
或者您可以保留共享内存(也可以使用
mmap()
)。如果操作正确,则可以将指向该共享内存的指针发送给进程 B,并且进程 B 可以访问它。
我想让我的发送/接收函数尽可能通用,以便能够使用相同的消息结构传递任何类型的数据(int、char、复杂结构):
这可能不是最好的主意,除非你的意思是你使用字节流(这本质上就是套接字、管道和文件),但是你不必编写任何新函数,因为已经存在的函数可以做那个。