Linux进程通信机制


Linux管道通信机制

管道是所有Unix及Linux都提供的一种进程间的通信机制,它是进程之间的一个单向数据流,一个进程可向管道写入数据,另一个进程可从管道中读取数据,从而达到数据交换的目的。

Liunx的管道通信机制有无名管道PIPE有名管道FIFO两种机制。


无名管道

  • 无名管道只能用于具有亲缘关系的进程之间的通信。
  • 无名管道是半双工的,具有固定的读写端。虽然pipe()系统调用返回了两个文件描述符,但每个进程在使用一个文件描述符之前应该先将另外一个文件描述符关闭。如果需要双向的数据流,则必须通过两次pipe()建立起两个管道。
  • 无名管道可以看作是一种特殊的文件,由一组VFS对象(虚拟文件系统)来实现,有对应的磁盘映像,只存在于内存的高速缓存中。Linux在2.6之后的版本中,把管道相关的VFS对象组织成一种特殊文件系统pipefs进行管理,但它在系统目录树中没有安装点,所以用户看不到。
    对管道的读写和对普通文件读写差不多,使用通用的read()、write()等,但内核最终会调用管道文件的读写操作函数

创建无名管道:

pipe()

int pipe(int fileds[2]);
 需要包含头文件<unistd.h>

参数:
	fileds[2] 是一个输出参数,返回两个文件描述符
	0 用于读管道
	1 用于写管道
	
功能:
* 在内缓冲区创建一个管道,主要是建立相关 VFS 对象,
* 并将读写该管道的一对文件描述符保存在filedes[2]中。
* 不再使用管道时,只需关闭两个文件描述符即可。

返回值:
	成功返回:0
	失败返回:-1,并且在error中存入错误码

从管道中读取数据

进程使用 read() 系统调用从管道中读取数据,内核最终会调用 pipe_read() 函数来实现。

#include <unistd>
ssize_t read(int filedes, void *buf, size_t nbytes);

返回:若成功则返回读到的字节数,若已到文件末尾则返回0,若出错则返回-1
filedes:文件描述符
buf:读取数据缓存区
nbytes:要读取的字节数

Linux2.6.10之前,每个管道仅有一个缓冲区(4KB);而在2.6.11之后,每个管道最多可有16个缓冲区(64KB)。

从管道中读取数据有两种方式:

  • 阻塞型读取数据
    管道大小(管道缓冲区中待读的字节数)为p,用户进程请求读取n个字节,则阻塞型读取情况如表所示:
管道大小至少有一个写进程没有写进程
p=01. 如果有睡眠写进程,读取n个字节并返回n,当管道缓冲区为空时等待写进程写数据。 2. 如果没有睡眠写进程,等待写进程写数据,然后读取数据返回0
0<p<n1. 有睡眠写进程同上; 2. 无睡眠写进程,读取p个字节并返回p,管道缓冲区中还剩0个字节读取p个字节并返回p;管道缓冲区中还剩0个字节。
p>=n读取n个字节,返回n,管道缓冲区中还剩 p-n 个字节读取n个字节,返回n,管道缓冲区中还剩 p-n 个字节

简单总结一下,阻塞的情况主要是产生在管道大小p小于n的情况,如果待读的字节数p已经大于等于n也不至于阻塞。

  • 非阻塞型读取数据
    非阻塞操作通常都是在open()系统调用中指定O_NONBLOCK(非阻塞方式)标志进行请求,但这个方法不适合无名管道,因为它没有open()操作,不过进程可以通过对相应的文件描述符发出 fcntl() 系统调用来请求对管道执行非阻塞操作。在非阻塞情况下,如果管道大小p小于n,则读取p个字节并返回p,读操作完成;否则读取n个字节并返回n,读操作完成。

fcntl
文件控制函数

文件控制函数          fcntl -- file control
头文件:
#include <unistd.h>
#include <fcntl.h>

函数原型:          
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);         
int fcntl(int fd, int cmd, struct flock *lock);

描述:
fcntl()针对(文件)描述符提供控制.参数fd是被参数cmd操作(如下面的描述)的描述符.            
针对cmd的值,fcntl能够接受第三个参数(arg)

fcntl函数有5种功能:
1.复制一个现有的描述符(cmd=F_DUPFD).
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

F_SETFL     
设置给arg描述符状态标志,可以更改的几个标志是:O_APPEND, O_NONBLOCK,O_SYNC和O_ASYNC


