进程间通信方式(管道、信号、信号量、共享内存、消息队列)

进程间通信(IPC)介绍

Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)

通讯目的:

  1. 数据传输:一个进程需要将数据发送给另外一个进程。
  2. 资源共享多个进程之间共享同样的资源。
  3. 通知事件:一个进程需要向另外一个/组进程发送消息,通知它们发生了某事件。
  4. 进程控制:有些进程希望完全控制另外一个进程的执行,此时控制进程希望能够拦截另外一个进程的所有操作,并能够及时知道它的状态改变。

一、管道

一个进程在管道的尾部写入数据,另一个进程从管道的头部读出数据。管道包括无名管道有名管道两种,前者只能用于父进程和子进程之间的通信,后者可与用于运行与同一系统中的任意两个进程间的通信。

管道通信的特点

  1. 管道通信是单向的,有固定的读端写端
  2. 数据被进程从管道读出后,在管道中该数据就不存在了。
  3. 当进程去读取空管道的时候,进程会阻塞
  4. 当进程往满管道写入数据时,进程会阻塞管道容量为64KB

1.无名管道

管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。

特点

  1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
  2. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

原型:

创建无名管道:

#include <unistd.h> 

int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1

管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:

  1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端
  2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
  3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。

练习:父子进程使用管道通信,父写入字符串,子进程读出并,打印到屏幕。

思考:为甚么,程序中没有使用sleep函数,但依然能保证子进程运行时一定会读到数据呢??????(wait阻塞等待)

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

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(void)
{
	pid_t pid;
	char buf[1024];
	int fd[2];
	char *p = "test for pipe\n";
    
	if (pipe(fd) == -1) 
		sys_err("pipe");

	pid = fork();
	if (pid < 0) 
		sys_err("fork err");
   
	else if (pid == 0)   // 子进程读数据
	{
		//sleep(1);
		close(fd[1]);
		int len = read(fd[0], buf, sizeof(buf));
		write(STDOUT_FILENO, buf, len);
		close(fd[0]);
	} 
	else 
	{
		close(fd[0]);
		write(fd[1], p, strlen(p));  // 父进程写数据
		wait(NULL);
		close(fd[1]);
	}
    
    return 0;
}

运行结果

管道的读写行为    

使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):

  1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
  2.  如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
  3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。
  4. 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

总结:

① 读管道: 1. 管道中有数据,read返回实际读到的字节数。

                    2. 管道中无数据:

                       (1) 管道写端被全部关闭,read返回0 (好像读到文件结尾)

                       (2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu,fcntl函数可以更改非阻塞)

② 写管道: 1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)

                   2. 管道读端没有全部关闭:

                      (1) 管道已满,write阻塞。

                      (2) 管道未满,write将数据写入,并返回实际写入的字节数。

练习1:使用管道实现父子进程间通信,完成:ls | wc –l假定父进程实现ls,子进程实现wc

ls命令正常会将结果集写出到stdout但现在会写入管道的写端wc –l 正常应该从stdin读取数据但此时会从管道的读端读。    

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

int main(void)
{
	pid_t pid;
	int fd[2];

	pipe(fd);
	pid = fork();

	if (pid == 0)
	{ 
		close(fd[1]);	                //子进程从管道中读数据,关闭写端
		dup2(fd[0], STDIN_FILENO);		// 标准输入重定向管道读端
		execlp("wc", "wc", "-l", NULL);	//wc命令默认从标准读入取数据

	} 
	else 
	{
		close(fd[0]);	                //父进程向管道中写数据,关闭读端
		dup2(fd[1], STDOUT_FILENO);		// 标准输出重定向管道写端
		execlp("ls", "ls", NULL);		//ls输出结果默认对应屏幕
	}

	return 0;
}

运行结果

程序执行,发现程序执行结束,shell还在阻塞等待用户输入。这是因为,shell → fork → ./pipe1, 程序pipe1的子进程将stdin重定向给管道,父进程执行的ls会将结果集通过管道写给子进程。若父进程在子进程打印wc的结果到屏幕之前被shell调用wait回收,shell就会先输出$提示符。

练习2:使用管道实现兄弟进程间通信。 兄:ls  弟: wc -l  父:等待回收子进程。要求,使用“循环创建N个子进程”模型创建兄弟进程,使用循环因子i标示。注意管道读写行为。

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

