0805|IO进程线程day8 IPC机制(进程间通信机制)管道+信号

一、什么是IPC机制

概念:        

        IPC机制:Inter Process Communication,即进程间通信机制

        进程与进程间的用户空间相互独立,内核空间共享。所以如果要实现进程间的通信,需要使用进程间通信机制。

分类(3类):

  1. 传统的进程间通信机制
        无名管道  pipe
        有名管道  fifo
        信号      signal
  2. system v操作系统的IPC对象
        消息队列  message queue
        共享内存  shared memory
        信号灯集  semaphore
  3. 可用于跨主机传输的通信
        套接字  socket

二、管道

2.1 管道的原理

        在进程的3~4G内核空间中,创建一个管道(特殊的文件),管道中的数据是直接存储在内存中的。

0805|IO进程线程day8 IPC机制(进程间通信机制)管道+信号_第1张图片

2.2 管道的特性

1) 管道可以看成是一个特殊的文件一般的文件存放在磁盘上,而管道中的内容存储在内存中

2) 管道遵循先进先出的原则。(队列形式实现);

3) 管道的大小64K = 64*1024 = 65536bytes ;

4) 对于管道的读操作是一次性,如果对管道进行读操作,那么被读取的数据会从管道中删除 ;

5) 管道是一种半双工的通信方式 :

单工:只能A发消息给B,B无法发消息给A;
半双工:同一时间,只能A发消息给B,或者B发消息给A;
全双工:同一时间,可以双方通信。

6) 当管道的读写端均被关闭的时候,此时会释放管道在内核内存中的空间 ;

7) 从管道中读取数据

         · 当管道的读写端均存在
           ① 管道中没有数据的时候读函数阻塞
           ② 管道中实际拥有的个数a(10个),大于要读取的数据个数b(5个),则实际读取b(5个);
           ③ 若管道中有5个数据,读取10个,实际读5个
         · 当管道的写端不存在(父子进程的写端均关闭)
           ① 此时去读取数据,若管道中原来存在数据,则先将数据读取完毕,
           ② 若没有数据了,read函数不阻塞,且read函数立即返回0;

8) 向管道中写入数据

         · 当管道的读写端均存在
           ① 管道写满后write函数会阻塞。

        · 当管道的写端不存在(父子进程的写端均关闭)
           ① 此时只要尝试向管道中写入数据(只要调用了write函数),调用write函数的进程会收到一个管道破裂信号,该信号会导致当前进程退出
           ② 管道破裂信号:SIGPIPE

2.3 无名管道(pipe)

1) 无名管道的特点

  1. 无名管道:顾名思义即没有名字的管道文件,在用户空间不可见的管道文件
  2. 无名管道只能用于具有亲缘关系的进程间通信
  3. 由于无名管道存在于内核空间中,所以我们需要直接对内核进行操作,此时只能使用系统调用函数,即只能使用文件IO函数,例如:open read write close 。 但是不能使用lseek函数。

无名管道只能用于具有亲缘关系的进程间通信,为什么?

  1. 无名管道在文件系统中不可见,所以无亲缘关系的进程无法拿到同一根管道的读写端。
  2. 子进程会克隆父进程的文件描述符表,所以可以在父进程中创建一根管道,然后fork一份资源给子进程,此时子进程可以拿到与父进程相同的文件描述符表。即可以拿到同一根无名管道的读写端。

2) pipe

功能:创建一个无名管道,同时打开无名管道的读写端;

原型:

       #include 

       int pipe(int pipefd[2]); int* pfd;  int pfd[]

参数:

    int pipefd[2]:函数运行完毕后,该参数指向的数组中会存储两个文件描述符;
        pipefd[0]: 读端文件描述符;
        pipefd[1]: 写端文件描述符;

返回值:

        成功,返回0;

        失败,返回-1,更新errno;

2.4 有名管道(fifo)

