进程间的通信
【管道】局限于没有名字,只能由亲缘关系的进程使用。基于此,提出有名管道(FIFO)进行改正。在实际应用中【管道】常用于具有共同祖先的进程间。
【管道】和【FIFO】都是使用通常的read和write函数访问
客户-服务器
1.客户端从标准输入(stdin)读进一个路径名,并将其写入IPC通道(客户端与服务器之间的虚线)
2.服务器从IPC通道读出路径名并打开读取对应的文件。
如果服务器能打开文件:服务器读出其中的内容,并写入IPC通道,作为客户端的响应;
若打不开:服务器端就响应一个出错信息;
3.客户从IPC通道中读出响应并把它写入标准输出(stdout)中,若服务器无法读取该文件,客户读出的响
应就是一个出错信息。
管道
所有的Unix都提供管道。它由pipe函数来创建,提供一个单路(单向)的数据流。
#include<unistd.h>
int pipe(int fd[2]); //返回值:若成功则为0,出错则为-1
pipe()函数返回两个文件描述符:fd[0]和fd[[1]],fd[0]表示打开来读,fd[[1]]表示打开来写。
管道的典型用途是实现两个不同的进程(一个父进程、一个子进程)提供进程间的通信手段。
1.首先父进程创建一个管道后调用fork函数派生一个自身的副本;
2.父进程关闭读端,子进程关闭写端,这样形成一个父子进程间的单向数据流(父进程向管道写数据,子进程从管道读数据)
通过shell命令:who | sort |ip 创建三个进程和其间的两个管道。每个进程的标准输出复制到管道的写入端,每个管道的读出端复制到进程的标准输入端。
如上所述的管道是半双工的即单向的,只提供一个方向上的数据流。当需要一个双向数据流时,必须创建两个管道,每个方向一个(读方向一个,写方向一个)。具体布局如下:
描述客户-服务器,main()函数创建两个管道并用fork生成一子进程。客户端作为父进程运行,服务器作为子进程运行。第一个管道用于从客户向服务器发送路径名,第二个管道用于服务器向客户发送该文件的内容(或出错内容)。客户-服务器端双向数据传输代码如下:
由于两个进程同时在运行,必要的时候,可以使用延时函数,使得某个进程下某个模块迟于另一个进程。
#include"unpipc.h"
#include <sys/types.h>
#include <sys/wait.h>
void client(int,int),server(int,int);
void client(int readfd,int writefd) //readfd、writefd就是索引值
{
size_t len;
ssize_t n;
char buff[MAXLINE];
fgets(buff,MAXLINE,stdin); //fgets()函数:从标准输入stdin中读取MAINLINE个字节存入buff中,并未限制必须读取MINLINE个字节才能返回,主要读到数据就马上返回
len=strlen(buff); //strlen()求字符串的实际长度,计算'\0'之前的字符数
if(buff[len-1]=='\n') //从标准输入读进路径后,删除由fgets存入的换行符,再写入管道
len--;
Write(writefd,buff,len); //将参数写至IPC通道中
while((n=Read(readfd,buff,MAXLINE))>0)
{
write(STDOUT_FILENO,buff,strlen(buff));
} //将buff中的数据写道标准输出
}
//从readfd中打开输入的文件名,并将文件名对应的文件内容写入文件描述符writefd
void server(int readfd,int writefd)
{
int fd;
ssize_t n;
char buff[MAXLINE+1];
/*从IPC管道中读取数据*/
if((n=read(readfd,buff,MAXLINE))==0)
err_quit("出错信息");
buff[n]='\0';
if((fd=open(buff,O_RDONLY))<0){
/*错误必须告知客户端*/
snprintf(buff+n,sizeof(buff)-n,":can't open,%s\n",strerror(errno));
n=strlen(buff);
write(writefd,buff,n);
}else{
/*打开成功:将文件复制至IPC通道*/
while(n=Read(fd,buff,MAXLINE)>0)
{
write(writefd,buff,strlen(buff));
}
Close(fd);
}
}
int main(int argc,char **argv)
{
int pipe1[2],pipe2[2];
pid_t childpid;
pipe(pipe1); //创建两个管道
pipe(pipe2);
if((childpid=Fork())==0){ //子进程
sleep(5); //子进程延迟5秒,等待父进程先操作
Close(pipe1[1]);
Close(pipe2[0]);
server(pipe1[0],pipe2[1]); //调用server()函数:子进程作为服务器端进行数据传输
exit(0);
}
/* 父进程 */
Close(pipe1[0]);
Close(pipe2[1]);
client(pipe2[0],pipe1[1]); //调用client()函数
waitpid(childpid,NULL,0); //等待子进程终止,waitpid()函数:指定某一个进程进行回收
exit(0);
}
//注:以上首字母大写的函数是需要定义的函数而不是系统可以直接调用的系统函数
错误封装函数 unpipc.c
#include "unpipc.h"
void err_quit(char *fmt,...){
int errno_save=errno;
va_list va;
va_start(va,fmt);
vfprintf(stderr,fmt,va);
if(errno_save != 0)
fprintf(stderr," :%s",strerror(errno_save));
fprintf(stderr,"%s\n",(char*)va);
va_end(va);
exit(0);
}
void Write(int fd,void *buf,size_t nbytes){
if(write(fd,buf,nbytes) != nbytes)
err_quit("write error");
}
ssize_t Read(int fd,void *buf,size_t nbytes){
size_t nread=read(fd,buf,nbytes);
if(-1 == nread)
err_quit("read error");
return nread;
}
int Open(const char *pathname,int flags,mode_t mode){
int fd=open(pathname,flags,mode);
if(-1 == fd)
err_quit("open error");
return fd;
}
void Close(int fd){
if(close(fd) == -1)
err_quit("close error");
}
void Unlink(const char *pathname){
if(unlink(pathname) == -1)
err_quit("unlink error");
}
pid_t Fork(void){
pid_t pid;
if((pid=fork())<0)
err_quit("fork error");
return pid;
}
void Fputs(const char *buf, FILE *fp){
if(fputs(buf,fp) == EOF)
err_quit("fputs error");
}
char *Fgets(char *buf,int nbytes,FILE *fp){
char *ptr=fgets(buf,nbytes,fp);
if((ptr==NULL) && (ferror(fp)))
err_quit("fgets error");
return ptr;
}
Sigfunc *Signal(int signo,Sigfunc *func){
Sigfunc *sigfunc=signal(signo,func);
if(SIG_ERR == sigfunc)
err_quit("signal error");
return(sigfunc);
}
void *Malloc(size_t size){
void *ptr=malloc(size);
if(NULL == ptr)
err_quit("malloc error");
return(ptr);
}
void Sigemptyset(sigset_t *set){
if(sigemptyset(set) == -1)
err_quit("sigemptyset error");
}
void Sigaddset(sigset_t *set,int signo){
if(sigaddset(set,signo) == -1)
err_quit("sigaddset error");
}
void Sigprocmask(int command,sigset_t *set,sigset_t *oset){
if(sigprocmask(command,set,oset) == -1)
err_quit("sigprocmask error");
}
void Sigwait(const sigset_t *set,int *signo){
if(sigwait(set,signo) != 0)
err_quit("sigwait error");
}
错误封装函数 unpipc.h
#ifndef _UNPIPC_H
#define _UNPIPC_H
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <limits.h>
#include <stdarg.h>
#include <signal.h>
#define MAXLINE 1024
//定义一个新类型Sigfunc,该类型的变量是一个带有一个int型参数,没有返回值的函数
typedef void Sigfunc(int);
void err_quit(char *fmt,...);
void Write(int fd,void *buf,size_t nbytes);
ssize_t Read(int fd,void *buf,size_t nbytes);
int Open(const char *pathname,int flags,mode_t mode);
void Close(int fd);
void Unlink(const char *pathname);
void Fputs(const char *buf,FILE *fp);
char *Fgets(char *buf,int nbytes, FILE *fp);
pid_t Fork(void);
Sigfunc *Signal(int signo,Sigfunc *);
void Sigemptyset(sigset_t *set);
void Sigaddset(sigset_t *set,int signo);
void Sigprocmask(int command,sigset_t *set,sigset_t *oset);
void Sigwait(const sigset_t *set,int *signo);
void *Malloc(size_t size);
#endif
运行附图:
全双工管道
半双工管道和全双工管道两种管道通信方式
半双工管道通信实现数据的单向流动:
全双工管道的实现可以如下图所示:整个管道只存在一个缓冲区,写入管道的任何数据都添加在该缓冲区的末尾,从管道读出的数据取自该缓冲区的开头数据。
但以上方式会出现错误:当一个进程往该管道中写入数据后,再对该管道调用read时,有可能读回刚写入的数据。基于此,通过如下图的方式真正实现全双工管道:
#include "unpipc.h"
int main(int argc,char **argv)
{
int fd[2],n;
char c;
pid_t childpid;
Pipe(fd); //某些系统提供全双工管道:SVR4的pipe函数以及很多内核提供的socketpair函数
/*子进程等待3秒,从管道中读一个字符,然后再写入一个字符*/
if((childpid=Fork())==0){
sleep(3); //子进程先睡眠3秒,保证父进程的read调用早于子进程的read调用
if((n=Read(fd[0],&c,1)!==1)
err_quit("child:read returned %d",n);
printf("child read %c\n",c);
Write(fd[0],"c",1);
exit(0);
}
/*父进程往管道总写入一个字符p,然后再读出一个字符*/
Write(fd[1],"p",1);
if((n=Read(fd[1],&c,1))!=1)
err_quit("parent:read returned %d",n);
printf("parent read %c\n",c);
exit(0);
}
注:以上代码在不支持管道全双工的环境下是运行不通的(逻辑和代码是✔的,环境是不支持的),父进程在read处,子进程在write处,分别报出错误。