#Linux中的GCC编程# 进程间通信

进程间通信(IPC)

基于早期UNIX进程间通信,基于System V进程间通信,基于Socket进程间通信和POSIX的进程间通讯

1. 进程间通信的概述

1.1 进程间通信的目的

数据传输,共享数据,通知事件,资源共享,进程控制。

1.2 有哪些进程间通讯方式

  • 管道pipe,命名管道FIFO
  • 信号siganl
  • 消息队列
  • 共享内存
  • 信号量
  • 套接字(socket)
  • 文件锁(系统IO中介绍)

2. 管道通信

  • 本地计算机的两个进程之间的通讯而设计的通讯方式。
  • 创建管道,获得两个文件描述符,一个用于读取数据,一个用于写入数据。
  • 管道是单工的,只有一个方向。
  • 管道实际上是内存中的一块缓存。每次写入添加的数据都是添加在尾部。读取的进程从头部读取。

2.1 管道的分类

  • 匿名管道 pipe
    两个通讯的进程必须有关系,一般来说 ,管道由父进程创建。

  • 命名管道 FIFO
    两个没有任何关系的进程之间可以通过FIFO通讯。

2.2 pipe

2.2.1 管道pipe的创建

相关函数

#include <unistd.h>
int pipe(int pipefd[2]);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>
int pipe2(int pipefd[2], int flags);

参考实例

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    int pipe_fd[2];
    if(pipe(pipe_fd)<0)
    {
        printf("pipe create error\n");
        return -1;
    }
    else
    {
        printf("pipe create success\n");
    }
    close(pipe_fd[0]);
    close(pipe_fd[1]);
}
2.2.2 管道pipe的通讯实例

管道主要用于不同进程通信。实际上,通常先创建一个管道,再通过fork函数创建一个子进程。
子进程会复制管道的fd[0],fd[1]。两个进程必须各自关掉一个管道方向。
如下图所示,父进程关闭fd[0],子进程关闭fd[1]。
pipe+fork
参考示例:父进程通过管道传输两个数据给子进程。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    int fd[2];
    //创建管道
    if(pipe(fd)<0)
    {
        printf("pipe create error\n");
        return -1;
    }

    pid_t pid;
    if((pid = fork())<0)
    {
        perror("pipe error");
        exit(1);
    }
    else if(pid>0){ //父进程 用来写入数据
        close(fd[0]);
        int start = 1,end =100;
        if(write(fd[1],&start,sizeof(int)) != sizeof(int))
        {
             perror("write error\n");
             exit(1);
        }
        if(write(fd[1],&end,sizeof(int)) != sizeof(int))
        {
             perror("write error\n");
             exit(1);
        }
        close(fd[1]);
        wait(0);//等待回收子进程
    }
    else if(pid == 0){ //子进程  用来读取数据
        close(fd[1]);
        int start,end;
        if(read(fd[0],&start,sizeof(int))<0 ){
             perror("read error\n");
             exit(1);
        }
        if(read(fd[0],&end,sizeof(int))<0 ){
             perror("read error\n");
             exit(1);
        }
        printf("start %d end %d \n",start,end);
        close(fd[0]);
    } 

    exit(0);
}

运行结果:

kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc test.c -o ttt
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./ttt 
start 1 end 100 
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ 

2.2.3 管道pipe结合execute命令执行

pipe 兄弟子进程
上图中,cat命令和grep命令相当于shell的两个子进程。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>

char *cmd1[3] = {"/bin/cat","./passwd",NULL};
char *cmd2[3] = {"/bin/grep","kshine",NULL};

int main(void)
{
    int fd[2];
    //创建管道
    if(pipe(fd)<0)
    {
        printf("pipe create error\n");
        return -1;
    }
    int i=0;
    pid_t pid;
    for(;i<2;i++)
    {
        pid = fork();
        if(pid<0)
        {
            exit(1);
        }
        else if(pid>0)
        { 
            if(i==1)
            {
               //父进程等到子进程全部创建完毕才去回收子进程
               close(fd[0]);
               close(fd[1]);
               wait(0);
               wait(0);
            }
        }
        else if(pid==0)
        {
            if(i==0){//第一个子进程负责写入数据
                close(fd[0]);//关闭读端
                //重定向 到写端
                if(dup2(fd[1],STDOUT_FILENO) != STDOUT_FILENO)
                {
                     perror("dup2 error");
                }
                close(fd[1]);//关闭写端(重定向已经做了复制)
                //执行命令         
                if(execvp(cmd1[0],cmd1)<0){perror("execvp error");exit(1);}

                break;
            }
            

            if(i==1){//第二个子进程负责读取
                close(fd[1]);//关闭写端
		        //grep 命令默认从标准输入读取内容,再过滤。
                //重定向 管道读端到标准输入
                if(dup2(fd[0],STDIN_FILENO) != STDIN_FILENO)
                {
                     perror("dup2 error");
                }
                close(fd[0]);//关闭读端(重定向已经做了复制)
                if(execvp(cmd2[0],cmd2)<0){perror("execvp error");exit(1);}

                break;
            }
        }
    }

    
    exit(0);
}

本地目标文件passwd的内容
目标文件
运行结果:

kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc test.c -o ttt
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./ttt 
kshine  123456
999  kshine
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ 

2.3 管道的读写特性

  • 使用两个管道可以实现双向的读写。
  • 从管道读取数据,没有数据时,读取方会被阻塞。
  • 向管道写入数据时,若管道已经满了,会报错。
  • 当管道的写端被关闭,所有数据被读取后,read会返回0,表示已经读完。
  • 当管道的读端被关闭,向里面写数据,会产生SIGPIPE信号。write返回-1,同时errno 为EPIPE。
2.3.1 不完整管道读写

读一个写端被关闭的管道,参考例子:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>

int main(void)
{
    int fd[2];
    //创建管道
    if(pipe(fd)<0)
    {
        printf("pipe create error\n");
        return -1;
    }
    //通过父子进程,完成不完整管道的测试
    pid_t pid;
    pid = fork();
  
	if(pid<0)
	{
	    exit(1);
	}
	else if(pid>0)
	{ 
		//父进程 从不完整管道中(写端关闭)读取数据
		sleep(1);//等待子进程写端关闭
		close(fd[1]);
		while(1)
		{
			char c;
			if(read(fd[0],&c,1)==0)
			{
				printf("\nwrite-end of pipe closed\n");
				break;
			}
			else
			{
				printf("%c",c);
			}
        }
		close(fd[0]);
       	wait(0);

	}
	else if(pid==0)
	{
		//子进程负责吧数据写入管道
		close(fd[0]);
		char *s = "1234";
		write(fd[1],s,sizeof(s));
		//写入数据后,关闭管道的写端
	    close(fd[1]);
	}
    exit(0);
}

运行结果

kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc test.c -o ttt
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./ttt 
1234
write-end of pipe closed  //读取完成后,发现写端已经关闭
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ 

写一个读端被关闭的管道

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
#include <errno.h>
void sig_handler(int signo)
{
	if(signo == SIGPIPE){
		printf("SIGPIPE occured\n");
	}
}

int main(void)
{
    int fd[2];
    //创建管道
    if(pipe(fd)<0)
    {
        printf("pipe create error\n");
        return -1;
    }
    //通过父子进程,完成不完整管道的测试
    pid_t pid;
    pid = fork();
  
	if(pid<0)
	{
	    exit(1);
	}
	else if(pid>0)
	{ 
	       //父进程 向不完整管道中(读端关闭)写入数据
            sleep(1);//等待子进程读端关闭
	        close(fd[0]);
            if(signal(SIGPIPE,sig_handler) == SIG_ERR)
            {
				perror("signal sigpipe error");
				close(fd[1]);
				exit(1);
			}
	       char *s ="1234";
	       if(write(fd[1],s,sizeof(s)) != sizeof(s) )
		   {
				fprintf(stderr,"%s, %s\n",strerror(errno),(errno == EPIPE)?"EPIPE":"unknow");
		   }
	       close(fd[1]);
	       wait(0);

	}
	else if(pid==0)
	{
          	//子进程 关闭全部管道
            close(fd[0]);
	       	close(fd[1]);
	}
    exit(0);
}

运行结果

kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc test.c -o ttt
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./ttt 
SIGPIPE occured
Broken pipe, EPIPE
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ 

2.4 标准库中的管道操作函数

前文的管道操作太麻烦,使用标准库函数,可以更容易的使用管道。

  • 使用popen()创建的管道,必须使用pclose()关闭。