1) 有名管道的特点

  1. 有名管道:顾名思义即有名字的管道文件,在用户空间可见的管道文件
  2. 有名管道能用于任意的进程间通信,因为在文件系统中可见
  3. 由于有名管道存在于内核空间中,所以我们需要直接对内核进行操作,此时只能使用系统调用函数,即只能使用文件IO函数,例如:open read write close 。但是不能使用lseek函数。

0805|IO进程线程day8 IPC机制(进程间通信机制)管道+信号_第2张图片

2) 创建有名管道文件

① 用shell指令

    mkfifo 有名管道名字
    mkfifo myfifo

② 用mkfifo函数创建

功能:创建一根有名管道;

原型:

       #include 
       #include 

       int mkfifo(const char *pathname, mode_t mode);

参数:

    char *pathname:指定要创建的有名管道的路径以及名字;
    mode_t mode:   有名管道的权限:0664 0777,真实的权限 (mode & ~umask)
                    the  permissions of the created file are (mode & ~umask)

返回值:

        成功,返回0;

        失败,返回-1,更新errno;

        errno == 17,文件已经存在的错误,这是一个允许存在的错误,忽略该错误

3) 操作有名管道文件

  • 操作有名管道与用文件IO操作普通文件一致.
  • open read write close函数操作有名管道即可

功能:open函数功能是打开一个文件;

原型:

       #include 
       #include 
       #include 

       int open(const char *pathname, int flags);

参数:

    int flags:
       O_RDONLY  只读
       O_WRONLY  只写
       O_RDWR    读写
    ---上述三种必须,且只能包含一种---
       O_NONBLOCK     非阻塞

1) int flags == O_RDONLY

  • open函数阻塞,此时需要另外一个进程或线程以写的方式打开同一根管道,open函数解除阻塞。

2) int flags == O_WRONLY

  • open函数阻塞,此时需要另外一个进程或线程以读的方式打开同一根管道,open函数解除阻塞。

3) int flags == O_RDWR

  • open函数不阻塞,此时管道的读写端均被打开。

        当管道的读写端均被打开的时候,此时open函数不阻塞。

4) int flags == O_RDONLY | O_NONBLOCK

  • open函数不阻塞,open函数运行成功,此时管道只有读端。

5) int flags == O_WRONLY | O_NONBLOCK

  • open函数不阻塞,open函数运行失败,此时管道的读写端均打开失败;

读端

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, const char *argv[])
{
    umask(0);

    if(mkfifo("./fifo", 0777) < 0)
    {
        //printf("errno = %d\n", errno);
        if(errno != 17)     //17 == EEXIST
        {
            perror("mkfifo");
            return -1;
        }
    }
    printf("create FIFO success\n");

    //open函数阻塞
    int fd = open("./fifo", O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }
    printf("open FIFO rdonly success  fd=%d\n", fd);

    char buf[128] = "";
    ssize_t res = 0;
    while(1)
    {
        bzero(buf, sizeof(buf));
        res = read(fd, buf, sizeof(buf));
        if(res < 0)
        {
            perror("read");
            return -1;                                  
        }
        else if(0 == res)
        {
            printf("对端关闭\n");
            break;
        }

        printf("%ld :%s\n", res, buf);
    }

    close(fd);

    return 0;
}

写端

#include                                     
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, const char *argv[])
{
    umask(0);

    if(mkfifo("./fifo", 0777) < 0)
    {   
        //printf("errno = %d\n", errno);
        if(errno != 17)     //17 == EEXIST
        {
            perror("mkfifo");
            return -1; 
        }
    }   
    printf("create FIFO success\n");

    //open函数阻塞
    int fd = open("./fifo", O_WRONLY);
    if(fd < 0)
    {   
        perror("open");
        return -1; 
    }   
    printf("open FIFO wronly success  fd=%d\n", fd);

    char buf[128] = ""; 
    while(1)
    {   
        printf("请输入>>>");
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf)-1] = 0;

        if(write(fd, buf, sizeof(buf)) < 0)
        {
            perror("write");
            return -1; 
        }
        printf("写入成功\n");
    }   

    close(fd);

    return 0;
}

