Linux教程

LINUX系统编程-- 8 进程间通信

本文主要是介绍LINUX系统编程-- 8 进程间通信,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

LINUX系统编程-- 8 进程间通信

  • 八 进程间通信
    • 0 模拟管道、队列进行通信(即顺序存储的循环队列通信)
    • 1 管道
    • 2 XSI
      • 1、消息队列
      • 2、信号量数组
      • 3、共享内存

八 进程间通信

主要内容:
0、用之前的内容设计一个管道
1、管道
内核提供、单工、自同步机制
匿名管道、命名管道
2、XSI
3、网络套接字socket

0 模拟管道、队列进行通信(即顺序存储的循环队列通信)

自己实现一个管道并进行通信,这里用的是顺序存储的循环队列模拟的。
下面代码是不完整版,尚未完成,且有bug

//mypipe.h
#ifndef MYPIPE_H__
#define MYPIPE_H__
#define MYPIPE_READ 0x00000001UL
#define MYPIPE_WRITE 0x00000002UL
#define PIPESIZE 1024

typedef void mypipe_t;
mypipe_t* mypipe_init(void);

int mypipe_register(mypipe_t * ptr,int opmap);
int mypipe_unregister(mypipe_t * ptr,int opmap);

int mypipr_read(mypipe_t *,void *buf,size_t count);

int mypipr_write(mypipe_t *,const void *buf, size_t size);

int mypipe_destroy(mypipe_t*);

#endif
//mypipe.c
#include<stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include "mypipi.h"

struct mypipe_st
{
   

    int head;
    int tail;
    char data[PIPESIZE];
    int datasize;
    int count_rd;
    int count_wr;
    pthread_mutex_t mut;
    pthread_cond_t cond;

};

mypipe_t* mypipe_init(void)
{
    struct mypipe_st *me;
    me=malloc(sizeof(*me));
    if(me==NULL)
        return NULL;

    me->head=0;
    me->tail=0;
    me->datasize=0;
    me->count_rd=0;
    me->count_wr=0;
    pthread_mutex_init(&me->mut,NULL);
    pthread_cond_init(&me->mut,NULL);

    return me;


}

int mypipe_register(mypipe_t * ptr,int opmap)
{

    
    pthread_mutex_lock(&me->mut);
    if(opmap&MYPIPE_READ)
        me->count_rd++;
    if(opmap & MYPIPE_WRITE)
        me->count_wr++;

    pthread_cond_broadcast(&me->cond);

    
    while(me->count_rd<=0||me->count_wr<=0)
        pthread_cond_wait(&me->cond,&me->mut);
    
    pthread_mutex_unlock(&me->mut);
    return 0;

}

int mypipe_unregister(mypipe_t * ptr,int opmap)
{
    pthread_mutex_lock(&me->mut);

    if(opmap & MYPIPE_READ)
        me->count_rd--;
    if(opmap & MYPIPE_WRITE)
        me->count_wr--;

    pthread_cond_broadcast(&me->cond);

    pthread_mutex_unlock(&me->mut);
}

static  int mypipe_readbyte_unlock(struct mypipe_st *me,char *datap)
{
    if(me->datasize<=0)
        return -1;
    *datap=me->data[me->head];
    me->head=next(me->head);
    me->datasize--;

    return 0;

}

int mypipr_read(mypipe_t *ptr,void *buf,size_t count)
{
    int i;
    struct mypipe_st *me=ptr;
    pthread_mutex_lock(&me->mut);

    while(me->datasize<=0 && me->count_wr>0)
        pthread_cond_wait(&me->cond,&me->mut);

    if(me->datasize<=0 && me->count_wr<=0)
    {
        pthread_mutex_unlock(&me->mut);
        return 0;
    }
    
    for(i=0;i<count;i++)
    {
        
       if( mypipe_readbyte_unlock(me,buf+i)!=0);
            break;

    }
    pthread_cond_broadcast(&me->cond);
    pthread_mutex_unlock(&me->mut);
    return i;
}

int mypipr_write(mypipe_t *,const void *buf, size_t size)
{
    
}

int mypipe_destroy(mypipe_t* ptr)
{
    struct mypipr_st *ptr;
    pthread_mutex_destroy(&me->mut);
    pthread_cond_destroy(&me->cond);


    free(ptr);
}