#include <stdio.h>
FILE *popen(const char *command, const char *type); //返回文件描述符,错误返回NULL
int pclose(FILE *stream);//返回终止状态,错误返回-1

popen
参考例子

#include <stdlib.h>
#include <stdio.h>
#include <memory.h>

int main(void)
{
    FILE* fp;
    fp = popen("cat ./passwd","r");
    char buf[512];
    memset(buf,0,sizeof(buf));
    while(fgets(buf,sizeof(buf),fp) != NULL){
	printf("%s",buf);
    }
    pclose(fp);

    //---------------------------------//
    printf("-------------------------\n");
    fp = popen("wc -l","w");//wc命令
    fprintf(fp,"1\n2\n3\n");//向fp结构体缓存中写入3行数据
    pclose(fp);
    exit(0);
}

运行结果
目标文件

kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc test.c -o ttt
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./ttt 
kshine  123456
handsome 1111
good job
999  kshine
-------------------------
3
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$

2.5 命名管道

相关函数:(man 3 mkfifo)

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); //文件路径,访问权限

#include <fcntl.h>           /* Definition of AT_* constants */
#include <sys/stat.h>
int mkfifoat(int dirfd, const char *pathname, mode_t mode);
  • 可以在无关系的进程之间进行通信。
  • 本质是内核的缓存。
  • 命名管道,读写必须都打开。否则单独读或者单独写都会导致堵塞。
  • 在文件系统中,只有路径没有数据块,数据在内核中。
  • mkfifo也是一条shell命令
  • 对fifo的操作和操作普通文件是一样的。创建一个fifo,也可以叫做创建一个管道文件。
参考实例:
  1. 创建命名管道
    两种方式,这里直接用命令的方式创建
mkfifo s.pipe
  1. 从命名管道读数据(独立的代码,运行成为独立的进程)
    这里的代码,就是基本的文件操作。从文件中读取数据。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <memory.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
	if(argc <2 ){
		printf("usage:%s fifo\n",argv[0]);
		exit(1);
	}
	printf("open fifo read...\n");
	//打开命名管道fifo
	int fd = open(argv[1],O_RDONLY);
	if(fd<0){
		perror("open error");
		exit(1);
	}else{
	 	printf("open file success");
	}
	//从命名管道读取数据
	char buf[512];
	memset(buf,0,sizeof(buf));
	while(read(fd,buf,sizeof(buf))<0){
		printf("read error");
	}
	printf("%s\n",buf);
	close(fd);
	exit(0);

}
  1. 向命名管道写数据(独立的代码,运行成为独立的进程)
    这里的代码,就是基本的文件操作。向文件写入数据。
#include <unistd.h>
#include <memory.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc,char* argv[])
{
	if(argc<2){
		printf("usage:%s fifo\n",argv[0]);
		exit(1);
	}
	printf("open fifo write...\n");
	int fd = open(argv[1],O_WRONLY);
	if(fd<0){
		perror("open error");
		exit(1);
	}else{
		printf("open fifo success:%d\n",fd);
	}
	char *s = "1234567890";
	size_t size = strlen(s);
	if(write(fd,s,size) != size){
		perror("write error");
	}
	close(fd);
	exit(0);
}
  1. 运行结果
    当只运行读取命令管道或者只运行写入命名管道时,进程会被阻塞,直到另一端操作开始。
    命名管道的使用

2.6 匿名管道和命名管道的异同区别

相同点:

  • 适用于网路通信socket。
  • 默认都是阻塞性读写。
  • 阻塞型不完整管道
  • 阻塞性完整管道
  • 非阻塞型不完整管道(一端关闭)
  • 非阻塞型完整管道(两端都开启)

不同点:

  • 打开方式不一样。
  • pipe通过fcntl系统调用,设置成非阻塞型O_NOBLOCK。
  • FIFO通过fcntl系统调用或者open函数,设置成非阻塞型。