练习:(半双工)打开两个终端,要求实现AB进程对话【两根管道】

 打开两个终端,要求实现AB进程对话

  1. A进程先发送一句话给B进程,B进程接收后打印
  2. B进程再回复一句话给A进程,A进程接收后打印
  3. 重复1.2步骤,当收到quit后,要结束AB进程
  • 提示:两根管道

A终端代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 

/***************************A终端***************************/

int main(int argc, const char *argv[])
{
	//创建管道1    A终端写入--->管道1--->B终端读取
	if(mkfifo("./fifo",0664) < 0)
	{
		if(errno != 17)//如果错误是已有管道,则跳过,可正常运行
		{
			perror("mkfifo");
			return -1;
		}
	}
	printf("mkfifo pipe1 success __%d__\n",__LINE__);

	//创建管道2    B终端写入--->管道2--->A终端读取
	if(mkfifo("./myfifo",0664) < 0)
	{
		if(errno != 17)
		{
			perror("mkfifo");
			return -1;
		}
	}
	printf("mkfifo pipe2 success __%d__\n",__LINE__);

	//以写的方式打开管道1
	int fd_w=open("./fifo",O_WRONLY);
	if(fd_w < 0)
	{
		perror("open");
		return -1;
	}
	printf("open pipeA success __%d__\n",__LINE__);
	//以读的方式打开管道B
	int fd_r=open("./myfifo",O_RDONLY);
	if(fd_r < 0)
	{
		perror("open");
		return -1;
	}
	printf("open pipeB success __%d__\n",__LINE__);


	char buf[128]="";
	ssize_t res = 0;
	int c=-1;
	while(1)
	{
		//管道1操作(写入数据)
		printf("请输入要对B说的话>>> ");
		fgets(buf,sizeof(buf),stdin);    //从终端获取数据

		buf[strlen(buf)-1] = '\0';   //将\n改成\0
		if((write(fd_w,buf,sizeof(buf))) < 0)  //将字符串写进管道A  
		{
			perror("write");
			return -1;
		}
		//当管道1的读段关闭,管道1的写段尝试写入数据,则管道破裂,退出进程



		//管道2操作(读取数据)
		bzero(buf,sizeof(buf));//清空字符串
		res=read(fd_r,buf,sizeof(buf));//读取B管道中的数据
		
		c=strcmp(buf,"quit");//将读到的数据与quit比较
		if(0 == c)//如果相同,c为0,达到退出条件,可以退出循环
		{
			break;
		}
		//printf("写入数据成功 res=%ld\n",res);

		if(res < 0)//read函数执行失败,返回负数
		{
			perror("read");
			return -1;
		}   

		if(0 == res)//read执行成功,但读到了0个数据`
		{
			printf("对方进程退出\n");
			break;
		}
		//打印从管道2中读取到的数据
		printf("B:%s\n",buf);
	}
	
	//关闭管道1、管道2
	close(fd_r);
	close(fd_w);
	return 0;
}

B终端代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 

/***************************B终端***************************/  

