欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Linux新手必看:进程间通信(IPC)机制全面解析

最编程 2024-08-02 09:30:21
...
与命名管道相比,消息队列的优势在于,1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。

5. 信号量(Semaphore
信号量是一种计数器,用于控制对多个进程共享的资源进行的访问。它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。 
信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:等待(wait)和信号(signal)。(P、V操作,P用于等待,V用于信号) 
p(sv):如果sv的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行 
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1 
简单理解就是P相当于申请资源,V相当于释放资源 
信号量头文件: 
#include <sys/types.h> 
#include <sys/stat.h>
#include <sys/sem.h> 
 
  
内核为每个信号量集合都维护一个semid_ds结构:
struct semid_ds{
    struct ipc_perm sem_perm;
    unsigned short sem_nsems;
    time_t sem_otime;
    time_t sem_ctime;
    ...
}
信号量数据结构:
union semun{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
}
 信号量操作sembuf结构: 
  
struct sembuf{
    ushort sem_num;//信号量的编号
    short sem_op;//信号量的操作。如果为正,则从信号量中加上一个值,如果为负,则从信号量中减掉一个值,如果为0,则将进程设置为睡眠状态,直到信号量的值为0为止。
    short sem_flg;//信号的操作标志,一般为IPC_NOWAIT。
}
 常用函数: 
  
int semget(key_t key, int num_sems, int sem_flags); //semget函数用于创建一个新的信号量集合 , 或者访问一个现有的集合(不同进程只要key值相同即可访问同一信号量集合)。第一个参数key是ftok生成的键值,第二个参数num_sems可以指定在新的集合应该创建的信号量的数目,第三个参数sem_flags是打开信号量的方式。
eg.int semid = semget(key, 0, IPC_CREATE | IPC_EXCL | 0666);//第三个参数参考消息队列int msgget(key_t key,int msgflag);第二个参数。
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops); //semop函数用于改变信号量的值。第二个参数是要在信号集合上执行操作的一个数组,第三个参数是该数组操作的个数 。
eg.struct sembuf sops = {0, +1, IPC_NOWAIT};//对索引值为0的信号量加一。
semop(semid, &sops, 1);//以上功能执行的次数为一次。
int semctl(int sem_id, int sem_num, int command,...); //semctl函数用于信号量集合执行控制操作,初始化信号量的值,删除一个信号量等。 类似于调用msgctl(), msgctl()是用于消息队列上的操作。第一个参数是指定的信号量集合(semget的返回值),第二个参数是要执行操作的信号量在集合中的索引值(例如集合中第一个信号量下标为0),第三个command参数代表要在集合上执行的命令。
IPC_STAT:获取某个集合的semid_ds结构,并把它存储到semun联合体的buf参数指向的地址。
IPC_SET:将某个集合的semid_ds结构的ipc_perm成员的值。该命令所取的值是从semun联合体的buf参数中取到。
IPC_RMID:内核删除该信号量集合。
GETVAL:返回集合中某个信号量的值。
SETVAL:把集合中单个信号量的值设置成为联合体val成员的值。

6. 共享内存(Share Memory)
共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建的一个特殊地址范围,它将出现在该进程的地址空间(这里的地址空间具体是哪个地方?)中。其他进程可以将 同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是malloc分配的一样。如果一个进程向共享内存中写入了数据,所做的改动将立刻被其他进程看到。 
共享内存是 IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换。共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。
注意:共享内存本身并没有同步机制,需要程序员自己控制。 
  共享内存头文件:
#include <sys/types.h> 
#include <sys/stat.h>
#include <sys/shm.h> 
结构shmid_ds结构体(是不是很眼熟,看消息队列的 msgid_ds结构体):
strcut shmid_ds{
    struct ipc_perm    shm_perm;
    size_t    shm_segsz;
    time_t    shm_atime;
    time_t    shm_dtime;
    ......
}
共享内存函数定义:
int shmget(key_t key,size_t size,int shmflg);  //shmget函数用来创建一个新的共享内存段, 或者访问一个现有的共享内存段(不同进程只要key值相同即可访问同一共享内存段)。第一个参数key是ftok生成的键值,第二个参数size为共享内存的大小,第三个参数sem_flags是打开共享内存的方式。
eg.int shmid = shmget(key, 1024, IPC_CREATE | IPC_EXCL | 0666);//第三个参数参考消息队列int msgget(key_t key,int msgflag);
void *shmat(int shm_id,const void *shm_addr,int shmflg); //shmat函数通过shm_id将共享内存连接到进程的地址空间中。第二个参数可以由用户指定共享内存映射到进程空间的地址,shm_addr如果为0,则由内核试着查找一个未映射的区域。返回值为共享内存映射的地址。
eg.char *shms = (char *)shmat(shmid, 0, 0);//shmid由shmget获得
int shmdt(const void *shm_addr); //shmdt函数将共享内存从当前进程中分离。 参数为共享内存映射的地址。
eg.shmdt(shms);
int shmctl(int shm_id,int cmd,struct shmid_ds *buf);//shmctl函数是控制函数,使用方法和消息队列msgctl()函数调用完全类似。参数一shm_id是共享内存的句柄,cmd是向共享内存发送的命令,最后一个参数buf是向共享内存发送命令的参数。

消息队列、信号量以及共享内存的相似之处:
它们被统称为XSI IPC,它们在内核中有相似的IPC结构(消息队列的msgid_ds,信号量的semid_ds,共享内存的shmid_ds),而且都用一个非负整数的标识符加以引用(消息队列的msg_id,信号量的sem_id,共享内存的shm_id,分别通过msgget、semget以及shmget获得),标志符是IPC对象的内部名,每个IPC对象都有一个键(key_t key)相关联,将这个键作为该对象的外部名。

XSI IPC和PIPE、FIFO的区别:
1、XSI IPC的IPC结构是在系统范围内起作用,没用使用引用计数。如果一个进程创建一个消息队列,并在消息队列中放入几个消息,进程终止后,即使现在已经没有程序使用该消息队列,消息队列及其内容依然保留。而PIPE在最后一个引用管道的进程终止时,管道就被完全删除了。对于FIFO最后一个引用FIFO的进程终止时,虽然FIFO还在系统,但是其中的内容会被删除。
2、和PIPE、FIFO不一样,XSI IPC不使用文件描述符,所以不能用ls查看IPC对象,不能用rm命令删除,不能用chmod命令删除它们的访问权限。只能使用ipcs和ipcrm来查看可以删除它们。

7. 内存映射(Memory Map)
内存映射文件,是由一个文件到一块内存的映射。内存映射文件与 虚拟内存有些类似,通过内存映射文件可以保留一个地址的区域,
同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作。 每一个使用该机制的进程通过把同一个共享的文件映射到自己的进程地址空间来实现多个进程间的通信(这里类似于共享内存,只要有一个进程对这块映射文件的内存进行操作,其他进程也能够马上看到)。
使用内存映射文件不仅可以实现多个进程间的通信,还可以用于 处理大文件提高效率。因为我们普通的做法是 把磁盘上的文件先拷贝到内核空间的一个缓冲区再拷贝到用户空间(内存),用户修改后再将这些数据拷贝到缓冲区再拷贝到磁盘文件,一共四次拷贝。如果文件数据量很大,拷贝的开销是非常大的。那么问题来了,系统在在进行内存映射文件就不需要数据拷贝?mmap()确实没有进行数据拷贝,真正的拷贝是在在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,所以只进行一次数据拷贝。效率高于read/write。
内存映射头文件:
#include <sys.mman.h> 
void *mmap(void*start,size_t length,int prot,int flags,int fd,off_t offset); //mmap函数将一个文件或者其它对象映射进内存。 第一个参数为映射区的开始地址,设置为0表示由系统决定映射区的起始地址,第二个参数为映射的长度,第三个参数为期望的内存保护标志,第四个参数是指定映射对象的类型,第五个参数为文件描述符(指明要映射的文件),第六个参数是被映射对象内容的起点。成功返回被映射区的指针,失败返回MAP_FAILED[其值为(void *)-1]。
int munmap(void* start,size_t length); //munmap函数用来取消参数start所指的映射内存起始地址,参数length则是欲取消的内存大小。如果解除映射成功则返回0,否则返回-1,错误原因存于errno中错误代码EINVAL。 
int msync(void *addr,size_t len,int flags); //msync函数实现磁盘文件内容和共享内存取内容一致,即同步。第一个参数为文件映射到进程空间的地址,第二个参数为映射空间的大小,第三个参数为刷新的参数设置。