以FIFO为例,关于设置非阻塞的参考案例:

	//...
	printf("open fifo read...\n");
	//打开命名管道fifo
	//int fd = open(argv[1],O_RDONLY);
	int fd = open(argv[1],O_RDONLY|O_NONBLOCK);//非阻塞型
	//...
	read(fd,buf,sizeof(buf);//这里不会阻塞,没有数据直接通过。
	//...

3. System V的IPC对象

  • IPC对象(消息队列,共享内存,信号量)。
  • IPC对象 ,存在于内核中,必须由用户控制释放。不释放,则一直存在(除非关机重启)。可以通过以下命令查看:
ipcs 查看内核中的消息队列,共享内存,信号量
ipcs -q 查看消息队列
ipcs -m 查看共享内存
ipsc -s 查看信号量
  • ipc对象在内核空间有唯一性标识ID,在用户空间有唯一性标识key。
  • ipc对象是全局对象。
  • ipc对象由get函数创建,msgget,shmget,semget。调用get函数时,必须指定关键字key。(内核自动分配ID)

3.1 IPC对象的权限和所有者

ipc_perm 结构定义于中,原型如下:

struct ipc_perm
{
	uid_t   uid;            /* 所有者的用户ID*/
	gid_t   gid;            /* 所有者所属组的有效组ID*/
	uid_t   cuid;           /* 创建者的有效用户ID*/
	gid_t   cgid;			/* 创建者所属组的有效组ID*/
	unsigned short   mode;  /* 权限*/
	unsignedshort    seq;   /* 序列号*/
};

4. IPC对象 - 消息队列

  • 消息队列是一个链表。
  • 消息:内核将用户数据,用户ID,组ID,读写进程ID,优先级等打包成的一个数据包。
  • 允许多进程访问消息队列,读消息和写消息。
  • 一个消息被读取后会被自动删除。
  • 消息队列在内核中用唯一的IPC标识ID表示。
  • 四种操作:创建打开,发送,读取,控制消息。

4.1 消息队列属性

struct msqid_ds
  {
    struct ipc_perm msg_perm;  //放置权限和ID
    struct msg *msg_first;      /* first message on queue,unused  */
    struct msg *msg_last;       /* last message in queue,unused */
    __kernel_time_t msg_stime;  /* last msgsnd time */
    __kernel_time_t msg_rtime;  /* last msgrcv time */
    __kernel_time_t msg_ctime;  /* 最后一次消息改变的时间 */
    unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */
    unsigned long  msg_lqbytes; /* ditto */
    unsigned short msg_cbytes;  /* current number of bytes on queue */
    unsigned short msg_qnum;    /*消息的数量 */
    unsigned short msg_qbytes;  /* 消息总最大的字节数 */
    __kernel_ipc_pid_t msg_lspid;   /* 最后一次发送消息的进程pid */
    __kernel_ipc_pid_t msg_lrpid;   /* 最后一次接收消息的进程pid */
};

4.2 打开创建消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);//用户指定的key键值,权限标志(IPC_CREAT,IPC_EXCL)
  • key指定键值,或者设置为,IPC_PRIVATE。若进行查询,key不能设置为0,否则查询不到。

4.3 控制消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//消息队列ID,命令,消息队列属性指针
  • cmd命令,IPC_STAT获取消息队列的属性(总数,最大字节数,时间等等)。
  • cmd命令,IPC_SET设置消息队列的属性。
  • cmd命令,IPC_RMID删除消息队列。删除消息队列以及队列上所有数据。

4.4 发送数据到消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • msqid 消息队列ID
  • msgp 消息结构,如下:(自己定义)
struct msgbuf {
    long mtype;       /* message type, must be > 0 */
    char mtext[1];    /* message data */
};
  • msgsz 消息的大小(用来指定mtext[]数组的大小)。
  • msgflg 消息队列标志,如可以设置为 IPC_NOWAIT(类似于非阻塞,访问的进程不等待)。设置为0,阻塞。

4.5 从消息队列接收数据

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
  • msqid 消息队列ID
  • msgp 消息结构 缓存,用来存放数据。
  • msgsz 消息的大小(不包括mtype的大小)。
  • msgtyp 消息类型。0 获取第一个消息;大于0 获取该类型的消息的第一个消息;小于0 获取消息队列中 小于等于该值绝对值的消息(类型最小的)。
  • msgflg 消息队列标志,如可以设置为 IPC_NOWAIT(类似于非阻塞,访问的进程不等待)。设置为0,阻塞。

4.6 参考案例

程序1 向消息队列发送数据

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>

typedef struct{
	long type;//消息类型
	int data[2];
}MSG;
int main(int argc,char* argv[])
{

	if(argc<2){
		printf("usage:%s key\n",argv[0]);
		exit(1);
	}

	key_t key = atoi(argv[1]);
	//key_t key = IPC_PRIVATE;
	//key_t key = ftok(argv[1],0);//特殊的算法 一般传入存在的文件路径
	printf("key:%d\n",key);
	int msq_id;
	if((msq_id= msgget(key,IPC_CREAT|IPC_EXCL|0777))<0)
	{
		perror("msgget error");
	}
	printf("msq id:%d\n",msq_id);
	MSG m1 = {7,17,27};
	MSG m2 = {6,16,26};
	MSG m3 = {5,15,25};
	MSG m4 = {4,14,24};
	MSG m5 = {4,34,44};
	//发送信息到消息队列
																														
	if(msgsnd(msq_id,&m1,sizeof(MSG)-sizeof(long),IPC_NOWAIT)<0){
		perror("msgsnd error");
	}
	if(msgsnd(msq_id,&m2,sizeof(MSG)-sizeof(long),IPC_NOWAIT)<0){
		perror("msgsnd error");
	}
	if(msgsnd(msq_id,&m3,sizeof(MSG)-sizeof(long),IPC_NOWAIT)<0){
		perror("msgsnd error");
	}
	if(msgsnd(msq_id,&m4,sizeof(MSG)-sizeof(long),IPC_NOWAIT)<0){
		perror("msgsnd error");
	}
	if(msgsnd(msq_id,&m5,sizeof(MSG)-sizeof(long),IPC_NOWAIT)<0){
		perror("msgsnd error");
	}
	//获取当前消息队列中 消息的总数
	struct msqid_ds ds;
	if(msgctl(msq_id,IPC_STAT,&ds)<0){
		perror("msgctl error");
	}
	printf("msg total:%ld\n",ds.msg_qnum);

	exit(0);
}
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc msqwrite.c -o mw
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mw 
usage:./mw key
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mw 223
key:223
msq id:0
msg total:5
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ 
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ipcs -q

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      
0x000000df 0          kshine     777        40           5 

程序2 从消息队列中读取消息

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>

typedef struct{
	long type;//消息类型
	int data[2];
}MSG;

//传入key和类型
int main(int argc,char* argv[])
{

	if(argc<3){
		printf("usage:%s key type\n",argv[0]);
		exit(1);
	}

	key_t key = atoi(argv[1]);
	long type = atoi(argv[2]);
	//获取指定的消息队列
	int msq_id = msgget(key,0777);
	if(msq_id<0)perror("msgget error");
	printf("msq_id:%d\n",msq_id);

	//从消息队列中接收指定类型的消息
	MSG m;
	if(msgrcv(msq_id,&m,sizeof(MSG)-sizeof(long),type,IPC_NOWAIT)<0)
	{
		perror("msgrcv error");
	}else{
		printf("type:%ld data:%d %d\n",m.type,m.data[0],m.data[1]);
	}

	exit(1);
}

从消息队列中读取数据,测试完成后,需要用户删除改消息队列。

kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc msqread.c -o mr
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223
usage:./mr key type
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 7
msq_id:0
type:7 data:17 27
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 6
msq_id:0
type:6 data:16 26
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 5
msq_id:0
type:5 data:15 25
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 4
msq_id:0
type:4 data:14 24
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 4
msq_id:0
type:4 data:34 44
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 4
msq_id:0
msgrcv error: No message of desired type
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ipcs -q

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      
0x000000df 0          kshine     777        0            0           

kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ipcrm -q 0
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ipcs -q

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息

5. IPC对象 - 共享内存

  • 共享内存是被多个进程共享的一部分物理内存。
  • 共享内存被映射到进程的虚拟内存空间。通过虚拟地址操作共享内存。
  • 不提供同步机制。
  • 效率最高的IPC机制。
    共享内存

5.1 共享内存的属性

struct shmid_ds{
	struct ipc_perm shm_perm;	/* 权限,访问模式,创建用户信息等等*/
	int shm_segsz;             	/*共享内存段的大小(以字节为单位)*/
	time_t shm_atime;          	/*最后一个映射成功的时间*/
	time_t shm_dtime;          	/*最后一个解除映射的时间*/
	time_t shm_ctime;          	/*最后一个改变的时间*/
	unsigned short shm_cpid;   	/*创建共享内存的进程的id/
	unsigned short shm_lpid;  	/*最后一次调用该共享内存的进程的pid*/
	short shm_nattch;          	/*当前已经成功映射的进程的数量*/
/*下面是私有的*/
	unsigned short shm_npages;  /*段的大小(以页为单位)*/
	unsigned long *shm_pages;   /*指向frames->SHMMAX的指针数组*/
	struct vm_area_struct *attaches; /*对共享段的描述*/
};
  • 操作步骤:创建shmget,映射shmat。