int main(int argc, const char *argv[])
{
	//创建管道1   A终端写入--->管道1--->B终端读取
	if(mkfifo("./fifo",0664) < 0)
	{
		if(errno != 17)
		{
			perror("mkfifo");
			return -1;
		}
	}
	printf("mkfifo pipe1 success __%d__\n",__LINE__);

	//创建管道2   B终端写入--->管道2--->A终端读取
	if(mkfifo("./myfifo",0664) < 0)
	{
		if(errno != 17)
		{
			perror("mkfifo");
			return -1;
		}
	}
	printf("mkfifo pipe2 success __%d__\n",__LINE__);
	
	//以读的方式打开管道1
	int fd_r=open("./fifo",O_RDONLY);
	if(fd_r < 0)
	{
		perror("open");
		return -1;
	}
	printf("open pipe1 success __%d__\n",__LINE__);
	//以写的方式打开管道2
	int fd_w=open("./myfifo",O_WRONLY);
	if(fd_w < 0)
	{
		perror("open");
		return -1;
	}
	printf("open pipe2 success __%d__\n",__LINE__);

	char buf[128]="";
	ssize_t res = 0;
	int c=-1;
	while(1)
	{
		//管道1操作(读取数据)
		bzero(buf,sizeof(buf));
		res=read(fd_r,buf,sizeof(buf));

		c=strcmp(buf,"quit");//判断B终端输入的是否是quit
		if(0 == c)
		{
			break;//是quit则退出进程
		}
		if(res < 0)
		{
			perror("read");
			return -1;
		}
		if(0 == res )
		{
			printf("对方进程退出\n");
			break;
		}
		printf("A:%s\n",buf);

		//管道2操作(写入数据)
		printf("请输入>>> ");
		fgets(buf,sizeof(buf),stdin);
		buf[strlen(buf)-1] = '\0';
		if((write(fd_w,buf,sizeof(buf))) < 0)
		{
			perror("write");
			return -1;
		}
		//当管道2关闭,管道2的写段尝试写入数据,则管道破裂,退出进程
	}

	close(fd_r);
	close(fd_w);
	return 0;
}

A终端结果

ubuntu@ubuntu:02_fifo$ gcc 03_pipe_w.c -o w
ubuntu@ubuntu:02_fifo$ ./w
mkfifo pipe1 success __22__
mkfifo pipe2 success __33__
open pipeA success __42__
open pipeB success __50__
请输入要对B说的话>>> 你好,我是A
B:你好呀,我是B
请输入要对B说的话>>> 你吃饭了吗?
B:吃了,你呢
请输入要对B说的话>>> 不告诉你
ubuntu@ubuntu:02_fifo$ 

B终端结果

ubuntu@ubuntu:02_fifo$ gcc 02_pipe_r.c -o r
ubuntu@ubuntu:02_fifo$ ./r
mkfifo pipe1 success __22__
mkfifo pipe2 success __33__
open pipe1 success __42__
open pipe2 success __50__
A:你好,我是A
请输入>>> 你好呀,我是B
A:你吃饭了吗?
请输入>>> 吃了,你呢
A:不告诉你
请输入>>> quit
对方进程退出
ubuntu@ubuntu:02_fifo$ 

fd741777d8ed4957bfa812ba1a3af75e.png

附加题:(全双工)A能随时发信息给B,B能随时接收A发送的数据,反之亦然。

三、信号

3.1 信号的概念

1) 原理

① 信号是软件层次上,对中断的一种模拟

  • 中断:暂停当前的任务,先去执行其他任务

②信号是一种异步通信方式,AB进程各自运行各自的

  • 同步:多个任务之间有先后关系,必须把任务A结束后,才能执行任务B;
  • 异步:多个任务之间没有关系,各自运行各自的任务,通过CPU调度各个任务。

0805|IO进程线程day8 IPC机制(进程间通信机制)管道+信号_第3张图片

2) 进程对信号的处理方式【重点】 

① 执行默认操作\执行缺省操作

  • 每个信号都有自己默认的处理函数,当信号发生的时候,执行信号对应的默认处理函数。

② 忽略信号

  • 对信号不做处理,但是有两个信号不能忽略 9) SIGKILL 19) SIGSTOP

③ 捕获信号

  • 修改信号的默认处理函数为自定义处理函数,当信号发生的时候,执行自定义处理函数。
  • 9) SIGKILL 19) SIGSTOP,无法捕获。

3)常见的信号

kill     -l     查看所有信号     62个信号,其中32 33没有

 1) SIGHUP             2) SIGINT             3) SIGQUIT            4) SIGILL                 5) SIGTRAP
 6) SIGABRT            7) SIGBUS             8) SIGFPE             9) SIGKILL                10) SIGUSR1
