Linux 进程间通信(内容较长)

Linux系统中的进程通信方式主要以下几种:

  1. 同一主机上的进程通信方式:
    * UNIX进程间通信方式: 包括管道(PIPE), 有名管道(FIFO), 和信号(Signal)
    * System V进程通信方式:包括信号量(Semaphore), 消息队列(Message Queue), 和共享内存(Shared Memory)
  2. 网络主机间的进程通信方式
    * RPC: Remote Procedure Call 远程过程调用
    * Socket: 当前最流行的网络通信方式, 基于TCP/IP协议的通信方式.

本文记录经典的IPC:pipes, FIFOs, message queues, semaphores, and shared memory。


线程是一种轻量级的进程。

Linux系统中的线程通信方式主要以下几种:

  • 锁机制:包括互斥锁、条件变量、读写锁。互斥锁提供了以排他方式防止数据结构被并发修改的方法。使用条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
  • 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
  • 信号机制(Signal):类似进程间的信号处理


进程的通信机制主要包括无名管道、有名管道、消息队列、信号量、共享内存以及信号等。这些机制都是由linux内核来维护的,实现起来都比较复杂,而且占用大量的系统资源。
线程间的通信机制实现起来则相对简单,主要包括互斥锁、条件变量、读写锁和线程信号等。


1. 关于进程间通信

进程通信的概念

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到。所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信 (IPC,InterProcess Com)。进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。

用于进程间通讯(IPC)的四种不同技术:
1. 消息传递(管道,FIFO,posix和system v消息队列)
2. 同步(互斥锁,条件变量,读写锁,文件和记录锁,Posix和System V信号灯)
3. 共享内存区(匿名共享内存区,有名Posix共享内存区,有名System V共享内存区)
4. 过程调用(Solaris门,Sun RPC)
消息队列和过程调用往往单独使用,也就是说它们通常提供了自己的同步机制.相反,共享内存区通常需要由应用程序提供的某种同步形式才能正常工作.解决某个特定问题应使用哪种IPC不存在简单的判定,应该逐渐熟悉各种IPC形式提供的机制,然后根据特定应用的要求比较它们的特性.

进程通信的应用场景

数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

2. Linux下进程间通信的六种机制

一些概念

  1. 单工:数据传输只支持数据在一个方向上传输。
  2. 半双工:数据传输允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;
  3. 双工(全双工):数据通信允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。