5.2 创建共享内存

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
  • key 用户指定或者IPC_PRIVATE,或者ftok()
  • size 共享内存的大小
  • shmflag 权限组合,IPC_CREAT,IPC_EXCL

5.3 控制共享内存

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid 共享内存Id
  • cmd 控制命令。IPC_STAT 获取共享内存的属性。IPC_SET 设置属性,IPC_RMID 删除共享内存,SHM_LOCK 锁定,SHM_UNLOCK 解锁
  • buf 共享内存的属性指针

5.4 映射和解除映射

#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
  • 返回虚拟内存地址,失败返回-1
  • shmaddr 可以用户设置的虚拟地址,建议写0,由系统自动分配。
  • shmflag 标志,一般设置为0。

5.5 参考案例

两个进程对共享内存进行操作。一个进程操作,另一个阻塞,交替工作。
1)用于管理匿名管道的文件
tell.h

#ifndef _TELL_H_
#define _TELL_H_
//管道初始化
extern void init();
//利用管道进行等待
extern void wait_pipe();
//利用管道进行通知
extern void notify_pipe();
//销毁管道
extern void destory_pipe();
#endif

tell.c

#include "tell.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int fd[2];

//管道初始化
void init()
{
	if(pipe(fd)<0){

		perror("pipe error");
	}

}

//利用管道进行等待
void wait_pipe()
{
	char c;
	if(read(fd[0],&c,1)<0){
		perror("wait pipe error");
	}
}

//利用管道进行通知
void notify_pipe()
{
	char c='a';
	if(write(fd[1],&c,1)!=1){
		perror("notify pipe error");
	}
}

//销毁管道
void destory_pipe()
{
	close(fd[0]);
	close(fd[1]);
}

创建共享内存,并写入和读取数据

#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "tell.h"
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
	//创建共享内存
	int shmid;
	if((shmid = shmget(IPC_PRIVATE,1024,IPC_CREAT|IPC_EXCL|0777))<0)
	{
		perror("shmget error");
		exit(1);
	}

	pid_t pid;
	init();//初始化管道
	if((pid = fork())<0)
	{
		perror("fork error");
		exit(1);
	}else if(pid>0){
		int *pi = (int*)shmat(shmid,0,0);
		if(pi == (int*)-1){
			perror("shmat error");
			exit(1);
		}
		//写入数据
		*pi =100;
		*(pi+1)=200;
		//解除映射
		shmdt(pi);
		notify_pipe();//通知子进程
		destory_pipe();
		wait(0);
	}else if(pid ==0){
		wait_pipe();//子进程等待父进程的通知
		int *pi = (int*)shmat(shmid,0,0);
		if(pi == (int*)-1){
			perror("shmat error");
			exit(1);
		}
		printf("start:%d end:%d\n",*pi,*(pi+1));
		shmdt(pi);//解除映射
		shmctl(shmid,IPC_RMID,NULL);//在此,所有的映射都被解除,删除共享内存
		destory_pipe();
	}

	exit(1);
}

运行结果

kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc shm.c tell.c -I./ -o shm
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./shm
start:100 end:200
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$

6 IPC对象 - 进程间的信号量

6.1 进程信号量的概念

  • 用于进程间的互斥和同步。
  • 一个共享资源需要一个信号量。引入信号量集 来管理多个共享资源(类似于去图书馆借书,借阅多本的场景)。
  • 信号量集是多个信号量的集合。可以对集合内的信号量进行统一操作。
  • 信号量集里的信号量操作,可以要求全部成功,可以要求部分成功。
  • 二元信号量(0和1)。信号灯。
  • 对信号量进行PV操作。

6.2 信号量集属性

struct semid_ds {
    struct ipc_perm sem_perm;  /* 权限和用户ID相关 */
    time_t          sem_otime; /* 最后一次操作时间 */
    time_t          sem_ctime; /* 最后一次改变时间 */
    unsigned long   sem_nsems; /* 信号量集中信号灯的数量 */
};

