Unix进程小结(三)进程间通信方式总结

进程间通信(IPC)介绍

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

一、一些基本概念

进程间通信(IPC):进程之间交换数据的过程叫进程间通信。
    进程间通信的方式:
        简单的进程间通信:
            命令行:父进程通过exec函数创建子进程时可以附加一些数据。
            环境变量:父进程通过exec函数创建子进程顺便传递一张环境变量表。
            信号:父子进程之间可以根据进程号相互发送信号,进程简单通信。
            文件:一个进程向文件中写入数据,另一个进程从文件中读取出来。
            命令行、环境变量只能单身传递,信号太过于简单,文件通信不能实时。
        
        XSI通信方式:X/open 计算机制造商组织。
            共享内存、消息队列、信号量
        网络进程间通信方式:网络通信就是不同机器的进程间通信方式。
        传统的进程间通信方式:管道

二、管道
    1、管道是一种古老的通信的方式(基本上不再使用)
    2、早期的管道是一种半双工,现在大多数是全双工。
    3、有名管道(这种管道是以文件方式存在的)。
    int mkfifo(const char *pathname, mode_t mode);

例子:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>

int main()
{
	// 创建管道文件
	if(0 > mkfifo("test.txt",0644))
	{
		perror("mkfifo");
		return -1;
	}

	// 打开
	int fd = open("test.txt",O_RDWR);
	if(0 > fd)
	{
		perror("open");
		return -1;
	}

	// 准备缓冲区
	char buf[255] = {};
	// 写/读
	while(1)
	{
		printf(">");
		gets(buf);
		int ret = write(fd,buf,strlen(buf));
		printf("写入数据%d字节\n",ret);
		if('q' == buf[0])break;
		getchar();
		bzero(buf,sizeof(buf));
		ret = read(fd,buf,sizeof(buf));
		printf("读取数据%d字节,内容:%s\n",ret,buf);
		if('q' == buf[0])break;
	}
	// 关闭
	close(fd);
}


            
    管道通信的编程模式:
        进程A                进程B
        创建管道mkfifo
        打开管道open            打开管道
        写/读数据read/write    读/写数据
        关闭管道close            关闭管道
            
    4、无名管道:由内核帮助创建,只返回管道的文件描述符,看不到管道文件,但这种管道只能用在fork创建的父子进程之间。
        int pipe(int pipefd[2]);
        pipefd[0] 用来读数据
        pipefd[1] 用来写数据

以Linux中的C语言编程为例。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<strings.h>
#include<sys/types.h>
#include<signal.h>
int main()
{
	int fd[2];
	int pid;
	char buf[255]={};

	if(pipe(fd)<0)
	{
		perror("pipe");
		return -1;
	}
	if((pid=fork())<0)
	{
		perror("fork");
	}
	else if(pid>0)
	{
		
		printf("我是进程%d...",getpid());
		close(fd[0]);
		printf("请输入:\n");
		gets(buf);
		write(fd[1],buf,sizeof(buf));
		pause();		
		
	}
	else
	{
		getchar();
		close(fd[1]);
		bzero(buf,sizeof(buf));
		printf("我是子进程%d,我的父进程是%d...\n",getpid(),getppid());
		read(fd[0],buf,20);
		printf("我读到了%s\n",buf);
		kill(getppid(),2);
		
	}
}

此程序是一个简单的通过无名管道实现进程间的通信的程序!

三、共享内存
    1、由内存维护一个共享的内存区域,其它进程把自己的虚拟地址映射到这块内存,然后多个进程之间就可以共享这块内存了。
    2、这种进程间通信的好处是不需要信息复制,是进程间通信最快的一种方式。
    3、但这种通信方式会面临同步的问题,需要与其它通信方式配合,最合适的就是信号。
    
    共享内存的编程模式:
        1、进程之间要约定一个键值
        进程A        进程B    
        创建共享内存        
        加载共享内存    加载共享内存
        卸载共享内存    卸载共享内存
        销毁共享内存
    
    int shmget(key_t key, size_t size, int shmflg);
    功能:创建共享内存
    size:共享的大小,尽量是4096的位数
    shmflg:IPC_CREAT|IPC_EXCL
    返回值:IPC对象标识符(类似文件描述符)
    
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    功能:加载共享内存(进程的虚拟地址与共享的内存映射)
    shmid:shmget的返回值
    shmaddr:进程提供的虚拟地址,如果为NULL,操作系统会自动选择一块地址映射。
    shmflg:
        SHM_RDONLY:限制内存的权限为只读
        SHM_REMAP:映射已经存的共享内存。
        SHM_RND:当shmaddr为空时自动分配
        SHMLBA:shmaddr的值不能为空,否则出错
    返回值:映射后的虚拟内存地址
        
    int shmdt(const void *shmaddr);
    功能:卸载共享内存(进程的虚拟地址与共享的内存取消映射关系)
    
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    功能:控制/销毁共享内存
    cmd:
        IPC_STAT:获取共享内存的属性
        IPC_SET:设置共享内存的属性
          IPC_RMID:删除共享内存
    buf:
        记录共享内存属性的对象

例子:

扫描二维码关注公众号,回复: 2419812 查看本文章

程序A

#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<signal.h>
char* buf=NULL;

void sigint(int num)
{
	printf("\r接收到数据:%s\n",buf);
	printf(">");
	fflush(stdout);
}

int main()
{
	signal(SIGINT,sigint);

	key_t key=39242236;

	int pid=0;
	printf("我是进程:%d\n",getpid());
	printf("与我通信的进程是:");
	scanf("%d",&pid);
	getchar();

	int shmid=shmget(key,4096,IPC_CREAT|0744);
	if(0>shmid)
	{
		perror("shmget");
		return -1;
	}

	buf = shmat(shmid,NULL,SHM_RND);
	while(1)
	{
		printf("请输入要发送给进程%d的内容:\n",pid);
		gets(buf);
		kill(pid,SIGINT);
	}
	shmdt(buf);
}

程序B:

#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<signal.h>
char* buf=NULL;

void sigint(int num)
{
	printf("\r接收到数据:%s\n",buf);
	printf(">");
	fflush(stdout);
}

int main()
{
	signal(SIGINT,sigint);

	key_t key=39242236;

	int pid=0;
	printf("我是进程:%d\n",getpid());
	printf("与我通信的进程是:");
	scanf("%d",&pid);
	getchar();

	int shmid=shmget(key,4096,0);
	if(0>shmid)
	{
		perror("shmget");
		return -1;
	}

	buf = shmat(shmid,NULL,SHM_RND);
	while(1)
	{
		printf("请输入要发送给进程%d的内容:\n",pid);
		gets(buf);
		kill(pid,SIGINT);
	}
	shmdt(buf);
}

上面两个程序分别运行得到进程A和进程B

通过获取进程id用kill函数来发送信号,从而实现A和B的通信,两个程序通过共享内存通信       
四、消息队列
    1、消息队列是一个由系统内核负责存储和管理、并通过IPC对象标识符获取的数据链表。
    
    int msgget(key_t key, int msgflg);
    功能:创建或获取消息队列
    msgflg:
        创建:IPC_CREAT|IPC_EXEC
        获取:0
        
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    功能:向消息队列发送消息
    msqid:msgget的返回人值
    msgp:消息(消息类型+消息内容)的首地址
    msgsz:消息内存的长度(不包括消息类型)
    msgflg:
        MSG_NOERROR:当消息的实际长比msgsz还要长的话,
            则按照msgsz长度截取再发送,否则产生错误。
            
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
    功能:从消息队列接收消息
    msgp:存储消息的缓冲区
    msgsz:要接收的消息长度
    msgtyp:消息的的类型(它包含消息的前4个字节)
    msgflg:
        MSG_NOWAIT:如果要接收的消息不存在,直接返回。
            否则阻塞等待。
        MSG_EXCEPT:从消息队列中接收第一个不msgtyp类型的第一个消息。
        
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    功能:控制/销毁消息队列
    cmd:
        IPC_STAT:获取消息队的属性
        IPC_SET:设置消息队列的属性
        IPC_RMID:删除消息队列

例子:

A程序

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
//定义消息
typedef struct Msg
{
	long type;
	char buf[255];
}Msg;

int main()
{
	key_t key=ftok(".",1);
	int msgid=msgget(key,0777|IPC_CREAT);
	if(0>msgid)
	{
		perror("msgget");
		return -1;
	}
	while(1)
	{
		Msg msg={};
		msg.type=1;
		printf("请输入发送到消息队列中的内容:\n");
		gets(msg.buf);
		msgsnd(msgid,&msg,sizeof(msg.buf),0);
		//当发送消息首字母为q退出
		if('q'==msg.buf[0]) break;
		msgrcv(msgid,&msg,sizeof(msg.buf),2,0);
		printf("接收到:%s\n",msg.buf);
		//当接收消息首字母为q退出
		if('q'==msg.buf[0]) break;
		

	}
}

B程序

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
//定义消息
typedef struct Msg
{
	long type;
	char buf[255];
}Msg;

int main()
{
	key_t key=ftok(".",1);
	int msgid=msgget(key,0);
	if(0>msgid)
	{
		perror("msgget");
		return -1;
	}
	while(1)
	{
		Msg msg={};
		msgrcv(msgid,&msg,sizeof(msg.buf),1,0);
		printf("接收到:%s\n",msg.buf);
		//当接收消息首字母为q退出
		if('q'==msg.buf[0]) break;
		printf("请输入发送到消息队列中的内容:\n");
		gets(msg.buf);
		msg.type=2;
		msgsnd(msgid,&msg,sizeof(msg.buf),0);
		//当发送消息首字母为q退出
		if('q'==msg.buf[0]) break;
		

	}
}

进程A和B通过消息队列完成IPC

五、信号量

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

1、特点

  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

  2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

  4. 支持信号量组。

2、原型

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

复制代码

