-进程间通信的概念:指至少两个进程或线程间传送数据或信号的一些技术或方法。
-Linux进程间通信方式:管道(pipe)和有名管道(FIFO)、信号(signal)、消息队列、共享内存、信号量、套接字(socket)。
-进程间通信的目的:
●数据传输:进程间需要相互传输数据
●共享数据:多个进程间操作共享数据
●通知事件:进程间需要通知某个事件的发生
●资源共享:多个进程之间共享同样的资源,需要内核提供锁和同步机制
●进程控制:有些进程需要完全控制另一个进程的执行(如Debug进程)
-多进程编程的优缺点:
优点:
●每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系
●通过增加CPU,就可以容易扩充性能
●可以尽量减少线程加锁/解锁的影响,极大提高性能
●每个子进程都有4GB地址空间和相关资源,总体能够达到的性能上限非常大
缺点:
●逻辑控制复杂,需要和主程序交互
●需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
●多进程调度开销比较大
信号本质:
信号(Signal)是在软件层次上对中断机制的一种模拟,信号的实质是软件中断。
信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。
信号的来源:
信号事件的发生有两个来源:
硬件来源(比如我们按下了键盘或者其它硬件故障);
软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。
信号的种类:
可以从两个不同的分类角度对信号进行分类:
●可靠性方面:可靠信号与不可靠信号;
●与时间的关系上:实时信号与非实时信号。
信号的特点:
在一个信号的生命周期中有两个阶段:生成和传送。
信号没有固有的优先级;也没有机制用于区分同一种类的多个信号。
信号的局限性:
●信号的花销太大。发送信号要做系统调用;内核要中断接收进程、要管理它的堆栈、要调用处理程序、要恢复被中断的进程等。
●信号种类有限,而且信号能传递的信息量十分有限。
●信号没有优先级,也没有次数的概念。
●信号对于事件通知很有效,但对于复杂的交互操作却难以胜任。
管道允许在进程之间按先进先出的方式传送数据,是进程间通信的一种常见方式。
管道是Linux 支持的最初Unix IPC形式之一,具有以下特点:
●管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
●匿名管道只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
●单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,
而是自立门户,单独构成一种文件系统,并且只存在与内存中。
管道分为pipe(无名管道)和fifo(命名管道)两种,除了建立、打开、删除的方式不同外,这两种管道几乎是一样的。他们都是通过内核缓冲区实现数据传输。
●pipe用于相关进程之间的通信,例如父进程和子进程,它通过pipe()系统调用来创建并打开,当最后一个使用它的进程关闭对他的引用时,pipe将自动撤销。
●FIFO即命名管道,在磁盘上有对应的节点,但没有数据块——换言之,只是拥有一个名字和相应的访问权限,
通过mknode()系统调用或者mkfifo()函数来建立的。一旦建立,任何进程都可以通过文件名将其打开和进行读写,
而不局限于父子进程,当然前提是进程对FIFO有适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。
管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时, 就唤醒等待队列中的进程继续读写。
引入了三种高级进程间的通信机制:消息队列、信号灯和共享内(message queues,semaphores and shared memory)。
①消息队列
是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
特点:
●消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
●消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
●消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
②信号量
信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
特点:
●信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
●信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
●每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
●支持信号量组。
③共享内存
指两个或多个进程共享一个给定的存储区。
特点
●共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
●因为多个进程可以同时操作,所以需要进行同步。
●信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
进程接收到核心程序所发出的信号后,处置方式如下:
●忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;
●捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;
●执行缺省操作,Linux对每种信号都规定了默认操作,注意,进程对实时信号的缺省反应是进程终止。
●暂停进程的执行;
●重新启动刚才被暂停的那个进程。
POSIX.1支持的信号
信号 值 动作 意义
SIGHUP 1 A 挂断控制终端,当一个终端被切断时,核心程序就将此信号传给该终端所控制的一切进程
SIGINT 2 A 控制终端的中断键被按下
SIGQUIT 3 A 从键盘中断中退出
SIGILL 4 C 不正确的硬件指令,应用程序通常会捕捉此信号以响应程序执行时的错误
SIGABRT 6 C 调用abort系统函数放弃信号
SIGFPE 8 C 浮点溢出错误
SIGKILL 9 AEF 删除一个或一组进程,本信号不能忽略
SIGSEGV 11 C 不合法的内存引用
SIGPIPE 13 A 断开的的管道:一个进程不停地将数据写入管道,但是没有进程读数据,即读管道的进程非正常终止了
SIGALRM 14 A 时钟,用于检测进程的真实时间(不是CPU时间),alarm系统调用就是用来设定此信号
SIGTERM 15 A 终止进程,kill系统调用就发送这个信号
SIGUSR1 30,10,16 A 用于自定义信号1
SIGUSR2 31,12,17 A 用于自定义信号2
SIGCHLD 20,17,18 B 子进程暂停或终止
SIGCONT 19,18,25 如果进程暂停,那么继续执行
SIGSTOP 17,19,23 DEF 暂停进程
SIGTSTP 18,20,24 D 把停止的信号送给联机会话进程,通常由Ctrl+Z来产生此信号
SIGTTIN 21,21,26 D 后台执行中的进程要从控制终端读取数据
SIGTTOU 22,22,27 D 后台执行中的进程企图对控制终端写入数据
Linux支持的其他信号
信号 值 动作 意义
SIGTRAP 5 CG 程序跟踪中断点。这是一个给调试程序(如gdb)专用的信号
SIGIOT 6 CG I/O中断点,通常是由于硬件故障
SIGEMT 7,-,7 G 硬件仿真程序捕俘
SIGBUS 10,7,10 AG 总线错误
SIGSYS 12,-,12 G 系统调用参数错误(SVID)
SIGSTKFLT -,16,- AG 协处理器堆栈错误
SIGURG 16,23,21 BG 这个信号通知系统有要求立即处理的SOCKET
SIGIO 23,29,22 AG I/O操作可以执行,例如,可以对某个文件描述字进行操作
SIGPOLL AG 与SIGIO同义
SIGCLD -,-,18 G 与SIFCHLD同义
SIGXCPU 24,24,30 AG 进程超出了所设定给它的最大CPU使用时限
SIGXFSZ 25,25,31 AG 进程超出了所设定给它的最大文件权限
SIGVTALRM 26,26,28 AG 用于测量进程的虚拟时间(实际被执行进程的时间)
SIGPORF 27,27,29 AG 用于测量进程的概括时间(指虚拟时间加核心程序执行进程实际时间)
SIGPWR 29,30,19 AG 电源故障
SIGINFO 29,-,- G 与SIGPWR同义
SIGLOST -,-,- AG 文件锁丢失
SIGWINCH 28,28,20 BG X Window窗口改变大小
SIGUNUSED -,31,- AG 未使用的信号
注:
●信号值为“-”表示没有此信号
●每一个信号值分为3列,信号是与CPU相关的,第1个是alpha和sparc上的信号值,第2个是i386和PowerPC上的信号值,第3个是mips上的信号值。
●信号值29在alpha上位SIGINFO/SIGPWR,在sparc上为SIGLOST
●动作一栏中的字符意义如下所述:
A:默认的动作是终止进程
B:默认的动作是忽略信号
C:默认的动作是内核转储
D:默认的动作的暂停进程
E:信号不能被俘获
F:信号不能被忽略
G:不是POSIX.1兼容信号
●SIGIO和SIGLOST有相同的信号值,SIGLOST是在内核中定义的,但是应用程序仍旧把信号值为29当作SIGLOST。
①信号注册调用函数
#include
void (*signal (int signumber,void (*func)(int)))(int);
第一个参数指定信号的值;
第二个参数指定针对前面信号值的处理:
●常数SIG_IGN,则向内核表示忽略此信号(有两个信号SIGKILL和SIGSTOP不能忽略);
●常数SIG_DFL,则表示接到此信号后的动作是系统默认动作;
●当接到此信号后要调用的函数的地址。称此为捕捉此信号。
如果signal()调用成功,返回最后一次为安装信号signumber而调用signal()时的handler值;失败则返回SIG_ERR。
注意:
●不能为SIGKILL和SIGSTOP设置信号处理函数;
●并非程序执行到signal行时立即会对该信号做什么操作,使用signal函数只是告诉系统对这个信号用什么程序来处理;
●一段程序代码中可以定义对多个信号的处理函数。可以是一个信号对应一个特定处理函数或多个信号对应一个处理函数。
●signal函数是阻塞的,比如当进程正在执行SIGUSR1信号的处理函数,此时又来一个SIGUSR1信号,
signal会等到当前信号处理完后才继续处理后来的SIGUSR1,
不管后来的多少个SIGUSR1信号,同义看作一个来处理。
②高级信号处理sigaction()
#include
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号;
第二个参数是指向结构体类型sigaction的一个实例的指针,在结构体类型sigaction的实例中,指定了对特定信号的处理,
可以为空,进程会以缺省方式对信号处理;
第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,
那么该函数可用于检查信号的有效性。
第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等。
成功返回0,错误返回-1。
struct sigaction结构体:
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask; /* additional signals to block */
int sa_flags; /* signal options */
void (*sa_sigaction)(int, siginfo_t *, void *); /* alternate handler */
}
sa_hanlder: 一个带有int参数的函数指针, 或者SIG_IGN(忽略), 或者SIG_DFL(默认)。
sa_mask: 信号屏蔽字(集)。当该信号处理函数返回时, 屏蔽字恢复。
sa_sigaction: 替代的信号处理程序, 当使用了SA_SIGINFO标志时, 使用该信号处理程序。
struct siginfo_t结构体:
struct siginfo_t
{
int si_signo; /*Signal number*/
int si_errno; /*An errno value*/
int si_code; /*Signal code*/
pid_t si_pid; /*Sending process ID*/
uid_t si_pid; /*Real user ID of sending process*/
int si_status; /*Exit value or signqal*/
clock_t si_utime; /*User Time consumed*/
clock_t si_stime; /*System time consumed*/
signal_t si_value; /*Signal value*/
int si_int; /*POSIX.1b signal*/
void *si_ptr; /*POSIX.1b signal*/
void *si_addr; /*Memory location that caused fault*/
int si_band; /*Band event*/
int si_fd; /*File descriptor*/
}
sa_flags指示处理函数的不同选项:
sa_flags 对应设置
SA_NOCLDSTOP 用于指定信号SIGCHLD,当子进程被中断时,不产生此信号,当且仅当子进程结束时产生该信号
SA_NOCLDWAIT 当信号为SIGCHLD时,此选项可以避免子进程的僵死
SA_NODEFER 当信号处理程序正在运行时,不阻塞对于信号处理函数自身的信号功能
SA_NOMASK 同SA_NODEFER
SA_ONESHOT 当用户注册的信号处理函数被调用过一次之后,该信号的处理程序恢复为默认的处理函数
SA_RESETHAND 同SA_ONESHOT
SA_RESTART 使本来不能进行自动更新运行的系统调用自动重新启动
SA_SIGINFO 表明信号处理函数是由sa_sigaction指定,而不是由da_handler指定。它将显示更多处理函数的信息
①信号集的概念
信号集是用来表示多个信号的数据类型。
#include
int sigemptyset(sigset_t *set); //初始化信号集合set,将set设置为空;调用成功返回0,否则返回-1。
int sigfillset(sigset_t *set); //初始化信号集合,只是将信号集合设置为所有信号的集合;调用成功返回0,否则返回-1。
int sigaddset(sigset_t *set,int signo); //将信号signo加入到信号集合之中;调用成功返回0,否则返回-1。
int sigdelset(sigset_t *set,int signo); //将信号从信号集合中删除;调用成功返回0,否则返回-1。
int sigismember(sigset_t *set,int signo); //查询信号是否在信号集合之中,如果在返回1,否则返回0。
②信号集的操作
sigprocmask函数
#include
int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
第一个参数:
SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中。
SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合。
SIG_SETMASK:将当前的信号集合设置为信号阻塞集合。
set用以设置新的信号屏蔽字,oset返回当前信号屏蔽字。
set:指向一个信号集的指针
oset:用于备份原来的信号屏蔽字,不想备份时可设置为NULL
●若set为非空指针,则根据参数how更改进程的信号屏蔽字;
●若oset是非空指针,则读取进程当前的信号屏蔽字通过oset参数传出;
●若set、oset都非空,则将原先的信号屏蔽字备份到oset中,然后根据set和how参数更改信号屏蔽字
返回值:成功返回0,出错返回-1
注:调用这个函数才能改变进程的屏蔽字,之前的函数都是为改变一个变量的值而已,并不会真正影响进程的屏蔽字。
sigpending函数
int sigpending (sigset_t *set);
这个函数返回在送往进程时被阻塞挂起的信号的集合。这个信号集合通过参数set返回。
函数调用成功返回0,否则返回-1,并设置errno表明错误原因
sigsuspend函数
int sigsuspend(const sigset_t *sigmask); //用于挂起进程等待特定信号。
参数sigmask指向一个信号集,当函数被调用时,sigmask所指向的信号集中的信号被赋值给信号屏蔽码。之后进程被挂起,
直至进程捕捉到信号调用处理函数并执行完毕返回时,函数sigsuspend返回。信号掩码恢复为函数调用前的值。
注:如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对待处理信号的阻塞时,待处理信号就会立刻被处理。
补:屏蔽信号的处理
●屏蔽信号的两种方式
因此,屏蔽某个信号有两种方式,,下面以SIGUSER1为例进行说明:
第一种方式为使用SIG_BLOCK操作方式,代码如下:
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset,SIGUSER1);
sigprocmask(SIG_BLOCK,&sigset,NULL);
第二种方式为使用SIG_SETMASK操作方式,代码如下:
sigset_t set;
sigprocmask(SIG_SETMASK,NULL,&set); //先得到当前的信号掩码
sigaddset(&set,SIGUSER1);//将要屏蔽的信号加入
sigprocmask(SIG_SETMASK,&set,NULL)
●解除屏蔽信号的两种方式
同样,要解除对信号的屏蔽,也有两种方式,仍以SIGUSER1为例进行说明。
第一种方式,使用使用SIG_UNBLOCK操作方式,代码如下:
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset,SIGUSER1);
sigprocmask(SIG_UNBLOCK,&sigset,NULL);
第二种方式,使用SIG_SETMASK操作方式,代码如下:
sigset_t set;
sigprocmask(SIG_SETMASK,NULL,&set); //先得到当前的信号掩码
sigdelset(&set,SIGUSER1);//将要屏蔽的信号去除
sigprocmask(SIG_SETMASK,&set,NULL);
①raise函数
#include
int raise(int signum);
raise函数用于向一个进程本身发送信号。
参数signum为所发送的信号编码。
调用成功时,返回值为0,调用失败返回值为-1。
②kill函数
kill函数发信号给一个进程或一组进程。
参数pid表示kill函数发送信号对象的进程或进程组号。
●pid>0 将信号发送给进程ID为pid的进程。
●pid == 0 将信号发送给同组的进程。
●pid < 0 将信号发送给其进程组ID等于pid绝对值进程。
●pid ==-1 将信号发送给所有进程。
当信号成功发送时,kill函数返回;发送错误时,返回-1。
③alarm函数
使用alarm函数可以设置一个时间值(闹钟时间),当所设置的时间值到了时,产生SIGALRM信号。如果不忽略或不捕捉此信号,则其默认动作是终止该进程。
#include
unsigned int alarm(unsigned int seconds) ;
seconds:经过了指定的seconds秒后会产生信号SIGALRM
返回:0或以前设置的闹钟时间的余留秒数。
④pause函数
pause函数使调用进程挂起直至捕捉到一个信号。
#include
int pause(void);
返回:-1,errno设置为EINTR
只有执行了一个信号处理函数后,挂起才结束。
①管道的概念
管道,就是将一个进程的标准输出和另一个进程的标准输入联系到一起,以供两个进程相互通信的方法。
特点:
●管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
●只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
●单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,
而是自立门户,单独构成一种文件系统,并且只存在与内存中。
●数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,
并且每次都是从缓冲区的头部读出数据。
②管道的内部实现
●当管道被创建时,系统内核为其准备两个文件描述符,一个用于管道的输入一个用于管道的输出。
●为了实现不同进程间数据通信,会创建子进程,并且子进程从父进程继承读写管道的文件描述符,因此建立了父、子进程间通信的管道。
●确定传输方向,父、子进程关闭与之无关的描述符。
-父进程调用pipe创建管道,得到两个文件描述符指向管道的两端。
-父进程调用fork创建子进程,子进程继承父进程的两个文件描述符指向同一管道。
-父进程关闭管道读端,子进程关闭管道写端,数据从父进程写入子进程读出。管道是环形队列实现的,数据从写端流入从读端流出,实现进程间通信。
③管道的读写操作
●如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;
●只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIGPIPE信号,应用程序可以处理该信号,
也可以忽略(默认动作则是应用程序终止)。
●当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,
则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。
#include
int pipe(int fdes[2]);
其中,参数fdes为整数数组名,在C语言中,数组名即是指向数组的指针。
所以,调用这个函数后,系统为通道分配的两个文件描述符将通过这个数组返回到用户进程中。
fdes中的第1个整数是读通道,第2个整数是写通道。
调用成功时,pipe返回0,否则返回-1。
程序示例:
int pipefd[2];
pipe(pipefd);
switch(fork())
{
case -1:
/*fork failed,error handler here*/
case 0: /*子进程*/
close(pipefd[1]); /*关闭掉写入端对应的文件描述符*/
/*子进程可以对pipefd[0]调用read*/
break;
default: /*父进程*/
close(pipefd[0]); /*父进程关闭掉读取端对应的文件描述符*/
/*父进程可以对pipefd[1]调用write,写入想告知子进程的内容*/
break;
}
管道有以下一些特点:
●管道没有名字,它是为了一次使用而创建的。
●管道的两个描述符是同时打开的。
●管道不允许文件定位。读和写操作都是顺序的,读从文件的开始处读,写则写至文件尾。
是一种基于文件流的管道,主要用来创建一个一个连接到另一个进程的管道,这里的“另一个进程”也就是一个可以进行一定操作的可执行文件,因此标准流管道就将一系列的创建过程合并到一个函数popen()中完成。
它所完成的工作有以下几步:
●创建一个管道。
●fork()一个子进程。
●在父子进程中关闭不需要的文件描述符。
●执行exec函数族调用。
●执行函数中所指定的命令。
用popen()创建的管道必须使用标准I/O函数进行操作,但不能使用前面的read()、write()一类不带缓冲的I/O函数。
#include
FILE *popen (const char*cmdstring, const char *type);
int pclose (FILE *fp);
函数popen用于创建管道。它内部调用fork和exec函数执行命令行cmdstring,返回一个FILE结构的指针,即用于访问管道的指针。
popen中的参数const char *cmdstring就是一个命令行。所有的shell命令行参数和选项都可以使用。
popen中的参数const char *type指出管道的类型。可以是“r”和“w”之一,但不能是rw或wr。
函数pclose是用来关闭管道的。它关闭标准输入输出流,等待命令行执行完毕,然后返回结束时的状态。
①特点
●FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中。
●当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
●FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。
●在shell环境下,命名管道文件名后面紧跟着一个竖线,作为其标志。
②操作
●从FIFO中读取数据
从一个包含p字节的管道或FIFO读取n字节的含义:
| p=0且存在写入端 | p=0且所有写入端 | |
| 描述符尚未关闭 | 描述符均已关闭 | p=n
————————————————————————————————————————————————————————————————————————————————
未启用O_NONBLOCK | 阻塞 | 返回0(EOF) | 读取p字节 | 读取n字节
————————————————————————————————————————————————————————————————————————————————
启用O_NONBLOCK | 失败(EAFAIN) | 返回0(EOF) | 读取p字节 | 读取n字节
————————————————————————————————————————————————————————————————————————————————
-如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。
对于没有设置阻塞标志的读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
-对于设置了阻塞标志的读操作来说,造成阻塞的原因有两种:一种是当前FIFO内有数据,但有其它进程
再读这些数据;另一种是FIFO内没有数据,阻塞原因是FIFO中有新的数据写入,而不论新写入数据量的
大小,也不论读操作请求多少数据量。
-读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操
作被唤醒并完成读操作后,其他将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也
一样(此时,读操作返回0)
-如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞
-如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,
读操作会返回FIFO中现有的数据量。
●向FIFO中写入数据
向管道写入n字节:
| 无O_NONBLOCK标志位 | 有O_NONBLOCK标志位
————————————————————————|———————————————————————————————————————————————————————————————————————————————————
写入字节数n<=PIPE_BUF | 当空闲区域不足以容纳n字节时,陷入阻塞, | 当管道空闲区域不足以容纳n字节时,
(保证写入的原子性) | 等待读取进程取走管道的部分内容 | 立即返回失败,并置error为EAGAIN
————————————————————————|———————————————————————————————————————————————————————————————————————————————————
写入字节数n>PIPE_BUF | 使命必达的策略。当空闲区域不足以容纳 | 尽力而为的策略。当写满管道时,返回,
(不保证写入的原子性) | n字节时,陷入阻塞,待管道空间足够时 | 实际写入字节数在1~n之间。用户需要判
| 再写入。但成功返回时,写入字节一定是n | 断返回值,来确定写入的字节数
————————————————————————————————————————————————————————————————————————————————————————————————————————————
对于设置了阻塞标志的写操作:
-当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要
写入的字节数,则进入睡眠,知道当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
-当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就
会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
对于没有设置阻塞标志的写操作:
-当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。再写满所有FIFO空闲缓冲区后,写操作返回。
-当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节
数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写。
●FIFO的一些注意问题:
-管道数据的FIFO处理方式:首先放入管道的数据,在端口首先被读出
-管道数据的不可再现性:已读取的数据在管道里消失,不能再读
-管道长度的有限性
-SIGPIPE信号:如果一个进程试图写入到一个没有读取进程的管道中,系统内核产生SIGPIPE信号
③应用
●shell命令行使用命名管道将数据从一条命令传到另一条命令,而不需要创建中间的临时文件。
●在客户-服务器结构中,使用命名管道在客户和服务器之间交换数据。
④创建
●在命令行上创建命名管道
可以通过命令行命令 mkfifo 或 mknod 创建命名管道:
$ mkfifo /tmp/testp
$ mknod /tmp/testp p
可以通过 ls 命令查看命名管道的文件属性:
命令:ls -lF /tmp/testp
输出:prw-r--r-- ...... /tmp/testp|
输出中的第一个字符为 p,表示这个文件的类型为管道。最后的 | 符号是有 ls 命令的 -F 选项添加的,也表示这个一个管道。
●在程序中创建命名管道
在程序中创建命名管道,可以使用 mkfifo函数,其签名如下:
#include
#include
int mkfifo(const char *pathname, mode_t mode);
参数 pathname 是一个字符串指针,用于存放命名管道的文件路径。
参数 mode 用于表示指定所创建文件的权限(同文件系统open函数中的mode)。
该函数调用成功时返回 0;调用失败时返回 -1。
mkfifo函数是一个专门用来创建命名管道的函数,而另外一个函数 mknod 却可以兼职创建命名文件,其函数签名如下:
#include
#include
int mknod(char *pathname, mode_t mode, dev_t dev);
创建命名管道只是 mknod 函数的功能之一,它的前两个参数和 mkfifo 函数相同。
在创建命名管道时,为第三个参数 dev 传递 0 就可以了。
该函数调用成功时返回 0;调用失败时返回 -1。
打开FIFO文件文件的不同情况:
打开标志位 | 行为模式
—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
O_RDONLY | 当已存在写打开该FIFO文件的进程时,成功返回
| 当不存在写打开该FIFO文件的进程时,会陷入阻塞,直到有进程以O_WRONLY模式(或者O_RDWR模式)打开该FIFO文件,方能返回。
—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
O_RDONLY+O_NONBLOCK | 当已存在写打开该FIFO文件的进程时,成功返回
| 当不存在写打开该FIFO文件的进程时,亦成功返回
—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
O_WRONLY | 当已存在读打开该FIFO文件的进程时,成功返回
| 当不存在读打开该FIFO文件的进程时,会陷入阻塞,直到有进程以O_RRONLY模式(或者O_RDWR模式)打开该FIFO文件,方能返回。
—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
O_WRONLY+O_NONBLOCK | 当已存在读打开该FIFO文件的进程时,成功返回
| 当不存在读打开该FIFO文件的进程时,返回-1,并置errno为ENXIO
—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
①关键字和标识符
每一个IPC资源都有两个唯一的标志与之相连:关键字(key)和标识符(id)。
●标识符:每个system V的进程通信机制中的对象都和唯一的一个引用标识符相联系,如果进程要访问此IPC对象,则需要在系统中传递这个唯一的引用标识符。
标识符的唯一局限是在相应的IPC对象的类别内。
●关键字:用来定位System V中IPC机制的对象的引用标识符的。当创建一个对象时,必须指定一个关键字。
关键字的类型key_t是系统中预先定义的。在头文件中。
②ipc_perm结构介绍
这是一个结构体。他的英文含义是:ipc permission(IPC权限)
struct ipc_perm
{
key_t key; /*关键字*/
uid_t uid; /*所有者的有效用户ID */
gid_t gid; /* 所有者所属组的有效组ID*/
uid_t cuid; /* 创建者的有效用户ID*/
gid_t cgid; /* 创建者所属组的有效组ID*/
unsigned short mode; /* 访问权限*/
unsigned short seq; /* 序列号*/
};
三种IPC机制都有对应的结构体,这些结构体中有一个共同的成员就是这个ipc_perm,用来标识IPC对象的权限。
IPC机制的权限:
权限 | 消息队列 | 信号量 | 共享内存
—————————————|——————————————|———————————————|————————————
用户可读 | MSG_R | SEM_R | SHM_R
用户可写 | MSG_W | |
—————————————————————————————————————————————————————————
组用户可读 | MSG_R>>3 | SEM_R>>3 | SHM_R>>3
组用户可写 | MSG_W>>3 | SEM_A>>3 | SHM_W>>3
—————————————————————————————————————————————————————————
其他用户可读 | MSG_R>>6 | SEM_R>>6 | SHM_R>>6
其他用户可写 | MSG_W>>6 | SEM_A>>6 | SHM_W>>6
—————————————————————————————————————————————————————————
③ipcs命令
●查看IPC对象信息
命令:ipcs [-aqms]
参数说明:
-a:查看全部IPC对象信息。
-q:查看消息队列信息。
-m:查看共享内存信息。
-s:查看信号量信息。
●删除IPC对象
命令1:ipcrm -[qms] ID
命令2:ipcrm -[QMS] key
参数说明:
-q或-Q:删除消息队列信息。
-m或-M:删除共享内存信息。
-s或-S:删除信号量信息。
注:如果指定了qms,则用IPC对象的标识符(ID)作为输入;如果指定了QMS,则用IPC对象的键值(key)作为输入。
总结:System V IPC具有相似的语法,一般操作如下:
●选择IPC关键字,可以使用如下三种方式:
-IPC_PRIVATE。由内核负责选择一个关键字然后生成一个IPC对象并把IPC标识符直接传递给另一个进程。
-直接选择一个关键字。
-使用ftok()函数生成一个关键字。
关键字是一个32位的整数。函数ftok()就是用来产生关键字的,它把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键。
其实,这个关键字的作用就是不同进程根据它来创建IPC的标识符的。
函数声明:
#include
#include
key_t ftok(const char *path, int id);
这个函数创建key值的过程当中使用到了path中文件属性的st_dev和st_ino。
(31--24)bit:为id的低8位;
(23--16)bit:为该文件的st_dev属性的底8位;
(15--0)bit:为该文件的st_ino属性的低16位。
●使用semget()/shmget()/msgget()函数根据IPC关键字key和一个标志flag创建或访问IPC对象。
如果key是IPC_PRIVATE;或者key尚未与已经存在的IPC对象相关联且flag中包含IPC_CREAT标志,那么就会创建一个全新的IPC对象。
●使用semctl()/shmctl()/msgctl()函数修改IPC对象的属性。
●使用semctl()/shmctl()/msgctl()函数和IPC_RMID标志销毁IPC实例。
概念:消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式。进程可以向其中按照一定的规则添加新消息;另一些进程则可以从消息队列中读走消息。
这种消息的发送方式是:发送方不必等待接收方检查它所收到的消息就可以继续工作下去,而接收方如果没有收到消息也不需等待。
新的消息总是放在队列的末尾,接收的时候并不总是从头来接收,可以从中间来接收。
①数据结构
●消息缓冲区(msgbuf)
mymsg结构是一个存放消息数据的模板,它在include/linux/msg.h中声明。描述如下:
/*msgsnd和msgrcv系统调用使用的消息缓冲区*/
struct mymsg {
long mtype;/*消息的类型,必须为正数*/
char mtext[1];/*消息正文*/
};
注意:对于消息数据元素(mtext),不要受其描述的限制。实际上,这个域不仅能保存字符数组,也能保存任何形式的任何数据。
这个域本身是任意的,这个结构本身可以由程序员重新定义,如:
struct my_msgbuf {
long mtype; /* 消息类型 */
long request_id; /* 请求识别号 */
struct client info; /* 客户消息结构 */
};
消息的类型还是和前面一样,但是结构的剩余部分由两个其它的元素代替,而且有一个是结构。这就是消息队列的优美之处,
内核根本不管传送的是什么样的数据,任何信息都可以传送。
但是,消息的长度还是有限制的,在Linux中,给定消息的最大长度在include/linux/msg.h中定义如下:
#define MSGMAX 8192 /* max size of message (bytes) */
//消息总的长度不能超过8192字节,包括mtype域,它是4字节长。
●消息结构(msg)
内核把每一条消息存储在以msg结构为框架的队列中,它在include/linux/msg.h中定义如下:
struct msg {
struct msg *msg_next; /* 队列上的下一条消息 */
long msg_type; /*消息类型*/
char *msg_spot; /* 消息正文的地址 */
short msg_ts; /* 消息正文的大小 */
};
注意:msg_next是指向下一条消息的指针,它们在内核地址空间形成一个单链表。
●消息队列结构(msgid_ds)
/* 在系统中的每一个消息队列对应一个msqid_ds 结构 */
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* 队列上第一条消息,即链表头*/
struct msg *msg_last; /* 队列中的最后一条消息,即链表尾 */
time_t msg_stime; /* 发送给队列的最后一条消息的时间 */
time_t msg_rtime; /* 从消息队列接收到的最后一条消息的时间 */
time_t msg_ctime; /* 最后修改队列的时间*/
ulong msg_cbytes; /*队列上所有消息总的字节数 */
ulong msg_qnum; /*在当前队列上消息的个数 */
ulong msg_qbytes; /* 队列最大的字节数 */
pid_t msg_lspid; /* 发送最后一条消息的进程的pid */
pid_t msg_lrpid; /* 接收最后一条消息的进程的pid */
};
②系统调用
●打开/创建消息队列msgget()
#include
#include
#include
int msgget(key_t key, int flag)
key: 键值,由ftok获得;
flag: 标志位;
IPC_CREAT 创建新的消息队列
IPC_EXCL 与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则返回错误。单独使用无用。
IPC_NOWAIT 读写消息队列要求无法得到满足时,不阻塞。
返回值:成功时返回与键值key相对应的消息队列描述字;否则返回-1。
在以下两种情况下,将创建一个新的消息队列:
-如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
-key参数为IPC_PRIVATE。
打开消息队列的方法:
将key取为要打开的消息队列的关键字的值,而flag中绝对不能设置IPC_EXCL位。
例如创建一个打开或创建消息队列的函数:
int open_queue( key_t keyval )
{
int qid;
if((qid = msgget( keyval, IPC_CREAT | 0660 )) == -1)
{
return(-1);
}
return(qid);
}
●消息传递msgsnd()
#include
#include
#include
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag)
功能: 向消息队列中发送一条消息。函数成功返回0,否则返回-1。
参数说明:
-msqid:消息队列标识符(由msgget生成)
-ptr:指向用户自定义的缓冲区
-nbytes:接收信息的大小。范围在0~系统对消息队列的限制值
-flag:指定在达到系统为消息队列限定的界限时应采取的操作。
IPC_NOWAIT 如果需要等待,则不发送消息并且调用进程立即返回,errno为EAGAIN
如果设置为0,则调用进程挂起执行,直到达到系统所规定的最大值为止,并发送消息
例如发送消息的函数:
int send_message( int qid, struct mymsgbuf *qbuf )
{
int result, length;
/* mymsgbuf结构的实际长度 */
length = sizeof(struct ) - sizeof(long);
if((result = msgsnd( qid, qbuf, length, 0)) == -1)
{
return(-1);
}
return(result);
}
●接收消息msgrcv()
#include
#include
#include
int msgrcv (int msqid, void *ptr, size_t nbytes, long type,int flag)
功能: 从msqid代表的消息队列中读取一个type类型的消息,并把消息存储在ptr指向的mymsg结构中。在成功读取了一条消息以后,队列中的这条消息将被删除。
成功,则为拷贝到消息缓冲区的字节数,失败为-1。
参数说明:
-msqid:消息队列标识符
-ptr:指向用户自定义的缓冲区
-nbytes:如果收到的消息大于nbytes,并且flag&MSG_NOERROR为真,则将该消息截至nbytes字节,并且不发送截断提示
-type:用于指定请求的消息类型:
type=0:收到的第一条消息,任意类型。
type>0:收到的第一条type类型的消息。
type<0:收到的第一条最低类型(小于或等于type的绝对值)的消息。
-flag:用于指定所需类型的消息不再队列上时的将要采取的操作:
如果设置了IPC_NOWAIT,若需要等待,则调用进程立即返回,同时返回-1,并设置errno为ENOMSG
如果未设置IPC_NOWAIT,则调用进程挂起执行,直至出现以下任何一种情况发生:
某一所需类型的消息被放置到队列中。
msqid从系统只能怪删除,当该情况发生时,返回-1,并将errno设为EIDRM。
调用进程收到一个要捕获的信号,在这种情况下,未收到消息,并且调用进程按signal(SIGTRAP)中指定的方式恢复执行。
例如接收消息的函数:
int read_message( int qid, long type, struct mymsgbuf *qbuf )
{
int result, length;
/*计算mymsgbuf结构的实际大小*/
length = sizeof(struct mymsgbuf) - sizeof(long);
if((result = msgrcv( qid, qbuf, length, type, 0)) == -1)
{
return(-1);
}
return(result);
}
●控制消息队列msgctl()
msgctl函数用于对消息队列执行如下控制操作:
-查看消息队列相连的数据结构;
-改变消息队列的许可权限;
-改变消息队列的拥有者;
-改变消息队列的字节大小;
-删除一个消息队列。
函数原型:
#include
#include
#include
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:该函数对msqid指定的消息队列执行参数cmd要求的控制操作。如果调用成功,返回,否则返回-1。
参数说明:
-msqid:正整数,必须是由msgget返回的消息队列的id;
-cmd:为执行的控制命令(在ipc.h中定义):
#define IPC_RMID 0
#define IPC_SET 1
#define IPC_STAT 2
#define IPC_INFO 3
IPC_RMID
删除消息队列。从系统中删除给消息队列以及仍在该队列上的所有数据,这种删除立即生效。
仍在使用这一消息队列的其他进程在它们下一次试图对此队列进行操作时,将出错,并返回EIDRM。 此命令只能由如下两种进程执行:
其有效用户ID等于msg_perm.cuid或msg_perm.guid的进程。
另一种是具有超级用户特权的进程。
IPC_SET
设置消息队列的属性。按照buf指向的结构中的值,来设置此队列的msqid_id结构。
该命令的执行特权与上一个相同。
IPC_STAT
读取消息队列的属性。取得此队列的msqid_ds结构,并存放在buf*中。
IPC_INFO
读取消息队列基本情况。
-buf:指向类型为msqid_ds的结构,该结构由用户分配存储空间,它用于存放IPC_STAT命令的返回结果,或IPC_SET命令要设置的值。
①信号量的概述
多个进程可能为了完成同一个任务会相互协作,这样形成进程之间的同步关系。
而且在不同进程之间,为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程之间的互斥关系。
进程之间的互斥与同步关系存在的根源在于临界资源。
临界资源是在同一个时刻只允许有限个(通常只有一个)进程可以访问(读)或修改(写)的资源,通常包括硬件资源(处理器、内存、存储器以及其他外围设备等)和软件资源(共享代码段,共享结构和变量等)。
访问临界资源的代码叫做临界区,临界区本身也会成为临界资源。
信号量是一种用于对多进程访问共享资源进行控制的机制。共享资源通常分为两大类:一类是互斥共享资源,即任一时刻只允许一个进程访问该资源;
一类是同步共享资源,同一时刻允许多个进程访问该资源。
信号量的实质是整数计数器,其中记录了可供访问的共享资源的单元个数。
信号量是用来解决进程之间的同步与互斥问题的一种进程之间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(P和V操作)。其中信号量对应于某一种资源,取一个非负的整型值。信号量值指的是当前可用的该资源的数量,若它等于0则意味着目前没有可用的资源。
PV原子操作的具体定义为:
●P原语操作的动作是:
信号量的值减1;
若信号量的值减1后仍大于或等于零,则进程继续执行;
若信号量的值减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转进程调度。
●V原语操作的动作是:
信号量的值加1;
若相加结果大于零,则进程继续执行;
若相加结果小于或等于零,则从该信号的等待队列中唤醒一等待进程,然后再返回原进程继续执行或转进程调度。
信号量值的分类:
●二值信号量:信号量的值只能取0或1,类似于互斥锁。但两者有不同:信号量强调共享资源,只要共享资源可用,
其他进程同样可以修改信号量的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
●计数信号量:信号量的值可以任意非负值。
当一个进程要访问某个共享资源时,操作步骤如下:
●测试控制该资源的信号量。
●若此信号量的值为正,则允许进行使用该资源。进程将信号量减1。
●若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤1。
●当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
②信号量的数据结构
●系统中每个信号量的数据结构(sem)
struct sem {
ushort semval; /* 信号量的当前值 */
pid_t sempid; /*在信号量上最后一次操作的进程识别号 */
ushort semncnt; /*等待信号值增长,即等待可利用资源出现的进程数*/
ushort semzcnt; /*等待信号值减少到零,即等待全部资源可被独占的进程数*/
};
●系统中表示信号量集合(set)的数据结构(semid_ds)
struct semid_ds {
struct ipc_perm sem_perm; /* IPC权限 */
long sem_otime; /* 最后一次对信号量操作(semop)的时间 */
long sem_ctime; /* 对这个结构最后一次修改的时间 */
struct sem *sem_base; /* 在信号量数组中指向第一个信号量的指针 */
struct sem_queue *sem_pending; /* 待处理的挂起操作*/
struct sem_queue **sem_pending_last; /* 最后一个挂起操作 */
struct sem_undo *undo; /* 在这个数组上的undo 请求 */
ushort sem_nsems; /* 在信号量数组上的信号量号 */
};
●系统中每一信号量集合的队列结构(sem_queue)
struct sem_queue {
struct sem_queue * next; /* 队列中下一个节点 */
struct sem_queue ** prev; /* 队列中前一个节点, *(q->prev) == q */
struct wait_queue * sleeper; /* 正在睡眠的进程 */
struct sem_undo * undo; /* undo 结构*/
int pid; /* 请求进程的进程识别号 */
int status; /* 操作的完成状态 */
struct semid_ds * sma; /*有操作的信号量集合数组 */
struct sembuf * sops; /* 挂起操作的数组 */
int nsops; /* 操作的个数 */
};
●创建或打开信号量集semget()
#include
#include
#include
int semget(key_t key, int nsems, int flag);
返回值:如果成功,则返回信号量集的IPC标识符。如果失败,则返回-1;
参数说明:
-key: 键值,由ftok获得;
-nsems: 指定打开或者新创建的信号量集中将包含信号量的数目,通常为1。
-flag:标识,同消息队列。
●信号量操作semop()
#include
#include
#include
int semop(int semid, struct sembuf *sops, unsigned nsops);
功能:改变信号量的值。如果成功,返回0。失败:-1。
参数说明:
-semid: 是由semget函数所返回的信号量标识符。
-sops:是一个指向结构数组的指针,其中的每一个结构至少包含下列成员:
struct sembuf{
short sem_num;/*要处理的信号量在信号量集中的序号*/
short sem_op;/*要执行的操作,可正可负或为零*/
short sem_flg;/*操作标记*/
};
成员解读:
▲sem_num为信号量的编号
▲sem_op是要进行的操作(PV操作):
若sem_op是正数,信号量的值加上sem_OP的值。如果sem_flg中的SEM_UNDO位被置为1,那么信号量的调整值就会减去sem_op的值。
若sem_op是负数,表示进程希望使用资源。
-如果信号的值不小于sem_op的绝对值,表示可用资源足够分给这个进程,那么信号量的值减去sem_op的值绝对值,表示分配给进程这些资源;
如果sem_flg中的SME_UNDO位被置1,那么信号量的调整值就会加上sem_op的绝对值。
-如果信号的值小于sem_op的绝对值,表示资源不够了,那么根据sem_flag的值有不同的操作:
*如果sem_flg中的IPC_NOWAIT位被置为1,这个函数就会立即带错返回。
*如果sem_flg中的IPC_NOWAIT没有被置位,与这个信号相关的sem结构中的semncnt域的值加1,这个进程进入休眠状态,直到其他进程
返回了资源,信号量的值不小于sem_op的绝对值。那么进程就会被唤醒,semncnt的值减1,转入第一种情况的处理或者这个信号量被
删除,在这种情况下,这个函数带错返回。
若sem_op为零,这表示了进程要一直等待,直到信号量的值变为0,
-如果信号量的值恰好为零,函数立即返回。
-如果信号量的值不为零,与这个信号量相关的sem结构中的semzcnt域的值加1,这个进程进入休眠状态,直到信号量的值变成了0,那么
进程就被唤醒,semzcnt的值减1;或者这个信号量被删除,在这种情况下,这个函数带错返回。
▲sem_flg为操作标识:
IPC_NOWAIT:如果不能对信号量集合进行操作,则立即返回
SEM_UNDO:当进程退出后,该进程对sem进行的操作将撤销
-nsops:数组中元素的个数
操作失败时:
errno=E2BIG(nsops大于最大的ops数目)
EACCESS(权限不够)
EAGAIN(使用了IPC_NOWAIT,但操作不能继续进行)
EFAULT(sops指向的地址无效)
EIDRM(信号量集已经删除)
EINTR(当睡眠时接收到其他信号)
EINVAL(信号量集不存在,或者semid无效)
ENOMEM(使用了SEM_UNDO,但无足够的内存创建所需的数据结构)
ERANGE(信号量值超出范围)
●信号量控制semctl()
#include
#include
#include
int semctl(int semid, int semnum, int cmd, [union semun arg]);
功能:对信号量集进行控制。如果成功,返回0;失败,返回-1。
参数说明:
-semid:信号量集引用标志符
-semnum:集合中信号量的编号
如果标识某个信号量,此值为信号量下标(0~n-1)
如果标识整个信号量集合,则设置为0
-cmd:表示调用该函数执行的操作,其取值和对应操作如下:
IPC_STAT 读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
IPC_SET 设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
IPC_RMID 将信号量集从内存中删除。
GETALL 用于读取信号量集中的所有信号量的值。
GETNCNT 返回正在等待资源的进程数目。
GETPID 返回最后一个执行semop操作的进程的PID。
GETVAL 返回信号量集中的一个单个的信号量的值。
GETZCNT 返回这在等待完全空闲的资源的进程数目。
SETALL 设置信号量集中的所有的信号量的值。
SETVAL 设置信号量集中的一个单独的信号量的值。
-arg是一个semun的联合,定义如下:
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};
如果cmd为SETVAL,那么该参数为val
如果cmd为IPC_STAT&IPC_SET,那么该参数为struct semid_ds结构体变量
如果cmd为GETVAL&SETALL,则该参数为数组地址array。
如果cmd为IPC_INFO,则该参数为strcut seminfo结构体变量__buf
④死锁(补)
●死锁的定义:
死锁是指多个进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象(互相挂起等待),
若无外力作用,它们都将无法推进下去。
●几种常见的死锁
▲线程将自己锁住
为了保证线程之间的同步和互斥,我们往往需要给其加锁,有时候,线程申请了锁资源,还没有等待释放,又一次申请这把锁,
结果就是挂起等待这把锁的释放,但是这把锁是被自己拿着,所以就会永远挂起等待,就造成了死锁。
▲多线程竞争资源循环等待
有两个线程P1和P2,P1首先申请得到了锁L1,P2申请得到了锁L2,这个时候P1有向去申请锁L2,
结果是被挂起等待P2释放锁L2,而P2恰好也想申请锁L1,结果是挂起等待P1释放锁L1,此时就造成两个线程互相僵持,造成死锁。
▲进程推进顺序不当引起的死锁问题
有三个线程,P1,P2和P3,分别生产数据M1,M2,M3,同时分别接收别的线程产生的数据M3,M2,M1,如果线程推进的顺序正确,
即三个线程都先生产数据,再接收,那么没有问题,但是一旦线程先接受数据,再生产数据,因为一开始没有数据产生,那么就会造成三个线程的死锁问题。
●死锁产生的原因和必要条件
▲死锁产生的主要原因
-系统的资源不足。
-进程(线程)推进的顺序不对。
-资源的分配不当。
-当系统的资源很充沛的时候,每个进程都可以申请到想要的资源,那么出现死锁的概率就很低,线程的调度顺序和速度不同,也会导致死锁问题。
▲死锁产生的四个必要条件
-互斥条件:进程(线程)申请的资源在一段时间中只能被一个进程(线程)使用。
-请求与等待条件:进程(线程)已经拥有了一个资源,但是又申请新的资源,拥有的资源保持不变 。
-不可剥夺条件:在一个进程(线程)没有用完,主动释放资源的时候,不能被抢占。
-循环等待条件:多个进程(线程)之间存在资源循环链。
▲处理死锁的方法
-预防死锁:破坏死锁产生的四个条件之一,注意,互斥条件不能破坏。
-避免死锁:合理的分配资源。
-检查死锁:利用专门的死锁机构检查死锁的发生,然后采取相应的方法。
-解除死锁:发生死锁时候,采取合理的方法解决死锁。一般是强行剥夺资源。
▲如何打破四个产生条件
-打破互斥条件:改造独占性资源为虚拟大资源,但是大部分资源无法改造,因此不建议使用这个方法。
-打破请求与保持条件:在进程(线程)运行之前,就把需要申请的资源一次性申请到位,满足则运行,不满足就等待,
这样就不会造成在占有资源的情况下,还要申请新资源。
-打破不可剥夺条件:在占有资源并且还想要申请新资源的时候,归还已经占有的资源。
-打破循环等待条件:实现资源的有序分配,即对所有的设备进行分类编号,只能以升序的方式来申请资源。
比如说进程P1,使用资源的顺序是R1,R2,进程P2,使用资源的顺序是R2,R1,如果采取动态分配的方式,就很有可能造成死锁。
我们对设备进行分类编号,那么P1,P2只能以R1,R2的顺序来申请资源。就可以打破环形回路,避免死锁。
▲银行家算法
大致实现方法:
-当一个进程对资源的最大需求量不超过系统中的资源数时可以接纳该进程。
-进程可以分期请求资源,当请求的总数不能超过最大需求量。
-当系统现有的资源不能满足进程尚需资源数时,对进程的请求可以推迟分配,但总能使进程在有限的时间里得到资源。
-当系统现有的资源能满足进程尚需资源数时,必须测试系统现存的资源能否满足该进程尚需的最大资源数,
若能满足则按当前的申请量分配资源,否则也要推迟分配。
银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。
因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)
及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。
安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,
然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。
共享内存可以被描述成内存一个区域(段)的映射,这个区域可以被更多的进程所共享。
这是IPC机制中最快的一种形式,因为它不需要中间环节,而是把信息直接从一个内存段映射到调用进程的地址空间。
共享内存实现分为四个步骤:
●创建共享内存,使用shmget函数。
●映射共享内存,将这段创建的共享内存映射到具体的进程空间去,使用shmat函数。
●分离共享内存
●共享内存控制
①数据结构
/*在系统中 每一个共享内存段都有一个shmid_ds数据结构.*/
struct shmid_ds {
struct ipc_perm shm_perm; /* 操作权限 */
int shm_segsz; /* 段的大小(以字节为单位) */
time_t shm_atime; /* 最后一个进程附加到该段的时间 */
time_t shm_dtime; /* 最后一个进程离开该段的时间 */
time_t shm_ctime; /* 最后一次修改这个结构的时间 */
unsigned short shm_cpid; /*创建该段进程的 pid */
unsigned short shm_lpid; /* 在该段上操作的最后一个进程的pid */
short shm_nattch; /*当前附加到该段的进程的个数 */
/* 下面是私有的 */
unsigned short shm_npages; /*段的大小(以页为单位) */
unsigned long *shm_pages; /* 指向frames -> SHMMAX的指针数组 */
struct vm_area_struct *attaches; /* 对共享段的描述 */
};
②系统调用
●共享内存的创建与打开
#include
#include
int shmget(key_t key, int size, int flag);
功能:创建共享内存,如果成功,返回共享内存段标识符;如果失败,则返回- 1:
参数说明:
-key:标识共享内存的键值
操作方式 key值 flag标志
创建 IPC_PRIVATE 无效
创建 新值 IPC_CREAT
创建 非IPC_PRIVATE IPC_CREAT、IPC_EXCL
打开 已存在的值 IPC_CREAT
-size:欲创建的共享内存段的大小
-flag:调用函数的操作类型,也可用于设置共享内存的访问权限
创建失败失败时:
errno = EINVAL (无效的内存段大小)
EEXIST (内存段已经存在,无法创建)
EIDRM (内存段已经被删除)
ENOENT (内存段不存在)
EACCES (权限不够)
ENOMEM (没有足够的内存来创建内存段)
●共享内存的操作
#include
#include
#include
void *shmat(int shmid, const void *addr, int flag);
功能:如果成功,则返回共享内存映射到进程中的地址。如果失败,则返回- 1:
参数说明:
-shmid:shmget函数返回的共享内存标识符
-addr:指定共享内存的映射地址
▲如果addr为0,系统自动查找进程地址空间,将共享内存区域附加到第1块有效内存区域上,此时flag无效;
▲如果addr不为0,而flag未设置SHM_RND位,则共享内存区域附加到由addr指定的地址处;
▲如果addr不为0,而flag设置了SHM_RND位,则共享区域附加到由addr-(addr%SHMLBA)指定的地址处。
-flag:指定共享内存的访问权限和映射条件:
▲SHM_RDONLY //只读
▲SHM_RND //取整,取向下一个SHMLBA边界
▲SHM_REMAP //take-over region on attach
▲SHM_EXEC //执行权限
如果设置为0的话,则是读写权限
#include
#include
#include
int shmdt(const void *addr);
功能:把共享内存从进程地址空间中脱离。如果成功,返回0;如果失败,则返回- 1:errno = EINVAL (无效的连接地址)
●共享内存的控制
#include
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
-shmid:共享内存的引用标识符。
-cmd:控制字段
▲公共的IPC选项(ipc.h中):
IPC_STAT :检索一个共享段的shmid_ds结构,把它存到buf参数的地址中。
IPC_SET :对一个共享段来说,从buf 参数中取值设置shmid_ds结构的ipc_perm域的值。
IPC_RMID :把一个段标记为删除
IPC_INFO:(只有Linux有)返回系统级的限制,结果放在buf中
▲共享内存自己的选项(shm.h中)【需要root权限】
SHM_LOCK //锁定共享内存段
SHM_UNLOCK //解锁共享内存段
-buf:指向shmid_ds的结构指针