向管道中写入数据

进程使用 write() 系统调用向管道中写入数据,内核最终会调用pipe_write() 函数来实现。

#include <unistd>
ssize_t write(int filedes, const void *buf, size_t nbytes);
 返回:若成功则返回写入的字节数,若出错则返回-1
 filedes:文件描述符
 buf:待写入数据缓存区
 nbytes:要写入的字节数

函数说明:write()会把参数buf所指的内存写入count个字节到参数放到所指的文件内。
返回值:如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。

POSIX标准要求涉及少量字节数(<=4096B)的写操作必须“原子”地进行,更确切的说,如果两个或多个进程并发地写同一个管道,那么任何不超过4096B(PIPE_BUF,管道缓冲区)的写操作必须单独完成,不能与其他进程的写操作交叉进行。但是超过4096B的写操作是可以分割的。

向管道中写入数据也有两种方式,阻塞型和非阻塞型。
若管道缓冲区还有u字节空闲空间,进程请求写入n个字节

缓冲区剩余空间至少有一个读进程 、阻塞写至少有一个读进程、非阻塞写没有读进程
u<n<=4096等待,直到n-u个字节被读出为止,写入n个字节并返回n返回-EAGAIN,提醒以后再写写入失败,内核向写进程发送SIGPIPE信号,并返回-EPIPE
n>4096写入n个字节(必要时等待)并返回n如果u>0,则写入u字节并返回u;否则,就返回-EAGAIN同上
u>=n写入n个字节并返回n同左同上

关闭管道

不再使用管道时,只需关闭两个文件描述符即可。
用close函数把两个文件描述符关闭。
close

int close(int fd);
fd就是之前用open()获得的一个文件描述符。

在内核中,打开的文件会被维护一个引用计数,每次close()会把文件的引用计数减一,
引用计数减少到0的文件才会从内核中释放资源。

有名管道

无名管道应用的一个很大的限制是只能用于具有亲缘关系的进程间通信,而有名管道(named pipe或FIFO)克服了该限制。有名管道不同于无名管道之处在于它是有文件名的。以FIFO文件形式真实地存在于磁盘上的文件系统中。这样即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该文件,就能够彼此通过有名管道相互通信,从而实现不相关进程间的数据交换。

此外,有名管道是一种双向通信管道,进程能以读写模式打开一个有名管道文件,但一般不建议这么做,因为可能导致进程读取自己写入的数据。有名管道也严格遵循先进先出的原则,对管道的读总是从开始处返回数据,对管道的写则把数据添加到末尾。有名管道和无名管道都不支持诸如lseek()等文件定位操作


创建有名管道:

mkfifo()

	int mkfifo(const char* pathname, mode_t mode);
	需要包含头文件<sys/types.h><sys/stat.h>
	
参数:
	pathname是路径名(含有名管道的文件名)
	mode是文件的权限

打开有名管道:

open()
与普通文件类似,有名管道在使用之前必须先进行open操作。
函数原型:

	int open(const char* pathname, int flags);
	需要包含头文件<sys/types.h> <sys/stat.h> <fcntl.h>
	pathname是路径名
	flags是打开方式,有O_RDONLY O_WRONLY O_RDWR O_NONBLOCK(非阻塞方式)
	成功返回文件描述符,失败返回-1



POSIX通信

POSIX : Portable Operating System Interface 可移植操作系统接口
IPC:Inter-Process Communication 进程间通信

POSIX IPC 包括semaphore(信号量)shared memory(共享内存)message queue(消息队列)


POSIX信号量

用于进程间同步。
分为有名信号量无名信号量

Linux实现有名信号量的方式是创建一个同名文件,进程间通信的载体就是该文件。有名信号量通过IPC名字进行进程间的同步。

无名信号量又称为基于内存的信号量,常用于多线程间的同步,也可用于相关进程间的同步。内存信号量没有名字这种情况下,通信的进程需要共享存放信号量的内存。如果是多线程通信,一个全局变量即可。如果是进程间通信,需要内存映射。

注意!!!
使用Posix信号量时必须包含头文件#include<semaphore.h>,
且在编译程序的时候,应该加上 -pthread选项,
gcc -o test1 -pthread test1.c


有名信号量

有名信号量的特点是把信号量的值保存在文件中,所以对于相关进程来说,子进程继承了父进程的文件描述符,自然共享了保存在文件中的有名信号量。因此,可以使用有名信号量实现相关进程间的同步。