int main(void)
{
	pid_t pid;
	int fd[2], i;
	
	pipe(fd);

	for (i = 0; i < 2; i++) 
	{
		if((pid = fork()) == 0) 
		{
			break;
        }
    }

	if (i == 0) 
	{			//兄
		close(fd[0]);				//写,关闭读端
		dup2(fd[1], STDOUT_FILENO);		
		execlp("ls", "ls", NULL);	
	} 
	else if (i == 1) 
	{	//弟
		close(fd[1]);				//读,关闭写端
		dup2(fd[0], STDIN_FILENO);		
		execlp("wc", "wc", "-l", NULL);		
	} 
	else 
	{
		close(fd[0]);
		close(fd[1]);
		for(i = 0; i < 2; i++)		//两个儿子wait两次
		wait(NULL);
	}

	return 0;
}

运行结果

测试:是否允许,一个pipe有一个写端,多个读端呢?是否允许有一个读端多个写端呢?

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

int main(void)
{
	pid_t pid;
	int fd[2], i, n;
	char buf[1024];

	int ret = pipe(fd);
	if(ret == -1)
	{
		perror("pipe error");
		exit(1);
	}

	for(i = 0; i < 2; i++)
	{
		if((pid = fork()) == 0)
			break;
		else if(pid == -1)
		{
			perror("pipe error");
			exit(1);
		}
	}

	if (i == 0) 
	{			
		close(fd[0]);				
		write(fd[1], "hello ", strlen("hello "));
	} 
	else if(i == 1) 
	{	
		sleep(1);
		close(fd[0]);				
		write(fd[1], "world\n", strlen("world\n"));
	} 
	else 
	{
		sleep(2)
		close(fd[1]);       //父进程关闭写端,留读端读取数据    
		n = read(fd[0], buf, 1024);     //从管道中读数据
		write(STDOUT_FILENO, buf, n);

		for(i = 0; i < 2; i++)		//两个儿子wait两次
			wait(NULL);
	}

	return 0;
}

运行结果

inux@ubuntu64-vm:~/workdir/test$ ./pipe3 
hello world
linux@ubuntu64-vm:~/workdir/test$ 

道缓冲区大小

  • 可以使用ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:
  • pipe size            (512 bytes, -p) 8     ulimit -a查看
  • 也可以使用fpathconf函数,借助参数 选项来查看。使用该宏应引入头文件<unistd.h>
  • long fpathconf(int fd, int name); 成功:返回管道的大小 失败:-1,设置errno

管道的优劣

  • 优点:简单,相比信号,套接字实现进程间通信,简单很多。
  • 缺点:1. 只能单向通信,双向通信需建立两个管道; 2. 只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决。

2.FIFO

  • FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。
  • FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信

FIFO文件的特点:

  1. 读取FIFO文件的进程只能以RDONLY方式打开FIFO文件。
  2. 写入FIFO文件的进程只能以WRONLY方式打开FIFO文件。
  3. FIFO文件里面的内容被读取后就消失了,但普通文件里面的内容被读取后还存在。
  4. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。(与无名管道不同)

原型:

创建有名管道:

  • 1. 命令:mkfifo 管道名
  • 2. 库函数:int mkfifo(const char *pathname,  mode_t mode);  成功:0; 失败:-1
  • 一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它
  • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 。如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

例子

FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。下面的例子演示了使用 FIFO 进行 IPC 的过程:

代码

fifo_w.c

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

void sys_err(char *str)
{
    perror(str);
    exit(-1);
}

int main(void)
{
    int fd, i;
    char buf[4096];

    fd = open("./myfifo", O_WRONLY);
    if (fd < 0) 
        sys_err("open");

    i = 0;
    while (1) 
	{
		memset(buf, 0x00, sizeof(buf));
		sprintf(buf, "hello itcast %d\n", i++);
		write(fd, buf, strlen(buf));
		sleep(1);
    }
    
	close(fd);
    return 0;
}

fifo_r.c

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

void sys_err(char *str)
{
    perror(str);
    exit(-1);
}

