进程间的通信

为什么进程间要通信?要完成什么工作?

数据交互、资源共享,通知,控制

Linux/Unix平台通信方式的发展

1早期通信方式

2AT&T的贝尔实验室,对Unix早期的进程间通信进行了改进和扩充,形成了“system V IPC”,其通信进程主要局限在单个计算机内。

3BSD(加州大学伯克利分校的伯克利软件发布中心),跳过了只能在同一计算机通信的限制,形成了基于套接字(socket)的进程间通信机制。

进程间通信方式

1)早期的进程间通信:基本通信方式有三种:

无名管道(pipe)、有名管道(fifo)、信号(sem)

2)基于IPC对象有三种:systerm V IPC对象:IPC:InterProcess Communication

共享内存(share memory)、消息队列(message queue)、信号灯集(semaphore)

3)基于BSD

套接字(socket)

  • 无名管道

1、原理图

进程间的通信_第1张图片

进程间的通信_第2张图片

2、创建无名管道 pipe

#include 
int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端  fd[1]:写端
返回值:成功 0
		失败 -1

3.特点及验证:

1)只能用于具有亲缘关系的进程(父子进程)之间的通信

2)单工的管道,能实现半双工通信,具有固定的读端和写端

3)管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。

4)管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符fd[0]和 fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。   

5)无名管道的操作属于一次性操作,如果对无名管道进行读操作,数据会被全部读走

6) 无名管道的大小是固定的,管道一旦满,写操作就会导致进程阻塞,管道的大小是64K。 只有读出大于等于4K之后,写操作解除阻塞。

进程间的通信_第3张图片

7)当管道中无数据时,执行读操作,读操作阻塞。

进程间的通信_第4张图片

8)无名管道不保证操作的原子性,如果当前管道,满足读写条件,读写可同时进行。   

管道所传送的数据不是无格式的,这要求管道的读出方和写入方必须约定好数据的格式, 如多少字节算一个消息

进程间的通信_第5张图片

9)向无名管道中写去数据,将读端关闭,管道破裂,进程收到信号(SIGPIPE),默认这个信 号会将进程杀死 。

进程间的通信_第6张图片

10)当管道中有数据,将写端关闭,读操作可以执行,之后数据读完,可以继续读取(非阻塞 ),直接返回0。

11)无需open,但需手动close。

12)不支持如lseek() 操作。队列形式,先进先出,属于一次性操作。

 练习:结合无名管道,实现一个进程读源文件写入管道,另一个进程从管道中

读出内容写入复制的文件中。实现cp file1 file2

进程间的通信_第7张图片

练习:结合无名管道,实现一个进程读源文件写入管道,另一个进程从管道中

读出内容写入复制的文件中。实现cp file1 file2

进程间的通信_第8张图片

二、有名管道:

  1.特点:

1)有名管道可以使互不相关的两个进程互相通信。

2)有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。

3)进程通过文件IO来操作有名管道

4)有名管道以队列的方式先进先出

5)一次性操作,不支持lseek() 

  2.相关函数:

  1)通过命令创建 mkfifo my_fifo

命令:$ mkfifo  filename  -m  mode  

mkfifo  myfifo   -m  0666(创建一个命名管道myfifo,其权限为0666)

进程间的通信_第9张图片

       创建管道文件成功后,只是对应在磁盘中有这个文件了,并没有对应的内存,只有在一个进程用open打开这个文件后,这个管道缓冲区(数组)才会开辟存在。close关闭后其缓冲区释放不存在。

2)创建有名管道mkfifo

#include 
#include 
int mkfifo(const char *pathname, mode_t mode);
	功能:创建有名管道
	参数:filename:有名管道文件名路径
		  mode:权限
	返回值:成功:0	
			失败:-1,并设置errno号	
			注意对错误的处理方式:
			如果错误是file exist时,注意加判断,如:
			if(errno == EEXIST)
				....			

3.注意:

以O_WRONLY打开管道,写阻塞

进程间的通信_第10张图片

以O_RDONLY打开管道,读阻塞

进程间的通信_第11张图片

以O_RDWR打开管道,管道中没有内容,读阻塞

进程间的通信_第12张图片

文件已存在会报错解决文件存在报错

进程间的通信_第13张图片

解决文件存在报错

进程间的通信_第14张图片

 练习:两个没有亲缘关系的进程实现全双工通信,当其中一个进程输入quit的时候退出。

进程间的通信_第15张图片