linux下进程间通信主要有6种方式:

  1. 管道(Pipe 又叫 匿名管道)有名管道(named pipe)匿名管道是一种半双工的通信方式,数据只能单向流动,且只能用于有亲缘关系进程间的通信,进程的亲缘关系通常是指父子进程关系。有名管道也是半双工的通信方式,但是克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
    管道包括三种:
    ①. 普通管道PIPE: 通常有两种限制,一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用.
    ②. 流管道s_pipe: 去除了第一种限制,为半双工,只能在父子或兄弟进程间使用,可以双向传输.
    ③. 命名管道:name_pipe:去除了第二种限制,可以在许多并不相关的进程之间进行通讯.
    匿名管道的特征或者说局限性:
    ①. 只提供单向通信。当两个进程都能访问这个文件,假设进程1往文件内写东西,那么进程2 就只能读取文件的内容。
    ②. 只能用于具有血缘关系的进程间通信,通常用于父子进程建通信
    ③. 基于字节流来通信的,即所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式。
    比如多少字节算作一个消息(或命令、或记录)等等;
    ④. 依赖于文件系统,它的生命周期随进程的结束结束(随进程)
    ⑤. 其本身自带同步互斥效果
    ⑥. 没有名字;
    ⑦. 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);

    管道的应用:
    * shell:管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入。
    比如,当在某个shell程序键入who│wc -l后,相应shell程序将创建who以及wc两个进程和这两个进程间的管道
    * 用于具有亲缘关系的进程间通信

    匿名管道虽然实现了进程间通信,但是它具有一定的局限性:首先,这个管道只能是具有血缘关系的进程之间通信;第二,它只能实现一个进程写另一个进程读,而如果需要两者同时进行时,就得重新打开一个管道。 为了使任意两个进程之间能够通信,就提出了命名管道(named pipe 或 FIFO)。
    1、与匿名管道的区别:提供了一个路径名与之关联,以 FIFO文件 的形式存储于文件系统中,能够实现任何两个进程之间通信。而匿名管道对于文件系统是不可见的,它仅限于在父子进程之间的通信。
    2、FIFO是一个设备文件,在文件系统中以文件名的形式存在,因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信,前提是可以访问该路径。
    3、FIFO(first input first output)总是遵循先进先出的原则,即第一个进来的数据会第一个被读走。

    命名管道与匿名管道使用的区别:
    命名管道创建完成后就可以使用,其使用方法与管道一样,区别在于:命名管道使用之前需要使用open()打开。这是因为:命名管道是设备文件,它是存储在硬盘上的,而管道是存在内存中的特殊文件。但是需要注意的是,命名管道调用open()打开有可能会阻塞,但是如果以读写方式(O_RDWR)打开则一定不会阻塞;以只读(O_RDONLY)方式打开时,调用open()的函数会被阻塞直到有数据可读;如果以只写方式(O_WRONLY)打开时同样也会被阻塞,知道有以读方式打开该管道。

    实现管道通信的步骤:
    ①. 调用pipe函数,由父进程创建管道,得到两个文件描述符指向管道的两端
    ②. 父进程调用fork创建子进程,则对于子进程,也有两个文件描述符指向管道的两端
    ③. 父进程关闭读端,只进行写操作;子进程关闭写端,只进行读操作。管道是用唤醒队列实现的,数据从写段流入到读端,这样就形成了进程间通信。

    使用匿名管道需要注意的4种特殊情况:
    如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么文件内的所有内容被读完后再次read就会返回0,就像读到文件结尾。如果有指向管道写端的文件描述符没有关闭(管道写段的引用计数大于0),而持有管道写端的进程没有向管道内写入数据,假如这时有进程从管道读端读数据,那么读完管道内剩余的数据后就会阻塞等待,直到有数据可读才读取数据并返回。如果所有指向管道读端的文件描述符都关闭,此时有进程通过写端文件描述符向管道内写数据时,则该进程就会收到SIGPIPE信号,并异常终止。如果有指向管道读端的文件描述符没有关闭(管道读端的引用计数大于0),而持有管道读端的进程没有从管道内读数据,假如此时有进程通过管道写段写数据,那么管道被写满后就会被阻塞,直到管道内有空位置后才写入数据并返回。
  2. 信号(Signal):信号是比较复杂的通信方式,用于通知接收进程有某种事件生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期 信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上, 该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,sigaction函数重新实现了signal函数);
  3. 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  4. 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  5. 信号量(semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix 系统上:Linux和System V的变种都支持套接字。

3. 匿名管道和有名管道

匿名管道:http://www.linuxidc.com/Linux/2013-06/85904p2.htm

有名管道:http://www.linuxidc.com/Linux/2013-06/85904p3.htm

Linux进程间通信--匿名管道:http://www.cnblogs.com/melons/p/5791798.html

匿名管道

管道:pipe
没有名字的匿名管道是一种最基本的IPC机制,由pipe函数创建:
调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通 过 fd 参数传出给用户程序两个文件描述符:
1. fd[0] :指向管道的读端
2. fd[1] :指向管道的写端

(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来就像一个打开 的文件,通过read(fd[0]);或者write(fd[1]);向这个文件读写数据其实是在读写内核缓冲区。

pipe函数调用成功返回0,调用失败返回-1, 开辟了管道之后如何实现两个进程间的通信呢?看下面:

父进程创建管道,父进程fork出子进程,现在就创建出了具有血缘关系的父子进程

下面要说的就是两进程间的通信:

存在于两个进程间的管道,两遍都有读和写的功能,匿名管道是单向的,能写就不能读,能读就不能写,所以,图3中,父进程关闭了读端,也就意味着,父亲要写数据到管道;而子进程关闭了写端,也就意味着,子进程只能读管道间的数据,这样就完成了一次进程间的通信。有专业一点的术语讲原理,是这样的:


1. 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。

2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。

3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。


Linux函数原型(man pipe)

#include 
int pipe(int fd[2]);

fd[0]用于读出数据,读取时必须关闭写入端,即close(fd[1]);
fd[1]用于写入数据,写入时必须关闭读取端,即close(fd[0])。

演示代码:

上面提到:有血缘关系,单向之类的关键字,下面就说说匿名管道这种通信机制的条件或者说限制:
1.两个进程通过一个管道只能实现单向通信。比如上面的例子,父进程写子进程读,如果有时候也需要子进程写父进程读,就必须另开一个管道。
2.管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。上面的例子是父进程把文件描述符传给子进程之后父子进程之间通信,也可以父进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信, 总之需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。 也就是说,管道通信是需要进程之间有关系。

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

1.如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。


2.如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。

3.如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止.

4.如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的 进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。


有名管道

一、原理:

管道的一个不足之处是没有名字,因此,只能用于具有亲缘关系的进程间通信,在命名管道(named pip或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。

值得注意的是,FIFO(first input first output)总是按照先进先出的原则工作,第一个被写入的数据将首先从管道中读出。

二、命名管道的创建与读写

Linux下有两种方式创建命名管道。一是在Shell下交互地建立一个命名管道,二是在程序中使用系统函数建立命名管道。Shell方式下可使用mknod或mkfifo命令,下面命令使用 mknod创建了一个命名管道:

mknod namedpipe  

创建命名管道的系统函数有两个:mknod 和 mkfifo。两个函数均定义在头文件sys/stat.h,

函数原型如下:

#include   
#include   

int mknod(const char *path,mode_t mod,dev_t dev);  
int mkfifo(const char *path,mode_t mode);  

函数mknod参数中path为创建的命名管道的全路径名:mod为创建的命名管道的模式,指明其存取权限;dev为设备值,该值取决于文件创建的种类,它只在创建设备文件时才会用到。这两个函数调用成功都返回0,失败都返回-1。

三、实例

用mkfifo创建命名管道:


其中_PATH_是文件路径名的宏定义:如下:

“S_IFIFO|0666”指明创建一个命名管道且存取权限为0666,即创建者、与创建者同组的用户、其他用户对该命名管道的访问权限都是可读可写。

命名管道创建后就可以使用了,命名管道和管道的使用方法基本是相同的。只是使用命名管道时,必须用open()将其打开。因为命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。

需要注意的是,调用open()打开命名管道的进程可能会被阻塞。但如果同时用读写方式(O_RDWR)打开,则一定不会导致阻塞;如果以只读方式(O_RDONLY)打开,则调用open()函数的进程将会被阻塞直到有写方打开管道;同样以写方式(O_WRONLY)打开也会阻塞直到有读方式打开管道。

四:结束

文件系统中的路径名是全局的,各进程都可以访问,因此可以用文件系统中的路径名来标识一个IPC通道。 命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。

由于Linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,也使它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使用。

五、命名管道的安全问题

前面的例子两个进程之间的通信问题,也就是说,一个进程向FIFO文件写数据,而另一个进程则在FIFO文件中读取数据。试想这样一个问题,只使用一个FIFO文件,如果有多个进程同时向同一个FIFO文件写数据,而只有一个读FIFO进程在同一个FIFO文件中读取数据时,会发生怎么样的情况呢,会发生数据块的相互交错是很正常的?而且个人认为多个不同进程向一个FIFO读进程发送数据是很普通的情况。
为了解决这一问题,就是让写操作的原子化。怎样才能使写操作原子化呢?答案很简单,系统规定:在一个以O_WRONLY(即阻塞方式)打开的FIFO中, 如果写入的数据长度小于等待PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。如果所有的写请求都是发往一个阻塞的FIFO的,并且每个写记请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据决不会交错在一起。


使用有名管道实现类似 QQ 功能示例(总共 5 个文件):

qq_ipc.h

#ifndef QQ_IPC_H
#define QQ_IPC_H

struct QQ_DATA_INFO 
{
    int protocal;
    char srcname[20];
    char destname[20];
    char data[100];
};
/*
 * protocal     srcname      destname      data
 * 1            登陆者       NULL
 * 2            发送方       接收方         数据
 * 3            NULL(不在线)
 * 4            退出登陆用户(退出登陆)
 * 
 * */

#endif

mylink.h (相当于联系人操作)

#ifndef _MYLINK_H_
#define _MYLINK_H_

typedef struct node *mylink;
struct node 
{
	char item[20];
    int fifo_fd;
	mylink next; //struct node *next;
};

void mylink_init(mylink *head);
mylink make_node(char *name, int fd);
void mylink_insert(mylink *head, mylink p);
mylink mylink_search(mylink *head, char *keyname);
void mylink_delete(mylink *head, mylink p);
void free_node(mylink p);
void mylink_destory(mylink *head);
void mylink_travel(mylink *head, void (*vist)(mylink));

#endif

mylink.c

#include 
#include 
#include 
#include "mylink.h"

void mylink_init(mylink *head)		//struct node **head = &head
{
	//head = NULL;
	*head = NULL;
}

mylink make_node(char *item, int fd)
{
	mylink p = (mylink)malloc(sizeof(struct node));
	strcpy(p->item, item);				//(*p).itme = item;
    p->fifo_fd = fd;

	p->next = NULL;				//#define NULL (void *)0
	return p;
}

void mylink_insert(mylink *head, mylink p)		//头插法
{
	p->next = *head;
	*head = p;
}

mylink mylink_search(mylink *head, char *keyname)
{
	mylink p;
	for (p = *head; p != NULL; p = p->next)
		if (strcmp(p->item,keyname) == 0)
			return p;
	return NULL;
}
void mylink_delete(mylink *head, mylink q)
{
	mylink p;
	if (q == *head) {
		*head = q->next;
		return;
	}
	for (p = *head; p != NULL; p = p->next)
		if (p->next == q) {
			p->next = q->next;
			return;
		}

}
void free_node(mylink p)
{
	free(p);
}

void mylink_destory(mylink *head)
{
	mylink p= *head, q;
	while (p != NULL) {
		q = p->next;
		free(p);
		p = q;
	}
	*head = NULL;
}
void mylink_travel(mylink *head, void (*vist)(mylink))
{
	mylink p;
	for (p = *head; p != NULL; p = p->next)
		vist(p);
}

qq_ipc_sercer.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "qq_ipc.h"
#include "mylink.h"

#define SERVER_PROT "SEV_FIFO"

mylink head = NULL;

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

/**
 * @brief login_qq  
 *
 * @param buf
 * @param head
 *
 * @return 
 */
int login_qq(struct QQ_DATA_INFO *buf, mylink *head)
{
    int fd;
    fd = open(buf->srcname, O_WRONLY);
    mylink node = make_node(buf->srcname, fd);
    mylink_insert(head, node);
#ifdef DEBUG
    printf("create %s OK
", buf->srcname);
#endif
    return 0;
}
void transfer_qq(struct QQ_DATA_INFO *buf, mylink *head)
{
    mylink p = mylink_search(head, buf->destname);
    if (p == NULL) {
        struct QQ_DATA_INFO lineout = {3};
        strcpy(lineout.destname, buf->destname);
        mylink q = mylink_search(head, buf->srcname);
        write(q->fifo_fd, &lineout, sizeof(lineout));
    }
    else
        write(p->fifo_fd, buf, sizeof(*buf));

}
int logout_qq(struct QQ_DATA_INFO *buf, mylink *head)
{
    mylink p = mylink_search(head, buf->srcname);
    close(p->fifo_fd);
    mylink_delete(head, p);
    free_node(p);
}
void err_qq(struct QQ_DATA_INFO *buf)
{
    fprintf(stderr, "bad client %s connect 
", buf->srcname);
}
int main(void)
{
    int server_fd;
    struct QQ_DATA_INFO dbuf;
    

    if (access(SERVER_PROT, F_OK) != 0) {
        mkfifo(SERVER_PROT, 0777);
    }
    if ((server_fd = open(SERVER_PROT, O_RDONLY)) < 0)
        sys_err("open");

    mylink_init(&head);

    while (1) {
        //接收公共管道数据
        //协议包分析
        //根据协议,作相应的处理
        read(server_fd, &dbuf, sizeof(dbuf));
        switch (dbuf.protocal) {
            case 1: login_qq(&dbuf, &head); break;
            case 2: transfer_qq(&dbuf, &head); break;
            case 4: logout_qq(&dbuf, &head); break;
            default: err_qq(&dbuf);
        }
    }

    close(server_fd);
}

qq_ipc_client.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "qq_ipc.h"
#include "mylink.h"

#define SERVER_PROT "SEV_FIFO"


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

int main(int argc, char *argv[])
{
    int server_fd, client_fd, flag, len;
    struct QQ_DATA_INFO dbuf;
    char cmdbuf[256];

    if (argc < 2) {
        printf("./client name
");
        exit(-1);
    }

    if ((server_fd = open(SERVER_PROT, O_WRONLY)) < 0)
        sys_err("open");

    mkfifo(argv[1], 0777);
    struct QQ_DATA_INFO cbuf, tmpbuf, talkbuf;
    cbuf.protocal = 1;
    strcpy(cbuf.srcname,argv[1]);
    client_fd = open(argv[1], O_RDONLY|O_NONBLOCK);

    flag = fcntl(STDIN_FILENO, F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(STDIN_FILENO, F_SETFL, flag);

    write(server_fd, &cbuf, sizeof(cbuf));

    while (1) {
        len = read(client_fd, &tmpbuf, sizeof(tmpbuf));
        if (len > 0) {
            if (tmpbuf.protocal == 3) {
                printf("%s is not online
", tmpbuf.destname);
            }
            else if (tmpbuf.protocal == 2) {
                printf("%s : %s
", tmpbuf.srcname, tmpbuf.data);
            }
        }
        else if (len < 0) {
            if (errno != EAGAIN)
                sys_err("client read");
        }
        len = read(STDIN_FILENO, cmdbuf, sizeof(cmdbuf));
        if (len > 0) {
            char *dname, *databuf;
            memset(&talkbuf, 0, sizeof(talkbuf));
            cmdbuf[len] = '\0';
            //destname#data
            dname = strtok(cmdbuf, "#
");
            if (strcmp("exit", dname) == 0) {
                talkbuf.protocal = 4;
                strcpy(talkbuf.srcname, argv[1]);
                write(server_fd, &talkbuf, sizeof(talkbuf));
                break;
            }
            else {
                strcpy(talkbuf.destname, dname);
                strcpy(talkbuf.srcname, argv[1]);
                talkbuf.protocal = 2;

                databuf = strtok(NULL, "\0");
                strcpy(talkbuf.data, databuf);
            }
            write(server_fd, &talkbuf, sizeof(talkbuf));
        }

    }
    unlink(argv[1]);
    close(client_fd);
    close(server_fd);
    return 0;
}



标准流管道


进程间通信---标准流管道:http://blog.csdn.net/zzyoucan/article/details/9226599

popen, pclose - pipe stream to or from a process

#include 

FILE *popen(const char *command, const char *type);

int pclose(FILE *stream);

4. 信号


Linux进程间通信--信号:http://www.cnblogs.com/melons/p/5791795.html

Linux异步之信号(signal)机制分析:http://blog.csdn.net/freeking101/article/details/78338497

Linux环境进程间通信:信号(上)和 信号(下):https://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html


信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise,alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

1. 信号的种类

可靠信号与不可靠信号, 实时信号与非实时信号
可靠信号就是实时信号, 那些从UNIX系统继承过来的信号都是非可靠信号, 表现在信号
不支持排队,信号可能会丢失, 比如发送多次相同的信号, 进程只能收到一次. 信号值小于SIGRTMIN的都是非可靠信号.
非可靠信号就是非实时信号, 后来, Linux改进了信号机制, 增加了32种新的信号, 这些信
号都是可靠信号, 表现在信号支持排队, 不会丢失, 发多少次, 就可以收到多少次. 信号值
位于 [SIGRTMIN, SIGRTMAX] 区间的都是可靠信号.

2. 信号的安装

早期的Linux使用系统调用 signal 来安装信号

#include 
void (*signal(int signum, void (*handler))(int)))(int); 

该函数有两个参数, signum指定要安装的信号, handler指定信号的处理函数.
该函数的返回值是一个函数指针, 指向上次安装的handler


经典安装方式:

if (signal(SIGINT, SIG_IGN) != SIG_IGN) 
{
    signal(SIGINT, sig_handler);
}

先获得上次的handler, 如果不是忽略信号, 就安装此信号的handler

由于信号被交付后, 系统自动的重置handler为默认动作, 为了使信号在handler处理期间, 仍能对后继信号做出反应, 往往在handler的第一条语句再次调用 signal

sig_handler(ing signum)
{

    /* 重新安装信号 */
    signal(signum, sig_handler);
    ......
}

我们知道在程序的任意执行点上, 信号随时可能发生, 如果信号在sig_handler重新安装
信号之前产生, 这次信号就会执行默认动作, 而不是sig_handler. 这种问题是不可预料的.


使用库函数 sigaction 来安装信号

为了克服非可靠信号并同一SVR4和BSD之间的差异, 产生了 POSIX 信号安装方式, 使用sigaction安装信号的动作后, 该动作就一直保持, 直到另一次调用 sigaction建立另一个动作为止. 这就克服了古老的 signal 调用存在的问题

#include  
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));



/* 设置SIGINT */

action.sa_handler = sig_handler;

sigemptyset(&action.sa_mask);

sigaddset(&action.sa_mask, SIGTERM);

action.sa_flags = 0;



/* 获取上次的handler, 如果不是忽略动作, 则安装信号 */

sigaction(SIGINT, NULL, &old_action);

if (old_action.sa_handler != SIG_IGN) {

sigaction(SIGINT, &action, NULL);

}


基于 sigaction 实现的库函数: signal

sigaction 自然强大, 但安装信号很繁琐, 目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。

3. 如何屏蔽信号

所谓屏蔽,并不是禁止递送信号,而是暂时阻塞信号的递送,。解除屏蔽后,信号将被递送,不会丢失。相关API为

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
int sigsuspend(const sigset_t *mask);
int sigpending(sigset_t *set);
------------------------------------------------------------------------------------------
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset));
sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:
* SIG_BLOCK 在进程当前阻塞信号集中添加set指向信号集中的信号
* SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞
* SIG_SETMASK 更新进程阻塞信号集为set指向的信号集屏蔽整个进程的信号:

4. 信号的生命周期

从信号发送到信号处理函数的执行完毕,对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要事件来刻画:信号诞生;信号在进程中注册完毕;信号在进程中的注销完毕;信号处理函数执行完毕。

下面阐述四个事件的实际意义:

信号"诞生"。

信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函kill()或sigqueue()等)。

信号在目标进程中"注册";

进程的task_struct结构中有关于本进程中未决信号的数据成员:

struct sigpending pending:

struct sigpending{
    struct sigqueue *head, **tail;
    sigset_t signal;
};


第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号链表")的首尾,
链表中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:

struct sigqueue{
    struct sigqueue *next;
    siginfo_t info;
}

信号的注册

信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且加入未决信号链表的末尾。 只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号链表中添加多次. 当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号链表中,至多占有一个sigqueue结构.

一个非实时信号诞生后,

(1)、如果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失.

(2)、如果进程的未决信号中没有相同信号,则在进程中注册自己。

信号的注销

在进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不应该在进程的未决信号集中删除该信号(信号注销完毕)。 进程在执行信号相应处理函数之前,首先要把信号在进程中注销。


信号生命终止。

进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。

system V 提供的进程间通信的三种方式

system V 提供的进程间通信的三种方式:消息队列,信号量、共享内存

linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室 和 BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来,如图示

Posix,全称“Portable operating System Interface”,可移植操作系统接口。 是IEEE开发的一套编码标准, 它已经是ISO/IEC采纳的国际标准。

System V, 曾经也被称为 AT&T System V,是Unix操作系统众多版本中的一支。它最初由 AT&T 开发。

一般System V IPC,都需要 ftok() 使用路径名生成key值(这种信号量机制用的比较少,已经被淘汰了。),

而 Posix IPC 直接使用路径名,且Posix IPC接口函数里面都有 "_" 连接符, 例如: mq_open/sem_open() 等。

5. 报文(消息队列)

Linux下进程间通信--消息队列:http://www.cnblogs.com/melons/p/5791792.html

消息队列用于运行于同一台机器上的进程间通信,它和管道很相似,是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现。消息链表中节点的结构用msg声明。

事实上,它是一种正逐渐被淘汰的通信方式,我们可以用流管道或者套接字的方式来取代它

消息队列是消息的连接表,存放在内核中并由消息队列标识符标识。

msgget用于创建一个新队列或打开一个现有的队列。msgsnd将新消息添加到队列尾端。每个消息都包含一个unsigned long int字段,一个非负长度字段,以及实际数据(对应长度字段)。msgrcv用于从队列中取消息。并不一定要以先进先出的方式取消息,也可以按消息的类型字段取消息。

每一个消息队列都对应一个msqid_ds结构:

struct msqid_ds {
    struct ipc_perm msg_perm; /*see Section 15.6.2 */
    msgqnum_t msg_qnum; /*#of messages on queue */
    msglen_t msg_qbytes; /*max # of bytes on queue */
    pid_t msg_lspid; /*pid of last msgsnd() */
    pid_t msg_lrpid; /*pid of last msgrcv() */
    time_t msg_stime; /*last-msgsnd() time */
    time_t msg_rtime; /*last-msgrcv() time */
    time_t msg_ctime; /*last-change time */
    ......
};

此结构规定了当前队列的状态。

msgget:创建或获取现有的队列。

#include 
int msgget(key_t key, int flag);
//Returns: message queue ID if OK, −1 on error

当msgget用于创建一个新的队列时,需要初始化msqid_ds的下列成员:

ipc_perm:    该结构mode成员按flag中的相应权限位进行设置。
msg_qnum, msg_lspid, msg_lrpid, msg_stime, and msg_rtime  都设置为零。
msg_ctime     设置为当前的时间。
msg_qbytes    设置为系统限制值。

msgctl:对队列执行各种操作。

#include 
int msgctl(int msqid, int cmd, struct msqid_ds *buf );
//Returns: 0 if OK, −1 on error

msgctl和semctl,shmctl是XSI IPC类似与ioctl的函数。cmd参数指定队列要执行的命令。具体命令及使用可参考man手册。

msgsnd:将数据放到消息队列中。

#include 
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
//Returns: 0 if OK, −1 on error

msgrcv:取消息。

#include 
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
//Returns: size of data portion of message if OK, −1 on error


6. 共享内存

Linux环境进程间通信:共享内存(上) 和 共享内存(下):https://www.ibm.com/developerworks/cn/linux/l-ipc/part5/index1.html

使用 shell命令来查看与释放已经分配的共享内存

Linux中通过API函数shmget创建的共享内存一般都是在程序中使用shmctl来释放的,但是有时为了调试程序,开发人员可能通过Ctrl + C等方式发送中断信号来结束程序,此时程序申请的共享内存就不能得到释放,当然如果程序没有改动的话,重新运行程序时仍然会使用上次申请的共享内存,但是如果我们修改了程序,由于共享内存的大小不一致等原因会导致程序申请共享内存错误。因此,我们总是希望每次结束时就能释放掉申请的共享内存。
有两种方法可以用来释放共享内存:
第一种:如果总是通过Crtl+C来结束的话,可以做一个信号处理器,当接收到这个信号的时候,先释放共享内存,然后退出程序。
第二种:不管你以什么方式结束程序,如果共享内存还是得不到释放,那么可以通过linux命令ipcrm shm shmid来释放,在使用该命令之前可以通过ipcs -m命令来查看共享内存。

root@kali:~# ipcs -h

Usage:
 ipcs [资源选项...] [输出选项]
 ipcs -m|-q|-s -i 

Show information on IPC facilities.

选项:
 -i, --id   print details on resource identified by 
 -h, --help     display this help and exit
 -V, --version  output version information and exit

资源选项:
 -m, --shmems      显示共享内存段
 -q, --queues      显示消息队列
 -s, --semaphores  显示信号量
 -a, --all         显示所有(默认)

输出选项:
 -t, --time        show attach, detach and change times
 -p, --pid         show PIDs of creator and last operator
 -c, --creator     show creator and owner
 -l, --limits      show resource limits
 -u, --summary     show status summary
     --human       show sizes in human-readable format
 -b, --bytes       show sizes in bytes

For more details see ipcs(1).

使用 ipcs -m 查看 本机所有的 共享内存

第一列:就是共享内存的key(十六进制);
第二列:是共享内存的编号shmid;
第三列:就是创建的用户owner;
第四列:就是权限perms;
第五列:为创建的大小bytes;
第六列:为连接到共享内存的进程数nattach;
第七列:是共享内存的状态status。

备注:这里简单解释一下为什么会出现“dest”这个状态。Linux下删除任何内容,都会先检查一下这个内容的引用计数(就是文件的使用数,n个进程使用,引用计数为n)。若引用计数为0,就会真正的删除该内容(这里就是删除共享内存)。不为0,表示仍有进程使用,则正在使用的进程可以正常使用,直至引用计数降为0后,系统才会将该内容真正意义上的删除掉。对这里用共享内存来说同理,显示“dest”是表示该共享内存已经被删除,但是还有进程在使用它。这时操作系统将共享内存的mode标记为SHM_DEST,key标记为0x00000000,并对外显示status为“dest”。当用户调用 shmctl 的 IPC_RMID 时,系统会先查看这个共享内存的引用计数,即多少个进程与这个内存关联着,如果引用计数为0,就会销毁这段共享内存,否者设置这段内存的 mod 的 mode 位为 SHM_DEST,如果所有进程都不用则删除这段共享内存。

通过程序创建一个 共享内存。程序源码(shm.c):

#include 
#include 
#include 
#include 
#include 
#include 

typedef struct
{
        char name[4];
        int age;
} people;

void main(void)
{
        int shm_id, i;
        key_t key;
        char temp;

        people *p_map;
        shm_id = shmget((key_t)12345, sizeof(people), 0666 | IPC_CREAT);  // 12345 十进制 转成 16进制 就是 3039
        if(shm_id == -1) printf("shmget fail
");
        else printf("shmget success and shm_id is %d
", shm_id);
}

使用 shell 命令 释放 共享内存

要释放共享内存,需要使用 ipcrm 命令,使用 shmid 作为参数。详细使用查看 man 帮助(man ipcrm) 。

共享内存大小修改

查看共享内存的大小:# cat /proc/sys/kernel/shmmax

修改共享内存大小:
    临时修改:在root用户下执行# echo 268435456 > /proc/sys/kernel/shmmax把共享内存大小设置为256MB;
    永久修改:在root用户下修改/etc/rc.d/rc.local文件,加入下面一行:
              echo 268435456 > /proc/sys/kernel/shmmax
              即可每次启动时把共享内存修改为256MB。


一般System V IPC,都需要 ftok() 使用路径名生成key值(这种信号量机制用的比较少,已经被淘汰了。),

而 Posix IPC 直接使用路径名,且Posix IPC接口函数里面都有 "_" 连接符, 例如: mq_open/sem_open() 等。


共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。是针对其他通信机制运行效率较低而设计的。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。

特别提醒:共享内存并未提供同步机制,多个进程共享同一块内存区域,必然需要某种同步机制,所以使用共享内存需要自己提供同步机制。如使用互斥锁、信号量等同步机制。

共享存储允许两个或更多进程共享一给定的存储区,因为数据不需要在客户端和服务器之间进行复制,所以这是一种最快的IPC。使用共享存储唯一需要注意的是多个进程之间对一给定存储区的同步访问。若服务器进程正在将数据放入共享存储区,那么它在完成这一操作之前,客户进出不应该去取这些数据。通常,信号量被用来实现对共享存储访问的同步。


得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。前一种方式不给系统带来额外的开销,但在现实中并不常用,因为它控制存取的将是 实际的物理内存,在Linux系统下,这只有通过限制Linux系统存取的内存才可以做到,这当然不太实际。常用的方式是通过shmXXX函数族来实现利 用共享内存进行存储的。


系统V共享内存原理

  进程间需要共享的数据被放在一个叫做IPC共享内存区域的地方,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。系统V共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。内核在保证shmget获得或创建一个共享内存区,初始化该共享内存区相应的shmid_kernel结构体的同时,还将在特殊文件系统shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentry及inode结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。所有这一切都是系统调用shmget完成的。

注:每一个共享内存区都有一个控制结构struct shmid_kernel,shmid_kernel是共享内存区域中非常重要的一个数据结构,它是存储管理和文件系统结合起来的桥梁,定义如下:

struct shmid_kernel /* private to the kernel */
{    
    struct kern_ipc_perm shm_perm; /* operation permission structure */
    struct file *shm_file; /* pointer in kernel */
    unsigned long shm_nattch; /* number of current attaches */
    unsigned long shm_segsz; /* size of segment in bytes */
    time_t shm_atim; /* last-attach time */
    time_t shm_dtim; /* last-detach time */
    time_t shm_ctim; /* last-change time */
    pid_t shm_cprid; /* pid of creator */
    pid_t shm_lprid; /* pid of last shmop() */
};

内核为每个共享存储设置了一个shmid_ds (共享内存 ID 描述符)结构。

struct shmid_ds {
    struct ipc_perm shm_perm;    /* see Section 15.6.2  */
    size_t shm_segsz;            /* size of segment in bytes  */
    pid_t shm_lpid;              /* pid of last shmop() */
    pid_t shm_cpid;              /* pid of creator  */
    shmatt_t shm_nattch;         /* number of current attaches */
    time_t shm_atime;            /* last-attach time */
    time_t shm_dtime;            /* last-detach time */
    time_t shm_ctime;            /* last-change time */
    ......
};

  正如消息队列和信号灯一样,内核通过数据结构struct ipc_ids shm_ids维护系统中的所有共享内存区域。上图中的shm_ids.entries变量指向一个ipc_id结构数组,而每个ipc_id结构数组中有个指向kern_ipc_perm结构的指针。到这里读者应该很熟悉了,对于系统V共享内存区来说,kern_ipc_perm的宿主是 shmid_kernel结构,shmid_kernel是用来描述一个共享内存区域的,这样内核就能够控制系统中所有的共享区域。同时,在 shmid_kernel结构的file类型指针shm_file指向文件系统shm中相应的文件,这样,共享内存区域就与shm文件系统中的文件对应起来。

  在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用shmat()完成此项功能。由于在调用shmget()时,已经创建了文件系统 shm中的一个同名文件与共享内存区域相对应,因此,调用shmat()的过程相当于映射文件系统shm中的同名文件过程,原理与mmap()大同小异。

系统V共享内存 API 头文件 和 使用的 函数:

#include 
#include 


int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmdt(const void *shm_addr);
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);


shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。
shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就可以方便地对共享区域进行访问操作。
shmdt()调用用来解除进程对共享内存区域的映射。
shmctl实现对共享内存区域的控制操作。

系统V共享内存限制
  在/proc/sys/kernel/目录下,记录着系统V共享内存的一下限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等,可以手工对其调整,但不推荐这样做。

shmget 函数:

获得一个共享存储标示符。

#include 
int shmget(key_t key, size_t size, int shmflg);
//Returns: shared memory ID if OK, −1 on error

key 以及 shmflg 参数类似 msgget 中的参数,size是该共享存储段的长度。通常该长度为页大小的整数倍。如果size不是页大小的整数倍,那么最后一页的余下部分是不可用的。如果正在创建一个新存储段(一般是在服务进程中),则必须指定其size,如果引用一个显存的段,则size为0,当创建一个新段的时候,内容初始化为0。

这个函数有点类似大家熟悉的malloc函数,系统按照请求分配size大小的内存用作共享内存。Linux系统内核中每个IPC结构都有的一个非负整数 的标识符,这样对一个消息队列发送消息时只要引用标识符就可以了。这个标识符是内核由IPC结构的关键字得到的,这个关键字,就是上面第一个函数的 key。数据类型key_t是在头文件sys/types.h中定义的,它是一个长整形的数据。

不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget函数的返回值),只有shmget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。


shmat函数

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

void *shmat(int shm_id, const void *shm_addr, int shmflg);    
//Returns: pointer to shared memory segment if OK, −1 on error

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

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

当共享内存创建后,其余进程可以调用shmat()将其连接到自身的地址空间中

shmid为shmget函数返回的共享存储标识符,addr和flag参数决定了以什么方式来确定连接的地址,函数的返回值即是该进程数据段所连接的实际地址,进程可以对此进程进行读写操作。 使用共享存储来实现进程间通信的注意点是对数据存取的同步,必须确保当一个进程去读取数据时,它所想要的数据已经写好了。通常,信号量被要来实现对共享存 储数据存取的同步,另外,可以通过使用shmctl函数设置共享存储内存的某些标志位如SHM_LOCK、SHM_UNLOCK等来实现

共享存储段连接到调用进程的那个地址上与addr参数以及在flag中是否指定SHM_RND有关。

  • 如果addr为0,则此段连接到由内核选择的第一个可用地址上。这是推荐的方式。
  • 如果addr非0,并且没有指定SHM_RND,则此段连接到addr所指定的地址上。
  • 如果addr非0,并且指定了SHM_RND,则此段连接到((addr − (addr modulus SHMLBA)))所表示的地址上,SHM_RND命令的意思是”取整“。SHMLBA的意思是“低边界地址倍数”,它总是2的乘方。该算式是将地址向下取最近1个SHMLBA的倍数。

除非特殊情况,一般应指定addr为0,以便由内核选择地址。


shmctl 函数:

对共享存储段执行各种操作。与信号量的semctl函数一样,用来控制共享内存,它的原型如下:

#include 
int shmctl(int shmid, int cmd, struct shmid_ds *buf );
//Returns: 0 if OK, −1 on error

第一个参数:shmid是shmget函数返回的共享内存标识符。

第二个参数,comd是要采取的操作,它可以取下面的三个值 :
    IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
    IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
    IPC_RMID:删除共享内存段

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

其cmd等参数信息可参考man手册。


shmdt 函数:

用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它或者从系统中删除该段的标识符以及其数据结构,只是使该共享内存对当前进程不再可用。直到shmctl命令删除它。它的原型如下:

#include 
int shmdt(const void *addr);
//Returns: 0 if OK, −1 on error

内核将以地址0连接的共享存储段放在什么位置上与系统密切相关。共享存储段紧靠在栈之下。

mmap函数可将一个文件的若干部分映射到进程地址空间。类似于用shmmat连接一共享存储段。

两者之间的主要区别是,用mmap映射的存储段是与文件相关联的。而XSI共享存储段则并无这种关联。

系统V共享内存范例

/***** testwrite.c *******/
#include 
#include 
#include 
#include 

typedef struct
{
    char name[4];
    int age;
} people;

void main(int argc, char** argv)
{
    int shm_id,i;
    key_t key;
    char temp;
    people *p_map;
    char* name = "/dev/shm/myshm2";
    key = ftok(name,0);
    if(key == -1)
        perror("ftok error");
    shm_id = shmget(key,4096,IPC_CREAT);    
    if(shm_id == -1)
    {
        perror("shmget error");
        return;
    }
    p_map=(people*)shmat(shm_id,NULL,0);
	
    temp='a';
    for(i = 0;i<10;i++)
    {
        temp+=1;
        memcpy((*(p_map+i)).name,&temp,1);
        (*(p_map+i)).age=20+i;
    }
    if(shmdt(p_map)==-1)
        perror(" detach error ");
}



/********** testread.c ************/
#include 
#include 
#include 
#include 

typedef struct
{
    char name[4];
    int age;
} people;

void main(int argc, char** argv)
{
    int shm_id,i;
    key_t key;
    people *p_map;
    char* name = "/dev/shm/myshm2";
    key = ftok(name,0);
    if(key == -1)
        perror("ftok error");
		
    shm_id = shmget(key,4096,IPC_CREAT);    
    
	if(shm_id == -1)
    {
        perror("shmget error");
        return;
    }
    p_map = (people*)shmat(shm_id,NULL,0);
    for(i = 0;i<10;i++)
    {
        printf( "name:%s
",(*(p_map+i)).name );
        printf( "age %d
",(*(p_map+i)).age );
    }
	
    if(shmdt(p_map) == -1)
        perror(" detach error ");
}

示例:

两个不相关的进程来说明进程间如何通过共享内存来进行通信。其中一个文件shmread.c创建共享内存,并读取其中的信息,另一个文件shmwrite.c向共享内存中写入数据。为了方便操作和数据结构的统一,为这两个文件定义了相同的数据结构,定义在文件shmdata.c中。结构shared_use_st中的written作为一个可读或可写的标志,非0:表示可读,0表示可写,text则是内存中的文件。

/* shmdata.h的源代码 */

#ifndef _SHMDATA_H_HEADER  
#define _SHMDATA_H_HEADER  
  
#define TEXT_SZ 2048  
  
struct shared_use_st  
{  
    int written;//作为一个标志,非0:表示可读,0表示可写  
    char text[TEXT_SZ];//记录写入和读取的文本  
};  
  
#endif 



/* 源文件shmread.c的源代码: */ 

#include   
#include   
#include   
#include   
#include "shmdata.h"  
  
int main()  
{  
    int running = 1;//程序是否继续运行的标志  
    void *shm = NULL;//分配的共享内存的原始首地址  
    struct shared_use_st *shared;//指向shm  
    int shmid;//共享内存标识符  
    //创建共享内存  
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);  
    if(shmid == -1)  
    {  
        fprintf(stderr, "shmget failed
");  
        exit(EXIT_FAILURE);  
    }  
    //将共享内存连接到当前进程的地址空间  
    shm = shmat(shmid, 0, 0);  
    if(shm == (void*)-1)  
    {  
        fprintf(stderr, "shmat failed
");  
        exit(EXIT_FAILURE);  
    }  
    printf("
Memory attached at %X
", (int)shm);  
    //设置共享内存  
    shared = (struct shared_use_st*)shm;  
    shared->written = 0;  
    while(running)//读取共享内存中的数据  
    {  
        //没有进程向共享内存定数据有数据可读取  
        if(shared->written != 0)  
        {  
            printf("You wrote: %s", shared->text);  
            sleep(rand() % 3);  
            //读取完数据,设置written使共享内存段可写  
            shared->written = 0;  
            //输入了end,退出循环(程序)  
            if(strncmp(shared->text, "end", 3) == 0)  
                running = 0;  
        }  
        else//有其他进程在写数据,不能读取数据  
            sleep(1);  
    }  
    //把共享内存从当前进程中分离  
    if(shmdt(shm) == -1)  
    {  
        fprintf(stderr, "shmdt failed
");  
        exit(EXIT_FAILURE);  
    }  
    //删除共享内存  
    if(shmctl(shmid, IPC_RMID, 0) == -1)  
    {  
        fprintf(stderr, "shmctl(IPC_RMID) failed
");  
        exit(EXIT_FAILURE);  
    }  
    exit(EXIT_SUCCESS);  
}  



/* 源文件shmwrite.c的源代码 */

#include   
#include   
#include   
#include   
#include   
#include "shmdata.h"  
  
int main()  
{  
    int running = 1;  
    void *shm = NULL;  
    struct shared_use_st *shared = NULL;  
    char buffer[BUFSIZ + 1];//用于保存输入的文本  
    int shmid;  
    //创建共享内存  
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);  
    if(shmid == -1)  
    {  
        fprintf(stderr, "shmget failed
");  
        exit(EXIT_FAILURE);  
    }  
    //将共享内存连接到当前进程的地址空间  
    shm = shmat(shmid, (void*)0, 0);  
    if(shm == (void*)-1)  
    {  
        fprintf(stderr, "shmat failed
");  
        exit(EXIT_FAILURE);  
    }  
    printf("Memory attached at %X
", (int)shm);  
    //设置共享内存  
    shared = (struct shared_use_st*)shm;  
    while(running)//向共享内存中写数据  
    {  
        //数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本  
        while(shared->written == 1)  
        {  
            sleep(1);  
            printf("Waiting...
");  
        }  
        //向共享内存中写入数据  
        printf("Enter some text: ");  
        fgets(buffer, BUFSIZ, stdin);  
        strncpy(shared->text, buffer, TEXT_SZ);  
        //写完数据,设置written使共享内存段可读  
        shared->written = 1;  
        //输入了end,退出循环(程序)  
        if(strncmp(buffer, "end", 3) == 0)  
            running = 0;  
    }  
    //把共享内存从当前进程中分离  
    if(shmdt(shm) == -1)  
    {  
        fprintf(stderr, "shmdt failed
");  
        exit(EXIT_FAILURE);  
    }  
    sleep(2);  
    exit(EXIT_SUCCESS);  
}  

运行结果:

1、程序shmread创建共享内存,然后将它连接到自己的地址空间。在共享内存的开始处使用了一个结构struct_use_st。该结构中有个标志written,当共享内存中有其他进程向它写入数据时,共享内存中的written被设置为0,程序等待。当它不为0时,表示没有进程对共享内存写入数据,程序就从共享内存中读取数据并输出,然后重置设置共享内存中的written为0,即让其可被shmwrite进程写入数据。

2、程序shmwrite取得共享内存并连接到自己的地址空间中。检查共享内存中的written,是否为0,若不是,表示共享内存中的数据还没有被完,则等待其他进程读取完成,并提示用户等待。若共享内存的written为0,表示没有其他进程对共享内存进行读取,则提示用户输入文本,并再次设置共享内存中的written为1,表示写完成,其他进程可对共享内存进行读操作。


示例

/*共享内存允许两个或多个进程进程共享同一块内存(这块内存会映射到各个进程自己独立的地址空间) 
  从而使得这些进程可以相互通信。 
  在GNU/Linux中所有的进程都有唯一的虚拟地址空间,而共享内存应用编程接口API允许一个进程使 
  用公共内存区段。但是对内存的共享访问其复杂度也相应增加。共享内存的优点是简易性。 
  使用消息队列时,一个进程要向队列中写入消息,这要引起从用户地址空间向内核地址空间的一次复制, 
  同样一个进程进行消息读取时也要进行一次复制。共享内存的优点是完全省去了这些操作。 
  共享内存会映射到进程的虚拟地址空间,进程对其可以直接访问,避免了数据的复制过程。 
  因此,共享内存是GNU/Linux现在可用的最快速的IPC机制。 
  进程退出时会自动和已经挂接的共享内存区段分离,但是仍建议当进程不再使用共享区段时 
  调用shmdt来卸载区段。 
  注意,当一个进程分支出父进程和子进程时,父进程先前创建的所有共享内存区段都会被子进程继承。 
  如果区段已经做了删除标记(在前面以IPC——RMID指令调用shmctl),而当前挂接数已经变为0, 
  这个区段就会被移除。 
*/  
 
/* 
  shmget(  )  创建一个新的共享内存区段 
              取得一个共享内存区段的描述符 
  shmctl(  )  取得一个共享内存区段的信息 
              为一个共享内存区段设置特定的信息 
              移除一个共享内存区段 
  shmat(  )   挂接一个共享内存区段 
  shmdt(  )   于一个共享内存区段的分离 
*/  

//创建一个共享内存区段,并显示其相关信息,然后删除该内存共享区  
#include   
#include   //getpagesize()  
#include   
#include   
#define MY_SHM_ID 67483  
int main()  
{  
    //获得系统中页面的大小  
    printf("系统页面大小 = %d/n",getpagesize());  
    int shmid,ret;  
	
	//创建了一个4KB大小共享内存区段。指定的大小必须是当前系统架构  中页面大小的整数倍
    shmid=shmget(MY_SHM_ID, 4096, 0666|IPC_CREAT);  
	
    if(shmid>0) printf( "创建的共享内存 ID = %d/n",shmid );  
	
    //获得一个内存区段的信息  
    struct shmid_ds shmds;  
    //shmid=shmget( MY_SHM_ID,0,0 );//示例怎样获得一个共享内存的标识符  
    ret = shmctl(shmid, IPC_STAT, &shmds);
    if( ret==0 )  
    {  
        printf( "Size of memory segment is %d/n",shmds.shm_segsz );  
        printf( "Numbre of attaches %d/n", (int)shmds.shm_nattch );  
    }  
    else  
    {  
        printf( "shmctl() call failed/n" );  
    }  
	
    //删除该共享内存区  
    ret=shmctl(shmid, IPC_RMID, 0);  
    if(ret == 0) printf( "shmctl 删除共享内存成功/n");  
    else printf("shmctl 删除共享内存失败/n");  
    return 0;  
}  
  
//共享内存区段的挂载,脱离和使用  
//理解共享内存区段就是一块大内存  
#include   
#include   
#include   
#include  
 
#define MY_SHM_ID 67483  

int main()  
{  
    //共享内存区段的挂载和脱离  
    int shmid,ret;  
    void* mem;  
    shmid = shmget( MY_SHM_ID,0,0 );  
    if( shmid >= 0 )  
    {  
        mem=shmat( shmid,( const void* )0, 0);  
        //shmat()返回进程地址空间中指向区段的指针  
        if( (int)mem != -1 )  
        {  
            printf( "Shared memory was attached in our address space at %p/n",mem );  
            //向共享区段内存写入数据  
            strcpy( (char*)mem, "This is a test string./n");  
            printf("%s/n", (char*)mem);  
			
            //脱离共享内存区段  
            ret=shmdt(mem);  
            if( ret==0 ) printf( "Successfully detached memory /n" );  
            else printf( "Memory detached failed %d/n",errno );  
        }  
        else  
            printf( "shmat() failed/n");  
    }  
    else  
        printf( "shared memory segment not found/n" );  
    return 0;  
}  


/*内存共享区段与旗语和消息队列不同,一个区段可以被锁定。 
  被锁定的区段不允许被交换出内存。这样做的优势在于,与其 
  把内存区段交换到文件系统,在某个应用程序调用时再交换回内存, 
  不如让它一直处于内存中,且对多个应用程序可见。从提升性能的角度来看,很重要的。 
 */  
int shmid;  
//...  
shmid=shmget( MY_SHM_ID,0,0 );  
ret=shmctl( shmid,SHM_LOCK,0 );  
if( ret==0 )  
    printf( "Locked!/n" );  
  
/*使用旗语协调共享内存的例子 
  使用和编译命令 
  gcc -Wall test.c -o test 
  ./test create 
  ./test use a & 
  ./test use b & 
  ./test read & 
  ./test remove  
 */  
 
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#define MY_SHM_ID 34325  
#define MY_SEM_ID 23234  
#define MAX_STRING 200  

typedef struct  
{  
    int semID;  
    int counter;  
    char string[ MAX_STRING+1 ];  
}MY_BLOCK_T;  

int main(int argc,char** argv)  
{  
    int shmid,ret,i;  
    MY_BLOCK_T* block;  
    struct sembuf sb;  
    char user;  
    //make sure there is a command  
    if( argc>=2 )  
    {  
        //create the shared memory segment and init it  
        //with the semaphore  
        if( !strncmp(argv[ 1 ],"create",6) )  
        {  
            //create the shared memory segment and semaphore  
            printf( "Creating the shared memory/n" );  
            shmid=shmget( MY_SHM_ID,sizeof( MY_BLOCK_T ),( IPC_CREAT|0666 ) );  
            block=( MY_BLOCK_T* )shmat( shmid,( const void* )0,0 );  
            block->counter=0;  
            //create the semaphore and init  
            block->semID=semget(MY_SEM_ID,1,( IPC_CREAT|0666 ));  
            sb.sem_num=0;  
            sb.sem_op=1;  
            sb.sem_flg=0;  
            semop( block->semID,&sb,1 );  
            //now detach the segment  
            shmdt( ( void* )block );  
            printf( "Create the shared memory and semaphore successuflly/n" );  
              
        }  
        else if( !strncmp(argv[ 1 ],"use",3) )  
        {  
            /*use the segment*/  
            //must specify  also a letter to write to the buffer  
            if( argc<3 ) exit( -1 );  
            user=( char )argv[ 2 ][ 0 ];  
            //grab the segment  
            shmid=shmget( MY_SHM_ID,0,0 );  
            block=( MY_BLOCK_T* )shmat( shmid,( const void* )0,0 );  
              
            /*##########重点就是使用旗语对共享区的访问###########*/  
            for( i=0;i<100;++i )  
            {  
                sleep( 1 ); //设置成1s就会看到 a/b交替出现,为0则a和b连续出现  
                //grab the semaphore  
                sb.sem_num=0;  
                sb.sem_op=-1;  
                sb.sem_flg=0;  
                if( semop( block->semID,&sb,1 )!=-1 )  
                {  
                    //write the letter to the segment buffer  
                    //this is our CRITICAL SECTION  
                    block->string[ block->counter++ ]=user;  
                      
                    sb.sem_num=0;  
                    sb.sem_op=1;  
                    sb.sem_flg=0;  
                    if( semop( block->semID,&sb,1 )==-1 )  
                        printf( "Failed to release the semaphore/n" );  
                      
                }  
                else  
                    printf( "Failed to acquire the semaphore/n" );  
            }  
              
            //do some clear work  
            ret=shmdt(( void*)block);                
        }  
        else if( !strncmp(argv[ 1 ],"read",4) )  
        {  
            //here we will read the buffer in the shared segment  
            shmid=shmget( MY_SHM_ID,0,0 );  
            if( shmid!=-1 )  
            {  
                block=( MY_BLOCK_T* )shmat( shmid,( const void* )0,0 );  
                block->string[ block->counter+1 ]=0;  
                printf( "%s/n",block->string );  
                printf( "Length=%d/n",block->counter );  
                ret=shmdt( ( void*)block );  
            }  
            else  
                printf( "Unable to read segment/n" );          
        }  
        else if( !strncmp(argv[ 1 ],"remove",6) )  
        {  
            shmid=shmget( MY_SHM_ID,0,0 );  
            if( shmid>=0 )  
            {  
                block=( MY_BLOCK_T* )shmat( shmid,( const void* )0,0 );  
                //remove the semaphore  
                ret=semctl( block->semID,0,IPC_RMID );  
                if( ret==0 )  
                    printf( "Successfully remove the semaphore /n" );  
                //remove the shared segment  
                ret=shmctl( shmid,IPC_RMID,0 );  
                if( ret==0 )  
                    printf( "Successfully remove the segment /n" );  
            }  
        }  
        else  
            printf( "Unkonw command/n" );  
    }  
    return 0;  
      
}  

++++++++++++++++++++++++++++++++++++++++++
以下两个程序是一个进程间通信的例子。这两个程序分别在不同的进程中运行,使用了共享内存进行通信。b从键盘读入数据,存放在共享内存中。a则从共享内存中读取数据,显示到屏幕上。由于没有使两个进程同步,显示的内容将是杂乱无章的.实例b程序负责向共享内存中写入数据,a程序负责从内存中读出共享的数据,它们之间并没有添加同步操作。

b.c

#include      
#include      
#include      
#include      
    
#define BUF_SIZE 1024     
#define MYKEY 25     
int main()    
{    
    int shmid;    
    char *shmptr;    
    
    if((shmid = shmget(MYKEY,BUF_SIZE,IPC_CREAT)) ==-1)    
    {    
        printf("shmget error 
");    
        exit(1);    
    }    
    
    if((shmptr =shmat(shmid,0,0))==(void *)-1)    
    {    
        printf("shmat error!
");    
        exit(1);    
    }    
    
    while(1)    
    {    
        printf("input:");    
        scanf("%s",shmptr);    
    }    
    
    exit(0);    
}   

=======================================
a.c

#include      
#include      
#include      
#include      
    
#define BUF_SIZE 1024     
#define MYKEY 25     
int  main()    
{    
    int shmid;    
    char * shmptr;    
    
    if((shmid = shmget(MYKEY,BUF_SIZE,IPC_CREAT)) ==-1)    
    {    
        printf("shmget error!
");    
        exit(1);    
    }    
    
    if((shmptr = shmat(shmid,0,0)) == (void *)-1)    
    {    
        printf("shmat error!
");    
        exit(1);    
    }    
    
    while(1)    
    {    
        printf("string :%s
",shmptr);    
        sleep(3);    
    }    
    
    exit(0);    
}    


内存映射

Linux 内存映射:http://blog.chinaunix.net/uid-26983295-id-3552906.html

Linux的mmap内存映射机制解析:http://blog.csdn.net/zqixiao_09/article/details/51088478

linux编程之内存映射:https://www.cnblogs.com/yuuyuu/p/5137345.html


在讲解内存映射之前,不得不去探讨Linux内存管理方面的知识。需要说明的是,我们并不需要深入的理解Linux虚拟内存才能去实现Linux的内存映射,所以对于Linux内存管理方面的知识也仅限于最基础的概念。


一、Linux的内存管理

Linux的内存管理子系统是采用请求调页式的虚拟存储器技术实现的,有关虚拟存储器方面的知识可以参考《深入理解计算机系统》第二版的第9章内容,在这里就不做说明。

1、Linux进程的虚拟空间及其划分

在32位硬件平台上,Linux的逻辑地址为32位,因此,每个进程的虚拟地址空间为4GB,在4GB的空间中,操作系统占用了高端的1GB,而低端的3GB则留给用户程序使用。如下图所示:

1) Linux内核虚拟存储器

Linux中1GB的内核虚拟存储器空间又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区这几个区域。
一般情况下,物理内存映射区最大长度为896MB,系统的物理内存被顺序映射到物理内存映射区中。当系统物理内存大于896MB时,超过系统物理内存的那部分内存称为高端内存(小于896MB的系统物理内存称为常规内存),内核在存取高端内存时必须将它们映射到高端内存映射区中。下图可以反映出Linux内核虚拟存储器与物理内存之间的映射关系。

注意:物理内存中0~896MB区域通常由内核使用,当然内核不用时用户程序可以使用;896MB以上的区域通常由用户程序来使用。

2) Linux用户虚拟存储器

Linux用户虚拟存储器总是通过页表访问内存,决不会直接访问。如下图所示:

2、进程空间的描述

内核为系统中的每个进程维护一个单独的任务结构task_struct。任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(例如,PID、指向用户栈的指针、可执行目标文件的名字以及程序计数器)。
task_struct中的一个条目指向mm_struct,它描述进程使用的地址空间,我们感兴趣的两个字段是pgd和mmap,其中pgd指向第一级页表(页全局目录)的基址,而mmap指向一个vm_area_struct(区域结构)的链表,每个vm_area_struct结构描述的是进程的一个用户区。如下图所示:

Linux的内存映射

内存映射就是将磁盘上的文件映射到系统内存中,对内存的修改可以同步到对磁盘文件的修改。可以对大数据文件处理,并且可以提高文件的读写速度。至于文件内存映射,说简单点就是把文件内容放到内存中,然后对内存中的数据进行各种操作,最后对其同步到文件。

基本流程都是:打开文件--->映射文件到内存----->同步到文件--->关闭

mmap函数:

Linux提供了内存映射函数mmap,它把文件内容映射到一段内存上(准确说是虚拟内存上),通过对这段内存的读取和修改,实现对文件的读取和修改 。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作

头文件:   
   
   
  
原型: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offsize);   
  
/* 
返回值: 成功则返回映射区起始地址, 失败则返回MAP_FAILED(-1).  
 
参数:  
    addr: 指定映射的起始地址, 通常设为NULL, 由系统指定.  
    length: 将文件的多大长度映射到内存.  
    prot: 映射区的保护方式, 可以是:  
        PROT_EXEC: 映射区可被执行.  
        PROT_READ: 映射区可被读取.  
        PROT_WRITE: 映射区可被写入.  
        PROT_NONE: 映射区不可访问.  
    flags: 映射区的特性, 可以是一个或者多个以下位的组合体:  
        MAP_SHARED: 对映射区域的写入数据会复制回文件, 且允许其他映射该文件的进程共享.  
                    与其它所有映射这个对象的进程共享映射空间。
                    对共享区的写入,相当于输出到文件。
                    但是在调用msync()或者munmap()之前,文件实际上不会被更新。
        MAP_PRIVATE: 对映射区域的写入操作会产生一个映射的复制(copy-on-write), 对此区域所做的修改不会写回原文件. 
                  
        此外还有其他几个flags不很常用, 具体查看linux C函数说明.  
    fd: 由open返回的文件描述符, 代表要映射的文件.  
    offset: 被映射对象内容的起点。以文件开始处的偏移量, 必须是分页大小的整数倍, 通常为0, 表示从文件头开始映射. 
*/  


int msync ( void * addr, size_t len, int flags)    // 同步映射区
    进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。
    可以通过调用msync()函数来实现磁盘文件内容与共享内存区中的内容一致,即同步操作.

参数:
    addr:文件映射到进程空间的地址,即要同步的内存起始地址。
    len:映射空间的大小,即要同步的字节长度。
    flags:刷新的参数设置,可以取值MS_ASYNC/ MS_SYNC/ MS_INVALIDATE
    其中:
        取值为MS_ASYNC(异步)时,调用会立即返回,不等到更新的完成,即此操作内核会先把内容写到内核的缓冲区,某个合适的时候再写到磁盘。
        取值为MS_SYNC(同步)时,调用会等到更新完成之后返回;
        取MS_INVALIDATE(通知使用该共享区域的进程,数据已经改变)时,在共享内容更改之后,
	使得文件的其他映射失效,从而使得共享该文件的其他进程去重新获取最新值;


int munmap(void *addr, size_t length);   // 成功返回 0, 失败返回 -1.
    addr:要解除内存的起始地址。如果addr不在刚刚映射区域的开始位置,解除一部分后内存区域可能会分成两半!!!
    length:要解除的字节数。
当映射关系解除后,对原来映射地址的访问将导致段错误发生。

mmap的作用是映射文件描述符fd指定文件的 [off,off + len]区域至调用进程的[addr, addr + len]的内存区域,。

如下图所示:

mmap系统调用的实现过程是

1.先通过文件系统定位要映射的文件;

2.权限检查,映射的权限不会超过文件打开的方式,也就是说如果文件是以只读方式打开,那么则不允许建立一个可写映射;

3.创建一个vma对象,并对之进行初始化;

4.调用映射文件的mmap函数,其主要工作是给vm_ops向量表赋值;

5.把该vma链入该进程的vma链表中,如果可以和前后的vma合并则合并;

6.如果是要求VM_LOCKED(映射区不被换出)方式映射,则发出缺页请求,把映射页面读入内存中.


munmap函数

munmap(void * start, size_t length):

该调用可以看作是mmap的一个逆过程.它将进程中从start开始length长度的一段区域的映射关闭,如果该区域不是恰好对应一个vma,则有可能会分割几个或几个vma.

msync(void * start, size_t length, int flags):

把映射区域的修改回写到后备存储中.因为munmap时并不保证页面回写,如果不调用msync,那么有可能在munmap后丢失对映射区的修改.其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATE, MS_SYNC要求回写完成后才返回, MS_ASYNC发出回写请求后立即返回, MS_INVALIDATE使用回写的内容更新该文件的其它映射.该系统调用是通过调用映射文件的sync函数来完成工作的.

brk(void * end_data_segement):

将进程的数据段扩展到end_data_segement指定的地址,该系统调用和mmap的实现方式十分相似,同样是产生一个vma,然后指定其属性.不过在此之前需要做一些合法性检查,比如该地址是否大于mm->end_code, end_data_segement和mm->brk之间是否还存在其它vma等等.通过brk产生的vma映射的文件为空,这和匿名映射产生的vma相似,关于匿名映射不做进一步介绍.库函数malloc就是通过brk实现的.


示例代码:

#include
#include
#include
#include
#include

#define NumReconds 100

typedef struct
{
    int iNum;
    char sName[24];
} Recond;

int main(void)
{                                
    Recond recond, *mapped;
    int i,f;
    FILE *fp;

    fp=fopen("recond.dat","w+");
    for( i=0; i < NumReconds; i++)
    {
        recond.iNum = i;
        sprintf(recond.sName, "Recond-%d
", i);
        fwrite(&recond,sizeof(Recond), 1, fp);
    } 
    fclose(fp);

    fp = fopen("recond.dat","r+");  //使用传统方式修改文件内容   
    fseek(fp,43*sizeof(recond),SEEK_SET);  //获得要修改文件的位置
    
    fread(&recond,sizeof(recond),1,fp);
    recond.iNum = 143;
    sprintf(recond.sName,"Recond-%d",recond.iNum);
    fwrite(&recond,sizeof(recond),1,fp);
    fclose(fp);

    //使用内存映射的方式打开文件,修改文件内存
    //注意这里是open 打开,而不是fopen!!!!  open 是系统调用,fopen 是库函数
    f = open("recond.dat",O_RDWR);
    
    //获得磁盘文件的内存映射
    mapped = (Recond *) mmap(0 , NumReconds * sizeof(Recond) , PROT_READ|PROT_WRITE, MAP_SHARED, f, 0);
 
    mapped[43].iNum = 999;    
    sprintf(mapped[43].sName,"Recond-%d",mapped[43].iNum);
    
    //将修改同步到磁盘中
    msync((void *)mapped,NumReconds*sizeof(recond),MS_ASYNC);
    
    //关闭内存映射
    munmap((void *)mapped,NumReconds*sizeof(recond));
    close(f);
    exit(0);
}

示例代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

struct mt 
{
    int num;
    pthread_mutex_t mutex;
    pthread_mutexattr_t mutexattr;
};

int main(void)
{
    int fd, i;
    struct mt *mm;
    pid_t pid;

    fd = open("mt_test", O_CREAT | O_RDWR, 0777);
    /* 不需要write,文件里初始值为0 */
    ftruncate(fd, sizeof(*mm));

    mm = mmap(NULL, sizeof(*mm), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    memset(mm, 0, sizeof(*mm));
    /* 初始化互斥对象属性 */
    pthread_mutexattr_init(&mm->mutexattr);
    /* 设置互斥对象为PTHREAD_PROCESS_SHARED共享,即可以在多个进程的线程访问,PTHREAD_PROCESS_PRIVATE
     * 为同一进程的线程共享 */
    pthread_mutexattr_setpshared(&mm->mutexattr,PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&mm->mutex, &mm->mutexattr);

    pid = fork();
    if (pid == 0)
    {
        /* 加10次。相当于加10 */
        for (i=0;i<10;i++)
        {
            pthread_mutex_lock(&mm->mutex);
            (mm->num)++;
            printf("num++:%d
",mm->num);
            pthread_mutex_unlock(&mm->mutex);
            sleep(1);
        }
    }
    else if (pid > 0) 
    {
        /* 父进程完成x+2,加10次,相当于加20 */
        for (i=0; i<10; i++)
        {
            pthread_mutex_lock(&mm->mutex);
            mm->num += 2;
            printf("num+=2:%d
",mm->num);
            pthread_mutex_unlock(&mm->mutex);
            sleep(1);
        }
        wait(NULL);
    }
    pthread_mutex_destroy(&mm->mutex);
    pthread_mutexattr_destroy(&mm->mutexattr);
    /* 父子均需要释放 */
    munmap(mm,sizeof(*mm));
    unlink("mt_test");
    return 0;
}

示例代码

*********************************************************************/  
#include  /* for mmap and munmap */  
#include  /* for open */  
#include  /* for open */  
#include      /* for open */  
#include     /* for lseek and write */  
#include   
  
int main(int argc, char **argv)  
{  
    int fd;  
    char *mapped_mem, * p;  
    int flength = 1024;  
    void * start_addr = 0;  
  
    fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);  
    flength = lseek(fd, 1, SEEK_END);  
    write(fd, "\0", 1); /* 在文件最后添加一个空字符,以便下面printf正常工作 */  
    lseek(fd, 0, SEEK_SET);  
    mapped_mem = mmap(start_addr, flength, PROT_READ,        //允许读  
        MAP_PRIVATE,       //不允许其它进程访问此内存区域  
            fd, 0);  
      
    /* 使用映射区域. */  
    printf("%s
", mapped_mem); /* 为了保证这里工作正常,参数传递的文件名最好是一个文本文件 */  
    close(fd);  
    munmap(mapped_mem, flength);  
    return 0;  
}  


/*******************************

编译运行此程序:
gcc -Wall mmap.c
./a.out text_filename
上面的方法因为用了PROT_READ,所以只能读取文件里的内容,不能修改,如果换成PROT_WRITE就可以修改文件的内容了。
又由于 用了MAAP_PRIVATE所以只能此进程使用此内存区域,如果换成MAP_SHARED,则可以被其它进程访问,
比如下面的程序

*******************************/
#include  /* for mmap and munmap */  
#include  /* for open */  
#include  /* for open */  
#include      /* for open */  
#include     /* for lseek and write */  
#include   
#include  /* for memcpy */  
  
int main(int argc, char **argv)  
{  
    int fd;  
    char *mapped_mem, * p;  
    int flength = 1024;  
    void * start_addr = 0;  
  
    fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);  
    flength = lseek(fd, 1, SEEK_END);  
    write(fd, "\0", 1); /* 在文件最后添加一个空字符,以便下面printf正常工作 */  
    lseek(fd, 0, SEEK_SET);  
    start_addr = 0x80000;  
    mapped_mem = mmap(start_addr, flength, PROT_READ|PROT_WRITE,        //允许写入  
        MAP_SHARED,       //允许其它进程访问此内存区域  
        fd, 0);  
  
    * 使用映射区域. */  
    printf("%s
", mapped_mem); /* 为了保证这里工作正常,参数传递的文件名最好是一个文本文 */  
    while((p = strstr(mapped_mem, "Hello"))) { /* 此处来修改文件 内容 */  
        memcpy(p, "Linux", 5);  
        p += 5;  
    }  
      
    close(fd);  
    munmap(mapped_mem, flength);  
    return 0;  
}  


mmap和共享内存对比

共享内存允许两个或多个进程共享一给定的存储区,因为数据不需要来回复制,所以是最快的一种进程间通信机制。共享内存可以通过mmap()映射普通文件(特殊情况下还可以采用匿名映射)机制实现,也可以通过系统V共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通信,往往还与信号灯等同步机制共同使用。

对比如下:

mmap机制:就是在磁盘上建立一个文件,每个进程存储器里面,单独开辟一个空间来进行映射。如果多进程的话,那么不会对实际的物理存储器(主存)消耗太大。

shm机制:每个进程的共享内存都直接映射到实际物理存储器里面。

1、mmap保存到实际硬盘,实际存储并没有反映到主存上。优点:储存量可以很大(多于主存);缺点:进程间读取和写入速度要比主存的要慢。

2、shm保存到物理存储器(主存),实际的储存量直接反映到主存上。优点,进程间访问速度(读写)比磁盘要快;缺点,储存量不能非常大(多于主存)

使用上看:如果分配的存储量不大,那么使用shm;如果存储量大,那么使用mmap。


7. 信号量

Linux进程间通信--信号量:http://www.cnblogs.com/melons/p/5791794.html

http://blog.csdn.net/eroswang/article/details/1772350

信号量绝对不同于信号,一定要分清

Linux--进程间通信(信号量,共享内存):http://www.cnblogs.com/forstudy/archive/2012/03/26/2413724.html

信号量: 解决进程之间的同步与互斥的IPC机制

多个进程同时运行,之间存在关联

  •同步关系

  •互斥关系

互斥与同步关系存在的根源在于临界资源

  •临界资源是在同一个时刻只允许有限个(通常只有一个)进程可以访问(读)或修改(写)的资源

    –硬件资源(处理器、内存、存储器以及其他外围设备等)

    –软件资源(共享代码段,共享结构和变量等)

  •临界区,临界区本身也会成为临界资源

一个称为信号量的变量

  •信号量对应于某一种资源,取一个非负的整型值

  •信号量值指的是当前可用的该资源的数量,若它等于0则意味着目前没有可用的资源

在该信号量下等待资源的进程等待队列

对信号量进行的两个原子操作(PV操作)

  •P操作

  •V操作

最简单的信号量是只能取0 和1 两种值,叫做二维信号量

编程步骤:

  创建信号量或获得在系统已存在的信号量

    •调用semget()函数

    •不同进程使用同一个信号量键值来获得同一个信号量

  初始化信号量

    •使用semctl()函数的SETVAL操作

    •当使用二维信号量时,通常将信号量初始化为1

  进行信号量的PV操作

    •调用semop()函数

    •实现进程之间的同步和互斥的核心部分

  如果不需要信号量,则从系统中删除它

    •使用semclt()函数的IPC_RMID操作

    •在程序中不应该出现对已被删除的信号量的操作


什么是信号量

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法。比如在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。信号量与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。

信号量有以下两种类型:

二值信号量:最简单的信号量形式,信号灯的值只能取0或1,类似于互斥锁。
计算信号量:信号量的值可以取任意非负值(当然受内核本身的约束)。


信号量的本质是一种数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。

当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值来判断资源是否可用大于0,资源可以请求,等于0,无资源可用,进程会进入睡眠状态(进程挂起等待)直至资源可用。 当进程不再使用一个信号量控制的共享资源时,信号量的值+1(信号量的值大于0),对信号量的值进行的增减操作均为原子操作,这是由于信号量主要的作用是维护资源的互斥或多进程的同步访问。 而在信号量的创建及初始化上,不能保证操作均为原子性。

为社么要使用信号量

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法, 它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。 临界区域是指执行数据更新的代码需要独占式地执行(这个房子暂时我一个人可以用, 房子好比临界区域)。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它, 也就是说信号量是用来调协进程对共享资源的访问的。其中共享内存的使用就要用到信号量。

信号量的工作原理

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

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

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

为了获取共享资源,进程需要执行下列操作:

  1. 测试控制该资源的信号量
  2. 若此信号量的值为正,则允许进行使用该资源。进程将信号量减1。
  3. 若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1)。
  4. 当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。

维护信号量状态的是Linux内核操作系统而不是用户进程。我们可以从头文件/usr/src/linux/include/linux/sem.h 中看到内核用来维护信号量状态的各个结构的定义。信号量是一个数据集合,用户可以单独使用这一集合的每个元素。要调用的第一个函数是semget,用以获 得一个信号量ID。

信号相关的结构:

struct semid_ds {
    struct ipc_perm sem_perm;/*see Section 15.6.2 */
    unsigned short sem_nsems;/*# of semaphores in set */
    time_t sem_otime;/*last-semop() time */
    time_t sem_ctime;/*last-change time */
    ......
};

为了正确使用信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。使用信号量涉及到的函数如下,具体使用方法参考man手册。

//获取信号量ID
#include 
int semget(key_t key, int nsems, int flag);
//Returns: semaphore ID if OK, −1 on error

//各种信号量操作
#include 
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */ );
//Returns: (see following)

//原子操作,自动执行信号量集合上的操作数组
#include 
int semop(int semid, struct sembuf semoparray[], size_t nops);
//Returns: 0 if OK, −1 on error

int semget(key_t key, int num_sems, int sem_flags);
//semget()创建一个新信号量集或取得一个已有信号量集, 第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量。 key的值可以为IPC_PRIVATE,这时这个信号量集用于线程间同步。 这个函数返回的是信号量标识符, semop() 和 semctl()能使用它。信号量标识符是int型,指向一个结构体地址,图如下:

struct semid_ds{} 是一个信号量集结构体,sem_base是一个指针,指向一组信号量。 sem_nsems 标识信号量集中有多少个信号量。

semget() 函数中形参 int num_sems指定集合中信号量个数。 如果我们不创建一个新的信号量集,而只是访问一个已存在的集合,那就可以把该参数指定为0,也可指定为已存在集合的信号量个数.

semget() 仅仅只是创建一个信号量集,不进行初始化。 使用semctl(semid, 0, SETVAL, arg) 或者semctl(semid, 0,SETALL, arg) 进行初始化。

int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops); //改变信号量的值。
int semctl(int semid,int semnum,int cmd,union semun arg); //该函数用来直接控制信号量信息

注意:
a.) semget()使用时,有一个特殊的信号量key值,IPC_PRIVATE(通常为0),其作用是创建一个只有创建进程可以访问的信号量,可以在线程间使用,不能在进程间使用。
(某些Linux系统上,手册页将IPC_PRIVATE并没有阻止其他的进程访问信号量作为一个bug列出。)
b.) 在semget() 创建一个信号量后,需要使用 semctl( SETVAL) 来初始化这个信号量。


Linux的信号量机制

Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。

semget函数:它的作用是创建一个新信号量或取得一个已有信号量,原型为:

int semget(key_t key, int num_sems, int sem_flags);  
第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,
        程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),
        只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。
        如果多个程序使用相同的key值,key将负责协调工作。

第二个参数num_sems指定需要的信号量数目,它的值几乎总是1。

第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,
        即使给出的键是一个已有信号量的键,也不会产生错误。
        而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

semget函数成功返回一个相应信号标识符(非零),失败返回-1.



semop函数:它的作用是改变信号量的值,原型为:

int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);  
sem_id是由semget返回的信号量标识符,sembuf结构的定义如下:

struct sembuf{  
    short sem_num;//除非使用一组信号量,否则它为0  
    short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,  
                    //一个是+1,即V(发送信号)操作。  
    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,  
                    //并在进程没有释放该信号量而终止时,操作系统释放信号量  
};  



semctl函数

int semctl(int sem_id, int sem_num, int command, ...);  
如果有第四个参数,它通常是一个union semum结构,定义如下:

union semun{  
    int val;  
    struct semid_ds *buf;  
    unsigned short *arry;  
};  
前两个参数与前面一个函数中的一样,command通常是下面两个值中的其中一个 
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。 
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

示例

这个程序的临界区为main函数for循环的semaphore_p和semaphore_v函数中间的代码。
分析:同时运行一个程序的两个实例,注意第一次运行时,要加上一个字符作为参数,例如本例中的字符‘O’,它用于区分是否为第一次调用,同时这个字符输出到屏幕中。因为每个程序都在其进入临界区后和离开临界区前打印一个字符,所以每个字符都应该成对出现,正如你看到的上图的输出那样。在main函数中循环中我们可以看到,每次进程要访问stdout(标准输出),即要输出字符时,每次都要检查信号量是否可用(即stdout有没有正在被其他进程使用)。所以,当一个进程A在调用函数semaphore_p进入了临界区,输出字符后,调用sleep时,另一个进程B可能想访问stdout,但是信号量的P请求操作失败,只能挂起自己的执行,当进程A调用函数semaphore_v离开了临界区,进程B马上被恢复执行。然后进程A和进程B就这样一直循环了10次。

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章