共享内存和内存映射文件的区别:
内存映射文件是利用虚拟内存文件映射到进程的地址空间中去,在此之后进程操作文件,就像操作进程空间里的地址一样了,比如使用c语言的memcpy等内存操作的函数。这种方法能够很好的应用在需要频繁处理一个文件或者是一个大文件的场合,这种方式处理IO效率比普通IO效率要高
共享内存是内存映射文件的一种特殊情况,内存映射的是一块内存,而非磁盘上的文件。共享内存的主语是进程(Process),操作系统默认会给每一个进程分配一个内存空间,每一个进程只允许访问操作系统分配给它的哪一段内存,而不能访问其他进程的。而有时候需要在不同进程之间访问同一段内存,怎么办呢?操作系统给出了 创建访问共享内存的API,需要共享内存的进程可以通过这一组定义好的API来访问多个进程之间共有的内存,各个进程访问这一段内存就像访问一个硬盘上的文件一样。

内存映射文件与虚拟内存的区别和联系:
内存映射文件和虚拟内存都是操作系统内存管理的重要部分,两者有相似点也有不同点。
联系:虚拟内存和内存映射都是将一部分内容加载到内存,另一部放在磁盘上的一种机制。对于用户而言都是透明的。
区别:虚拟内存是硬盘的一部分,是内存和硬盘的数据交换区,许多程序运行过程中把暂时不用的程序数据放入这块虚拟内存,节约内存资源。内存映射是一个文件到一块内存的映射,这样程序通过内存指针就可以对文件进行访问。
虚拟内存的硬件基础是分页机制。另外一个基础就是局部性原理(时间局部性和空间局部性),这样就可以将程序的一部分装入内存,其余部分留在外存,当访问信息不存在,再将所需数据调入内存。而内存映射文件并不是局部性,而是使虚拟地址空间的某个区域银蛇磁盘的全部或部分内容,通过该区域对被映射的磁盘文件进行访问,不必进行文件I/O也不需要对文件内容进行缓冲处理。

8. 套接字 
套接字机制不但可以单机的不同进程通信,而且使得跨网机器间进程可以通信。 
套接字的创建和使用与管道是有区别的,套接字 明确地将客户端与服务器 区分开来,可以实现多个客户端连到同一服务器。 
服务器套接字连接过程描述: 
首先,服务器应用程序用socket创建一个套接字,它是系统分配服务器进程的类似文件描述符的资源。 接着,服务器调用bind给套接字命名。这个名字是一个标示符,它允许linux将进入的针对特定端口的连接转到正确的服务器进程。 然后,系统调用listen函数开始接听,等待客户端连接。listen创建一个队列并将其用于存放来自客户端的进入连接。 当客户端调用connect请求连接时,服务器调用accept接受客户端连接,accept此时会创建一个新套接字,用于与这个客户端进行通信。 
客户端套接字连接过程描述: 
客户端首先调用socket创建一个未命名套接字,让后将服务器的命名套接字作为地址来调用connect与服务器建立连接。 
只要双方连接建立成功,我们就可以像操作底层文件一样来操作socket套接字实现通信。 
几个基础函数定义: 
#include <sys/types.h> 
#include <sys/socket.h> 
int socket(it domain,int type,int protocal); 
int bind(int socket,const struct sockaddr *address,size_t address_len); 
int listen(int socket,int backlog); 
int accept(int socket,struct sockaddr *address,size_t *address_len); 
int connect(int socket,const struct sockaddr *addrsss,size_t address_len); 
详细请看: http://blog.****.net/a987073381/article/details/51869000
还记得消息队列中的msgbuf结构吗?在socket编程中也同样适用,在socket编程中,一个服务可以接受多个客户端的连接,可以为每个客户端设定一个消息类型,服务器和客户端直接的通信可以通过此消息类型来发送和接受消息,而且多个客户端之间也可以通过消息类型来区分。

参考:
http://www.cnblogs.com/mickole/p/3192210.html
http://blog.****.net/nodeathphoenix/article/details/23284157
http://blog.****.net/ljianhui/article/details/10287879
http://www.cnblogs.com/kunhu/p/3608589.html
http://blog.****.net/kobejayandy/article/details/18863543
http://www.cnblogs.com/lbsx/archive/2009/08/03/1537698.html
http://baike.baidu.com/link?url=ak6DtjdUQddNHUW0zOP1Qy1UrIX0zJoLG77RCoRsfhNu7O2H1JfcYPVjUKuLTzuAm7g0HAxE0OVqRODOWYwr7_
http://blog.****.net/hongchangfirst/article/details/11599369
http://blog.sina.com.cn/s/blog_4eee98350100abbr.html
《linux网络编程》
《unix环境高级编程》