一个进程与另一个进程之间由自身启动的最简单的通信形式是通过命令行参数传递信息。除此之外,IPC还有很多方式,包括发送方打开用于写入的通用文件,接收方用于读取。
这是我在评论中已经写的。我忍不住做了一个小演示。
testSimpleIPC.c:
#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
/* This is done in one program which is started
* - without command line arguments to behave as parent
* - with command line argument to behave as child.
*/
int mainParent(void);
int mainChild(int argc, char **argv);
int main(int argc, char **argv)
{
if (argc == 1) { /* becomes parent */
return mainParent();
} else {
return mainChild(argc, argv);
}
}
int mainParent(void)
{
const char *ipcFileName = "testSimpleIPC.txt";
/* open file for IPC */
FILE *fIn = fopen(ipcFileName, "a+");
if (!fIn) {
perror("Failed to fopen() for reading");
return -1;
}
/* fork and start child */
int pid;
switch (pid = fork()) {
case -1: /* failed */
perror("Failed to fork()");
return -1;
case 0: /* returned in child process */
execlp("./testSimpleIPC",
"./testSimpleIPC", ipcFileName, NULL);
/* If this point is reached execlp failed! */
perror("Failed to execlp()");
return -1;
default:
printf("testSimpleIPC spawned child with PID %d\n", pid);
}
/* read messages from child */
char buffer[80];
for (;;) {
if (fgets(buffer, sizeof buffer, fIn) != NULL) {
/* clip line-ending from buffer end */
for (size_t len = strlen(buffer); len--;) {
if (isspace(buffer[len])) buffer[len] = '\0';
else break;
}
/* report */
printf("Parent received :'%s'\n", buffer);
/* bail out in case */
if (strcmp(buffer, "quit") == 0) break;
}
}
fclose(fIn);
/* done */
return 0;
}
int mainChild(int argc, char **argv)
{
assert(argc == 2);
const char *const ipcFileName = argv[1];
/* write messages to parent */
FILE *fOut = fopen(ipcFileName, "a");
if (!fOut) {
perror("Failed to fopen() for writing");
return -1;
}
for (int i = 1; i < 10; ++i) {
printf("Sending 'message %d'...\n", i);
if (fprintf(fOut, "message %d\n", i) < 0
|| fflush(fOut)) {
perror("Failed to fprintf()");
return -1;
}
}
if (fprintf(fOut, "quit\n") < 0 || fclose(fOut)) {
perror("Failed to fprintf()");
return -1;
}
/* done */
return 0;
}
在 Windows 10 上的 cygwin64 中编译和测试:
$ gcc -std=c11 -o testSimpleIPC testSimpleIPC.c
$ ./testSimpleIPC
testSimpleIPC spawned child with PID 27320
Sending 'message 1'...
Sending 'message 2'...
Parent received :'message 1'
Sending 'message 3'...
Parent received :'message 2'
Sending 'message 4'...
Parent received :'message 3'
Sending 'message 5'...
Parent received :'message 4'
Sending 'message 6'...
Parent received :'message 5'
Sending 'message 7'...
Parent received :'message 6'
Sending 'message 8'...
Parent received :'message 7'
Sending 'message 9'...
Parent received :'message 8'
Parent received :'message 9'
Parent received :'quit'
$
注意事项:
起初,我忘记将可执行文件的文件路径作为execlp() 中的第一个命令行参数传递。因此,孩子一开始只有一个论据——承认自己是父母。绝望地按下 CtrlC 并没有帮助——我不得不杀死我的xterm(在它杀死我的系统之前)。所以,不要忘记:argv[0] 按照惯例是可执行文件本身的文件路径,但您必须明确将其作为参数提供给 execlp()。
最后添加的是 mainChild() 中的 fflush()。在我这样做之前,孩子甚至在父母开始接收之前就写下了所有内容。虽然原因对我来说很明显,但我觉得这值得一提。
关于execlp()的更多细节:
我用execlp(3) - Linux man page回忆了细节。
execlp()的签名是
int execlp(const char *file, const char *arg, ...);
参数:
-
file 提供与正在执行的文件关联的文件名。
-
arg 是传递给可执行文件的第一个参数。
-
... 可以是传递给可执行文件的任意数量的附加参数。
请注意,最后传递的参数必须是 NULL 才能终止列表。 (否则execlp()的调用可能会导致Undefined Behavior)。
一个令人讨厌的陷阱是第一个参数(在execlp() 的第二个nd 中,名为arg)。
如果在int main(int argc, char **argv) 中使用argc 和argv,我们习惯于argc 始终至少为1,并且argv[0] 提供了调用可执行文件本身的文件路径。
坏消息:这是一个约定(在任何地方都可以考虑)。因此,某处没有内置自动机制——execlp() 的调用者必须考虑这一点:
在提供 0 ... n 个附加参数之前,将可执行文件的文件名作为第一个参数重复,不要忘记最后的 NULL。
在上面的例子中:
execlp("./testSimpleIPC",
"./testSimpleIPC", ipcFileName, NULL);
回想一下我的 1. 注意(上),当我忘记第一个参数"./testSimpleIPC" 时,ipcFileName 的内容成为第一个,并作为argv[0] 传递给子进程。由于代码不使用argv[0],我没有注意到这一点。但是,argv[1] 丢失的事实导致子进程再次将自己标识为父进程。因此,我得到了难以停止的大量启动进程。
实际上,我没有忘记assert(argc == 2);,它检查了argc 的预期值。但是,这仅在调试代码中有效,并没有拯救我,因为我在没有 -g 的情况下编译,因此 assert 变得不活动。
就在今天,我发现 1251 个进程(我最近的测试留下的)占用了 40% 的 CPU 负载,直到我设法阻止它们
for I in $( ps | grep testSimpleIPC | awk '{ print $1 }' ); do kill $I ; done