练习:两个没有亲缘关系的进程实现全双工通信,当其中一个进程输入quit的时候退出。

//A进程代码
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int fd_r, fd_w;

void *send_thread(void *arg)
{
    char buf[128];
    while (1)
    {
        fgets(buf, sizeof(buf), stdin);
        write(fd_w, buf, sizeof(buf));
        if (strncmp(buf, "quit", 4) == 0)
          exit(0);
    }
}

int main(int argc, char const *argv[])
{
    //创建两个有名管道
    if (mkfifo("./fifo1", 0777) < 0)
    {
        if (errno == EEXIST)
            printf("fifo1 eexist.\n");
        else
        {
            perror("fifo1 err.");
            return -1;
        }
    }
    if (mkfifo("./fifo2", 0777) < 0)
    {
        if (errno == EEXIST)
            printf("fifo2 eexist.\n");
        else
        {
            perror("fifo2 err.");
            return -1;
        }
    }
    //打开管道使用
    fd_r = open("./fifo1", O_RDWR);
    if (fd_r < 0)
    {
        perror("open fifo1 err.");
        return -1;
    }
    fd_w = open("./fifo2", O_RDWR);
    if (fd_w < 0)
    {
        perror("open fifo2 err.");
        return -1;
    }

    pthread_t tid;
    pthread_create(&tid, NULL, send_thread, NULL);
    //循环发送消息或读取消息
    char buf[128];
    while (1)
    {
        read(fd_r, buf, sizeof(buf));
        printf("recv:%s\n", buf);
        if (strncmp(buf, "quit", 4) == 0)
            exit(0);
    }
    
    close(fd_r);
    close(fd_w);
    pthread_join(tid, NULL);
    return 0;
}

//B进程代码
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
int fd_r, fd_w;

void *send_thread(void *arg)
{
    char buf[128];
    while (1)
    {
        fgets(buf, sizeof(buf), stdin);
        write(fd_w, buf, sizeof(buf));
        if (strncmp(buf, "quit", 4) == 0)
            exit(0);
    }
}
int main(int argc, char const *argv[])
{
    //创建两个有名管道
    if (mkfifo("./fifo1", 0777) < 0)
    {
        if (errno == EEXIST)
            printf("fifo1 eexist.\n");
        else
        {
            perror("fifo1 err.");
            return -1;
        }
    }
    if (mkfifo("./fifo2", 0777) < 0)
    {
        if (errno == EEXIST)
            printf("fifo2 eexist.\n");
        else
        {
            perror("fifo2 err.");
            return -1;
        }
    }
    //打开管道使用
    fd_w = open("./fifo1", O_RDWR);
    if (fd_w < 0)
    {
        perror("open fifo1 err.");
        return -1;
    }
    fd_r = open("./fifo2", O_RDWR);
    if (fd_r < 0)
    {
        perror("open fifo2 err.");
        return -1;
    }
      pthread_t tid;
    pthread_create(&tid, NULL, send_thread, NULL);
    //循环发送消息或读取消息
    char buf[128];
    while (1)
    {
        read(fd_r, buf, sizeof(buf));
        printf("recv:%s\n", buf);
        if (strncmp(buf, "quit", 4) == 0)
            exit(0);
    }
    close(fd_r);
    close(fd_w);
    pthread_join(tid,NULL);
    return 0;
}

无名管道与有名管道对比

无名管道

有名管道

使用场景

具有亲缘关系的进程间

不相关两个进程可以使用

特点

1)半双工的通信方式

2)看做一种特殊文件,通过文件IO操作

3)固定读端fd[0]和写端fd[1]

1)在文件系统中会存在管道文件,数据存放在内核空间

2)通过文件IO进行操作

3)半双工的通信方式

4)不支持lseek操作,遵循先进先出

函数

pipe()

直接read、write

mkfifo()

先打开open,再读写read/write

读写特性

1)当管道写满数据时,写阻塞

2)当管道中没有数据时,读阻塞;当管道中没有数据时,写端关闭,读立即返回

3)读端关闭,往管道写数据导致管道破裂

1)只读方式,读阻塞,一直到另一个进程把写打开

2)只写方式,写阻塞,一直到另一个进程把读打开

3)可读可写,如果管道中没有数据,读阻塞;

  • 信号:

kill -l:查看linux信号

kill -num pid:给指定pid进程发送num信号

  1.概念:

1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式

中断:当我正在干一件事的时候,突然被另一件打断,去执行另一件事,执行完了另一件事再回来执行本来的。