1 管道

  • 匿名管道:piepe(),磁盘上看不到文件,相当于直接给了我们一个文件描述符或者FILE* 。导致最直接的结果就是 :如果两个进程之间没有血缘关系,是不能用匿名管道进行通信的,因为另一个进程找不到文件位置。

  • 命名管道:mkfifo(), 从磁盘上看到的文件类型为P 的文件。没有血缘关系的进程之间可以用命名管道进行通信。本质上就是当前磁盘上存在的一个文件。P类型的文件.

1、匿名管道的创建与使用

int pipe(int pipefd[2]);

pipe()返回两个文件描述符填充到 pipefd[]数组中。
pipefd[0]为读端,pipefd[1]为写端,

2、命名管道的创建
mkfifo()和 mkfifoat()

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

指定我们所创建的命名管道的名字
指定操作权限


注意:mkfifo也是一个shell命令,类似于mkdir,是创建一个fifo文件

例子1:
匿名管道:父子进程 父写子读, 匿名管道

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

#define BUFSIZE 1024

int main()
{
	int pd[2];
	pid_t pid;
	char buf[BUFSIZE];
	int len;

	if(pipe(pd) < 0)//先创建匿名管道
	{
		perror("poipe()");
		exit(1);
	}


	pid = fork();//再创建子进程
	if(pid < 0)
	{
		perror("fork()");
		exit(1);
	}
	if(pid == 0) // Child read
	{
		close(pd[1]); //关闭写段
		len = read(pd[0],buf,BUFSIZE); //读取读段
		write(1,buf,len);//将读取的内容输出到终端
		close(pd[0]);//关闭读端
		exit(0);
	}
	else //Parent write
	{
		close(pd[0]); //关闭读端
		write(pd[1],"Hello!",6);//向写段写
		close(pd[1]);//关闭写端
		wait(NULL);//等收尸
		exit(0);
	}
	exit(0);
}

补充:
shell命令行中的 | 就是管道的意思,意思是,将 | 前的输出到标准输出的内容作为 | 后面命令的标准输入。

补充,:
特别注意一下管道的自同步机制,如下面例子,创建一个命名管道文件(非父子进程也能通过这个通信),并在一个进程中向这个文件中些内容,除非从另一个进程中读这个管道文件,否则第一个进程不会结束。
第一个终端:

ubuntu@VM-0-7-ubuntu:~/unix_pragrame/IPC$ mkfifo namedfifo
ubuntu@VM-0-7-ubuntu:~/unix_pragrame/IPC$ date
Tue Sep 21 16:03:49 CST 2021
ubuntu@VM-0-7-ubuntu:~/unix_pragrame/IPC$ date > namedfifo 

会发现这个进程一直会阻塞。

打开第二个终端:

ubuntu@VM-0-7-ubuntu:~/unix_pragrame/IPC$ cat namedfifo 
Tue Sep 21 16:05:39 CST 2021

第一个终端的向管道写操作成功返回:

ubuntu@VM-0-7-ubuntu:~/unix_pragrame/IPC$ date > namedfifo 
ubuntu@VM-0-7-ubuntu:~/unix_pragrame/IPC$ 

也就是说,管道必须凑集读写双方才能实现!(自同步机制)

补充:
管道的另一个特点是单工,也就是一个管道必定是往一个方向流动的。要想实现双工,可以使用两个管道。

2 XSI

XSI包括:消息队列、信号量数组、共享内存

这三个既可以进行有血缘关系的进程间的通信、也可以进行无血缘关系的进程间的通信。

使用命令ipcs,可以查看当前环境的所有的正在使用的XSI。
在这里插入图片描述

主动端:先发包的一方;
被动端:先收包的一方(先运行);

关于key,ftok()

  • 产生某一个key值。 使得通信双方拿到同一个机制。
  • 其中参数fname是指定的文件名,这个文件必须是存在的而且可以访问的。id是子序号,它是一个8bit的整数。即范围是0~255。当函数执行成功,则会返回key_t键值,否则返回-1。在一般的UNIX中,通常是将文件的索引节点取出,然后在前面加上子序号就得到key_t的值。

XSI都有如下的几个函数:

  • xxxget 获取
  • xxxop 操作
  • xxxctl 控制

1、消息队列

  • msgget()
  • msgop():
    msgsnd()
    msgrcv()
  • msgctl()
//发送
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid:消息队列ID
msgp :待发送数据位置
msgsz :待发送数据大小
msgflg :特殊要求

    
//接收
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msqid:消息队列ID
msgp :收取消息存放位置,注意空间格式
msgsz :真正收取有效数据信息的大小,而不是全部的数据信息小:sizeof(rbuf)-sizeof(long)
msgtyp:是否挑选消息来接收,比如接收第几个包,所以msg消息队列本质上已经不算是队列了,因为队列一定是先进先出。
msgflg:特殊指定操作

     
注意:
The msgp argument is a pointer to a caller-defined structure of the following general form:

struct msgbuf 
{
 long mtype;       /* message type, must be > 0 */这个参数采标的是数据包的类型
 char mtext[1];    /* message data */
};

实验:进程间通信 – 消息队列, 这里是无亲缘关系的进程间通信。当然了,有亲缘关系的进程也可以使用消息队列通信。

//proto.h
#ifndef PROTP_H_
#define PROTO_H_


#define KEYPATH "/etc/services"
#define KEYPROJ 'g'

#define NAMESIZE 32

struct msg_st
{
 long mtype;// 仿照   struct msgbuf 
	char name[NAMESIZE];
	int math;
	int chinese;
};

#endif
//rcver.c 接收端进程
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>

#include "proto.h"

int main(void)
{
	key_t key;
	int msgid;
	struct msg_st rbuf;

	key = ftok(KEYPATH,KEYPROJ);//获取IPC 键值
	if(key < 0)
	{
		perror("ftok");
		exit(1);
	}

	msgid = msgget(key,IPC_CREAT|0600); //创建 消息队列 返回IP,被动端先运行
	if(msgid < 0)
	{
		perror("msgget");
		exit(1);
	}

	while(1)
	{
 //从目标消息队列 接受数据,注意接收数据的大小是有效数据的大小
		if(msgrcv(msgid,&rbuf,sizeof(rbuf)-sizeof(long),0,0) < 0)
		{
			perror("msgrcv");
			exit(1);
		}
		printf("NAME = %s\n",rbuf.name);
		printf("MATH = %d\n",rbuf.math);
		printf("CHINESE = %s\n",rbuf.chinese);

	}

	msgctl(msgid,IPC_RMID,NULL);//销毁消息队列

	
	exit(0);
}
//snder.c 发送端进程
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <string.h>
#include "proto.h"

int main()
{
	key_t key;
	int msgid;
	struct msg_st sbuf;

	key = ftok(KEYPATH,KEYPROJ);//获取消息队列 IPC 键值
	if(key < 0)
	{
		perror("ftok");
		exit(1);
	}


	msgid = msgget(key,0);//获得已创建好的目标消息队列 的 ID
	if(msgid < 0)
	{
		perror("msgget");
		exit(1);
	}

//初始化数据
	sbuf.mtype = 1;
	strcpy(sbuf.name,"MHR");
	sbuf.math = rand()%100;
	sbuf.chinese = rand()%100;

// 发送数据
	if(msgsnd(msgid,&sbuf,sizeof(sbuf)-sizeof(long),0) < 0)
	{
		perror("msgsnd");
		exit(1);
	}
	puts("OK");
	exit(0);
}

需要注意的是:

  • 如果先执行 10次 发送端进程,再执行一次接收端进程,那么接收端会一次性接受 之前10次的 发送端发送的数据。这是因为消息队列有一个缓存消息的能力,具体能缓存多大的数据,可以 ulimit -a 查看。

  • 上面程序没有体现出mtype的作用,因为这个例子知识发送了一种类型的包,当发送的是多种类型的包时,也就是说当定义了多个msg_st结构的时候,这个参树就可以代表是哪个数据包。

  • 上面的程序,接受端是永远不会执行到mesctl的,而这里这个函数的作用是销毁消息队列,当我们使用ctrl+c杀死这个进程时候,实际上是使接收端非正常终止,而消息队列还没有销毁,可以使用ipcs命令查看。这种情况下,可以加入信号机制,设置ctrl+c的信号处理函数,在信号处理函数中销毁或者在信号处理函数中执行一些清理工作。如果没有设置信号处理函数,也可以通过shell命令杀死指定的ipc。