6.3 创建信号量集

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
  • key 用户指定的键值
  • 信号量集中的信号量个数
  • samflg IPC_CREAT,IPC_EXCL等权限组合

6.4 控制信号量集

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
  • semid 信号量集ID
  • semnum 0表示对所有的信号量进行操作,信号量编号从0开始
  • cmd 操作指定,IPC_STAT,IPC_SET,IPC_RMID,GETVAL,SETVAL,GETALL,SETALL
  • 信号量属性结构定义,根据操作不同使用联合体中的相应类型变量
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) */
           };

6.5 信号量集的操作

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops,const struct timespec *timeout);
  • semid 信号量集ID
  • sops sembuf结构体数组指针
  • nsops 第二个参数sops中结构体数组的长度。
struct sembuf{
    unsigned short sem_num;  /*信号量ID*/
    short          sem_op;   /* semaphore operation */
    short          sem_flg;  /* operation flags */
};
  • sem_num 信号量编号
  • sep_op 正数为V操作,负数为P操作。0可用于测试 共享资源是否已用完的。
  • sem_flg SEM_UNDO标志,表示在进程结束时,操作将被取消。如果进程没有释放共享资源,内核将代为释放。 IPC_NOWAIT 非阻塞标志。

6.6 参考实例

封装一个信号量集模块(方便于调用)
定义一个文件pv.h

#ifndef __PV_H_
#define __PV_H_
//初始化 semnums个信号灯
int I(int semnums,int value);
//对信号量集(semid)中的信号灯(semnum)做P(value)操作
void P(int semid,int semnum,int value);
//对信号量集(semid)中的信号灯(semnum)做V(value)操作
void V(int semid,int semnum,int value);
//销毁信号量集
extern void D(int semid);
#endif

创建pv.c文件

#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <malloc.h>
#include "pv.h" 

union semun{
	int val;
	struct semid_ds *buf;
	unsigned short *array;
};

//初始化 semnums个信号灯
int I(int semnums,int value)
{
	//创建信号量集
	int semid = semget(IPC_PRIVATE,semnums,IPC_CREAT|IPC_EXCL|0777);
	if(semid<0){
		return -1;
	}
	union semun un;
	unsigned short* array = (unsigned short* )calloc(semnums,sizeof(unsigned short));
	int i;
	for(i=0;i<semnums;i++){
		array[i]=value;
	}
	un.array =array;
	//初始化信号量集中所有信号灯的初值
	if(semctl(semid,0,SETALL,un)<0)
	{
		perror("semctl error");
		return -2;
	}
	free(array);
	return semid;
}
//对信号量集(semid)中的信号灯(semnum)做P(value)操作
void P(int semid,int semnum,int value)
{
	assert(value>=0);
	//定义sembuf类型的结构体数组,放置若干结构体变量
	struct sembuf ops[] = {
		{semnum,-value,SEM_UNDO}//1个成员
		//{},
		//{}
	}
	if(semop(semid,ops,sizeof(ops)/sizeof(struct sembuf))<0){
		perror("semop error");
	}
}
//对信号量集(semid)中的信号灯(semnum)做V(value)操作
void V(int semid,int semnum,int value)
{
	assert(value>=0);
	//定义sembuf类型的结构体数组,放置若干结构体变量
	struct sembuf ops[] = {
		{semnum,value,SEM_UNDO}//1个成员
		//{},
		//{}
	}
	if(semop(semid,ops,sizeof(ops)/sizeof(struct sembuf))<0){
		perror("semop error");
	}
}
//销毁信号量集
void D(int semid)
{
	if(semctl(semid,0,IPC_RMID,NULL)<0){
		perror("semctl error");
	}
}

关于调用:

I(semid,1);//初始化信号量集,初值为1

//...

//进程1
P(semid,0,1);//减操作,值变成0(如果此时值已经为0,则阻塞在此)
//... 对共享资源进行操作(如果异常提前跳出,记得需要做V操作)
V(semid,0,1);//(正常退出占用)加操作,值变成1

//进程2
P(semid,0,1);//减操作,值变成0(如果此时值已经为0,则阻塞在此)
//... 对共享资源进行操作(如果异常提前跳出,记得需要做V操作)
V(semid,0,1);//(正常退出占用)加操作,值变成1

6.结束

如有写错的地方欢迎指正,我将及时更正。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值