Linux进程间通信
进程是程序运行资源分配的最小单位。每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,Inter-Process Communication)。
匿名管道pipe
管道概述
-
pipe只能用于有血缘关系的进程进行单向通信。
-
调用 pipe 函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过 fd 参数传出给用户程序两个文件描述符, fd[0] 指向管道的读端, fd[1] 指向管道的写端。支持多端读或多端写,但不支持一端同时读写。也就是说,管道是半双工通信(即双方都可以发送信息,但是双方不能同时发送信息)。
-
它可以看作一个特殊的文件,对于它的读写也可以使用普通的read()和write()等函数。
-
一个进程向管道中写的内容被管道另一端的进程读出。写入的内容添加在管道缓冲区的末尾,并从缓冲区的头部读出数据。
-
当一个管道共享多对文件描述符时,若将其中一对读写文件描述符都删除,则该管道就失效
我们使用man 2 pipe来查看pipe的系统调用
可知pipe(fd[2])
可表示一个管道,头文件为#include <fcntl.h>
和#include <unistd.h>
文件描述符
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开的文件的记录表。当程序打开一个文件时,内核向进程返回一个文件描述符。
file descriptors: 由用户程序维护的记录表,记录的是该用户程序打开的所有的文件的fd。每个进程会预留三个默认的fd:stdin(0)、stdout(1)、stderr(2)。
file table:该表是全局唯一的,由系统内核维护,记录了所有进程打开的文件的状态、偏移量、访问模式(可读写)、文件类型、该文件对应的inode对象引用等。
Inode table: 全局唯一的表,是硬盘存储的文件的元数据的集合。
三个表的关系如下图所示:
简而言之,fd就是系统维护的file table表的某一项entry的指针,应用程序通过它能读写硬盘里文件。应用程序用它来跟内核打交道,让内核以fd定位应用程序所需访问的文件并帮忙读写数据
下图可以较为清楚地表明Pipe的功能:
pipe原型
#include <unistd.h>
int pipe(int pipefd[2]);
传入的参数是一个大小为2的数组,然后就得到了两个文件描述符pipefd[0]和pipefd[1]
pipe举例
(1)举一个简单的栗子:
这里我们用一个父子进程来举例,如果要实现父子进程间的通信,在fork前就需要创建一个pipe管道
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
#define MAXLINE 80
int main(void)
{
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0)//如果管道的文件描述符小于0
{
perror("pipe");
exit(1);
}
if ((pid = fork()) < 0)//子进程如果创建成功了,返回的pid值一定大于0
{
perror("fork");
exit(1);
}
if (pid > 0)
{ /* parent */
close(fd[0]);
write(fd[1], "hello world\n", 12);
wait(NULL);
}
else
{ /* child */
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
return 0;
}
运行结果如下:
可见这是父进程把字符串“hello world”写入到管道,子进程再从管道里面读取出来并且打印到标准输出上面来。
(2)运行博客园老师所给的pipedemo.c
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
int len, i, apipe[2];//两个文件描述符
char buf[BUFSIZ];//长度为BUFSIZ
if ( pipe ( apipe ) == -1 ){
perror("could not make pipe");
exit(1);
}
printf("Got a pipe! It is file descriptors: { %d %d }\n",
apipe[0], apipe[1]);
while ( fgets(buf, BUFSIZ, stdin) ){//从输入端获取字符,存入buf数组中
len = strlen( buf );
if ( write( apipe[1], buf, len) != len ){//apipe[1]是写入端,这里write()函数将buf指针指向的内存的len长个字节写入到apipe[1]所指向的管道缓冲区中。
perror("writing to pipe");
break;
}
for ( i = 0 ; i<len ; i++ )
buf[i] = 'X' ;
len = read( apipe[0], buf, BUFSIZ ) ;
if ( len == -1 ){
perror("reading from pipe");
break;
}
if ( write( 1, buf,len ) != len ){
perror("writing to stdout");
break;
}
}
}
运行结果如下:
运行云班课里所给的代码pipedemo2.c
结果如下:
pipe管道的局限性
-
只支持单向数据流
-
只能用于具有亲缘关系的进程之间
-
没有名字,不方便操作
-
管道的缓冲区大小有限
命名管道fifo
fifo简介
FIFO(First In First Out)文件在磁盘上没有数据块,仅仅是内核中一条通道,各进程可以读写从而实现的进程间通信。支持多端读或多端写;
严格遵循先进先出原则;
不支持诸如seek()等文件定位操作;
我们输入 man -k pipe | grep named
所需头文件:
#include <sys/types.h>
#include <sys/stat.h>
命令:mkfifo 管道名
库函数:int mkfifo(const char *pathname, mode_t mode);
-
pathname: 普通的路径名,也就是创建后 FIFO 的名字。
-
mode: 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同。
-
返回值:成功:0
失败:如果文件已经存在,则会出错且返回 -1。 -
命名管道fifo可以使不相关的独立进程之间互相通信,通过路径名识别,文件系统中可见。
-
命名管道建立后,进程间可像普通文件一样操作,可使用open(),write(),read()等函数。为了读取操作而打开的命名管道可在open时设置
O_RDONLY
;为写入操作而打开的命名管道可在open时设置O_WRONLY
。 -
命名管道fifo遵循先入先出原则,读时从头部读取数据,写时从尾部写入数据。
-
命名管道fifo与普通文件操作之间的区别是不支持如
lseek()
等文件定位,命名管道fifo默认打开是阻塞的。如果需要非阻塞,需要在open时设置O_NONBLOCK
。
实现fifo通信
我们首先需要用mkfifo myfifo
生成一个fifo文件用于两个进程之间的通信
然后在同一个目录下创建两个程序文件:
wr.c:
// file: wr.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd, ret, i = 0;
char buf[256];
fd = open("myfifo", O_WRONLY);
if(fd < 0)
{
perror("open error");
}
printf("write start!\n");
while(i < 100)
{
snprintf(buf, 256, "hello %d\n", i);
ret = write(fd, buf, strlen(buf));
if(ret < 0)
{
perror("write error");
}
printf("write ok: %d\n", i);
i++;
sleep(1);
}
return 0;
}
rd.c:
// file: rd.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd, ret;
char buf[4096];
fd = open("myfifo", O_RDONLY);
if(fd < 0)
{
perror("open error");
}
printf("read start!\n");
while(1)
{
ret = read(fd, buf, 4096);
write(STDOUT_FILENO, buf, ret);
sleep(1);
}
return 0;
}
运行结果如下:
进入myfifo文件夹下,首先编译运行testmf.c,创建一个fifo文件,并命名为myyfifo,该文件也处在该目录下,再编译老师所给的consumer.c和producer.c代码,运行结果如下:
可见消费者端读取出了当时在生产者端写入的hahahah,fifo管道建立成功。
signal信号
signal理解
我们先来理解以下signal.h这个函数:
函数原型:
#include <signal.h>//头文件
`signal(SIGINT,SigIntHandler);`
signal 的第1个参数`signum`表示要捕捉的信号,第2个参数是个**函数指针**,**表示要对该信号进行捕捉的函数**,**该参数也可以是SIG_DEF(表示交由系统缺省处理,相当于白注册了)或SIG_IGN(表示忽略掉该信号而不做任何处理)**。
**`signal`如果调用成功,返回以前该信号的处理函数的地址,否则返回 `SIG_ERR`。**
`sighandler_t`是信号捕捉函数,由`signal`函数注册,注册以后,在整个进程运行过程中均有效,并且对不同的信号可以注册同一个信号捕捉函数。该函数只有一个参数,表示信号值。
int sigaction(int signum, const struct sigaction* action, struct sigaction* prevaction)
- signum:要处理的信号
- action:指向描述如何响应信号的结构体
- prevation:被替换的结构体,可以为NULL。成功返回0,失败返回-1
其中sigaction结构体的结构如下所示:
struct sigaction
{
void(*sa_handler)();//中断处理时调用的函数
void(*sa_sigaction)(int,siginfo_t*,void*);
sigset_t sa_mask;//被阻塞的信号集
int sa_flags;//SA_RESETHAND,SA_NODEFER,SA_RESTART,SA_SIGINFO
}
-
SA_RESETHAND:当处理函数被调用时需要重置才再次有效
-
SA_NODEFER:允许递归调用信号处理函数
-
SA_RESTART:当输入被中断后需要重新输入
-
SA_SIGINFO:处理函数使用sa_sigaction。
①云班课中所给代码sigdemo1.c如下:
#include <stdio.h>
#include <signal.h>
void f(int);
int main()
{
int i;
signal( SIGINT, f );
for(i=0; i<5; i++ ){
printf("hello\n");
sleep(2);
}
return 0;
}
void f(int signum)
{
printf("OUCH!\n");
}
运行结果如下:
可知此时的中断处理函数是输出一个OUCH
②云班课中所给代码sigdemo2.c如下:
#include <stdio.h>
#include <signal.h>
main()
{
signal( SIGINT, SIG_IGN );//SIG_IGN为默认信号忽略
printf("you can't stop me!\n");
while( 1 )
{
sleep(1);
printf("haha\n");
}
}
如下图所示:
运行结果如下图所示:
③云班课中所给代码 sigactdemo.c如下:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#define INPUTLEN 100
void inthandler();
int main()
{
struct sigaction newhandler;//定义一个sigaction结构体newhandler
sigset_t blocked;
char x[INPUTLEN];
newhandler.sa_handler = inthandler;//处理中断时调用inthandler()函数。
newhandler.sa_flags = SA_RESTART|SA_NODEFER
|SA_RESETHAND; //被阻断时**重新输入**且允许递归调用处理函数,但当处理函数被调用时需要重置才再次有效
sigemptyset(&blocked);
sigaddset(&blocked, SIGQUIT);
newhandler.sa_mask = blocked;
if (sigaction(SIGINT, &newhandler, NULL) == -1)
perror("sigaction");
else
while (1) {
fgets(x, INPUTLEN, stdin);
printf("input: %s", x);
}
return 0;
}
void inthandler(int s)//中断处理时调用的函数
{
printf("Called with signal %d\n", s);//输出这个操作代表的的信号量
sleep(s * 4);
printf("done handling signal %d\n", s);
}
运行结果如下:
我们输入2,会提示input :2,而我们按下ctrl+C,会提示可见ctrl+c为signal 2。
④对sigactdemo2.c的代码进行理解、注释:
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void sig_alrm( int signo )
{
/*do nothing*/
}
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction newact, oldact;
unsigned int unslept;
newact.sa_handler = sig_alrm; //在内核注册SIGALRM信号的处理函数sig_alrm
sigemptyset( &newact.sa_mask );//初始化sa_mask所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
newact.sa_flags = 0; //当某个信号的处理函数被调用时候,当前的信号被加入到进程的信号阻塞集,如果想让其他信号也加入到信号阻塞集合就通过sa_mask来说明。
sigaction( SIGALRM, &newact, &oldact );//这里被替换的结构体是oldact,原来的结构体是newact
//通过传入newact修改了SIGALRM信号的处理动作,传入oldact读取SIGALRM原来的处理动作
//把SIGALRM对应的编号传入信号处理注册函数(sig_alrm)的参数列表中。
alarm( nsecs ); //在当前进程设定闹钟,时间一到就终止当前进程
pause();//将进程挂起,直到有信号抵达
unslept = alarm ( 0 );
sigaction( SIGALRM, &oldact, NULL );
return unslept;
}
int main( void )
{
while( 1 )
{
mysleep( 2 );
printf( "Two seconds passed\n" );
}
return 0;
}
我们运行云班课中所给的sigactdemo2.c,运行结果如下: