进程间通信(上)基础函数篇

本文深入讲解了进程间通信(IPC)的各种方法,包括匿名管道、命名管道、消息队列、共享内存和信号量的原理与代码实现。通过具体示例,展示了如何在不同进程间高效传递数据和同步操作。

一、匿名管道

从一个进程连接到另一个进程的一个数据流称为一个管道

#include <unistd.h>

功能:创建一个无名管道

int  pipe(int  fd[2])

参数:

fd:文件描述符数组,其中fd[0]表示读端fd[1]表示写端

返回值:成功返回0,失败返回-1

其内部操作如下图所示

 

管道读写规则:

1.若read端文件描述符被关了,则write端会产生SIGPIPE(13号)信号,进程退出

2.当写入数据大小不大于PIPE_BUF时,linux将保证写入的原子性

3.当写入数据大小大于PIPE_BUF时,linux将不保证写入的原子性

管道特点:

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

2.内核会对管道操作进行互斥同步处理

3.进程退出,管道释放(其生命周期随进程

4.管道是单向的,数据只能朝一个方向流动

代码思想:

1.用父进程先创建一个管道(调用pipe()函数)

2.fork()一个子进程,则子进程的文件描述符继承父进程的

3.子进程关闭write端,close(fds[1])

   父进程关闭read端,close(fds[0])

4.父进程写,子进程读,就可以进行通信了

 代码:

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
int main()  
{           
     int fds[2];
     if(pipe(fds)<0){         //匿名管道,
         perror("pipe:");
         exit(1);
     }       
     //  printf("%d %d\n",fds[0],fds[1]);    //3和4,相当于用两种方式打开了1个文件
     pid_t id=fork();         //创建子进程
     if(id == 0){
         //child  "w"
         close(fds[0]);              //关闭读的方式打开的文件,引用计数-1 ,(硬链接)
         printf("Please talk: \n");
         const char* msg="i have a dream!\n";  
         int i=5;
         while(i--){
              write(fds[1],msg,strlen(msg));       
              sleep(1);
         }   
         close(fds[1]);           //关闭pipe文件
         exit(0);
     }       
     else if(id > 0){
         //parent  "r"
         close(fds[1]);                //关闭写的方式打开的文件,引用计数-1 ,(硬链接)    
         char buf[1024];
         while(1){
              ssize_t s=read(fds[0],buf,sizeof(buf)-1);  //从缓冲区读文件,留一个字节放EOF
              if(s>0){
                  buf[s-1]=0;           //把换行符一覆盖     
                  printf("%s\n",buf);
              }
              else{
                  break;
              }
          }
          waitpid(id,NULL,0);         //等待子进程退出
      }   
      else{
          perror("fork:");
          exit(1);
      }   
      return 0;
  }                                                                                                                        

 二、命名管道

    顾名思义,这是一个显示的特殊类型的文件,它是有名字的

创建方法:

(1)在命令行上创建   -->    $ mkfifo  filename

(2)调用函数     int  mkfifo(const  char*  filename,mode_t  mode)

         参数:filename  文件名

         mode  权限

与匿名管道之间的区别:

1.匿名管道由pipe创建并打开

2.命名管道由mkfifo函数创建,打开用open

3.匿名管道一般用于父子进程之间通信,而命名管道可用于任意两个进程之间通信

 

代码操作思想:

1.先创建一个管道文件

2.一个进程以读的方式打开管道文件,一个进程以写的方式打开管道文件

3.就可以通信了

三、消息队列

基本概念:

1.从一个进程向另一个发送消息块的方法

2.每个消息块都有一个类型(标签)

3.但是每个消息的最大长度有限(MSGMAX),每个消息队列的字节总数(MSGMNB)是有限的,系统上消息队列总数是有上限的(MSGMNI)

思想:两个进程根据 type 去接收自己想接受的消息块

操作函数:

1.msgget()函数

功能:

        用来创建和访问一个消息队列

原型:

        int  msgget(key_t  key , int  msgflg);

参数:

        key:某个消息队列的名字

        msgflg:权限,由9个权限标志组成      

返回值:

        成功返回该消息队列的标识码,失败返回1;

mode权限:0644 | IPC_CREAT | IPC_EXCL    队列存在则返回错误,不存在则创建,其文件的权限为644

                    IPC_CREAT        队列存在,则访问;不存在则创建

key一般是 ftok() 函数的返回值          key_t  _key=ftok(PATHNAME ,  PROJ_ID);   参数是宏

2.msgctl()函数

功能:

        用来控制消息队列

原型:

        int  msgctl (int  msqid,  int  cmd,  struct  msqid_ds  *buf);

参数:

        msqid:  由msgget函数返回的消息队列标识码

        cmd:  是将要采取的动作     IPC_RMID  删除消息队列

        *buf:  缺省为NULL

返回值:

        成功返回0,失败返回-1

3.msgsnd()函数

功能:

        把一条消息添加到消息队列

原型:

        int  msgsnd (int  msqid, const  void*  msgp,  size_t  msgsz,  int  msgflg);

参数:

        msgid:  消息队列标识码

        msgp:  是一个指针,指针指向准备发送的消息

        msgsz:  是msgp指向的消息长度,这个长度不含保存消息类型的那个long  int长整型

        msgflg:  控制着当前消息队列满或达到上限时将要发生的事情  =IPC_NOWAIT表示队列满不等待,返回EAGAIN错误

返回值:

        成功返回0,失败返回-1;

消息结构:

        必须小于系统规定的上限值,必须以一个long  int开始,就是这个消息的标签

    struct  msgbuf{

                long  mtype;     消息类型

                char  mtext[1];    消息内容,大小自己设置

    }

4.msgrcv()函数

功能:

        从一个消息队列接收消息

原型:

        ssize_t  msgrcv (int  msqid, void*  msgp, size_t  msgz,  long  msgtyp, int  msgflg);

参数:

        msqid:  消息队列标识码

        msgp:  指针,指向准备接收的消息

        msgsz:  消息长度,不包含long  int 长整型

        msgtype:  可实现接收优先级的简单形式

        msgflg:  控制着队列中没有相应类型的消息可供接收时将要发生的事

返回值:成功返回实际接受的字符个数,失败返回-1

代码思想:

1.先用一个进程创建消息队列,(server)

2.另一个进程根据消息队列名字,访问之,返回这个消息队列的标识码

3.这样两个进程就可以相互通信了

上代码!!!

有详细注释

comm.c

#include "comm.h"
int CreateMsgQueue()      
{
	return CommGetMsgQueue(IPC_CREAT | IPC_EXCL | 0666);   //不存在,则创建,若存在,则错误
}

int OpenMsgQueue()
{
	return CommGetMsgQueue(IPC_CREAT);   //消息队列不存在,则创建,若存在,则打开它
}

int CommGetMsgQueue(int flags)
{
	key_t _key=ftok(PATHNAME,PROJ_ID);
	if(_key<0){
		perror("ftok");
		return -1;
	} 
	int msgid=msgget(_key,flags);    //返回消息队列的标识码
	if(msgid<0){
		perror("msgget");
		return -1;
	}
	return msgid;
}

int SendMsg(int msgid,long whotype,char* msg)  //发送消息,(标识码,什么类型,消息内容)
{
	struct msgbuf buf;       //消息结构体
	buf.mtype=whotype;       
	strcpy(buf.mtext,msg);
	if(msgsnd(msgid,(void*)&buf,sizeof(buf.mtext),0)<0){  //(标识码,消息结构体,消息大小,缺省为0)
		perror("send");
		return -1;
	}
	return 0;
}
int RecvMsg(int msgid,long recvType,char* out)  //接收消息,(标识码,接收类型,存到哪)
{
	struct msgbuf buf;
	ssize_t s=msgrcv(msgid,(void*)&buf,sizeof(buf.mtext),recvType,0);  //(标识码,消息结构体,消息大小,消息类型,缺省为0)
	if(s<0){
		perror("msgrcv");
		return -1;
	}
	strcpy(out,buf.mtext);
	return 0;
}
void DestoryMsgQueue(int msgid)
{
	if(msgctl(msgid,IPC_RMID,NULL)<0){   //(标识码,删除指令,NULL)
		perror("msgctl");
	}
}

server.c

#include "comm.h"  //服务器端
int main()
{
	int msgid=CreateMsgQueue();   //创建消息队列
	if(msgid<0){
		perror("create");
		return -1;
	}
	char buf[1024]={0};          //缓冲区
	while(1){
		RecvMsg(msgid,CLIENT_TYPE,buf);   //接收消息(标识码,用户类型,放到缓冲区)
		if(!strcmp(buf,"q")){             //收到q,则退出
			printf("client# %s\n",buf);
			printf("client quit! i quit too!\n");
			break;
		}
		printf("client# %s\n",buf);
		printf("Please stdin!\nserver# ");
		fflush(stdout);
		ssize_t s=read(0,buf,sizeof(buf)-1);  //读输入到缓冲区中
		if(s<0){
			perror("read");
			return -1;
		}
		buf[s-1]=0;
		SendMsg(msgid,SERVER_TYPE,buf);   //发送消息 (标识码,服务器类型,从哪发)
		printf("send done! Wait recv...\n");
	}
	DestoryMsgQueue(msgid);       //销毁消息队列 (标识码)
	return 0;
}

client.c

#include "comm.h"   //用户端
int main()
{
	int msgid = OpenMsgQueue();    //打开消息队列
	char buf[1024]={0};            //缓冲区
	while(1){
		printf("Please stdin!\nclient# ");
		fflush(stdout);
		ssize_t s=read(0,buf,sizeof(buf)-1);  //从标准输入读到缓冲区
		if(s<0){
			perror("read");
			return -1;
		}
		buf[s-1]=0;  //把换行符覆盖掉
		SendMsg(msgid,CLIENT_TYPE,buf);  //(标识码,用户类型,内容)
		if(!strcmp(buf,"q")){            //如果输入q,则用户端退出 
			printf("i will quit!\n");
			break;
		}
		printf("send done , wait recv...\n");
		
		RecvMsg(msgid,SERVER_TYPE,buf);  //(标识码,服务器类型,放到缓冲区)
		printf("server# %s\n",buf);
		
	}
	return 0;
}

四、共享内存

思想:向系统申请一块内存空间,之后,让两个进程同时看到这块内存空间即可

操作函数:

shmget函数

功能:用来创建共享内存

原型:int  shmget(key_t  key, size_t  size, int  shmflg);

参数:key:  共享内存的名字。由ftok函数得来

          size:  共享内存的大小  (按页计)  1页==4KB==4096字节

          shnflg:  权限标志位

返回值:成功返回该该共享内存的标识码,失败返回-1

注意:即使用户申请了4097个字节,系统也会分配两页,但用户只能用4097个字节,其余4095个字节就浪费了

shmat函数

功能:挂接,将共享内存连接到进程地址空间

原型:void*  shmat(int  shmid,  const  void*  shmaddr,  int   shmflg);

参数:shmid : 共享内存标识码,

           shmaddr:  指定连接的地址, 一般缺省为NULL,让系统自己找,找自己进程空间的地址

           shmfig:  可取两值,SHM_RND(读和写),SHM_RDONLY(只读)

返回值:成功返回指针,指向共享内存的第一个字节,失败返回-1;

shmdt函数

功能:分离,将共享内存段与当前进程脱离

原型:int  shmdt(const  void*  shmaddr);

参数:shmaddr:  由shmat返回的指针

返回值:成功返回0,失败返回-1;

shmaddr函数

功能:  int  shmctl(int  shmid,  int  cmd,  struct  shmid_ds,  *buf);

参数:shmid:  共享内存的标识码

           cmd:  命令将要采取的三个动作(IPC_RMID)删除,另外两个不重要

共享内存的特点:

1.共享内存区是最快的IPC形式,一旦这样的内存映射到共享它的进程的地址空间,这些进程间的数据传递将不再涉及内核

2.但没有同步与互斥的保护机制

3.其生命周期是随内核的

其示意图如下:

代码示例:

shm.h


#ifndef __SHM_H__
#define __SHM_H__

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATHNAME "."
#define PROJ_ID 0x1111
#define MAX_SIZE 4096

int CreateShm(int size);
int DestoryShm(int shmid);
int GetShm(int size);
int CommShm(int size,int flags);
#endif //__SHM_H__

shm.c

#include "shm.h"

int CreateShm(int size)
{
	return CommShm(size,IPC_CREAT | IPC_EXCL | 0666);  //若共享内存已存在,则错误;若不存在,则创建
}

int DestoryShm(int shmid)
{
	if(shmctl(shmid,IPC_RMID,NULL)<0){     //控制共享内存,(标识码,删除操作,缺省为空)
		perror("rmid");
		return -1;
	}
	return 0;
}

int GetShm(int size)
{
	return CommShm(size,IPC_CREAT);  //若共享内存不存在,则创建;若存在,则访问
}

int CommShm(int size,int flags)
{
	key_t _key=ftok(PATHNAME,PROJ_ID);  //生成名字_key
	if(_key<0){
		perror("ftok");
		return -1;
	}
	int shmid=shmget(_key,size,flags);  //打开共享内存(名字,大小,权限)
	if(shmid<0){
		perror("shmget");
		return -1;
	}
	return shmid;   //返回标识码
}




server.c

#include "shm.h"

int main()
{
	int shmid=CreateShm(MAX_SIZE);
	char* addr=(char*)shmat(shmid,NULL,0);
	sleep(3);
	int i=0;
	while(i++<26){
		printf("client# %s\n",addr);
		sleep(1);
	}
	shmdt(addr);
	sleep(2);
	DestoryShm(shmid);
	return 0;
}

client.c

#include "shm.h"
int main()
{
	int shmid=GetShm(MAX_SIZE);  //打开共享内存,4096字节,4KB
	char* addr=(char*)shmat(shmid,NULL,0);  //挂接到共享内存(标识码,缺省为NULL,缺省为0) 返回共享内存首地址
	sleep(1);
	int i=0;
	while(i<26){
		addr[i]='A'+i;
		sleep(1);
		i++;
		addr[i]=0;
	}
	shmdt(addr);	//解挂(内存地址)
	sleep(2);
	return 0;
}

注意:应先运行server.c,开辟一个共享内存

五、信号量

信号量本质上是一个计数器

struct  semphore{           
     int   value;         
     pointer_PCB  queue;     //指向进程队列的指针
}

其思想是这样的:多个进程想访问一份临界资源,而这份临界资源一次只能供一个进程访问(显示器),信号量可以用来描述临界资源数目。进程每申请一次,信号量值减1,用完后每释放一次,信号量值加1.如果一开始就将信号量值设置为1,就可以做到互斥利用资源(二元信号量)

P、V原语

信号量:

        互斥:P、V在同一进程中

        同步:P、V在不同进程中

信号量值的含义:

        S > 0 :  S表示可用资源数目

        S = 0 :  表示无可用资源,无等待进程

        S < 0 :  | S |表示等待进程数目

信号量集函数

semget函数

功能:创建和访问一个信号量集

原型:int  semget(key_t  key, int  nsems, int semflg);

参数:key: 信号量集的名字

           nsems:  信号量集中信号量的个数,这些信号量分管不同的临界资源(下标)

           semflg:权限

返回值:成功返回标识码,失败返回-1

semctl函数

功能:用于控制信号量集

原型:int  semctl (int  semid, int  semnum,  int  cmd, ...)

参数:semid:  标识码

           semnum:  要操作信号量集中的第几号信号量

           cmd:  将要采取的动作,最后一个参数根据命令的不同而不同,

           SETVAL          设置信号量的计数值 (初始化)在一个联合体中设置

           IPC_RMID           主要用于删除

返回值:成功返回0,失败返回-1

semop函数

功能:完成对信号量的P、V操作

原型:int  semop( int  semid,  struct  sembuf*  sops,  unsigned  nsops);

参数:semid: 标识码

           sops: 指向一个结构体的指针

           nsops: 信号量的个数,常设为1,指完成对一个信号量的操作

返回值:成功返回0,失败返回-1

sembuf结构体

struct  sembuf{

           short  sem_num;    数组下标,信号量集合中的信号量编号

           short  sem_op;      若传入的值s > 0,进行V操作,信号量值+s,反之,信号量值 - |s|

           short  sem_flg;       一般缺省为0

}

sembuf结构体已经在<sys/sem.h>头文件中声明,直接拿来定义即可

代码思想:(以二元信号量为例)

1.先用一个进程创建一个信号量集,并初始化它;  创建arr[1],  初始化 arr[0]=1;

2.再创建一个进程,根据信号量集的名字_key访问它,并得到信号量集的标识码

3.设这两个进程都要在显示器上打印消息,则可对指定信号量进行P、V操作,可做到进程间互斥

代码:

comm.c

#include "comm.h"

static int CommSemSet(int nums,int flags)
{
	key_t _key=ftok(PATHNAME,PROJ_ID);
	if(_key < 0){
		perror("ftok");
		return -1;
	}
	int semid = semget(_key,nums,flags); //创建时nums是信号量集中信号量的个数  //访问时是信号量集中的第几号信号量
	if(semid < 0){
		perror("semget");
		return -1;
	}
	return semid;  
}

int CreatSemSet(int nums)
{
	return CommSemSet(nums,IPC_CREAT | IPC_EXCL | 0666);
}

int GetSemSet(int nums)
{
	return CommSemSet(nums,IPC_CREAT);
}

int initSem(int semid,int nums,int initVal)
{
	union semun _un;   //nums是第0号信号量,initVal是初始值
	_un.val = initVal;
	if(semctl(semid,nums,SETVAL,_un)<0){
		perror("semctl");
		return -1;
	}
	return 0;
}

static int CommPV(int semid,int who,int op)
{
	struct sembuf _sf;
	_sf.sem_num = who;  //操作第who号信号量,
	_sf.sem_op = op;   // <1,P操作,申请临界资源,>1,V操作,释放临界资源
	_sf.sem_flg = 0;
	if(semop(semid,&_sf,1)<0){
		perror("semop");
		return -1;
	}
	return 0;
}

int P(int semid,int who)
{
	return CommPV(semid,who,-1);
}

int V(int semid,int who)
{
	return CommPV(semid,who,1);
}

int DestorySemSet(int semid)
{
	if(semctl(semid,0,IPC_RMID)<0){
		perror("destory");
		return -1;
	}
	return 0;
}


test.c

#include "comm.h"

int main()
{
	int semid = CreatSemSet(1); //创建一个信号量集,返回标识码即arr[1];
	initSem(semid,0,1);  //0表示信号集中信号量的下标,1表示设置该信号量的计数值
	pid_t id=fork();  //创建子进程
	if(id==0){  //child
		int _semid = GetSemSet(0); //访问信号量集中第0号信号量
		int i=2;
		while(i--){
			P(_semid,0);  //对0号信号量进行P操作,占用临界资源
			printf("A");  //此时显示器就是临界资源
			fflush(stdout);
			sleep(1);
			printf("A");
			fflush(stdout);
			sleep(1);
			V(_semid,0);  //对0号信号量进行V操作,释放临界资源
		}
		exit(0);
	}
	else{
		int i=3;
		while(i--){
			P(semid,0);
			printf("B");
			fflush(stdout);
			sleep(1);
			printf("B");
			fflush(stdout);
			sleep(1);
			V(semid,0);
		}
		wait(NULL);
	}
	printf("\n");
	DestorySemSet(semid);
	return 0;
}

 

 

 

 

 

 

 

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值