先查看 目标消息队列 id
mhr@ubuntu:~/msg/basic$ ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
0x670100d2 0 mhr 600 0 0

删除:
mhr@ubuntu:~/msg/basic$ ipcrm -q 0

   -M, --shmem-key shmkey
          Remove the shared memory segment created with shmkey after the last detach is performed.

   -m, --shmem-id shmid
          Remove the shared memory segment identified by shmid after the last detach is performed.

   -Q, --queue-key msgkey
          Remove the message queue created with msgkey.

   -q, --queue-id msgid
          Remove the message queue identified by msgid.

   -S, --semaphore-key semkey
          Remove the semaphore created with semkey.

   -s, --semaphore-id semid
          Remove the semaphore identified by semid.

   -V, --version
          Display version information and exit.

   -h, --help
          Display help text and exit.

补充:关于strcpy
假设有char buf[100],若要给这个数组赋值,是不能使用buf=“liujj”的,因为buf是常量,不能赋值。所以要使用strcpy(buf,“liujj”)。

2、信号量数组

semget
semop
semctl

基本函数:

int semget(key_t key, int nsems, int semflg);

  • 创建一个新的信号量数组 或获取一个已经存在的信号量数组 的ID
    key
  • 如果没有亲缘关系的进程中 创建和获取 和之前 消息队列是一样的如果在有亲缘关系的进程中,fork()之后,每一个子进程都可以拿到父进程创建的 key值,此时不再关心key值,此时可以设置为 IPC_PRIVATE,表示该IPC 为 匿名IPC 不需要ftok
  • nsems: 指定当前信号量数组中有多少个元素
  • semflg:其他特殊要求,当key 为IPC_PRIVATE时, semflg设置为IPC_CREAT

int semctl(int semid, int semnum, int cmd, …)

  • 对目标信号量数组 执行各类操作,不过最常用的是删除它。
  • semid : IPC ID
  • semnum :目标信号量 在数组中的下标值
  • cmd :命令
IPC_RMID:从系统中删除目标信号量集合
......
SETVAL:设置数组中下标为semnum的成员值,即设置某个信号量的资源量
......

最后参数:初始值,即资源总量

semop() 操作信号量数组 ,P + V
int semop(int semid, struct sembuf *sops, size_t nsops);

  • semid:目标IPC ID
  • sops :结构体数组起始位置
  • nsops:结构体数组中有多少个元素,即有多少个该结构体
结构体sembuf的内容为:

信号量编号,当使用单个信号量时候,为0
unsigned short sem_num; 
信号量操作,取值为-1,表示P操作。归还为+1,为释放操作
short          sem_op;  
short          sem_flg;  /* operation flags */

下面是错误:

   E2BIG  The argument nsops is greater than SEMOPM, the maximum number of operations allowed per system call.

   EACCES The calling process does not have the permissions required to perform the specified semaphore operations, and does not have the CAP_IPC_OWNER capability.

   EAGAIN 假错

   EFAULT An address specified in either the sops or the timeout argument isn't accessible.

   EFBIG  For some operation the value of sem_num is less than 0 or greater than or equal to the number of semaphores in the set.

   EIDRM  The semaphore set was removed.

   EINTR  While blocked in this system call, the thread caught a signal; see signal(7).

   EINVAL The semaphore set doesn't exist, or semid is less than zero, or nsops has a nonpositive value.

   ENOMEM The sem_flg of some operation specified SEM_UNDO and the system does not have enough memory to allocate the undo structure.

   ERANGE For some operation sem_op+semval is greater than SEMVMX, the implementation dependent maximum value for semval.

例子:信号量使用,20个进程写同一个文件,该例程中 信号量集合中只有一个信号量,功能和互斥锁差不多。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define PROCNUM 20
#define FNAME "/home/mhr/Desktop/xitongbiancheng/super_io/out"
#define LINESIZE 1024
static 	int semid;

//取资源量
static void P(void)
{
	struct sembuf op;
	
	op.sem_num = 0;
	op.sem_op = -1;
	op.sem_flg = 0;

	while(semop(semid,&op,1) < 0)
	{
		if(errno != EINTR || error != EAGAIN)
		{
			perror("semop()");
			exit(1);
		}
	}
}

