通过前面对进程概念,进程控制的了解,我们知道,每个进程都有自己独立的用户地址空间,任何一个进程中的全局变量在另一个进程中是看不到的。所以进程间的运行是相互独立的。这样做可以保证安全,当一个进程出现问题时,不至于影响其他进程。
但是,当不同进程间要进行传输数据,共享资源,通知消息,进程控制等操作,此时,相互独立的进程是做不到这些的。所以,为了进行这些操作,相互独立的进程间必须进行进程间通信。
一,进程间通信
进程间通信是使相互独立的进程间看到共同的资源,从而实现数据传输等目的。而这些共同的资源是有操作系统提供的。操作系统会开辟一块缓冲区,一个进程把数据从用户空间拷到缓冲区,另一进程从缓冲区将数据取走。这片缓冲区就是共同的资源。根据提供资源的不同,进程间通信主要有以下几种方式:
1. 管道:匿名管道,命名管道
2. System V IPC
1)System V 消息队列
2)System V 共享内存
3)System V 信号量
3. POSIX IPC
二,管道
管道是连接一个进程与另一进程的数据流。这个数据流相当于内存中的一片区域,供两个进程进行通信。我们知道,在Linux系统中,“一切皆文件”。一个进程将自己的标准输出重定向到管道文件去向管道中写数据,另一进程将自己的标准输入重定向到同一管道文件从管道中读取数据。这样,便可实现进程间通信。
1. 匿名管道
一个进程通过系统调用接口来使操作系统在内核中开辟一块缓冲区用于进程间通信。
匿名管道的创建:
int pipe(int fd[2]);//头文件:<unistd.h>
参数:fd是输出型数组,它提供两个文件描述符给调用该接口的进程。
其中f[0]表示读端,通过f[0]可以从管道中读取数据;通过f[1]可以向管道中写数据。
返回值:成功返回0,失败返回错误代码-1。
管道创建后如下图所示:
通过以下代码来演示如何对管道进行操作:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
//从键盘上读取数据写入管道,从管道读取数据写入屏幕
int main()
{
int fd[2];
char buf[100];
if(pipe(fd) == -1)//通过系统调用接口pipe创建一个管道
{
perror("pipe error");
exit(1);
}
//从标准输入即键盘上获取数据保存在buf中
while(fgets(buf,sizeof(buf),stdin))
{
if(write(fd[1],buf,strlen(buf)) == -1)//将获取的数据通过写端写入管道文件中
{
perror("write error");
break;
}
memset(buf,0x00,sizeof(buf));//清空buf
if(read(fd[0],buf,sizeof(buf)) == -1)//通过读端从管道文件中读取数据保存在buf中
{
perror("read error");
break;
}
if(write(1,buf,strlen(buf)) == -1)//将从管道中读取的数据写到标准输出即显示器上
{
perror("write error");
break;
}
}
return 0;
}
输出结果显示:
[admin@localhost pipe]$ ./a.out
skabvknm,mza
skabvknm,mza
jkabskj
jkabskj
avjabc
avjabc
vkja
vkja
以上看到可以将管道当做一个文件来使用。
那既然一个进程创建了一个管道,它既可以读,又可以写,但它并没有与其它进程通信啊?
前面我们已经知道父进程与子进程之间的很多资源是相同的,其中就包括文件描述符表。所以,当父进程创建了一个管道,可以向其中进行读写操作时,子进程因为拷贝了父进程的文件描述符表,同样可以向该管道中进行读写。但是,管道中的数据只能沿一个方向流动,所以,父子进程必须一个读,一个写。即必须一个关闭读端,一个关闭写端,这样才能保证可通过管道正常通信。如下图:
(1)父进程创建一个管道,得到管道的读写端文件描述符:
(2)父进程fork出子进程,子进程拷贝父进程的文件描述符表,得到该管道的相同读写端文件描述符:
(3)因为管道中的数据是单向流动的,所以,父进程关闭写端,子进程关闭读端。
(4)在进行了上述准备工作后,父子进程间就可以互相通信了。
以下通过代码来演示通过管道来实现进程间通信:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
//内核中宏函数的写法
//因为要执行多语句,所以要用do-while循环语句进行封装,但只执行一次
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
//在宏函数中,只执行一条语句,可以不用do-while循环语句
#define SUCC_EXIT() exit(EXIT_SUCCESS)
int main()
{
int pipe_fd[2];
char *msg = "hello world\n";
if(pipe(pipe_fd) == -1)//管道创建失败
{
ERR_EXIT("pipe error");//调用宏函数
}
pid_t pid;
pid = fork();//创建子进程
if(pid < 0)//子进程创建失败
{
ERR_EXIT("fork error");//调用宏函数
}
if(pid == 0)//子进程
{
close(pipe_fd[0]);//关闭子进程的读端
write(pipe_fd[1],msg,strlen(msg));//子进程往管道中写入内容
SUCC_EXIT();//调用宏函数,写完后,子进程成功退出
}
//父进程
close(pipe_fd[1]);//关闭写端
char buf[100] = {0};
int s = read(pipe_fd[0],buf,sizeof(buf));//父进程从管道中读取内容
if(s > 0)
{
buf[s] = 0;
}
printf("%s",buf);//输出读到的内容
return 0;
}
输出结果:
[admin@localhost pipe]$ ./a.out
hello world
通过上述代码,可以看出父子进程间可以通过管道来进行通信。
以上可以看到,因为子进程的文件描述符表是从父进程拷贝过来的,所以父子进程可以看到共同的管道资源。也就是说,上述pipe创建的管道只能用于具有共同祖先(即有亲缘关系)的进程间通信。
当没有亲缘关系的进程间如何通过管道进行通信呢?通过命名管道。
2. 命名管道
命名管道可以当做一个特殊的文件来处理。
创建一个命名管道:
(1)通过系统调用接口:
int mkfifo(const char* filename,mode_t mode);
参数:参数1为要创建的管道的名字。参数2为该管道文件的权限
返回值:管道文件创建成功,返回0,创建失败,返回-1。
(2)通过命令:
在命令行中输入:
mkfifo filenamme
该命令在执行过程中,其实也调用了上述接口来实现的。
使用管道
上述创建好后,可以将管道当做文件来使用。一个进程以只读的方式打开管道文件,获得可以读管道文件的文件描述符,另一进程以只写放入方式打开管道文件,获得可以写管道文件的文件描述符。一个进程向管道中写,一个进程从管道中读,这样便可实现互不相关的进程进行通信。当通信结束时,通过文件描述符关闭管道文件即可。
以下通过代码来实现两互不相关进程间的通信:
用于读的进程Server.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<unistd.h>
//异常退出宏函数
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
umask(0);
if(mkfifo("mypipe",0644) < 0)//管道创建失败
{
ERR_EXIT("mkfifo error");
}
int rfd = open("mypipe",O_RDONLY);//以只读方式打开管道文件
if(rfd < 0)//打开失败
{
ERR_EXIT("open error");
}
char buf[1024];
while(1)
{
printf("please wait!\n");
size_t s = read(rfd,buf,sizeof(buf) - 1);//从管道中读取内容
if(s > 0)
{
buf[s] = 0;
printf("client say#");
printf("%s\n",buf);
}
else if(s == 0)
{
printf("client quit,me too\n");
break;
}
else
{
ERR_EXIT("read error");
}
}
unlink("mypipe");
close(rfd);
return 0;
}
用于写的进程client.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
int wfd = open("mypipe",O_WRONLY);//以只写的方式打开server中创建的管道文件
if(wfd < 0)//打开失败
{
ERR_EXIT("open error");
}
char buf[1024];
while(1)
{
printf("please enter#");
fflush(stdout);
size_t s = read(0,buf,sizeof(buf)-1);//从标准输入中读取内容
if(s > 0)
{
buf[s] = 0;
write(wfd,buf,strlen(buf));//将读取的内容写入管道文件
}
else
{
ERR_EXIT("read error");
}
}
close(wfd);
return 0;
}
在两个终端下分别运行server.c和client.c,得到一些结果:
[admin@localhost fifo_server_client]$ ./serverPipe
please wait!
client say#cabjkj
please wait!
client say#ackn
please wait!
client say#ns
please wait!
client quit,me too
[admin@localhost fifo_server_client]$ ./clientPipe
please enter#cabjkj
please enter#ackn
please enter#ns
please enter#^C
可以看到命名管道可以实现没有亲缘关系的进程间通信。
3. 管道的读写规则
进程在对管道(两种管道均适用)进行读写操作时,遵循以下规则:
(1)读写双方均不关闭管道文件,写端一直写,读端不读。写端将管道写满后会阻塞等待读端读取后才会继续写。
(2)读写双方均不关闭管道文件,写端不写,读端一直读,当读完管道数据后,读方会阻塞等待直到有数据写入后才继续读;
(3)写方写入一定数据后关闭写端。读方读完数据后,最终会读到0
(4)写方一直写,读方读入一定数据后关闭读端。此时操作系统会发送SIGPIPE信号导致写进程退出。
下面通过命名管道来验证第(4)条:
用于读的进程Server.c
int main()
{
umask(0);
if(mkfifo("mypipe",0644) < 0)//管道创建失败
{
ERR_EXIT("mkfifo error");
}
int rfd = open("mypipe",O_RDONLY);//以只读方式打开管道文件
if(rfd < 0)//打开失败
{
ERR_EXIT("open error");
}
char buf[1024];
printf("please wait!\n");
size_t s = read(rfd,buf,sizeof(buf) - 1);//从管道中读取内容
if(s > 0)
{
buf[s] = 0;
printf("client say#");
printf("%s\n",buf);
}
else
{
ERR_EXIT("read error");
}
unlink("mypipe");
close(rfd);//读取内容后关闭读端
return 0;
}
用于写的进程client.c
void handler(int signo)
{
printf("catch signo %d\n",signo);
exit(1);
}
int main()
{
signal(SIGPIPE,handler);//自定义SIGPIPE信号的处理动作
int wfd = open("mypipe",O_WRONLY);//以只写的方式打开管道文件
if(wfd < 0)//打开失败
{
ERR_EXIT("open error");
}
char *buf = "hello world\n";
while(1)
{
write(wfd,buf,strlen(buf));//写端一直往管道中写入内容
}
close(wfd);
return 0;
}
运行结果:
[admin@localhost fifo_server_client]$ ./serverPipe
please wait!
client say#hello world
[admin@localhost fifo_server_client]$ ./clientPipe
catch signo 13
可以看到,写进程在读进程关闭管道文件后收到了SIGPIPE信号去执行自定义处理动作handler函数。所以写进程在读端关闭后会收到SIGPIPE信号,该信号的默认处理动作是终止进程,所以读端关闭后写进程会终止。
4. 管道的特点
关于以上两种类型的管道,有以下特征:
(1)匿名管道只能用于有亲缘关系的进程间通信,而命名管道可以用于任意进程间通信
(2)管道是单向通信的,若要进行双向通信,需在两进程间在创建一个管道
(3)管道自带互斥与同步机制
(4)进程退出,管道释放。所以管道的生命周期随进程
(5)管道提供面向字节流的服务,即读写格式任意。
其中:
互斥:任一时刻只能有一个临界区访问临界资源(临界资源:互不相干的两进程看到的共同资源。临界区:两进程访问临界资源的代码)。
同步:访问临界资源的顺序性。一个进程访问结束,下个进程才可访问。