在C语言中实现双向IPC的最佳Linux方法

5

当我被问到这个问题时,我遇到了一个例子无法完成的问题。
我在Google上寻找实现IPC的方法。
我不能确定哪种方式最适合编写我的程序。
我尝试了很多实现方式,并且遇到了很多麻烦。

我希望有以下要求:
1. 父进程管理子进程 - OK (模板)
2. 父进程和子进程必须实现新消息信号的回调
3. 一个进程不知道来自另一个进程的消息大小(char *)

我的代码:

header.h:

#ifndef MESSAGES_H
#define MESSAGES_H

#include <stdio.h>
#include <stdlib.h>

// need here: some includes and definitions

inline char * read_message( /* need here: some params */ ) {
    // need here: read message function
}

inline char * send_message( /* need here: some params */ ) {
    // need here: send message function
}
#endif

parent.c:

#include "header.h"

// parent specyfic includes and definitions

void on_message( /* need here: some parameters */ ) {
    char *message = read_message( /* need here: some other parameters */ );
    // do something with / if message etc.
}

int runChild(key) {
    int pid = fork();
    if (pid == 0) {
        execl("./child", "./child", /* params here */, null);
    }else{
        return pid;
    }
}

int main(int argc, char *argv[]) {
    // need here: prepare IPC
    // need here: on new message event call function "on_message"
    int childPid = runChild(key);
    // loop - for example: gtk_main()
    // need here: close childs
}

child.c

#include "header.h"

// child specyfic includes and definitions

void on_message( /* need here: some parameters */ ) {
    char *message = read_message( /* need here: some other parameters */ );
    // do something with / if message etc.
}

int main(int argc, char *argv[]) {
    // need here: prepare IPC
    // need here: on new message event call function "on_message"
    int pid = getpid();
    int parentPid = getppid();
    printf("Child with pid %d is ready for messages from parent with pid: %d", pid, parentPid);
    // event loop - for example: gtk_main()
}

在这个示例程序模板中,哪种IPC方式更好(安全和速度)?你能分享一个符合上述模板的真正简单的例子吗?


在头文件中编写函数实现而非内联是一种不良的编程实践。 - D3Hunter
@jujj 已经更正 - 谢谢 - s77s
2
在您的评论中,您提到正在使用GTK+。GTK+具有GtkSocketGtkPlug,它们允许一个GTK+应用程序嵌入来自另一个应用程序的小部件(该应用程序使用GTK+或Qt或其他遵循XEMBED规范的小部件)。 - Nominal Animal
如果您使用管道、命名管道或套接字进行通信,可以使用fcntl(descriptor, F_SETFL, O_ASYNC)将此描述符设置为生成SIGIO信号;最好使用fcntl(descriptor, F_SETSIG, SIGRTMIN+0)将其更改为实时信号。然而,信号处理程序必须仅使用异步信号安全函数,否则应用程序行为是未定义的。 - Nominal Animal
2个回答

17

有许多不同的方式来实现IPC。要进行良好的比较,请参见Stevens'书籍。经典的是'UNIX环境高级编程',但还有'UNIX网络编程卷2第二版:进程间通信'。我知道有时指向其他参考资料可能不被认为是好的形式,但无论是学术问题还是商业问题,大多数UNIX程序员都认为Stevens是一个宝贵的资源。