可以通过sem_open创建,通过sem_unlink销毁。
函数(用户空间)的原型如下:
创建有名信号量:

sem_open

打开一个已经存在的有名信号量,或创建并初始化一个有名信号量,并将其引用计数加1
sem_t * sem_open(const char* name, int oflag)
sem_t * sem_open(const char* name, int oflag, mode_t mode, unsigned int value)

name : 有名信号量的名称,
       【在linux下,sem都是创建在/dev/shm目录下,所以name不能写路径,直接"mysem"这样就好】
oflag : 说明创建方式
       O_CREATE : 若name指定的信号量不存在,则创建一个,且后面的mode和value参数必须有效;
                  若指定的信号量已经存在,直接打开,忽略后面两个参数
       O_CREATE|O_EXCL : 若name指定的信号量不存在,则创建一个;
       					 否则直接返回error
mode_t : 控制新建信号量的访问权限,如0644
value : 新建信号量的初始值   
返回值 : 成功时返回指向有名信号量的指针,出错时为SEM_FAILED(常量=2,表示失败)

有名信号量的删除需要两个步骤:
sem_close

1. 关闭信号量
将信号量引用计数-1,但并没有删除它,还是可以使用sem_open()打开				
int sem_close(sem_t * sem)
>>>
sem : 指向欲关闭的信号量的指针,即调用sem_open()的返回值
返回值 : 成功0,失败-1
>>>

sem_unlink

2. 删除信号量
从系统中彻底删除该信号量,注意,sem_unlink()只对引用计数为0的信号量有用,
对引用计数不为0的信号量不会有任何作用
【因为这是计数型信号量】
int sem_unlink(const char * name)
>>>
name : 有名信号量的标示符
返回值 : 成功0,失败-1
>>>

无名信号量

无名信号量本质上就是一块足够存放sem_t类型的变量的共享内存,不需要open,但必须使用sem_init初始化,释放内存前需要调用sem_destroy销毁。


信号量的使用

对信号量的使用上,两种方式并无差别,使用sem_post释放信号量,sem_wait获取信号量。

sem_post

int sem_post(sem_t *sem);
功能:释放资源操作,将指定信号量的值加1,若有线程/进程在等待,则会唤醒其中的一个线程/进程
参数:sem 信号量的名称
返回值:成功0,错误-1并且不改信号量的值

sem_wait

int sem_wait(sem_t *sem);
功能:阻塞型申请资源操作:测试信号量sem的值,若大于0,则将sem的值减1后返回;若等于0,
则调用线程/进程会进入阻塞状态直到另一个相关线程/进程执行sem_post, ……解除阻塞,然后
立即将sem减1,然后返回。
参数:sem 指定的信号量名称
返回值:函数执行成功返回0;错误时返回-1,信号量的值不改动。

补充说明
sem 是信号量名称,无名有名都有
name 是有名信号量名称,就有名有,其实是文件路径


IPC消息队列

在IPC消息队列通信机制中,若干个进程可以共享一个消息队列,系统允许其中的一个或多个进程向消息队列写入消息,同时也允许一个或多个进程从消息队列中读取消息,从而完成进程之间的信息交换,这种通信机制被称为消息队列通信机制。
消息队列服务通信机制是客户\服务器模型中常用的进程通信方式:客户向服务器发送请求信息,服务器读取消息并执行相应的请求。消息可以是命令也可以是数据。

相关的数据结构:

消息缓冲区
msgbuf,IPC机制中的消息缓冲区是由固定大小的首部和可变长度的正文组成,系统只是给出了缓冲区的基本定义模板。程序员可以根据这个重新定义。
该结构的第一个成员mtype必须是一个大于0的长整数,表示对应消息的类型,以允许进程有选择地从消息队列中获取消息。
可以像下面这样自定义:

struct my_msgbuf{
   long mtype;
   long sender_id, receiver_id;
   char mtext[1024];
}

Linux没有限定mtext的类型,但是限定了最大长度MSGMAX(一个宏 常量 8192 不同版本值不一定一样)。

消息结构
消息列表中的每个消息节点由msg_msg结构来描述,定义在include/linux/msg.h文件中。

m_list 指向消息队列中的下一条消息
m_type 消息类型
m_ts 消息正文的大小
next 消息的下一部分