int main(void)
{
    int fd, len;
    char buf[4096];

    fd = open("./myfifo", O_RDONLY);
    if (fd < 0) 
        sys_err("open");
    while (1) 
	{
		len = read(fd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, len);
		sleep(3);           //多個读端时应增加睡眠秒数,放大效果.
    }
    
	close(fd);
    return 0;
}

二、信号

在Linux系统中,信号(signal)是一种事件通知的进程通讯方式。

原型:

向一个进程发送信号

#include <sys/types.h>
#include <signal.h>
// 成功返回0,失败返回-1
int kill(pid_t pid, int sig);

pid:pid>0,pid参数指向接收信号的进程;sig:指明我们要发送的信号。

处理信号:参考之前写的博客

https://blog.csdn.net/qq_22847457/article/details/89278451

https://blog.csdn.net/qq_22847457/article/details/89113559

例子:A、B进程利用信号通讯:A进程发送SIGINT信号给B进程;B进程设置SIGINT的处理方式,然后等待pause。

代码:

bprocess.c

#include <signal.h>
#include <unistd.h>

void myfunc(int a)
{
    printf("Process B received SIGINT\n");	
}

void main()
{
    signal(SIGINT, myfunc);
    
    pause();	
}

aprocess.c

#include <sys/types.h>
#include <signal.h>
#include <stdio.h>

void main(int argc, char *argv[])
{
    pid_t pid;
    
    pid = atoi(argv[1]);    // B进程进程号
    
    kill(pid, SIGINT);
}

运行结果

三、信号量

信号量本质上是一个计数器(不设置全局变量是因为进程间是相互独立的,而这不一定能看到,看到也不能保证++引用计数为原子操作),用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。

  • 临界资源:操作系统中将同一时刻只允许一个进程访问的资源称为临界资源;
  • 进程互斥:当有若干个进程都要使用某一资源时,但该资源在某一时刻最多允许一个进程使用,这时其他进程必须等待,直到占用该资源者释放了该资源为止;

信号量的工作原理

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

  1. P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行;
  2. V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。

在信号量进行PV操作时都为原子操作(因为它需要保护临界资源)

注:原子操作:单指令的操作称为原子的,单条指令的执行是不会被打断的

二元信号量

二元信号量(Binary Semaphore)是最简单的一种锁(互斥锁),它只用两种状态:占用非占用。所以它的引用计数为1。

进程如何获得共享资源

  1. 测试控制该资源的信号量
  2. 信号量的值为正,进程获得该资源的使用权,进程将信号量减1,表示它使用了一个资源单位
  3. 若此时信号量的值为0,则进程进入挂起状态(进程状态改变),直到信号量的值大于0,若进程被唤醒则返回至第一步。

注:信号量通过同步与互斥保证访问资源的一致性

与信号量相关的函数

创建/打开信号量集合

int semget(key_t key,int nsems,int flags)//返回:成功返回信号集ID,出错返回-1
  1. 第一个参数key是长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值(键值)。通常情况下,该id值通过ftok函数得到(详见ftok()函数解析),由内核变成标识符,要想让两个进程看到同一个信号集,只需设置key值不变就可以。
  2. 第二个参数nsem指定信号量集中需要的信号量数目,它的值几乎总是1。
  3. 第三个参数flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。 
  4. 设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。一般我们会还或上一个文件权限。

删除和初始化信号量

int semctl(int semid, int semnum, int cmd, ...);

如有需要第四个参数一般设置为union semnu arg;定义如下

union semun
{  
    int val;  //使用的值
    struct semid_ds *buf;  //IPC_STAT、IPC_SET 使用的缓存区
    unsigned short *arry;  //GETALL,、SETALL 使用的数组
    struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区 
};  

  1. sem_id是由semget返回的信号量集合标识符;
  2. semnum当前信号量集的哪一个信号量;
  3. cmd通常是下面两个值中的其中一个 SETVAL用来把信号量初始化为一个已知的值。这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置 IPC_RMID用于删除一个已经无需继续使用的信号量标识符,删除的话就不需要缺省参数,只需要三个参数即可。

改变信号量的值

int semop(int semid, struct sembuf *sops, size_t nops); 
  1. semid:要操作的信号量集合标志符;
  2. nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作; 
  3. sops:对信号执行什么样的操作,sembuf的定义如下:
struct sembuf
{  
    short sem_num;   //除非使用一组信号量,否则它为0  
    short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数,                                         
                    //一个是-1,即P(等待)操作,  
                    //一个是+1,即V(发送信号)操作。  
    short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量,  
                  //并在进程没有释放该信号量而终止时,操作系统释放信号量  
};  

例子:利用信号量互斥,A、B同学往公告栏中写入数据

代码:

studentA.c:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>

void main()
{
    int fd = 0;
    key_t key;
    int semid;
    struct sembuf sops;
    
    // 1.创建键值 
    key = ftok("/home", 1);
    
    /* 2.创建并打开信号量集合 */
    semid= semget(key, 1, IPC_CREAT);
             
    /* 3.打开公告栏 */
    fd = open("./board.txt", O_RDWR | O_APPEND);
    
    semctl(semid, 0, SETVAL, 1);   // 设置初始值为1
	
    /* 4.获取信号量 */
    sops.sem_num = 0;
    sops.sem_op = -1;
    sops.sem_flg = SEM_UNDO;

    semop(semid, &sops, 1);
    
    /* 5.向公告板文件里写入“数学课” */
    write(fd, "class math", 11);
        
    /* 6.休息10秒钟 */
    sleep(10);
    
    /* 7.向公告板文件里写入“取消” */
    write(fd, "is cancel", 11);
    
    /* 8.释放信号量 */
    sops.sem_num = 0;
    sops.sem_op = +1;
    semop(semid, &sops, 1);
    
    /* 9.关闭公告板文件 */
    close(fd);
} 

studentB.c:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>

void main()
{
    int fd = 0;
    key_t key;
    int semid;
    int ret;
    struct sembuf sops;
      
    /* 1.创造键值 */
    key = ftok("/home", 1);
    
    /* 2.打开信号量集合 */
    semid= semget(key, 1, IPC_CREAT);
    
    /* 3.打开公告板 */
    fd = open("./board.txt", O_RDWR | O_APPEND);
    
    ret = semctl(semid, 0, GETVAL);
    printf("ret is %d\n",ret);
    
    /* 4.获取信号量 */
    sops.sem_num = 0;
    sops.sem_op = -1;
    semop(semid, &sops, 1);
       
    /* 5.写入英语课考试 */
    write(fd, "english exam ", 20);
    
    /* 6.释放信号量 */
    sops.sem_num = 0;
    sops.sem_op = +1;
    sops.sem_flg = SEM_UNDO;

    semop(semid, &sops, 1);
    
    /* 7.关闭公告板文件 */ 
    close(fd);
}

运行结果:

可见,A同学写入class math后休息的10s时间内,B同学进程一直在等待,直到A同学写完,释放信号量,B同学才执行写入操作。

例2:利用信号量同步编程,解决生产者与消费者问题,生产者释放信号量,消费者获取信号量。

producor.c

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sem.h>
#include <sys/ipc.h>

void main()
{
    int fd;
    key_t key;
    int semid;
    struct sembuf sops;
    
    // 1.创建键值
    key = ftok("/home", 2);
       
    // 2.创建信号量集合
    semid = semget(key, 1, IPC_CREAT);
    
    // 3.设置信号量的值为0
    semctl(semid, 0, SETVAL, 0);
    
    // 4.创建产品文件
    fd = open("./product.txt", O_RDWR | O_CREAT);	

    // 5.休息10s
    sleep(10);
    
    // 6.向产品文件里写入数据
    write(fd, "the product is fished", 25);
           
    // 7.释放信号量
    sops.sem_num = 0; 
    sops.sem_op = +1;
    sops.sem_flg = SEM_UNDO;
   
    semop(semid, &sops, 1);

    // 8.关闭产品文件
    close(fd);
}

customer.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <sys/sem.h>

void main()
{
    key_t key;
    int semid;
    struct sembuf sops;
    int ret;
        
    // 1.创建键值
    key = ftok("/home", 2);
    
    // 2.创建信号量集合
    semid = semget(key, 1, IPC_CREAT);
    
    // 3.获取信号量
    sops.sem_num = 0; 
    sops.sem_op = -1;
    sops.sem_flg = SEM_UNDO;
	
    ret = semop(semid, &sops, 1);
    
    // 4.打印信号量的值
    printf("ret is %d!\n",ret);
                   
    // 5.取走产品文件
    system("cp ./product.txt ./ship");  // system系统调用
}

四、共享内存

共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

特别提醒:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以我们通常需要用其他的机制来同步对共享内存的访问,例如前面说到的信号量。

与信号量一样,在Linux中也提供了一组函数接口用于使用共享内存,而且使用共享共存的接口还与信号量的非常相似,而且比使用信号量的接口来得简单。它们声明在头文件 sys/shm.h中。

shmget函数

创建\获取共享内存,它的原型为:

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

成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1。

  • key:共享内存键值;
  • size:以字节为单位指定需要共享的内存容量;
  • shmflg:权限标志,key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。

shmat函数 

映射共享内存,第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:

void *shmat(int shm_id, const void *shm_addr, int shmflg);

调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.

  • shm_id:由shmget函数返回的共享内存标识。
  • shm_addr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址
  • shm_flg:标志位,通常为0。

shmdt函数
该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:

int shmdt(const void *shmaddr);

参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.

shmctl函数
与信号量的semctl函数一样,用来控制共享内存,它的原型如下:

int shmctl(int shm_id, int command, struct shmid_ds *buf);
  • shm_id是shmget函数返回的共享内存标识符。
  • command是要采取的操作,它可以取下面的三个值 :

IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。

IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值

IPC_RMID删除共享内存段

  • buf是一个结构指针,它指向共享内存模式和访问权限的结构。

shmid_ds结构至少包括以下成员:

struct shmid_ds
{
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
};
  1. 当用shmget函数创建一段共享内存时,必须指定其 size;而如果引用一个已存在的共享内存,则将 size 指定为0 。
  2. 当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。
  3. shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
  4. shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。

实例:A进程创建创建内存,并将其映射到自己的内存地址空间,A进程往该空间中写入数据;B进程也将该共享内存映射到自己的内存地址空间,读取其中的数据。A、B进程断开与该共享内存的连接,A进程删除该共享内存。

代码

write.c

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

#define TEXT_SZ 2048

struct shared_use_st
{
    int written_by_you; // 标志
    char some_text[TEXT_SZ];	
};

int main()
{
    int running = 1;  // 循环标志
    int shmid;    
    struct shared_use_st *shared_stuff;
    char buffer[TEXT_SZ];
            
    // 1.创建共享内存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), IPC_CREAT | 0777);  // 键值1234
    
    if (shmid == -1)
    {
        printf("creat share memory fail!\n");
        exit(EXIT_FAILURE);	
    }
 
    // 2.映射共享内存
	shared_stuff = (struct shared_use_st *)shmat(shmid, NULL, 0);

    // 3.循环
    while (running)
    {
        while (shared_stuff->written_by_you == 1)
        {
            sleep(1);
            printf("wait read process!\n");
        }
        
        // 3.1获取用户输入
        fgets(buffer, TEXT_SZ, stdin);
              
        // 3.2将用户输入的字符串放入共享内存
        strncpy(shared_stuff->some_text, buffer, TEXT_SZ);
        shared_stuff->written_by_you = 1;
		
        if (strncmp(buffer, "end", 3) == 0)
            running = 0;
    }
    
    // 4.脱离共享内存
    shmdt((const void *)shared_stuff);

    return 1;
}

read.c

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

#define TEXT_SZ 2048

struct shared_use_st
{
    int written_by_you;   // 标志
    char some_text[TEXT_SZ];	
};

int main()
{
    int shmid;
    int running = 1;  // 循环标志
    struct shared_use_st *shared_stuff;
    
    // 1.获取共享内存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), IPC_CREAT | 0777);
    
    // 2.映射共享内存
    shared_stuff = (struct shared_use_st *)shmat(shmid, NULL, 0);
    shared_stuff->written_by_you = 0;
    
    // 3.循环
    while (running)
    {
        // 3.1打印共享内存
        if (shared_stuff->written_by_you == 1)
        {
            printf("write process write: %s\n",shared_stuff->some_text);	
    	    shared_stuff->written_by_you = 0;
    	    
    	    if (strncmp(shared_stuff->some_text, "end", 3)== 0)
                running = 0;
        }      
    }	
	
    // 4.脱离共享内存
    shmdt((const void *)shared_stuff);
    
    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, 0);
    
    return 1;
}