异步:同时执行,不按照顺序

同步:按照顺序执行

2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。

3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

  2.用户进程对信号的响应方式:

1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:

即SIGKILL及SIGSTOP。

2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。

3)执行缺省操作:Linux对每种信号都规定了默认操作 

  3.信号:kill -l

2)SIGINT:结束进程,对应快捷方式ctrl+c

3)SIGQUIT:退出的信号,对应快捷方式ctrl+\

9)SIGKILL:结束进程,不能被忽略不能被捕捉

14)SIGALRM:闹钟信号,alarm函数设置定时,当到设定的时间时,内核会向进程发送此信号结束进程。

15)SIGTERM:结束终端进程,kill 使用时不加数字默认是此信号

17)SIGCHLD:子进程状态改变时给父进程发的信号

19)SIGSTOP:暂停进程,不能被忽略不能被捕捉

20)SIGTSTP:暂停信号,对应快捷方式ctrl+z

SIGUSR1、SIGUSR2-->未定义默认功能的信号

4.相关函数:

1)kill 信号发送

#include 
#include 
int kill(pid_t pid, int sig);
	功能:信号发送
	参数:pid:指定进程
		  sig:要发送的信号
	返回值:成功 0
			失败 -1

2)raise 进程向自己发送信号

#include 
int raise(int sig);
	功能:进程向自己发送信号
	参数:sig:信号
	返回值:成功 0
			失败 -1

3)alarm 在进程中设置一个定时器

#include 
unsigned int alarm(unsigned int seconds);
功能:在进程中设置一个定时器
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则
	返回上一个闹钟时间的剩余时间,否则返回0。
		注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替,时间到了会给进程发送一个SIGALRM信号,这个信号也有结束进程的功能

4) pause 用于将调用进程挂起直到收到信号为止。

#include 
int pause(void);
	功能:用于将调用进程挂起直到收到信号为止。

5)signal信号处理函数

#include 
void (*signal(int signum, void (*handler)(int)))(int);
	   或者:   
typedef void (*sighandler_t)(int);	    
sighandler_t signal(int signum, sighandler_t handler)	 
	功能:信号处理函数(注册信号)
	参数:signum:要处理的信号
		  handler:SIG_IGN:忽略该信号。
				   SIG_DFL:采用系统默认方式处理信号。
				   自定义的信号处理函数指针
	返回值:成功:设置之前的信号处理方式
			失败:SIG_ERR

进程间的通信_第16张图片

#include 
#include 
#include 

//信号处理函数
void handler(int sig)
{
    if (sig == SIGINT)
        printf("ctrl c\n");
    else if (sig == SIGTSTP)
        printf("crtl z\n");
}

int main(int argc, char const *argv[])
{
    //1.忽略信号
    //signal(SIGINT, SIG_IGN); //信号是异步
    //2.执行缺省操作
    //signal(2,SIG_DFL);
    //3.捕捉信号
    signal(SIGINT, handler);
    signal(SIGTSTP, handler);
    printf("ok\n");
    pause(); //捕捉到信号为止
    // while (1)
    //     ;
    printf("hello\n");
    return 0;
}

5.练习

用信号的知识实现司机和售票员问题。

1)售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(let's gogogo)

2)售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)

3)司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)

4)司机等待售票员下车,之后司机再下车

#include 
#include 
#include 
#include 
#include 
#include 
pid_t pid;
void conductor(int sig)//信号处理函数
{
    if (sig == SIGINT)
    {
        kill(getppid(), SIGUSR1);
    }
    else if (sig == SIGQUIT)
    {
        kill(getppid(), SIGUSR2);
    }
    else if (sig == SIGUSR1)
    {
        printf("please get off the bus\n");
        exit(0);
    }
}
void driver(int sig)//信号处理函数
{
    if (sig == SIGUSR1)
    {
        printf("let`sgogogo\n");
    }
    else if (sig == SIGUSR2)
    {
        printf("stop the bus\n");
    }
    else if (sig == SIGTSTP)
    {
        kill(pid, SIGUSR1);
    }
}
int main(int argc, char const *argv[])
{
    pid = fork(); //创建子进程
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0) //conductor
    {
        signal(SIGINT, conductor);//信号处理
        signal(SIGQUIT, conductor);
        signal(SIGUSR1, conductor);
        signal(SIGTSTP, SIG_IGN);//忽略
        while (1)//挂起等信号来
        {
            pause();
        }
    }
    else //driver
    {
        signal(SIGUSR1, driver);
        signal(SIGUSR2, driver);
        signal(SIGTSTP, driver);
        signal(SIGINT, SIG_IGN);//忽略
        signal(SIGQUIT, SIG_IGN);//忽略
        wait(NULL);//阻塞等待
    }
    return 0;
}

四、共享内存

  1. 特点

1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝

2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间

3)进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。

4)由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等

进程间的通信_第17张图片

通信原理共享内存实际是操作系统在实际物理内存中开辟的一段内存。

        共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。

进程间的通信_第18张图片

2.共享内存的使用步骤:

0)创建key值

key_t key = ftok(路径名,一个字符)

//key值打印出来:0x610191c8   01是系统编号

//key("./app",'a')---a->0x61   app->1479112=0x191c8

1)创建或打开共享内存

int shmid = shmget(key, size, IPC_CREAT|IPC_EXCL|0666);

2)映射共享内存到用户空间

地址 = shmat(shmid, NULL, 0);

3)撤销映射

shmdt(地址);

4)删除共享内存

shmctl(shmid, IPC_RMID, NULL);

3.相关函数:

1)ftok 产生一个独一无二的key值

#include 
#include 
key_t ftok(const char *pathname, int proj_id);
	功能:产生一个独一无二的key值
	参数:
		Pathname:已经存在的可访问文件的名字
		Proj_id:一个字符(因为只用低8位)
	返回值:成功:key值
			失败:-1

key值组成:

61是字符a的16进制ascii的值

01系统编号

509b是传参文件inode号转化为十六进制的后四位

进程间的通信_第19张图片

进程间的通信_第20张图片

ftok中的参数可以随便填写,但是要符合格式,ftok只是利用参数,再运用一套算法,算出一个唯一的key值返回这个key值可以传给共享内存参数

2)shmget 创建或打开共享内存

#include 
#include 
int shmget(key_t key, size_t size, int shmflg);
	功能:创建或打开共享内存
    参数:
		key  键值
        size   共享内存的大小 
        shmflg   IPC_CREAT|IPC_EXCL|0777
	返回值:成功   shmid
            出错    -1

3)shmat 映射共享内存

#include 
#include 
void *shmat(int shmid, const void *shmaddr, int shmflg);
	功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
    参数:
		shmid   共享内存的id号
        shmaddr   一般为NULL,表示由系统自动完成映射
                  如果不为NULL,那么有用户指定
        shmflg:SHM_RDONLY就是对该共享内存只进行读操作shm_rdonly
                0     可读可写		
	返回值:成功:完成映射后的地址,
			出错:-1地址
		用法:if((p = (char *)shmat(shmid,NULL,0)) == (char *)-1)

4)shmdt 取消映射

#include 
#include 
int shmdt(const void *shmaddr);
	 功能:取消映射
	 参数:要取消的地址
	 返回值:成功0  
			失败的-1

5)shmctl 删除共享内存

#include 
#include 
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
	功能:(删除共享内存),对共享内存进行各种操作
	参数:
		shmid   共享内存的id号
      	cmd       IPC_STAT 获得shmid属性信息,存放在第三参数
             	   IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
                IPC_RMID:删除共享内存,此时第三个参数为NULL即可	
	返回:  成功0 
			失败-1
		用法:shmctl(shmid,IPC_RMID,NULL);

进程间的通信_第21张图片

#include 
#include 
#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    //获取key
    key_t key = ftok("a.txt", 'A');
    if (key < 0)
    {
        perror("key err");
        return -1;
    }
    printf("key = %#x\n", key);
    //创建或打开共享内存
    int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0777);
    if (shmid < 0)
    {
        if (errno == EEXIST)
        {
            printf("shmget eexist\n");
            shmid = shmget(key, 128, 0777); //已创建则打开使用
        }
        else
        {
            perror("shmget err");
            return -1;
        }
    }
    printf("shmid = %d\n", shmid);
    //3.建立映射关系
    char *sp = (char *)shmat(shmid, NULL, 0666);
    if (sp == (void *)-1)
    {
        perror("shmat err.");
        return -1;
    }
    //使用
    strcpy(sp, "hello world");
    printf("sp = %s\n", sp);
    //取消映射
    shmdt(sp);
    return 0;
}

4.命令

ipcs -m: 查看系统中的共享内存

ipcrm -m shmid:删除共享内存

ipcrm -M key:删除共享内存

你可能感兴趣的:(IO,IO,c语言,linux,1024程序员节)