进程间通信意味着两个不同的进程间可以交换数据,本节实现通过管道实现进程的通信。
基于管道的进程间通信结构模型如下,可以看出管道和套接字一样,并不属于进程的资源而是属于操作系统,所以不是fork函数复制的对象。两个进程通过操作系统提供的内存空间进行通信。
下面介绍创建管道的函数。
#include
int pipe (int filedes[2]);
父进程调用该函数将创建管道,同时获取对应出入口的文件描述符,此时父进程可以读写同一管道,但如果父进程的目的是与子进程进行数据交换,就需要调用fork函数将入口和出口中的一个文件描述符传递给子进程。
调用fork函数,子进程复制得到两个文件描述符。复制的并非管道,而是用于管道I/O的文件描述符。至此,父子进程同时拥有I/O文件描述符,子进程通过向管道传递字符串,父进程通过从管道接收字符串并输出。
上述程序的通信路径如下。
下面的代码子进程向父进程传递Who are you。父进程读取并输出
#include
#include
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
int fds[2];
char str[] = "Who are you?";
char buf[BUF_SIZE];
pid_t pid;
pipe(fds);
pid = fork();
if (pid == 0)
{
write(fds[1], str, sizeof(str)); //子进程写
}
else
{
read(fds[0], buf, BUF_SIZE); //父进程读
puts(buf);
}
return 0;
}
①2个进程通过1个管道进行双向通信
由单向通信可知,父子进程都拥有两个文件描述符,所以可以考虑进行双向通信
程序实现就是在单向通信的基础上父子进程各加一各write和read函数,但是请注意两个sleep函数,我们将在后面讨论。
#include
#include
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
int fds[2];
char str1[] = "Who are you?";
char str2[] = "Thank you for your message";
char buf[BUF_SIZE];
pid_t pid;
pipe(fds);
pid = fork();
if (pid == 0)
{
write(fds[1], str1, sizeof(str1));
sleep(2);
read(fds[0], buf, BUF_SIZE);
printf("Child proc output: %s \n", buf);
}
else
{
read(fds[0], buf, BUF_SIZE);
printf("Parent proc output: %s \n", buf);
write(fds[1], str2, sizeof(str2));
sleep(3);
}
return 0;
}
程序的输出如我们所料,说明完成了双向通信,父进程收到Who are you并传递给子进程Thank you for your message。
但是如果我们注释了父进程的sleep(3)会发生什么呢?如下图所示,由于子进程需要执行2秒,父进程先行结束,子进程又过了1秒子进程才会在终端打印消息。请对比和上图打印位置的区别。
那么,为什么子进程中需要增加sleep(2)这句代码呢?我们把它注释掉输出如下图,子进程正常结束,而父进程一直在等待子进程传输的数据。原因是数据进入管道时变为无主数据,先读的进程会把他读走,也就是说子进程读走子进程write的消息,而父进程一直等待子进程发送的消息。
②2个进程通过2个管道进行双向通信
由上面讨论知,使用一个管道进行通信绝非易事,所以我们创建两个管道,各自负责不同的数据流即可。
程序实现很简单,创建两个管道再调用fork函数,子进程可以通过数组fds1指向的管道向父进程传输数据,父进程可以通过数组fds2指向的管道向子进程传输数据。
#include
#include
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
int fds1[2], fds2[2];
char str1[] = "Who are you?";
char str2[] = "Thank you for your message";
char buf[BUF_SIZE];
pid_t pid;
pipe(fds1), pipe(fds2);
pid = fork();
if (pid == 0)
{
write(fds1[1], str1, sizeof(str1));
read(fds2[0], buf, BUF_SIZE);
printf("Child proc output: %s \n", buf);
}
else
{
read(fds1[0], buf, BUF_SIZE);
printf("Parent proc output: %s \n", buf);
write(fds2[1], str2, sizeof(str2));
sleep(3);
}
return 0;
}
如下图达到了双向通信的要求,而且使用两个管道程序看着十分简洁。
了解了管道通信后我们可以实现一个保存消息的回声服务器端。使用 9基于多任务的并发服务器实现 中的客户端不变,并对并发服务器进行扩充达到将回声客户端传输的字符串按序保存到文件中的要求。
总体思路是创建一个新进程,通过管道连接接收客户端服务的进程,读取传递的消息并保存在.txt中,新增的代码如下。
pipe(fds);
pid = fork();
if (pid == 0) //子进程负责保存消息
{
FILE *fp = fopen("echomsg.txt", "wt");
char msgbuf[BUF_SIZE];
int i, len;
for (i = 0; i < 10; i++) //接收10次消息
{
len = read(fds[0], msgbuf, BUF_SIZE);
fwrite((void *)msgbuf, 1, len, fp);//写入echomsg.txt
}
fclose(fp);
return 0;
}
完整代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 100
void error_handling(char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
int fds[2];
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
//sigaction
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
//socket
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
//bind
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
//listen
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
pipe(fds);
pid = fork();
if (pid == 0) //子进程负责保存消息
{
FILE *fp = fopen("echomsg.txt", "wt");
char msgbuf[BUF_SIZE];
int i, len;
for (i = 0; i < 10; i++) //接收10次消息
{
len = read(fds[0], msgbuf, BUF_SIZE);
fwrite((void *)msgbuf, 1, len, fp);//写入echomsg.txt
}
fclose(fp);
return 0;
}
while (1)
{
//accept
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
if (clnt_sock == -1)
continue;
else
puts("new client connected...");
pid = fork();
if (pid == 0) //子进程
{
close(serv_sock);
while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
{
write(clnt_sock, buf, str_len);
write(fds[1], buf, str_len);
}
close(clnt_sock);
puts("client disconnected...");
return 0;
}
else //父进程
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端代码如下,在上节有说明,在此不赘述。
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
int main(int argc, char *argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if (argc != 3) {
printf("Usage : %s \n" , argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("connect() error!");
pid = fork();
if (pid == 0)
write_routine(sock, buf);
else
read_routine(sock, buf);
close(sock);
return 0;
}
void read_routine(int sock, char *buf)
{
while (1)
{
int str_len = read(sock, buf, BUF_SIZE);
if (str_len == 0)
return;
buf[str_len] = 0;
printf("Message from server: %s", buf);
}
}
void write_routine(int sock, char *buf)
{
while (1)
{
fgets(buf, BUF_SIZE, stdin);
if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR);
return;
}
write(sock, buf, strlen(buf));
}
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}