1 #include <sys/sem.h>
2 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
3 int semget(key_t key, int num_sems, int sem_flags);
4 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
5 int semop(int semid, struct sembuf semoparray[], size_t numops);  
6 // 控制信号量的相关信息
7 int semctl(int semid, int sem_num, int cmd, ...);

复制代码

semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。

semop函数中,sembuf结构的定义如下:

复制代码

1 struct sembuf 
2 {
3     short sem_num; // 信号量组中对应的序号,0~sem_nums-1
4     short sem_op;  // 信号量值在一次操作中的改变量
5     short sem_flg; // IPC_NOWAIT, SEM_UNDO
6 }

复制代码

其中 sem_op 是一次操作中的信号量的改变量:

  • sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。

  • sem_op < 0,请求 sem_op 的绝对值的资源。

    • 如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
    • 当相应的资源数不能满足请求时,这个操作与sem_flg有关。
      • sem_flg 指定IPC_NOWAIT,则semop函数出错返回EAGAIN
      • sem_flg 没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
        1. 当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
        2. 此信号量被删除,函数smeop出错返回EIDRM;
        3. 进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
  • sem_op == 0,进程阻塞直到信号量的相应值为0:

    • 当信号量已经为0,函数立即返回。
    • 如果信号量的值不为0,则依据sem_flg决定函数动作:
      • sem_flg指定IPC_NOWAIT,则出错返回EAGAIN
      • sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
        1. 信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
        2. 此信号量被删除,函数smeop出错返回EIDRM;
        3. 进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR

semctl函数中的命令有多种,这里就说两个常用的:

  • SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
  • IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

3、例子

复制代码

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<sys/sem.h>
  4 
  5 // 联合体,用于semctl初始化
  6 union semun
  7 {
  8     int              val; /*for SETVAL*/
  9     struct semid_ds *buf;
 10     unsigned short  *array;
 11 };
 12 
 13 // 初始化信号量
 14 int init_sem(int sem_id, int value)
 15 {
 16     union semun tmp;
 17     tmp.val = value;
 18     if(semctl(sem_id, 0, SETVAL, tmp) == -1)
 19     {
 20         perror("Init Semaphore Error");
 21         return -1;
 22     }
 23     return 0;
 24 }
 25 
 26 // P操作:
 27 //    若信号量值为1,获取资源并将信号量值-1 
 28 //    若信号量值为0,进程挂起等待
 29 int sem_p(int sem_id)
 30 {
 31     struct sembuf sbuf;
 32     sbuf.sem_num = 0; /*序号*/
 33     sbuf.sem_op = -1; /*P操作*/
 34     sbuf.sem_flg = SEM_UNDO;
 35 
 36     if(semop(sem_id, &sbuf, 1) == -1)
 37     {
 38         perror("P operation Error");
 39         return -1;
 40     }
 41     return 0;
 42 }
 43 
 44 // V操作:
 45 //    释放资源并将信号量值+1
 46 //    如果有进程正在挂起等待,则唤醒它们
 47 int sem_v(int sem_id)
 48 {
 49     struct sembuf sbuf;
 50     sbuf.sem_num = 0; /*序号*/
 51     sbuf.sem_op = 1;  /*V操作*/
 52     sbuf.sem_flg = SEM_UNDO;
 53 
 54     if(semop(sem_id, &sbuf, 1) == -1)
 55     {
 56         perror("V operation Error");
 57         return -1;
 58     }
 59     return 0;
 60 }
 61 
 62 // 删除信号量集
 63 int del_sem(int sem_id)
 64 {
 65     union semun tmp;
 66     if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
 67     {
 68         perror("Delete Semaphore Error");
 69         return -1;
 70     }
 71     return 0;
 72 }
 73 
 74 
 75 int main()
 76 {
 77     int sem_id;  // 信号量集ID
 78     key_t key;  
 79     pid_t pid;
 80 
 81     // 获取key值
 82     if((key = ftok(".", 'z')) < 0)
 83     {
 84         perror("ftok error");
 85         exit(1);
 86     }
 87 
 88     // 创建信号量集,其中只有一个信号量
 89     if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
 90     {
 91         perror("semget error");
 92         exit(1);
 93     }
 94 
 95     // 初始化:初值设为0资源被占用
 96     init_sem(sem_id, 0);
 97 
 98     if((pid = fork()) == -1)
 99         perror("Fork Error");
100     else if(pid == 0) /*子进程*/ 
101     {
102         sleep(2);
103         printf("Process child: pid=%d\n", getpid());
104         sem_v(sem_id);  /*释放资源*/
105     }
106     else  /*父进程*/
107     {
108         sem_p(sem_id);   /*等待资源*/
109         printf("Process father: pid=%d\n", getpid());
110         sem_v(sem_id);   /*释放资源*/
111         del_sem(sem_id); /*删除信号量集*/
112     }
113     return 0;
114 }

复制代码

上面的例子如果不加信号量,则父进程会先执行完毕。这里加了信号量让父进程等待子进程执行完以后再执行。

总结:进程间通信是实现两个程序传输数据的重要手段,非常值得学习和掌握!

猜你喜欢

转载自blog.csdn.net/Dachao0707/article/details/81226166