运行结果

五、消息队列

消息队列,是消息的链接表,消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。  每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。

特点:

  1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
  3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

Linux用宏MSGMAXMSGMNB来限制一条消息的最大长度和一个队列的最大长度。

Linux提供了一系列消息队列的函数接口来让我们方便地使用它来实现进程间的通信。它的用法与其他两个System V PIC机制,即信号量和共享内存相似。

msgget函数

该函数用来创建和打开一个消息队列。它的原型为:

int msgget(key_t, key, int msgflg);

成功返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1.

  • key:消息队列键值
  • msgflg:msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。

msgsnd函数

该函数用来把消息添加到消息队列中。它的原型为:

int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
  • msgid:由msgget函数返回的消息队列标识符。
  • msg_ptr:是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。所以消息结构要定义成这样:
struct my_message
{
    long int message_type;
    /* The data you wish to transfer*/
};
  • msg_sz:msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。
  • msgflg:用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。

如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1.

msgrcv函数
该函数用来从一个消息队列获取消息,它的原型为

int msgrcv(int msgid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
  • msgid:由msgget函数返回的消息队列标识符。
  • msgtype:可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息
  • msgflg:用于控制当队列中没有相应类型的消息可以接收时将发生的事情。

调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1.

msgctl函数
该函数用来控制消息队列,它与共享内存的shmctl函数相似,它的原型为:

int msgctl(int msgid, int command, struct msgid_ds *buf);

command是将要采取的动作,它可以取3个值,

  •     IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
  •     IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
  •     IPC_RMID:删除消息队列

buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。msgid_ds结构至少包括以下成员:

struct msgid_ds
{
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
};

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

实例:A进程负责从键盘获取用户输入字符串,将之构造成消息,并添加到消息队列里面去;B进程负责从消息队列中取出消息,然后打印消息里面的信息。

代码

send.c

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

struct msgt
{
    long msgtype;  // 消息类型
    char msgtext[1024];	
};

void main()
{
    int msgid;
    int msg_type;
    char str[256];
    struct msgt msgs;
    
    // 1.创建消息队列
    msgid = msgget((key_t)1024, IPC_CREAT | 0777); // 键值1024
    
    // 2.循环
    while (1)
    {
        printf("please input message type, 0 for quit!\n");
        
        // 2.1获取消息类型
        scanf("%d",&msg_type);
                
        // 2.2如果用户输入的消息为0,退出该循环
        if (msg_type == 0)
            break;
        
        // 2.3获取消息数据
        printf("please input message content!\n");
        scanf("%s",str);
        
        msgs.msgtype = msg_type; 
        strcpy(msgs.msgtext,str);
                     
        //2.将消息加入消息队列
        msgsnd(msgid, &msgs, sizeof(msgs.msgtext), 0);
    }	
	
    // 3.删除消息队列
    msgctl(msgid, IPC_RMID, 0);
}	
	


receive.c

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

struct msgt
{
    long msgtype;
    char msgtext[1024];
};

int msgid = 0;

void childprocess()
{
    struct msgt msgs;
    
    while(1)
    {
        // 1.接收消息队列
        msgrcv(msgid, &msgs, sizeof(msgs.msgtext), 0, 0);
    
        // 2.打印消息队列的数
        printf("msg text: %s\n", msgs.msgtext);
    }
}

void main()
{     
    int i;
    int cpid;
	
    // 1.打开消息队列
    msgid = msgget((key_t)1024, IPC_EXCL);
    
    // 2.创建3个子进程
    for (i = 0; i < 3; i++)
    {
        cpid = fork();
        
        if (cpid < 0)
        {
            printf("creat child process error!\n");	
        }    
        else if (cpid ==0)
        {
            childprocess();	        
        }   
     }
}

五种通讯方式总结:

  1. 管道:速度慢,容量有限,只有父子进程能通讯;    
  2. FIFO:任何进程间都能通讯,但速度慢;    
  3. 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;    
  4. 信号量:不能传递复杂消息,只能用来同步;    
  5. 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。

猜你喜欢

转载自blog.csdn.net/qq_22847457/article/details/89294090
今日推荐