每条消息分开存放在一个或多个动态分配的内存页中,第一页起始部分存放消息头,即上面的msg_msg结构体,之后紧接着存放消息正文。如果消息正文超出4072B(第一页剩下空间),就继续存放在第二页,其地址存放在msg_msg结构中的next字段中;第二页以msg_msgseg结构体开始,该结构体只有一个成员:next指针,指向可选的第三页。

消息队列结构
msg_queue:系统中每个消息队列由一个msg_queue结构描述,定义在include/linux/msg.h文件中,,,不过这个结构体在ipc/msg.c文件里找到

Linux为避免资源耗尽,给出了几个限制:IPC消息队列数最多为16个,每个消息大小最大为8192B,一个消息队列中全部消息大小最大为16384B。不过系统管理员可以通过修改/proc/sys/kernel路径下的msgmni文件、msgmax文件及msgmnb文件来调整这些值。


IPC消息队列相关的系统调用
<sys/types.h> <sys/ipc.h> <sys/msg.h>

msgget
创建消息队列

int msgget(key_t key,int msgflg);

参数:
key: 消息队列的键值,若为0(IPC_PRIVATE),则创建一个新的消息队列;
若大于0(通常是通过ftok()函数生成的),则进一步依据msgflag参数确定本函数的行为

msgflg: 对消息队列的访问权限和控制命令的组合。
访问权限用三个八进制整数分别表示属主、同组用户和其他用户的权限
IPC_CREAT:如果key对应的消息队列不存在,则创建它;
如果已经存在,则返回其标识符
IPC_EXCL|IPC_CREATE:如果key对应的消息队列不存在,则创建它;
如果已经存在,则出错返回-1

功能:如果参数msgflag为IPC_CREATE,则msgget()新创建一个消息队列,
并返回其标识符。
或者返回具有相同键值的已存在的消息队列的标识符……

msgsnd
向消息队列发送消息

int msgsnd(int msqid, struct msgbuf* msgp, size_t msgsz, int msgflg);
参数:
msqid: 消息队列的标识符
msgp: 存放欲发送消息内容的消息缓冲区指针
msgsz: 消息正文(而非整个消息结构)的长度
msgflg: 发送标志
* 0:消息队列满时,调用进程(发送进程)将会阻塞,直到消息队列可写入该消息
* IPC_NOWAIT: 消息队列满时,调用进程立即返回-1
* MSG_NOERROR: 消息正文长度超过msgsz时,不报错,而是直接截去多余的部分,
并只将前面的msgsz字节发送出去

msgrcv
从消息队列接收消息

int msgrcv(int msqid, struct msgbuf* msgp, size_t msgsz, long msgtyp, int msgflg);
参数: 
msqid: 消息队列的标识符
msgp: 存放欲发送消息内容的消息缓冲区指针
msgsz: 消息正文(而非整个消息结构)的长度
msgtyp: 接收的消息类型
* 0: 接收消息队列中的第一个消息
* >0: 接收第一个类型为msgtyp的消息
* <0: 接收第一个类型小于等于msgtyp的绝对值的消息
msgflg: 接收消息时的标志
* 0: 没有可以接收的消息时,调用进程(接收进程)阻塞
* IPC_NOWAIT: 没有可以接收的消息时,立即返回-1
* MSG_EXCEPT: 返回第一个类型不是msgtyp的消息
* MSG_NOERROR: 消息正文长度超过msgsz字节时,将直接截取多余的部分

返回值: 接收成功,返回实际接收到消息正文的字节数; 否则返回-1

msgctl
获取或设置消息队列的属性信息,或删除消息队列

int msgctl(int msqid, int cmd, struct msqid_ds * buf);
参数: 
msqid: 消息队列的标识符
cmd: 将要在消息队列上执行的命令,包括IPC_STAT、IPC_SET和IPC_RMID
IPC_RMID是最常用的,删除消息队列,并且唤醒该消息队列上等待读或等待写的进程。
调用者必须有相应的权限。
buf: 用户空间中的一个缓存,接收或提供状态信息

返回值: 执行成功返回0; 否则返回-1

补充说明
Linux的消息队列(queue)实质上是一个链表, 它有消息队列标识符(queue ID). msgget创建一个新队列或打开一个存在的队列; msgsnd向队列末端添加一条新消息; msgrcv从队列中取消息, 取消息是不一定遵循先进先出的, 也可以按消息的类型字段取消息.

标识符(des)和键(key):