11) SIGSEGV            12) SIGUSR2           13) SIGPIPE           14) SIGALRM                15) SIGTERM
16) SIGSTKFLT          17) SIGCHLD           18) SIGCONT           19) SIGSTOP                20) SIGTSTP
21) SIGTTIN            22) SIGTTOU           23) SIGURG            24) SIGXCPU                25) SIGXFSZ
26) SIGVTALRM          27) SIGPROF           28) SIGWINCH          29) SIGIO                30) SIGPWR
31) SIGSYS             34) SIGRTMIN          35) SIGRTMIN+1        36) SIGRTMIN+2            37) SIGRTMIN+3
38) SIGRTMIN+4         39) SIGRTMIN+5        40) SIGRTMIN+6        41) SIGRTMIN+7            42) SIGRTMIN+8
43) SIGRTMIN+9         44) SIGRTMIN+10       45) SIGRTMIN+11       46) SIGRTMIN+12            47) SIGRTMIN+13
48) SIGRTMIN+14        49) SIGRTMIN+15       50) SIGRTMAX-14       51) SIGRTMAX-13            52) SIGRTMAX-12
53) SIGRTMAX-11        54) SIGRTMAX-10       55) SIGRTMAX-9        56) SIGRTMAX-8            57) SIGRTMAX-7
58) SIGRTMAX-6         59) SIGRTMAX-5        60) SIGRTMAX-4        61) SIGRTMAX-3            62) SIGRTMAX-2
63) SIGRTMAX-1         64) SIGRTMAX    

① 硬件能按出来的信号

  • 2) SIGINT       默认处理函数:退出进程 ctrl + c
  • 3) SIGQUIT    默认处理函数:退出进程 ctrl + \
  • 20) SIGTSTP  默认处理函数:挂起进程 ctrl + z 此时进程没有结束

② 无法被忽略、捕获的信号

  • 9) SIGKILL      默认处理函数:退出进程 kill -9 pid
  • 19) SIGSTOP  默认处理函数:退出进程

③ 常见的信号

  • 11) SIGSEGV  段错误信息,默认处理函数:退出进程
  • 13) SIGPIPE   管道破裂信号,默认处理函数:退出进程
  • 14) SIGALRM 时钟信号,默认处理函数:退出进程
  • 17) SIGCHLD 当子进程退出后,父进程会收到17号信号。默认处理函数不会导致进程退出。

3.2 信号的相关函数

1) signal

功能:捕获信号,为信号注册新的处理函数;

原型:

       #include 

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);     //void (*handler)(int);

参数:

    int signum:指定要捕获的信号对应的编号。可以填编号,也可以填对应的宏。2) SIGINT
    sighandler_t handler:函数指针,回调函数;
        1) SIG_IGN:忽略信号;     9) 19)号信号无法忽略;
        2) SIG_DFL:执行默认操作;
        3) 传入一个函数的首地址,代表捕获信号,且该函数的返回值必须是void类型,参数列表必须是int类型,
        例如:void handler(int sig)
        {    
                                
        }
typedef void (*sighandler_t)(int);     typedef void (*)(int)   sighandler_t; 
 
 typedef 旧的类型名  新的类型名;
 typedef int uint32_t;                     int a ----> uint32_t a;
 typedef int*    pint;                     int* pa ---> pint pa;
 typedef void (*)(int)   sighandler_t;     void (*ptr)(int) ----> sighandler_t ptr

返回值:

        成功,返回该信号的上一个信号处理函数的首地址; 默认处理函数的首地址获取不到,返回NULL;

        失败,返回SIG_ERR ((__sighandler_t)-1),更新errno;

2)用信号的方式回收僵尸进程【重点】

你可能感兴趣的:(#,IPC,IO进程线程,linux,c语言,c#)