话虽如此,以下是您进行IPC的主要选项:

  1. 在进程之间使用pipe()。格式始终是基于流的;如果您发送数据结构,这可能会很麻烦,因为您不仅需要担心序列化,还需要担心缓冲和将“数据包”转换回消息。管道是单向的,因此您需要两个管道进行双向通信。

  2. 使用命名管道或FIFO。这允许一对多通信并使FIFO在一个端退出后保持存在。否则与(1)相同。

  3. 在进程之间使用socketpair - 具体是Unix域套接字。套接字是双向的。您可以使用流套接字(SOL_STREAM)、数据报(不可靠,顺序不保证- SOCK_DGRAM)或可能更好的有序可靠的双向分组通信(SOCK_SEQPACKET)。基于数据包的通信意味着您可以将一个数据结构放入每个数据包中。

  4. 使用信号。实际上,您每次只能发送一个整数。信号与线程不兼容,处理中断的系统调用很困难,并且各种竞争条件使它们不可靠,除非您知道自己在做什么并且不太担心可移植性。在大多数情况下最好避免使用。

  5. 使用系统V信号量(semget等)或POSIX信号量(sem_open等)。用于在进程之间发送信号以实现同步,但没有更多作用。

  6. 使用共享内存(shmget等)- 将相同的页面展示给多个进程。您需要结合某种同步方法。

  7. System V消息队列(msgget等)- 在两个进程之间维护数据包(消息)队列。

  8. 以上方法的某些组合。

  9. 我省略了一些仅在内核分支(例如Binder)或正在开发中(例如KDBus)中出现的内容。

    几乎所有上述内容的示例和教程都可以在这里找到:这里

    现在,大多数这些方法都可以用于您提到的应用程序。看起来您想发送可变大小的消息,因此,如果使用基于流的协议,则将数据包长度作为前1、2或4个字节发送是常规技巧(取决于最大长度)。基于数据包的协议在此方面更容易(显然),但每种协议都有它们自己的最大数据包大小限制。您关心可靠性吗?您关心可移植性吗?您关心速度吗?在选择它们之间时,这些都是有效的考虑因素。

    最后,基于FD的方法(例如管道、socketpair)的一个好处是您可以将它们添加到正常的select()循环中;如果您的程序中有其他事情正在进行,这可能会有所帮助。

    您在评论中要求一些socketpair代码示例。我重申顶部的评论,即联系Stephens。如果没有,则:

    • C / Unix中的Socketpair()展示了使用fork()设置socketpair进行进程间通信的良好示例。
    • 上述提到的教程在此处有关于socketpair()的详细部分内容。

"你关心可靠性吗?"和"你关心速度吗?" - 是的,这非常重要。 "你关心可移植性吗?" - 起初不太关心,但后来是的。 然后我将使用具有自己循环的GTK+库。 - s77s
1
管道和套接字的速度非常快。如果您正在使用GTK+事件循环,我建议使用socketpair。 - abligh
谢谢。你能举个例子吗?最重要的是:当有新消息信号时读取消息。 - s77s
最后一个问题:在socketpair(第11.4节)中,sa_flags中的SIGIO是否适用于捕获新消息信号? - s77s
我需要类似于信号和处理程序的东西。应用程序执行某些操作,当有新消息可用时执行其他操作。我不能等待消息(这会冻结我的程序)。使用sa_flags中的SIGIO是否好? - s77s
显示剩余2条评论

2

以下是我最近编写的多进程程序的一些设置代码,使用select提供非阻塞等待。这似乎也是在C++中做到这一点的较好方法之一,因为据我所知,文件描述符在标准库中得不到很好的支持...

// Parent
int main(int argc, char **argv) {

    // Pipe, fork, exec (to run robot in child)
    int toParent[2], fromParent[2];
    pipe(toParent);
    pipe(fromParent);

    // Redirect childs stdin/stdout
    if (fork()) { // parent
        close(toParent[1]); // [1] == write
        close(fromParent[0]); // [0] == read
    }
    else {
        close(toParent[0]);
        close(fromParent[1]);
        dup2(toParent[1], 1);
        dup2(fromParent[0], 0);
        close(toParent[1]);
        close(fromParent[0]);
        execl("../robot/robot", "../robot/robot", (char *) NULL);
    }


    FILE * output = fdopen(fromParent[1], "w");
    FILE * input = fdopen(toParent[0], "r");

    // Set up for select() read of input pipe
    fd_set set;
    struct timeval timeout;

    // Initialize the file descriptor set.
    FD_ZERO(&set);
    FD_SET(toParent[0], &set);

    // Initialize the timeout data structure
    timeout.tv_sec = 0;
    timeout.tv_usec = 10;

    while(1) {
        // Non-blocking read of pipe
        // NOTE: only expecting to read one pipe so no need to check which pipe got data
        if (select(toParent[0]+1, &set, NULL, NULL, &timeout) > 0) {
            // read the input pipe here
        }
        // Reset select FD -- maybe only do this when an input has been read?
        FD_ZERO(&set);
        FD_SET(toParent[0],&set);
}

总体思路是让子进程通过其标准输入/输出(使用dup2())与父进程通信,然后使用FILE * output和input来写入子进程。唯一的注意事项是,如果子进程向stdout打印调试信息而父进程没有处理它,则可能会导致意外行为,因此通常最安全的做法是将子进程的调试消息打印到stderr中。

至于回调函数,您可以使用select,这是在其他地方已经很好地记录的东西。


使用管道函数两次的原因是什么? - pmverma
为了为每个数据发送方向创建单独的管道,通常管道不是双向的。 - chris
我该如何以异步模式完成它? - s77s
你是什么意思?子进程和父进程异步通信。 - chris
1
只需使用 socketpair 而不是管道。 - Maxim Egorushkin
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接