Linux进程通信(一)

来源:互联网 发布:转换视频格式软件 编辑:程序博客网 时间:2024/05/07 13:35

Linux进程通信

一个大型的应用系统,往往需要众多进程协作,因此进程间的通信便显得格外的重要了。

1.进程间通信方式概述

进程间通信就是在不同进程之间传播或交换信息,由于进程用户空间之间是互相独立的,不能互相访问,所以只能通过共享内存区来通信。
除此之外,双方也可以通过访问都能访问的外设来达到信息的交换,例如磁盘。当然,也有通过“注册表”或其他数据库中某些表项和记录交换信息,但一般都不算作进程间的通信。Linux的进程间通信方法有管道、消息队列、信号量、共享内存、套接字等。

Linux进程间通信(IPC)由以下几部分发展而来:
1.UNIX进程间通信
2.基于System V进程间通信
3.POSIX进程间通信:
POSIX(Portable Operating System Interface)表示可移植操作系统接口。电气和电子工程师协会(IEEE)最初开发 POSIX 标准,是为了提高 UNIX 环境下应用程序的可移植性。然而,POSIX 并不局限于 UNIX,许多其它的操作系统,例如 DEC OpenVMS 和 Microsoft Windows,都支持 POSIX 标准。

1.1进程间通信的目的

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


1.2进程间通信方式

Linux下的进程通信的几种主要的方式:
(1)管道(Pipe)和有名管道(FIFO)。
(2)信号(Signal)。
(3)消息队列。
(4)共享内存(Shared Memory)。
(5)信号量(Semaphore)。
(6)套接字(Socket)。


1.3管道通信

管道通信分为无名管道和有名管道,无名管道可以用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制。
管道是Linux支持的最初UNIX IPC形式之一,具有以下特点:
(1)管道是半双工的,数据只能向一个方向流动,而且先进先出;需要双方通信时,需要建立起两个管道。
(2)只能用于父子进程或者兄弟进程之间(具有亲缘关系)。
(3)单独构成一种独立的文件系统。管道对于管道两端的进程而言,就是一个文件,但它不是普通文件,不属于文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。
(4)数据的读出和写入,一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓存区的头部读出数据。
(5)数据被一个进程读出后,将被从管道中删除,其它读进程将不能再读到这些数据。
(6)管道提供了简单的流控制机制,进程试图读空管道时,进程将阻塞。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞。


1.3.1无名管道

pipe函数用于建立管道:

相关函数 mkfifo,popen,read,write,fork 头文件 unistd.h 定义函数 int pipe(int filedes[2]); 函数说明 pipe()会建立管道,并将文件描述符由参数filedes数组返回。filedes[0]为管道了的读取端,filedes[1]则为管道的写入端。 返回值 若成功则返回0,否则返回-1,错误原因存放在errno中 错误代码 EMFILE:进程已用完文件描述符的最大值。ENFILE:系统已无文件描述符可用。EFAULT:参数filedes数组地址不合法。

管道用于不同进程间的通信,通常先创建一个管道,再通过fork函数创建一个子进程,该子进程会继承父进程所创建的管道。
注:必须在系统调用fork()前调用 pipe(),否则子进程将不会继承文件描述符。

这里写图片描述

读写无名管道管道两端分别用文件描述符fd[0],fd[1]来描述,需要注意的是fd[0]称为读管道,只能用于读,fd[1]称为写管道只能用于写。一般文件函数都可以用于管道,如close,read,write等。

从管道中读取数据:
(1)如果管道写端不存在,则是认为已经读到了实际的末尾,读函数返回的读出字节数为0。
(2)当管道的写端存在是,如果恳求的字节数目大于 PIPE_BUF,则返回管道中现有的数据字节数;如果请求的字节数目不大于PIPE_BUF,则返回管道中现有的数据字节数,或者返回请求的字节数。
(3)管道写端关闭后,写入的数据将一直存在,知道读出为止。
2个注意点:
a.写端对读端存在依赖性;
b.Linux写管道的原子性验证;

用法:1.用于shell;管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入。2.用于具有亲缘关系进程间的通信;

1.3.2有名管道

相较于无名管道,克服了只能用于亲缘进程间通信的限制。它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样即使不是亲缘进程,只要能够访问该路径,就彼此能通过FIFO通信。
用mkfifo函数建立有名管道:

相关函数 pipe,popen,open,umask 头文件 sys/types.h sys.stat.h 定义函数 int mkfifo(const char *pathname, mode_t mode); 函数说明 mkfifo()会依参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限(mode%~umask),因此umask值也会影响到FIFO文件权限。mkfifo()创建的FIFO文件其他进程都可以用读写一般文件的方式获取,当使用open()打开FIFO文件时,O_NONBLOCK旗标会有影响。 返回值 若成功则返回0,否则返回-1,错误原因存放在errno中 错误代码 EACCESS:参数pathname所指定的目录路径无可执行权限。EEXIST:参数pathname所指定的文件已存在。ENAMETOOLONG:参数pathname的路径太长。ENOENT:参数pathname包含目录不存在。ENOSPC:文件系统的剩余空间不足。ENOTDIR:参数pathname路径目录存在但非真正目录。EROFS:参数pathname指定的文件存在于只读文件系统内