//归还资源量
static void V(void)
{
	struct sembuf op;
	
	op.sem_num = 0;
	op.sem_op = 1;
	op.sem_flg = 0;

	if(semop(semid,&op,1) < 0)
	{
		perror("semop()");
		exit(1);

	}
}

static void func_add(void)
{
	FILE *fp;
	int fd;
	char linebuf[LINESIZE];

	fp = fopen(FNAME,"r+");
	if(fp == NULL)
	{
		perror("fopen()");
		exit(1);
	}



P();//取资源量
	fgets(linebuf,LINESIZE,fp);
	fseek(fp,0,SEEK_SET);

	//sleep(1);	
	
	fprintf(fp,"%d\n",atoi(linebuf)+1);
	fflush(fp);
V();//归还资源量
	
	fclose(fp);
return;

} 

int main()
{
	int i,err;
	pid_t pid;

//创建信号量数组 IPC 返回值为id
	semid = semget(IPC_PRIVATE,1,IPC_CREAT);
	if(semid < 0)
	{
		perror("semget");
		exir(0);
	}

// 设置信号量集合中 第一个信号量的资源总量为1
	if(semctl(semid,0,SETVAL,1) < 0)
	{
		perror("semctl");
		exir(0);	
	}

	for(i = 0; i < PROCNUM; i++)
	{
		pid = fork();
		if(pid < 0)
		{
			perror("fork()");
			exit(1);
		}
		
		if(pid == 0)//Child 
		{
			func_add();
			exit(0);
		}

	}


	for(i = 0;i < PROCNUM; i++)
	{
		wait(NULL);
	}
	
	//从当前系统 删除该 信号量集合
	semctl(semid,0,IPC_RMID);

	exit(0);
	
}

3、共享内存

shmget、shmop、shmctl

shmget() 分配一个System V共享内存段

int shmget(key_t key, size_t size, int shmflg);

key:如果没有亲缘关系的进程中 创建和获取 和之前 消息队列是一样的如果在有亲缘关系的进程中,fork()之后,每一个子进程都可以拿到父进程创建的 key值,此时不再关心key值,此时可以设置为 IPC_PRIVATE,表示该IPC 为 匿名IPC 不需要ftok

size:要设置的共享内存的大小

shmflg : 当前共享内存创建的特殊要求,当key 为IPC_PRIVATE时, -semflg设置为IPC_CREAT

例子:shmid = shmget(IPC_PRIVATE,MEMSIZE,IPC_CREAT|0600);

shmat()把共享内存映射过来;shmdt() 把共享内存进行解除映射

int shmdt(const void *shmaddr);

shmid:共享内存ID
shmaddr :需要映射到我当前空间的具体位置,写空 表示函数帮忙在当前进程空间中寻找一块可用的内存地址。
shmflg : 特殊要求,0为没有特殊要求


例子:  void *shmat(int shmid, const void *shmaddr, int shmflg);

shmctl(), System V共享内存控制 如销毁

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

shmid,共享内存ID
cmd : 命令动作,如:IPC_RMID 销毁
buf,是否需要传参,需要传递的参数

实验:父子进程进 使用 共享内存 进行通信,对比之前使用内存映射mmap(),还是mmap()使用更简单一点。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
#include <wait.h>
#define MEMSIZE 1024

int main()
{
	int shmid;
	pid_t pid;
	char *ptr;

	shmid = shmget(IPC_PRIVATE,MEMSIZE,IPC_CREAT|0600);
	if(shmid < 0)
	{
		perror("msgget");
		exit(1);
	}

	pid = fork();
	if(pid < 0)
	{
		perror("fork() failed");
		exit(1);
	}

	if(pid == 0)
	{
		ptr = shmat(shmid,NULL,0);
		if(ptr == (void *)-1)
		{
			perror("shmat");
			exit(1);
		}	
		
		strcpy(ptr,"Hello!");
		shmdt(ptr);
		exit(0);
	}
	else
	{
		wait(NULL);
		ptr = shmat(shmid,NULL,0);
		if(ptr == (void *)-1)
		{
			perror("shmat");
			exit(1);
		}
		puts(ptr);
		shmdt(ptr);
		shmctl(shmid,IPC_RMID,NULL);
		exit(0);
	}	
	exit(0);
}
这篇关于LINUX系统编程-- 8 进程间通信的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!