进程间通信-信号-pipe-fifo

进程间通信-信号-pipe-fifo
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原型

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

posted @ 2022-11-13 21:39  常仁  阅读(27)  评论(0编辑  收藏  举报