1.从FIFO中读取数据
约定:如果一个进程为了从FIFO中读取数据而阻塞打开了FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。
(1)如果有进程写打开FIFO,且当前FIFO为空,则对于设置了阻塞标志的读操作来说,将一直阻塞下去,直到有数据可以读时才继续执行;对于没有设置阻塞标志的读操作来说,则返回0个字节,当前errno值为EAGAIN,提醒以后再试。
(2)对于设置了阻塞标志的读操作来说,造成阻塞的原因有两种:一、当前FIFO内有数据,但有其它进程在读这些数据;二、FIFO本身为空。
解阻塞的原因是:FIFO中有新的数据写入,不论写入数据量的大小,也不论读操作请求多少数据量,只要有数据写入即可。
(3)读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程中有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。
(4)如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。
(5)如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数少于请求的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。

2.从FIFO中写入数据:
约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。
FIFO的长度是需要考虑的一个很重要因素。系统对任一时刻在一个FIFO中可以存在的数据长度是有限制的。它由#define PIPE_BUF定义,在头文件limits.h中。在Linux和许多其他类UNIX系统中,它的值通常是4096字节,Red Hat Fedora9下是4096,但在某些系统中它可能会小到512字节。
虽然对于只有一个FIFO写进程和一个FIFO的读进程而言,这个限制并不重要,但只使用一个FIFO并允许多个不同进程向一个FIFO读进程发送请求的情况是很常见的。如果几个不同的程序尝试同时向FIFO写数据,能否保证来自不同程序的数据块不相互交错就非常关键了à也就是说,每个写操作必须“原子化”。

对于设置了阻塞标志的写操作:
(1)当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。即写入的数据长度小于等于PIPE_BUF时,那么或者写入全部字节,或者一个字节都不写入,它属于一个一次性行为,具体要看FIFO中是否有足够的缓冲区。
(2)当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。

对于没有设置阻塞标志的写操作:
(1)当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写。
(2)当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。

#include <sys/types.h>#include <sys/stat.h>int mkfifo(const char * pathname, mode_t mode)pathname:FIFO文件名mode:属性(见文件操作章节)一旦创建了一个FIFO,就可用open打开它,一般的文件访问函数(close、read、write等)都可用于FIFO

1.4信号通信

信号(signal)机制是Unix系统中最为古老的进程间通信机制,很多条件可以产生一个信号:
1、当用户按某些按键时,产生信号
2、硬件异常产生信号:除数为0、无效的存储访问等等。这些情况通常由硬件检测到,将其通知内核,然后内核产生适当的信号通知进程,例如,内核对正访问一个无效存储区的进程产生一个SIGSEGV信号
3、进程用kill函数将信号发送给另一个进程
4、用户可用kill命令将信号发送给其他进程

信号类型:

这里写图片描述


下面是几种常见的信号:§ SIGHUP: 从终端上发出的结束信号§ SIGINT: 来自键盘的中断信号(Ctrl-C)§ SIGKILL:该信号结束接收信号的进程§ SIGTERM:kill 命令发出的信号§ SIGCHLD:标识子进程停止或结束的信号§ SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止执行信号

信号处理:
当某信号出现时,将按照下列三种方式中的一种进行处理:
1、忽略此信号
大多数信号都按照这种方式进行处理,但有两种信号决不能被忽略,它们是:SIGKILL\SIGSTOP。
这两种信号不能被忽略的原因是:它们向超级用户提供了一种终止或停止进程的方法。
2、执行用户希望的动作
通知内核在某种信号发生时,调用一个用户函数。在用户函数中,执行用户希望的处理。
3、执行系统默认动作
对大多数信号的系统默认动作是终止该进程。


信号发送
发送信号的主要函数有 kill和raise。

区别:Kill既可以向自身发送信号,也可以向其他进程发送信号。与kill函数不同的是,raise函数是向进程自身发送信号#include <sys/types.h>#include <signal.h>int kill(pid_t pid, int signo)int raise(int signo)

kill的pid参数有四种不同的情况:
1、pid>0
将信号发送给进程ID为pid的进程。
2、pid == 0
将信号发送给同组的进程。
3、pid < 0
将信号发送给其进程组ID等于pid绝对值的进程。
4、pid ==-1
将信号发送给所有进程。


alarm:
使用alarm函数可以设置一个时间值(闹钟时间),当所设置的时间到了时,产生SIGALRM信号.如果不捕捉此信号,则默认动作是终止该进程。

#include <unistd.h>unsigned int alarm(unsigned int seconds)Seconds:  经过了指定的seconds秒后会产生信号SIGALRM。

每个进程只能有一个闹钟时间.如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,以前登记的闹钟时间则被新值代换。如果有以前登记的尚未超过的闹钟时间,而这次seconds值是0,则表示取消以前的闹钟。


pause:
pause函数使调用进程挂起直至捕捉到一个信号。

#include <unistd.h>int pause(void)只有执行了一个信号处理函数后,挂起才结束

信号的处理:
当系统捕捉到某个信号时,可以忽略该信号或是使用指定的处理函数来处理该信号,或者使用系统默认的方式信号处理的主要方法有两种,一种是使用简单的signal函数, 另一种是使用信号集函数组。

signal#include <signal.h>void (*signal (int signo, void (*func)(int)))(int)typedef void (*sighandler_t)(int) sighandler_t signal(int signum, sighandler_t handler))func可能的值是:1、SIG_IGN:忽略此信号2、SIG_DFL: 按系统默认方式处理3、信号处理函数名:使用该函数处理

Linux进程通信(二)