消息队列, 信号量和共享存储段, 都属于内核中的IPC结构, 它们都用标识符来描述. 这个标识符是一个非负整数, 与文件描述符不同的是, 创建时并不会重复利用通过删除回收的整数, 而是每次+1, 直到整数最大值回转到0.

标识符是IPC对象的内部名, 而它的外部名则是key(键), 它的基本类型是key_t, 在头文件<sys/types.h>中定义为长整型. 键由内核变换成标识符.




IPC共享内存通信

共享内存实际上是一段特殊的内存区域,它可以被两个或以上的进程映射到自身的地址空间中,就好像它是由C中的malloc()分配的内存一样。一个进程写入共享内存中的信息,可以被其他使用这个共享内存的进程读出,从而实现了进程间的通信。
这块虚拟共享内存的页面在每一个共享它的进程的页表中都有页表项引用,但是不需要在所有进程的虚拟内存中都有相同的地址。
Linux系统并没有为共享内存机制提供同步机制,程序员需要使用同步机制保证进程间的同步关系。

系统为每个共享内存区都设置了一个shmid_kernel结构,用来描述该共享内存区的属性及使用信息。

struct shmid_kernel{

struct kern_ipc_perm shm_perm; //共享内存区的kern_ipc_perm结构
struct file* shm_file; //共享内存区的特殊文件
unsigned long shm_nattch; //共享内存区当前的共享计数
unsigned long shm_segsz; //共享内存区字节数
time_t shm_atim; //最后访问时间
time_t shm_dtim; //最后分离时间
time_t shm_ctim; //最后修改时间
pid_t shm_cprid; //创建者的PID
pid_t shm_lprid; //最后访问进程的PID
struct user_struct* mlock_user; //使用共享内存区的用户的user_struct指针

}

共享内存机制的相关系统调用
Linux系统中的每个进程,都有很大的虚拟地址空间,其中有一部分放着代码、数据、堆和堆栈,剩余部分在初始化时是空闲的。一块共享内存一旦被链接attach,就会被映射到进程空闲的虚拟地址空间中。随后,进程就可以像对待普通内存区域那样读、写共享内存。

共享内存有四个相关系统调用,使用时需要包含以下两个头文件:
<sys/ipc.h>以及<sys/shm.h>

shmget

int shmget(key_t key, int size, int shmflg);
参数:
key: 标识共享内存的键值,可以是0(IPC_PRIVATE)或大于0(通常是由ftok()生成)
size: 所需共享内存的最小尺寸(以字节为单位)
shmflg: 共享内存的创建方式标志
IPC_CREAT  如果key对应的共享内存不存在,则创建它;如果已经存在则返回其标识符;
IPC_CREAT|IPC_EXCL 如果key对应的共享内存不存在,则创建它;如果已经存在则出错返回-1

功能:
创建一块共享内存;若已经存在,则返回其标识符

返回值:
若成功,则返回共享内存的标识符;否则返回-1

shmat

void *shmat(int shmid, const void * shmaddr, int shmgflg);
参数:
shmid : 共享内存的标识符
shmaddr : 指定共享内存映射到进程虚拟地址空间的位置,
若设置为NULL0,则让系统确定一个合适的地址位置
shmflg : 进程对共享内存的读写属性,SHM_RDONLY为只读模式,其他为读写模式。

功能:
把指定共享内存区映射到调用进程的虚拟地址空间。若成功则返回映射的起始地址,
并对shmid_kernel结构中的共享计数shm_nattch加1.

返回值:若成功,则返回已映射到的起始地址;否则返回-1

shmdt

int shmdt(const void* shmaddr);
参数:shmaddr表示欲断开映射的共享内存的起始地址。

功能:断开共享内存在调用进程中的映射,禁止本进程访问此共享内存。若成功,
则会对shmid_kernel结构中共享计数shm_nattch减1,
当shm_nattch为0时,系统才真正删除该共享内存。

返回值:若成功,则返回0,否则-1

shmctl

int shmctl(int shmid, int cmd, struct shmid_ds * buff);
参数:
shmid:欲处理的共享内存的标识符
cmd: 要进行的操作,包括IPC_STAT(获取状态)IPC_SET(设置状态)IPC_RMID(删除共享内存)。
最常用的是IPC_RMID,实际操作是把共享内存置为删除标记,当共享计数shm_nattch为0时,才真正删除。
buf: 用户空间中的一个缓存,接收或提供状态信息。

功能:获取或设置共享内存的属性信息或销毁一块共享内存。

返回值:若成功,则返回0,否则返回-1.
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页