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,运行结果如下: