进线程(三)——进程间通信

一、进程间通信方式


1. 传统通信方式:

   1、无名管道  (在内核开辟的一个缓冲区(默认64KB))
   2、有名管道	(在内存中开辟一个缓冲区,且在文件系统中会生成一个管道文件)
   3、信号	 (例如 kill)

2. IPC对象

   共享内存
   消息队列
   信号灯集

3. 套接字:

   socket

二、基本术语

1. 单工:

   单向通信

2. 半双工:

   可以相互通信,不能同时进行

3. 全双工:

   可以同时相互通信

4. 原子操作:

   不可被打断的操作

三、无名管道

单工通信,并且只适用具有亲缘关系的进程间通信;具有读端和写端。

流程:创建—>读/写----->关闭

pipe():创建无名管道

int pipe(int fd[2]);
返回值:0成功,-1失败
fd[0]:固定的读端
fd[1]:固定的写端

当读端存在时:

无名管道有空间:

1. 空间充足:直接写入数据,write函数返回写入数据的字节数
2. 空间不足:write会写入能够写入的数据,不保证原子操作,如果读进程不读走管道缓冲区中的数据,那么写操作将会一直阻塞。

读端不存在:

wirte出现管道破裂。

写端存在:

有数据: read直接返回实际读取的字节数。

无数据: read阻塞等待数据写入。

写端不存在:

有数据: read直接返回实际读取的字节数。

无数据:read直接返回0。

代码示例

/*===============================================
*   文件名称:Anonymous_pipe.c
*   创 建 者: 
*   创建日期:2023年08月22日
*   描    述:
================================================*/
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#define N 64
int main(int argc, char *argv[])
{ 
    pid_t pid0 , pid1 ; //定义两个进程号类型的变量
    int fd[2];//定义一个为2大小的数组,创建管道后fd[0]为读端,fd[2]为写端
        
    char buf[N];//缓冲区

    //创建无名管道
    if( pipe(fd) < 0 )
    {
        perror("pipe");
        exit(-1);
    }
    
    pid0 = fork(); //创建子进程0
    if( -1 == pid0 )
    {
        perror("fork0");
        exit(-1);
    }
    else if( 0 == pid0 )
    {
        //子进程0 定为写端

        close(fd[0]); // 关闭读端
        
        strcpy(buf,"I'm  son0 ");//写内容到buf
        write(fd[1],buf,N);//将buf内容写到无名管道
        close(fd[1]);//写完关闭无名管道
        
        exit(0);//退出该子进程
    }
    else
    {
        //父进程

        pid1 = fork();//创建子进程1 
        if( -1 == pid1 )
        {
            perror("fork1");
            exit(-1);
        }
        else if( 0 == pid1 )
        {
            //关闭读端
            close(fd[0]);
        strcpy(buf,"I'm son 1 ");//写内容到buf
        write(fd[1],buf,N);//将buf内容写到无名管道
        close(fd[1]);//写完关闭无名管道
        
        exit(0);//退出该子进程

        }
        else
        {
            //父进程

            close(fd[1]);//关闭写端
            
            read(fd[0],buf,N);//将管道内容读到buf
            puts(buf);//输出buf中内容
    
            read(fd[0],buf,N);//将管道内容读到buf
            puts(buf);//输出buf中内容

            wait(NULL);//一个等待进程结束

            close(fd[0]);//关闭读端

        }

    }

    return 0;
} 

结果:
在这里插入图片描述

四、有名管道

无名管道只能用于具有亲缘关系的进程之间,这就限制了无名管道的使用范围。

有名管道可以使互不相关的两个进程互相通信。有名管道可以通过路径名来指出,并且在文件系统中可见。

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

有名管道遵循先进先出规则。

不支持如lseek() 操作 。

mkfifo():创建有名管道

int mkfifo(const char *filename, mode_t mode);
返回值:成功0 失败-1
const char *filename:要创建有名管道的名字
mode_t mode:管道的权限 一般为0666

代码示例
两个进程使用有名管道实现通信

进程1

/*===============================================
*   文件名称:read.c
*   创 建 者: 
*   创建日期:
*   描    述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{ 
    

    //打开有名管道
    int rfd = open("/home/rfifo",O_RDONLY);
    if( -1 == rfd )
    {
        perror("open rfd");
        return -1;
    }
    int wfd = open("/home/Wfifo",O_WRONLY);
    if( -1 == wfd )
    {
        perror("open wfd");
        return -1;
    }

    puts("open fifo successed");

    char buf[32] ; //定义缓冲区

    while(1)
    {
        read(rfd,buf,32); //读管道中内容到buf
            
        if( 0 == strcmp(buf,"quit\n"))//输入quit退出
        {
            break;
        }
        
        printf("recv: ");
        puts(buf);//打印读到的内容

        //写内容到管道
        printf("Please send:");
        //memset(buf,0,32);
        
        fgets(buf,32,stdin);

        write(wfd,buf,32);//写内容到管道
 
        if( 0 == strcmp(buf,"quit\n"))//输入quit退出
        {
            break;
        }
    }

    //关闭管道
    close(rfd);
    close(wfd);
    return 0;
} 


进程2

/*===============================================
*   文件名称:write.c
*   创 建 者:  
*   创建日期:
*   描    述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{ 
    
    //创建管道
    /*
    if( mkfifo("/home/rfifo",0666))
    {
        perror("mkfifo r");
        return -1;
    }

    if( mkfifo("/home/Wfifo",0666))
    {
        perror("mkfifo w");
        return -1;
    }
*/
    //打开有名管道
    int wfd = open("/home/rfifo",O_WRONLY);
    if( -1 == wfd )
    {
        perror("W open rfd");
        return -1;
    }
    int rfd = open("/home/Wfifo",O_RDONLY);
    if( -1 == rfd )
    {
        perror("W open wfd");
        return -1;
    }

    puts("open fifo successed");

    char buf[32] ; //定义缓冲区

    while(1)
    {
        //写内容到管道
        printf("Please send:");
        
        fgets(buf,32,stdin);

        write(wfd,buf,32);//写内容到管道
 
        if( 0 == strcmp(buf,"quit\n"))//输入quit退出
        {
            break;
        }

        read(rfd,buf,32); //读管道中内容到buf
            
        //memset(buf,0,32);

        if( 0 == strcmp(buf,"quit\n"))//输入quit退出
        {
            break;
        }
        
        printf("recv: ");
        puts(buf);//打印读到的内容

    }

    //关闭管道
    close(rfd);
    close(wfd);
    return 0;
} 

五、信号通信

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

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

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



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

忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。

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

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

异步通信: 当我们关注某一个事件时,如果事件未发生,我们继续完成自己任务,
 				         当事件发生后,它会给我们传递一个信号,此时我们再回头完成对应事件
 		信号处理方式:	  
 		捕捉:信号发生后,我们通过某些方法(函数)获取对应信号然后自定义该信号的处理方式

		忽略:信号不在执行对应的默认操作(SIGKILL/SIGSTOP除外)

		默认:

 kill指令:
		kill -n pid: 给pid进程发送 第n个信号
		kill -n -pid: 给pid进程组发送第n个信号
		kill -n -1: 给除了init进程和本进程外其它所有进程发送第n个信号
		

int kill(int pid, int sig);
返回值: 成功 0 失败 -1
int pid: 0 同组进程; -1 除init外的所有进程 ; >0 进程号==pid
int sig: 对应的信号
		
unsinged int alarm(unsinged int s);
unsinged int s: 定时秒数
unsinged int: 失败 -1; 如果之前调用alarm函数,则返回上一个闹钟剩余时间

六、IPC对象

1.每个IPC对象有唯一的ID(操作对应IPC对象的凭证)
		2.IPC对象创建后一直存在,直到被显示地删除(函数/命令)
		3.每一个IPC对象有一个关联的key(多个进程找到同一个IPC对象,事先约定好)
		ipcs/ipcrm(查看操作指令)
		key == 0 表示私有
		key:
			IPC_PRIVATE(0):表示私有
		ftok():生产key值


key_t ftok(const char *path, int proj_id);
返回值: 成功 返回key 失败返回 EOF
const char *path: 存在切可访问的文件路径
int proj_id: 用于生成key的数字,不能为0 且低八位有效

七、共享内存

效率最高的进程间通信方式,进程可以直接读写内存,而不需要任何数据拷贝

共享内存在内核空间创建,只有被映射到用户空间才可以使用

当多个进程同时访问共享内存时,需要加入同步或者互斥机制配合使用
操作步骤:
1.创建/打开共享内存

int	shmget(key_t key, int size, int shmflg);
	返回值: 成功时返回共享内存ID, 失败返回EOF
	key_t key: 和共享内存关联的key, IPC_PRIVATE/ftok生成
	int size: 共享内存的大小,以字节为单位
	int shmflg: 共享内存标志位是否新建(IPC_CREAT|0666)及操作权限

2.映射到用户空间

void *shmat(int shmid, const void *shmaddr, int shmflg);
返回值: 成功 映射后的地址, 失败 返回(void *)-1
int shmid: 对应的共享内存ID
const void *shmaddr: 要映射后的地址, NULL表示由系统自动映射
int shmflg: 0表示可读可写;SHM_RDONLY表示只读

3.读/写共享内存(通过指针访问/指针类型根据共享内存中存放的数据类型决定)

fgets()
strcpy()
...

4.撤销共享内存映射

int shmdt(void *shmaddr);
返回值: 成功返回0, 失败返回EOF
void *shmaddr: 共享内存映射的地址
注意: 一般不使用共享内存时应及时撤销映射,进程结束时自动撤销所有的共享
	内存映射,若未取消映射,则无法删除对应的共享内存

5.删除共享内存(只是标记,直到所有相关进程退出时才删除)

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回值:成功 0,失败 EOF
int shmid: 要删除的共享内存ID
int cmd: 要执行的操作 IPC_STAT IPC_SET IPC_RMID
struct shmid_ds *buf: 存放共享内存的属性(删除时为NULL)

注意:
每块共享内存有大小限制: ipcs -l (cat /proc/sys/kernel/shmmax )
多进程使用时,应由第一个进程创建,所有映射取消时才会真正的删除

八、消息队列:

​ 1.使用最普遍
​ 2.消息队列ID来唯一标识
​ 3.是一个消息的列表,用户可以添加/读取消息
​ 4.消息队列可以按照类型来发送/接收
​ 使用步骤:
​ 1.打开/创建消息队列 msgget

int masgget(key_t key, int msgflg);
			返回值: 成功对应消息队列ID, 失败返回EOF
			key_t key: 关联的KEY 
			int msgflg: 标志位 IPC_CREAT|0666
		

​ 2.向消息队列发送消息 msgsnd

int msgsnd(int msgid, const void *msgp, size_t size, int msgflg);
		返回值: 成功返回0, 失败返回 -1
		int msgid: 对应消息队列ID
		const void *msgp: 要发送消息的缓冲区地址
		size_t size: 指定本次发送消息的长度(正文长度)
		int msgflg: 标志位 0/IPC_NOWAIT (0阻塞 IPC_NOWAIT非阻塞(消息队列满时))
		注:

​ 消息格式:

1.根据需求定义结构体类型
2.首成员类型必须为long,代表消息的类型
3.其它成员都属于正文
typedef struct{
	long mtype; //类型,正整数
	... 		//成员
}MSG;

​ 3.从消息队列接收消息 msgrcv

int msgrcv(int msgid, void *msgp, size_t size, long msgtype, int msgflg);
		返回值: 成功返回消息的长度, 失败 -1
		int msgid: 消息队列的id
		void *msgp: 接收的消息存放的缓冲区
		size_t: 指定接收消息的长度(小于实际长度,剩余消息丢失;超过无影响)
		long msgtype: 指定接收消息的类型(0 按时间接收最早的消息; 负数按照优先级接收(几乎没见到))
		int msgflg: 标志位 0/IPC_NOWAIT

​ 4.控制消息队列 msgctl

int msgctl(int msgid, int cmd, struct msqid_ds *buf);
		返回值: 成功0, 失败 -1
		int msgid: 消息队列ID
		int cmd: 要执行的操作 IPC_STAT/IPC_SET/IPC_RMID
		struct msqid_ds *buf: 存放消息队列属性的地址

代码示例
​使用消息队列实现两进程数据的交互

/*===============================================
*   文件名称:clientA.c
*   创 建 者:   
*   创建日期:
*   描    述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define N 64

typedef struct
{
    long mtype;
    char data[N];
}MSG;

#define LEN (sizeof(MSG)-sizeof(long))
#define TypeA 100 
#define TypeB 200


int main(int argc, char *argv[])
{ 
    //创建关联的key 
    key_t key = ftok("./",'a');
    if( -1 == key )
    {
        perror("ftok");
        return -1;
    }

    //创建结构体
    MSG m ;

    //创建或者打开消息队列
    int msg = msgget(key,IPC_CREAT|0666);
    if( EOF ==  msg )
    {
        perror("msgget creat err");
        return -1;
    }

    while(1)//不间断发送或接收消息
    {
        //发送消息
        printf("A send:");
        fgets(m.data,N,stdin);
        m.mtype = TypeB;
        if( -1 == msgsnd(msg,&m,LEN,0))
        {
            perror("mgsnd");
            return -1;
        }
        if( 0 == strcmp(m.data,"quit\n"))
        {
            break;
        }

        //接收消息 
        if( -1 == msgrcv(msg,&m,LEN,TypeA,0))
        {
            perror("msgrcv");
            return -1;
        }
        if( 0 == strcmp(m.data,"quit\n"))
        {
            msgctl(msg,IPC_RMID,NULL);
            break;
        }
        printf("Rcve B: %s ",m.data);
    }


    return 0;
} 
/*===============================================
*   文件名称:clientB.c
*   创 建 者:    
*   创建日期:
*   描    述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define N 64

typedef struct
{
    long mtype;
    char data[N];
}MSG;

#define LEN (sizeof(MSG)-sizeof(long))
#define TypeA 100 
#define TypeB 200


int main(int argc, char *argv[])
{ 
    //创建关联的key 
    key_t key = ftok("./",'a');
    if( -1 == key )
    {
        perror("ftok");
        return -1;
    }

    //创建结构体
    MSG m ;

    //打开消息队列
    int msg = msgget(key,IPC_CREAT|0666);
    if( EOF ==  msg )
    {
        perror("msgget creat err");
        return -1;
    }

    while(1)//不间断发送或接收消息
    {

        //接收消息 
        if( -1 == msgrcv(msg,&m,LEN,TypeB,0))
        {
            perror("msgrcv");
            return -1;
        }
        if( 0 == strcmp(m.data,"quit\n"))
        {
            msgctl(msg,IPC_RMID,NULL);
            break;
        }
        printf("Rcve A: %s ",m.data);

        //发送消息
        printf("B send:");
        fgets(m.data,N,stdin);
        m.mtype = TypeA;
        if( -1 == msgsnd(msg,&m,LEN,0))
        {
            perror("mgsnd");
            return -1;
        }
        if( 0 == strcmp(m.data,"quit\n"))
        {
            break;
        }

    }
    return 0;
} 

九、信号灯集:

​ 用于进程/线程实现同步或互斥机制
​ 信号灯的类型:
​ Posix 无名信号灯(信号量)
​ Posix 有名信号灯(有对应文件)
​ System V 信号灯
​ 含义:
​ 计数信号灯(代表某类资源):无名/有名
​ System V 信号灯是一个或多个计数信号灯的集合
​ 可同时操作集合中的多个信号灯
​ 申请多个资源时避免死锁
​ 使用步骤:
​ 1.打开/创建 semget

int semget(key_t key, int nsems, int semflg);
		返回值: 成功返回信号灯集的id;失败 -1
		key_t key: 关联的key
		int nsems: 集合中包含的计数信号灯个数
		int semflg: 标志位 IPC_CREAT|0666 IPC_EXECL(如果已创建则失败)

2.信号灯初始化 semctl

int semctl(int semid, int semnum, int cmd, ...);
		返回值: 成功返回0;失败 EOF
		int semid: 要操作的信号灯集ID
		int semnum: 要操作的集合中的信号灯编号(从0开始类似数组下标)
		int cmd: 执行的操作 SETVAL IPC_RMID
		union semun: 取决于cmd (需要自己定义这个共用体)

3.信号灯集的P/V操作 semop

int semop(int semid, struct sembuf *sops, unsigned nsops);
		返回值: 成功返回0;失败-1
		int semid: 要操作的信号灯集
		struct sembuf *sops: 描述对信号灯操作的结构体(多个信号灯操作应该定义对应结构体数组)
		unsigned nsops: 要操作的信号灯的个数
		系统头文件定义好的结构体:
		struct sembuf{
			short semnum;
			short sem_op;
			short sem_flg;
		};
		semnum: 信号灯编号
		sem_op: 负数(-1):P操作; 正数(1):V操作
		sem_flg: 0/IPC_NOWAIT

sgctl

int msgctl(int msgid, int cmd, struct msqid_ds *buf);
		返回值: 成功0, 失败 -1
		int msgid: 消息队列ID
		int cmd: 要执行的操作 IPC_STAT/IPC_SET/IPC_RMID
		struct msqid_ds *buf: 存放消息队列属性的地址


​ 练习:使用消息队列实现两进程数据的交互

3、信号灯集:

​ 用于进程/线程实现同步或互斥机制
​ 信号灯的类型:
​ Posix 无名信号灯(信号量)
​ Posix 有名信号灯(有对应文件)
​ System V 信号灯
​ 含义:
​ 计数信号灯(代表某类资源):无名/有名
​ System V 信号灯是一个或多个计数信号灯的集合
​ 可同时操作集合中的多个信号灯
​ 申请多个资源时避免死锁
​ 使用步骤:
​ 1.打开/创建 semget

int semget(key_t key, int nsems, int semflg);
		返回值: 成功返回信号灯集的id;失败 -1
		key_t key: 关联的key
		int nsems: 集合中包含的计数信号灯个数
		int semflg: 标志位 IPC_CREAT|0666 IPC_EXECL(如果已创建则失败)

2.信号灯初始化 semctl

int semctl(int semid, int semnum, int cmd, ...);
		返回值: 成功返回0;失败 EOF
		int semid: 要操作的信号灯集ID
		int semnum: 要操作的集合中的信号灯编号(从0开始类似数组下标)
		int cmd: 执行的操作 SETVAL IPC_RMID
		union semun: 取决于cmd (需要自己定义这个共用体)

3.信号灯集的P/V操作 semop

int semop(int semid, struct sembuf *sops, unsigned nsops);
		返回值: 成功返回0;失败-1
		int semid: 要操作的信号灯集
		struct sembuf *sops: 描述对信号灯操作的结构体(多个信号灯操作应该定义对应结构体数组)
		unsigned nsops: 要操作的信号灯的个数
		系统头文件定义好的结构体:
		struct sembuf{
			short semnum;
			short sem_op;
			short sem_flg;
		};
		semnum: 信号灯编号
		sem_op: 负数(-1):P操作; 正数(1):V操作
		sem_flg: 0/IPC_NOWAIT

共享内存和信号灯集代码示例

/*===============================================
*   文件名称:shm_sem.c
*   创 建 者:   
*   创建日期:
*   描    述:
================================================*/
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <unistd.h>
#include <signal.h>
union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                (Linux-specific) */

};
#define N 64
#define READ 0
#define WRITE 1

//信号灯集初始化
int sem_init(int semid, int n, int val[])
{
    union semun myun ; // 定义共用体
    for(int i = 0; i < n; i++)
    {
        myun.val = val[i];//对共用体里的成员赋值
        if(-1 == semctl(semid, i, SETVAL,myun))
        {
            return -1;
        }
    }
    return 0;
}

//定义函数来进行便捷的pv操作
void pv(int semid, int n, int op)
{
    struct sembuf buf;
    buf.sem_num = n;
    buf.sem_op = op;
    buf.sem_flg = 0;
    semop(semid, &buf, 1);
}
int main(int argc, char *argv[])
{ 
    //生成key
    key_t key = ftok(".", 'a');
    if(-1 == key)
    {
        perror("ftok");
        return -1;
    }

    //创建共享内存
    int shmid = shmget(key, N, IPC_CREAT|0666);
    if(-1 == shmid)
    {
        perror("shmget");
        return -1;
    }

    //映射
    char *shmadd = shmat(shmid, NULL, 0);
    if((char *)-1 == shmadd)
    {
        perror("shmadd");
        goto _error1;
    }

    //信号灯集创建
    int semid = semget(key, 2, IPC_CREAT|0666|IPC_EXCL);
    if(-1 == semid)
    {
        perror("semget");
        goto _error1;
    }

    //初始化信号灯
    int val[2] = {0,1};   //资源数
    if(-1 == sem_init(semid, 2, val))
    {
        goto _error2;
    }

    //创建子进程
    pid_t pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        goto _error2;
    }
    else if(0 == pid)
    {
        char *p, *q;
        while(1)
        {
            p = q = shmadd;
            pv(semid, READ, -1);
            while(*p)
            {
                if(*p != ' ')
                    *q++ = *p;
                p++;
            }
            *q = '\0';
            puts(shmadd);
            pv(semid, WRITE, 1);
        }
    }
    else
    {
        while(1)
        {
            pv(semid, WRITE, -1);   //写资源P操作
            printf("input> ");
            fgets(shmadd, N, stdin);
            if(0 == strcmp(shmadd, "quit\n"))
                    break;
            pv(semid, READ, 1);     //读资源V操作
        }
        kill(pid, SIGUSR1);
    }

_error2:
    semctl(semid, 2, IPC_RMID);
_error1:
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}
    

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值