进程都有父进程,父进程也有父进程,这就形成了一个以init进程为根的家族树。除此以外,进程还有其他层次关系:进程、进程组和会话。
进程组和会话在进程之间形成了两级的层次:进程组是一组相关进程的集合,会话是一组相关进程组的集合。
这样说来,一个进程会有如下ID:
进程,可以调用以下函数获取进程组ID跟会话ID.
pid_t getpgrp(void);
pid_t getsid(pid_t pid);
前面提到过,新进程默认继承父进程的进程组ID和会话ID,如果都是默认情况的话,那么追根溯源可知,所有的进程应该有共同的进程组ID和会话ID。但是调用ps axjf可以看到,实际情况并非如此,系统中存在很多不同的会话,每个会话下也有不同的进程组。
为何会如此呢?
就像家族企业一样,如果从创业之初,所有家族成员都墨守成规,循规蹈矩,默认情况下,就只会有一个公司、一个部门。但是也有些“叛逆”的子弟,愿意为家族公司开疆拓土,愿意成立新的部门。这些新的部门就是新创建的进程组。如果有子弟“离经叛道”,甚至不愿意呆在家族公司里,他别开天地,另创了一个公司,那这个新公司就是新创建的会话组。由此可见,系统必须要有改变和设置进程组ID和会话ID的函数接口,否则,系统中只会存在一个会话、一个进程组。
进程组和会话是为了支持shell作业控制而引入的概念。
当有新的用户登录Linux时,登录进程会为这个用户创建一个会话。用户的登录shell就是会话的首进程。会话的首进程ID会作为整个会话的ID。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。
在登录shell时,用户可能会使用管道,让多个进程互相配合完成一项工作,这一组进程属于同一个进程组。
当用户通过SSH客户端工具(putty、xshell等)连入Linux时,与上述登录的情景是类似的。
Linux下每个进程(PID)都隶属于一个进程组(PGID)。每个进程组都有一个组长进程,它的PGID和PID相同。进程组是一直存在的,除非进程组里面的进程都退出了,或者加入到其他进程组中。一个进程能够设置自己或者其子进程的PGID,另外,当子进程调用了exec系列函数之后,父进程也不能设置子进程的PGID。
进程获取进程组ID的接口如下:
pid_t getpgid(pid_t pid);
这是获取进程号为pid的进程所属的进程组号
若参数pid为0就是获取当前进程所属的进程组号
pid_t getpgrp(void);
这函数的作用是获取当前进程的集成组号
相当于 getpgid(0)
修改进程组ID的接口如下:
int setpgid(pid_t pid, pid_t pgid);
如果pid等于pgid,那么pid就成为了其所属的进程组的组长进程,
如果pid等于0,则标识把当前进程设置为pgid的组长进程
如果pgid等于0,则使用pid作为目标pgid
这个函数的含义是,找到进程ID为pid的进程,将其进程组ID修改为pgid,如果pid的值为0,则表示要修改调用进程的进程组ID。该接口一般用来创建一个新的进程组。
下面三个接口含义一致,都是创立新的进程组,并且指定的进程会成为进程组的首进程。如果参数pid和pgid的值不匹配,那么setpgid函数会将一个进程从原来所属的进程组迁移到pgid对应的进程组。
setpgid(0,0)
setpgid(getpid(),0)
setpgid(getpid(),getpid())
setpgid函数有很多限制:
·pid参数必须指定为调用setpgid函数的进程或其子进程,不能随意修改不相关进程的进程组ID,如果违反这条规则,则返回-1,并置errno为ESRCH。
·pid参数可以指定调用进程的子进程,但是子进程如果已经执行了exec函数,则不能修改子进程的进程组ID。如果违反这条规则,则返回-1,并置errno为EACCESS。
·在进程组间移动,调用进程,pid指定的进程及目标进程组必须在同一个会话之内。这个比较好理解,不加入公司(会话),就无法加入公司下属的部门(进程组),否则就是部门要造反的节奏。如果违反这条规则,则返回-1,并置errno为EPERM。
·pid指定的进程,不能是会话首进程。如果违反这条规则,则返回-1,并置errno为EPERM。
有了创建进程组的接口,新创建的进程组就不必继承父进程的进程组ID了。最常见的创建进程组的场景就是在shell中执行管道命令,代码如下:cmd1 | cmd2 | cmd3
下面用一个最简单的命令来说明,其进程之间的关系如图4-2所示。
ps ax|grep nfsd
ps进程和grep进程都是bash创建的子进程,两者通过管道协同完成一项工作,它们隶属于同一个进程组,其中ps进程是进程组的组长。
进程组的概念并不难理解,可以将人与人之间的关系做类比。一起工作的同事,自然比毫不相干的路人更加亲近。shell中协同工作的进程属于同一个进程组,就如同协同工作的人属于同一个部门一样。
引入了进程组的概念,可以更方便地管理这一组进程了。比如这项工作放弃了,不必向每个进程一一发送信号,可以直接将信号发送给进程组,进程组内的所有进程都会收到该信号。
前文曾提到过,子进程一旦执行exec,父进程就无法调用setpgid函数来设置子进程的进程组ID了,这条规则会影响shell的作业控制。出于保险的考虑,一般父进程在调用fork创建子进程后,会调用setpgid函数设置子进程的进程组ID,同时子进程也要调用setpgid函数来设置自身的进程组ID。这两次调用有一次是多余的,但是这样做能够保证无论是父进程先执行,还是子进程先执行,子进程一定已经进入了指定的进程组中。由于fork之后,父子进程的执行顺序是不确定的,因此如果不这样做,就会造成在一定的时间窗口内,无法确定子进程是否进入了相应的进程组。
用户在shell中可以同时执行多个命令。对于耗时很久的命令(如编译大型工程),用户不必傻傻等待命令运行完毕才执行下一个命令。用户在执行命令时,可以在命令的结尾添加“&”符号,表示将命令放入后台执行。这样该命令对应的进程组即为后台进程组。在任意时刻,可能同时存在多个后台进程组,但是不管什么时候都只能有一个前台进程组。只有在前台进程组中进程才能在控制终端读取输入。当用户在终端输入信号生成终端字符(如ctrl+c、ctrl+z、ctr+\等)时,对应的信号只会发送给前台进程组。
shell中可以存在多个进程组,无论是前台进程组还是后台进程组,它们或多或少存在一定的联系,为了更好地控制这些进程组(或者称为作业),系统引入了会话的概念。会话的意义在于将很多的工作囊括在一个终端,选取其中一个作为前台来直接接收终端的输入及信号,其他的工作则放在后台执行。
会话是一个或多个进程组的集合,以用户登录系统为例,可能存在如图4-3所示的情况。
会话是一个或者多个进程组的集合。
一个会话有一个控制终端,建立与控制终端相连接的会话首进程叫做控制进程。一个会话当中分为一个前台进程组和多个后台进程组。内核通常发信号给前台进程组的所有进程。
会话的意义在于将多个工作囊括在一个终端,并且取其中的一个工作作为前台,来直接接受该终端的输入输出以及终端信号。其他的工作在后台运行。
建立新会话,可以新打开一个终端也可以使用函数setsid来创建一个新的会话。
系统提供getsid函数来获取进程所属会话,其接口定义如下:
pid_t getsid(pid_t pid);
系统提供setsid函数来创建会话,其接口定义如下:
pid_t setsid(void);
如果这个函数的调用进程不是进程组组长,那么调用该函数会发生以下事情:
1)创建一个新会话,会话ID等于进程ID,调用进程成为会话的首进程。
2)创建一个进程组,进程组ID等于进程ID,调用进程成为进程组的组长。
3)该进程没有控制终端,如果调用setsid前,该进程有控制终端,这种联系就会断掉。
调用setsid函数的进程不能是进程组的组长,否则调用会失败,返回-1,并置errno为EPERM。
这个限制是比较合理的。如果允许进程组组长迁移到新的会话,而进程组的其他成员仍然在老的会话中,那么,就会出现同一个进程组的进程分属不同的会话之中的情况,这就破坏了进程组和会话的严格的层次关系了。
Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。
作业和进程组的区别:
在作业内部创建了子进程,该子进程属于进程组而不属于作业;
作业控制:一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,Shell可以运行一个前台作业和任意多个后台作业。
作业类似进程存在进程号一样,作业也存在作业号:
几个简单的命令:
1).& :在运行一个进程后面加上取地址,则说明让该进程到后台运行;例如:./a.out &;
2).jobs:查看所有的后台作业;
3).fg +作业号:将指定作业放置前台;
4).ctrl+z:将前台作业暂停;
6).ctrl+c:作业终止,进程发信号给所有的前台进程;
5).bg +作业号:将之前暂停的转到后台的作业运行起来;
终端,是一种仿真器,是一种模拟器
1).控制终端:用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端 。控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端;默认情况 下,每个进程的标准输入、标准输出和标准错误输出都指向控制终端。
2).ttyname()的使用:char *ttyname(int fd);
根据文件描述符来获取对应的文件名称;
查看终端对应的设备:
每个进程都可以通过一个特殊的设备文件/dev/tty(终端设备文件)访问它的控制终端,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。
在上述的例子中却是/dev/pts,那仫/dev/pts和/dev/tty有什仫区别呢?
总的来说可以总结为下面一句话:在界面模式下是/dev/pts,但是在黑屏模式下却是/dev/tty。
3).我们可以通过重定向的方式向另一个终端打印消息。
4).线路规程:相当于一个过滤器,如果是普通字符则直接忽略;如果是特殊的组合键则将其解释为信号;内核中处理终端设备的模块包括硬件驱动程序和线路规程。
在终端设备中既有输入队列也有输出队列,所以当存在这样一种情况:当你的输入在显示器上无序的时候也仅仅是回显到输出队列时被冲乱,但是在你的输入队列中依然是有序的。
接下来看一看什仫是终端登录过程:
5).所谓的终端登录过程指的是用户在输入用户名和密码验证的过程,下面是我理解的一张终端登录的过程图:
从上图可以看出终端登录过程可以是如下几个步骤:
1).系统启动时,init进程(也就是1号进程)根据配置文件/etc/inittab确定需要打开哪些终端;
2).getty根据命令行参数打开终端设备作为它的控制终端,把文件描述符0、1、2都指向控制终 端,然后提示用户输入帐号。用户输入帐号之后,getty的任务就完成了,它再执行login程序;
execle(“/bin/login”, “login”, “-p”, username, NULL, envp);
3).如果密码不正确,login进程终止,init会重新fork/exec一个getty进程;如果密码正确,login程序设置一些环境变量,设置当前工作目录为该用户的主目录,然后执行Shell;
execl(“/bin/bash”, “-bash”, NULL);
由于每个进程都有自己独立的运行环境,因此进程与进程间是相对封闭的。如何让两个封闭的进程之间实现数据通信是进程编程的重点与难点。
Linux的内的进程通信机制基本来源于Unix的系统对Unix的发展做出巨大贡献的两大主力--AT&T公司的贝尔实验室和加州大学伯克利分校 - 。在进程通信领域研究的侧重点不同。
贝尔实验室对Unix系统早期的进程间通信手段进行了改进与扩充,形成了System V IPC(进程间通信)。互相通信的进程被限定在单个计算机内。而伯克利分校则跳出了解System V IPC的限制,发展出了以套接字(socket)为基本点的进程间通信机制。
Linux的系统将二者的优势全部继承下来现在的Linux系统内比较常用的进程间通信方式有以下几种:
- 传统UNIX系统内的进程通信方式:
1)无名管道(管)和有名管道(FIFO):
管道提供了进程间通信消息传递的实体,其原型来自于数据结构的“队列”。无名管道用于具有亲缘关系的进程(例如父子进程,兄弟进程),而有名管道则允许不具有亲缘关系的进程使用。
2)信号(信号):
信号是在软件层面上对中断的一种模拟机制,用于通知进程某个事件发生。
-System V IPC进程通信方式:
3)消息队列(消息队列):
消息队列是消息所构成的链表,包括POSIX消息队列与系统V消息队列两种。消息队列克服了管道与信号两种通信方式中信息量有限的缺点。
4)共享内存(共享内存):
最有效的进程通信方式。它使得多个进程共享一块内存空间,不同进程间可以实时观察到其他进程的数据更新。不过使用该方式需要某种同步与互斥机制。
5)信号量(信号量):
主要作为进程间以及同一进程的不同线程间的同步与互斥手段。
-BSD进程通信方式:
6)套接字(socket):
更广泛的进程通信机制,常用于网络的不同主机之间的进程通信。
套接字在网络部分再学
管道是Linux中进程间通信的一种常用方式,它将一个程序的输出直接作为另一个程序的输入。Linux内的管道通信主要有无名管道与有名管道两种。管道本质是内核中的一块缓冲区
之前在文件IO中学习open函数的时候我们知道:
普通文件:默认是非阻塞的!!!!
设备文件:默认是阻塞的(比如管道文件)!!!
无名管道和有名管道总结(这里统称为管道):
匿名管道只能用于具有亲缘关系的进程间通信,命名管道用于任意的进程间通信
管道的生命周期随进程
管道提供流服务—字节流传输:数据放在缓冲区中(有序、连接、可靠,传输比较灵活)
优点:传输灵活
缺点:数据粘连(数据在缓冲区中堆积在一起了)
管道自带同步与互斥功能(读写操作和数据大小不超过PIPE_BUF大小,读写操作受保护)
互斥:对临界(公共)资源同一时间的唯一访问性(我操作时别人不能操作),对管道进行数据操作的大小不超过PIPE_BUF=4096的时候,则保证操作的原子性。
同步:对临界资源的时序可控性(我操作完了别人才能操作),避免一个人一直在操作,其他人操作不了
“互斥保证安全,同步保证合理”
管道的生命周期随进程
字节流传输就好像水流一样,而数据报传输就好像冰块一样,如果冰块太她了,就会出现传送不了的情况。
多个进程往一个管道写,一个进程读可不可以 为什么?
无名管道是Unix系统内一种原始的进程通信方法。使用无名管道需要注意:
1.只能用于具有亲缘关系的进程间通信(父子进程、兄弟进程)
2.半双工通信模式,即无法同时读写管道。管道具有固定的读端与写端
3.管道可以看做特殊的文件,可以使用read()/write()函数对管道进行读写操作(但是不能使用lseek()进行定位操作)。不过管道不属于文件系统,并且只存放在内存中
站在文件描述符角度-深度理解管道
站在内核角度-管道的本质
无名管道读写规则
1)如果读端写端都开启着
- 管道中若没有数据,则read会阻塞。
- 若管道中数据满了,则write会阻塞。
(这是因为此时无名管道读端和写端都开启着 会认为将继续读写 即使管道中没数据或者数据满了 使用read或者write也只会堵塞等待 而不是返回)
2)
- 若管道所有写端被关闭,则read读完数据后返回0(而不是阻塞),read返回0,表示管道没人写了(即写端全部关闭)没必要再继续读,进程退出。
- 若管道所有读端被关闭,则write写数据会触发异常返回SIGPIPE信号报错(导致进程退出)
总结:
① 读管道: 1. 管道中有数据,read返回实际读到的字节数。
2. 管道中无数据:
(1) 管道写端被全部关闭,read返回0 (读到文件结尾)。这个也是我们判断 对方断开连接的方式。
(2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
② 写管道: 1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
2. 管道读端没有全部关闭:
(1) 管道已满,write阻塞。
(2) 管道未满,write将数据写入,并返回实际写入的字节数。个人理解 :要尽量保证管道最终不能有数据 所以 当读端关闭 写端write会异常终止
当写端关闭 读端会将管道的数据读完后后返回0
- 因为管道的读写特性,用户在操作管理的时候最好是没有用到哪一端,则关闭掉。
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。(原子性操作不可被打断)
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
- 管道自带同步与互斥:
- 同步:对临界资源访问的时序可控性(时序控制–我操作完了别人才能操作)
- 互斥:对临界资源的同一时间唯一访问性(保护–我操作的时候别人不能操作)
- 管道提供字节流服务,传输方式灵活。但是造成了数据粘连(本质原因:数据之间没有边界)
管道特点
- 本质是内核的块缓冲区—>多个进程通过访问同一块缓冲区实现数据传输通信。
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道提供流式服务
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
无名管道是基于文件描述符的通信方式。当一个管道被创建时,它会创建两个文件描述符fd[0]与fd[1],其中fd[0]固定用于读管道内容,fd[1]固定用于写管道内容。
一般步骤:
1.创建包含两个文件描述符的数组int fd[ 2 ],其中fd[0]固定用于读管道,fd[1]固定用于写管道
2.使用pipe()创建一个无名管道
3.使用fork()创建子进程 ,这样父子进程就都各有fd[ 0 ] 和fd[ 1 ]. 其中fd[0]固定用于读管道内容, fd[1]固定用于写管道内容。
4.进行读写操作(例如:关闭父进程的f[ 0 ] 使用f[ 1 ]执行写操作到管道 ;关闭子进程的f[ 1 ] 使用 f[ 0 ] 执行读操作 读取管道的数据
通过这样就实现了方向为 父进程 -> 子进程 的通信)
1.定义
函数pipe()
所需头文件:#include
函数原型:int pipe(int fd[])
函数参数:
fd[ ] 包含两个文件描述符的数组,其中fd[0]固定用于读管道,fd[1]固定用于写管道
函数返回值:
成功:0
失败:-1
创建管道使用pipe()函数,而其余的操作诸如读取管道read()、写入管道write()、关闭管道close()函数与文件IO的函数使用方式相同,这里不再赘述。
那么如何使用无名管道实现父子进程间的通信呢?
由于无名管道具有固定的读端与写端,因此,如果父子进程需要使用无名管道进行通信,可以进行以下操作:
父进程->子进程:父进程对自己的fd[1]执行写操作,数据流入管道内,然后子进程对自己的fd[0]执行读操作,得到管道内数据
子进程->父进程:子进程对自己的fd[1]执行写操作,数据流入管道内,然后父进程对自己的fd[0]执行读操作,得到管道内数据
注意:无名管道的工作方式是半双工方式,即在一个进程内要么读管道,要么写管道,无法同时进行读写操作。也就是说,在同一时刻内,要么父进程写数据、子进程读数据(父进程->子进程),要么子进程写数据、父进程读数据(子进程->父进程),数据流动方向唯一,不能同时存在两个数据流动方向。在使用时,对于该进程内未使用的文件描述符应当关闭。
使用无名管道编程时需要注意以下事项:
1.无名管道只能用于具有亲缘关系的进程间(通常是父子进程间)
2.fd[0]固定用于读取管道,fd[1]固定用于写入管道,两个文件描述符不可弄混否则会报错
3.只有管道存在读端,向管道内写入数据才有意义,否则会返回SIGPIPE信号报错
4.如果管道使用完毕,关闭所有的文件描述符即可
示例:使用无名管道实现父子进程间的通信(子进程->父进程)
#include
#include
#include
#include
#define MAXLEN 100
int main()
{
int n;
int fd[2];
pid_t pid;
char message[MAXLEN]={0};
if(pipe(fd)<0)//创建一个无名管道
{
perror("cannot create a pipe");
exit(0);
}
if((pid = fork())<0)//创建子进程
{
perror("cannot fork");
exit(0);
}
else if(pid==0)//子进程
{
printf("This is Child Process\n");
close(fd[0]);//关闭该进程内的fd[0](读端),保留fd[1](写端),即接下来对该管道进行写操作
strcpy(message,"Helloworld\n"); //将字符串赋值到字符数组中
write(fd[1],message,strlen(message)); //将数组里的数据写入管道中
close(fd[1]);//管道使用完毕,关闭fd[1]
}
else//父进程
{
printf("This is Parent Process\n");
close(fd[1]);//关闭该进程内的fd[1](写端),保留fd[0](读端),即接下来对该管道进行读操作
sleep(1);//保证子进程先写数据
n = read(fd[0],message,MAXLEN); //fd[0]从无名管道中读取数据,读入到message数组中,n 为返回的字符个数
printf("Parent read %d characters, Message is:%s",n,message);
close(fd[0]);//管道使用完毕,关闭fd[0]
waitpid(pid,NULL,0);//父进程等待回收子进程
}
return 0;
}
/*******************管道的方向与流管道**********************/
细心的同学可能发现,我们在示例程序中使用管道的时候,关闭了父进程的写端与子进程的读端,相当于强行规定了管道的数据流动方向(子进程->父进程)。那么如果我们想复用该管道传输数据,实现“父进程->子进程”该怎么办呢?
非常遗憾,我们无法改变已经确定数据传输方向的管道的方向,即无法复用管道实现“父进程->子进程”的功能。那么管道为什么必须有方向呢?
实际上,管道方向算是一个历史遗留问题。管道通信是UNIX系统内最古老的通信方式。在早期的内核代码中,由于技术受限以及硬件性能不足,管道是必须确定方向的。现在的操作系统虽然已经足够强大,但是“管道必须确定传输方向”还是被保留下来。出于内核移植性的考虑,我们在使用管道的时候也必须确定管道的数据传输方向。
那么,有没有能够双向传输数据的管道呢?实际上是存在的,这种管道叫做“流管道”。使用流管道可以实现数据在管道内的双向流动。
如果想创建一个流管道,可以使用s_pipe()函数。遗憾的是,流管道只存在极个别的操作系统中。Linux系统是不支持流管道的。如果想使用流管道,我们可以使用socketpair()函数来模拟流管道。有关socketpair()函数的使用以及流管道的相关知识请同学们课外查阅资料,这里不再赘述。
/*******************管道的方向与流管道end*******************/
无名管道的使用范围比较狭隘,因为它只能实现有亲缘的进程之间的通信任务。如果想使用管道实现没有亲缘关系的进程间通信,我们可以采用有名管道的方式。
有名管道可以实现互不相干的两个进程之间的通信。有名管道在文件系统中可见,而且可以通过路径访问。进程通过文件IO的方式来读写管道内的数据。但是无法使用lseek()函数进行定位操作。
FIFO文件在磁盘上没有数据块,仅用来标识内核中的一条通道
//有名管道的原型来自于数据结构的队列,因此有名管道遵循“先进先出”的原则,这也是有名管道的名称(FIFO -- First In First Out)的由来。
除了本地通信外,有名管道的另一个典型应用是在网络中的客户机——服务器之间传输数据。如果有一个服务器,这个服务器与许多客户机有关,那么该服务器会创建一个“众所周知的FIFO”(即所有的客户机都知道该FIFO的访问路径),所有的客户机都可以使用该“众所周知的FIFO”向服务器提出请求。
但是这种通信方式的问题是,服务器如何将数据送回给客户机?通常情况下,服务器在接收到客户机的请求后,会专门建立一个FIFO与客户机进行通信,每个专用的FIFO都是与客户机的进程ID为基础的。
但是这种通信方式仍然具有弊端,服务器无法侦测客户机是否已经崩溃,因此有可能会有部分数据残留在管道内。
FIFO文件在磁盘上没有数据块,仅用来标识内核中的一条通道
有名管道的使用类似于创建一个文件,我们可以使用mkfifo()函数创建一个有名管道。
函数mkfifo()
它是一个c库函数
所需头文件:#include
#include
函数原型:int mkfifo(const char *pathname, mode_t mode)
函数参数:
pathname : 要创建的有名管道的路径名与文件名
mode : 创建的有名管道的文件权限码,通常用八进制数字表示
函数返回值:
成功:0
失败:-1
//Linux系统内,mkfifo同时也是一个用于创建有名管道的Shell命令。若不想调用该函数,则可以使用命令创建一个有名管道文件
创建有名管道后,我们可以使用文件IO的方式操作管道。
若读取管道内的数据,则使用read()函数;
若想向管道内写入数据,则使用write()函数。
使用read()函数读取设备文件的注意事项!!!
read读取设备文件时fd中的数据如果小于要读取的数据,就会返回实际可用的字节数。
以下情况read不会引起阻塞:
(1)常规文件不会阻塞,不管读到多少数据都会返回;
(2)从终端读不一定阻塞:如果从终端输入的数据没有换行符,调用read读终端设备会阻塞,其他情况下不阻塞;
(3)从网络设备读不一定阻塞:如果网络上没有接收到数据包,调用read会阻塞,除此之外读取的数值小于count也可能不阻塞
创建管道文件不能在共享文件夹里创建
mkfifo: 无法创建先进先出文件“1”: 没有那个文件或目录
归根结底是因为用的是共享文件夹,而window的文件系统又不支持管道文件。
创建的管道文件路径必须设为linux的本地文件夹。
我们知道命名管道的本质就是文件系统中的一个特殊设备文件,即管道文件(FIFO)所以打开管道文件默认是!!!阻塞的!!!!(可以通过open函数的O_NONBLOCK设置为以非阻塞的方式打开)。虽然它和其他普通文件的用途不同,但它的打开方式却和普通文件相同,都是通过调用open
函数来打开。通常而言,打开管道文件的方式有以下几种:
(1)只读且阻塞方式
open(const char *pathname, O_RDONLY);
(2)只读且非阻塞方式
open(const char *pathname, O_RDONLY | O_NONBLOCK);
(3)只写且阻塞方式
open(const char *pathname, O_WRONLY);
(4)只写且非阻塞方式
open(const char *pathname, O_WRONLY | O_NONBLOCK);
注:
在这里我们需要注意一点,就是不能以 O_RDWR 方式打开管道文件,这种行为是未定义的。倘若有一个进程以读写方式打开了某个管道,那么该进程写入的数据又会被该进程本身读取,而管道一般只用于进程间的单向数据通信。
**有名管道的打开规则**(打开规则和下面的读写规则和使用步骤结合起来看!!)
1.如果当前打开操作是为读而打开FIFO时:
(1)若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;
(2)否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。
2.如果当前打开操作是为写而打开FIFO时:
(1)如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;
(2)否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。
一般情况下(没有指定O_NONBLOCK),只读open要阻塞到某个其他进程为写而打开这个FIFO为止。类似的,只写open要阻塞到某个其他进程为读而打开它为止。
如果指定了O_NONBLOCK,则只读open立即返回,但是如果没有进程为读而打开一个FIFO,那么只写open将返回-1,并将errno设置成ENXIO。既 :如果以默认阻塞读或写打开一个FIFO会阻塞到另一端也打开(通过其他进程或线程)
有名管道的读写规则
1.从FIFO中读取数据:
约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。
如果有进程写打开FIFO,但是没有向FIFO内写数据:1)对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
2)则对于设置了阻塞标志的读操作来说,将一直阻塞。
对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:a.当前FIFO内有数据,但有其它进程在读这些数据;
b.另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。
值得注意的是:读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。
注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。
2.向FIFO中写入数据:
约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。
1)对于设置了阻塞标志的写操作:
当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
2)对于没有设置阻塞标志的写操作:
当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写
有名管道注意事项:
1、进程间通信之FIFO,在阻塞模式下,只有当读和写模式都打开时才返回,否则一直阻塞; 2、非阻塞模式下,当读端没打开,则打开写端无效,返回错误。 3、在写模式下,如果一个读关闭,会触发SIGPIPE信号,此时需要用signal()处理,否则程序会退出1. FIFO文件是受内核保护的,所以程序在执行的时候需要Root权限,否则会提示错误:EACCES
2. 程序执行时,需要创建一个FIFO文件,结束之后应该予以删除,避免造成垃圾文件
3. 除了通过mkfifo()来创建FIFO文件之外,其他跟使用普通文件的方法类似:open()/ read()/ write()
4.
一个给定的FIFO有多个写进程是很常见的如果想避免多个进程写数据交叉则需要考虑数据的原子操作
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。(原子性操作不可被打断)
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
关于FIFO文件的使用步骤:
1> 通过mkfifo()创建FIFO文件;
2> 读端通过open()打开FIFO文件,以供读入;(可指定阻塞或非阻塞读取)
如果是阻塞打开,就会阻塞到有进程打开写端
3> 写端通过open()打开FIFO文件,以供写入;(可指定阻塞或非阻塞写入)
注意:如果步骤2>和步骤3>顺序颠倒,则会出现两种可能性:如果写端要进行阻塞式的打开,即open()操作,则会一直等待,直到有读取端打开该FIFO文件;如果写端要进行非阻塞式的打开,则会返回ENXIO错误,提示:ENXIO: O_NONBLOCK | O_WRONLY is set, the named file is a FIFO and no process has the file open for reading.
4> 写端通过write()写入FIFO文件;
5> 读端通过read()读取FIFO文件;
注意:如果步骤4>和步骤5>顺序颠倒,即先读取,但是FIFO文件中没有数据,也会导致两种可能:如果读取端进行阻塞式的读取,即read()操作,则会一直等待,直到FIFO文件中被写入数据(如果之前读取到FIFO文件的末尾,则read()返回值为0,表示读取到了文件尾);如果读取端进行非阻塞式的读取,则会返回EAGAIN,表示FIFO文件中没有数据;
示例1:编写程序,创建一个有名管道文件
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
if(argc<2)
{
printf("too few arguments\n");
exit(0);
}
if(mkfifo(argv[1],0664)<0)
{
perror("cannot create fifo");
exit(0);
}
return 0;
}
运行该程序,则可以创建一个有名管道文件。我们可以使用ls -l命令或stat命令查看该文件的属性
//当然也可以使用mkfifo命令创建有名管道文件
示例2:在示例1的基础上,编写程序,实现两个进程使用有名管道通信
注意:为了模拟“两个没有亲缘关系的进程”,我们将代码分成两部分,一部分读管道,一部分写管道。两端代码要分别使用两个终端同时运行。
//写管道代码如下
//文件fifo_write.c
#include
#include
#include
#include
#include
#include
#include
#define MAX 256
int main(int argc, const char *argv[])
{
int fd;
char buffer[MAX]={0};
if(argc<2)
{
printf("too few arguments\n");
exit(0);
}
if((fd=open(argv[1],O_WRONLY))<0) //打开管道。因为需要写管道,所以使用O_WRONLY
{
perror("cannot open pipe");
exit(0);
}
printf("Please input string, if input 'quit' will stop:"); //输入"quit"程序停止
scanf("%[^\n]",buffer);
getchar();
while(strncmp(buffer,"quit",4)!=0)
{
write(fd,buffer,strlen(buffer)+1);
printf("Please input string, if input 'quit' will stop:");
scanf("%[^\n]",buffer);
getchar();
}
write(fd,buffer,strlen(buffer)+1); //将最后的"quit"写入管道中
close(fd); //关闭管道
return 0;
}
//读管道代码如下
//文件fifo_read.c
#include
#include
#include
#include
#include
#include
#include
#define MAX 256
int main(int argc, const char *argv[])
{
int nread;
if(argc<2)
{
printf("too few arguments\n");
exit(0);
}
char readbuffer[MAX]={0};
int fd;
if((fd=open(argv[1],O_RDONLY))<0) //打开管道,因为需要读管道,所以使用O_RDONLY
{
perror("cannot open pipe");
exit(0);
}
while(1)
{
if((nread=read(fd,readbuffer,MAX))<=0) //读取出错 或 管道内已无数据
{
printf("read fifo error, will exit\n");
break;
}
if(strncmp(readbuffer,"quit",4)!=0)
{
printf("read string:%s\n",readbuffer);
bzero(readbuffer,MAX);
}
else //读到"quit"程序停止
{
printf("read 'quit', will exit\n");
break;
}
}
close(fd);//关闭管道
return 0;
}
也可以多个发送端多个接收端,但是收到的信息都是一样的。可以自己尝试
Linux_信号与信号量_逝去的浪花-CSDN博客_信号量信号:信号机制是类UNIX系统中的一种重要的进程间通信手段之一。我们经常使用信号来向一个进程发送一个简短的消息。例如:假设我们启动一个进程通过socket读取远程主机发送过来的网络数据包,此时由于网络因素当前主机还没有收到相应的数据,当前进程被设置为可中断等待状态(TASK_INTERRUPTIBLE),此时我们已经失去耐心,想提前结束这个进程,于是可以通过kill命令想这个进程发送KILL信https://blog.csdn.net/sty23122555/article/details/51470949
信号机制是类UNIX系统中的一种重要的进程间通信手段之一。我们经常使用信号来向一个进程发送一个简短的消息。例如:假设我们启动一个进程通过socket读取远程主机发送过来的网络数据包,此时由于网络因素当前主机还没有收到相应的数据,当前进程被设置为可中断等待状态(TASK_INTERRUPTIBLE),此时我们已经失去耐心,想提前结束这个进程,于是可以通过kill命令想这个进程发送KILL信号,内核会唤醒该进程,执行它的信号处理函数,KILL信号的默认处理是退出该进程。
另外应用程序可以通过signal()等函数来为一个信号设置默认处理函数。例如当用户按下CTRL+C时,shell将会发出SIGINT信号,SIGINT的默认处理函数是执行进程的退出代码,如下所示:
可以通过类似下面的命令显式的给一个进程发送一个信号:
kill -2 pid
事实上,进程也不知道信号到底什么时候到达。信号是异步的,一个进程不可能等待信号的到来,也不知道信号会到来,那么,进程是如何发现和接受信号呢?实际上,信号的接收不是由用户进程来完成的,而是由内核代理。当一个进程P2向另一个进程P1发送信号后,内核接受到信号,并将其放在P1的信号队列当中。当P1再次陷入内核态时,会检查信号队列,并根据相应的信号调取相应的信号处理函数。
信号检测和响应时机
刚才我们说,当P1再次陷入内核时,会检查信号队列。那么,P1什么时候会再次陷入内核呢?陷入内核后在什么时机会检测信号队列呢?
- 当前进程由于系统调用、中断或异常而进入系统空间以后,从系统空间返回到用户空间的前夕。(看下面的信号的检测和处理流程图)
- 当前进程在内核中进入睡眠以后刚被唤醒的时候(必定是在系统调用中),或者由于不可忽略信号的存在而提前返回到用户空间。
进入信号处理函数
发现信号后,根据信号向量,知道了处理函数,那么该如何进入信号处理程序,又该如何返回呢?
我们知道,用户进程提供的信号处理函数是在用户态里的,而我们发现信号,找到信号处理函数的时刻处于内核态中,所以我们需要从内核态跑到用户态去执行信号处理程序,执行完毕后还要返回内核态。
#include
#include
void int_handler(int signum)
{
printf("\nSIGINT signal handler.\n");
printf("exit.\n");
exit(-1);
}
int main()
{
signal(SIGINT, int_handler);
printf("int_handler set for SIGINT\n");
while(1)
{
printf("go to sleep.\n");
sleep(60);
}
return 0;
}
相信各位对“中断”都不陌生。信号是在软件层次上对中断机制的一种模拟,从原理上来说,进程接收信号并处理与处理器接收中断并处理是一样的。
信号通信是异步的(即非实时性的):一个进程不必等待信号的到达。事实上,进程也不知道什么时候信号会到达。
信号通信可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以通过信号知道用户空间进程发生了哪些事件。如果某个进程未处于运行态而接收到了一个信号,那么该信号会被内核保存起来,直至该进程恢复运行再将该信号传递给进程;如果一个信号被设置为阻塞,则该信号的传递会被延迟,直至其阻塞被取消才能被传递给进程。
通常情况下,以下情景会使用信号通信:
1.某些后台进程的通信。例如xinetd进程。
2.两个无亲缘关系的进程且无法使用有名管道
3.某个进程只能使用标准输入与标准输出(即无法读写管道)
信号通信最早来自对硬件中断的一种模拟,不过经POSIX的扩展后功能变得更加强大,不仅能够发送或接收信号,信号本身还可以附加信息。
信号的产生有硬件来源与软件来源。硬件来源常见的有按下键盘、定时器到时、硬件故障等;软件来源常见的有信号处理函数、非法操作等。我们可以使用kill -l命令查看所有信号与编号,其中1~31是传统UNIX支持的信号(非可靠信号,不支持嵌套),32~63是后来扩充的信号(可靠信号,支持嵌套)。
SIGCHILD信号 :子进程结束时,系统向其父进程发送SIGCHILD信号(子进程已经停止或者终止)
进程可以有三种方式来响应一个信号:
1.忽略信号:即对信号不做任何处理。但是SIGKILL(9)与SIGSTOP(19)信号不能被忽略。
2.捕捉信号:自定义信号处理函数,当接收到信号时执行相应的信号处理函数。
3.执行默认操作:Linux对每种信号都规定了默认操作,当接收到信号时进程执行信号的默认操作。
linux中使用信号的实例:
(1)父进程调用wait函数后阻塞 等待子进程结束
(2)子进程结束时,系统向其父进程发送SIGCHILD信号(子进程已经停止或者终止)
(3)父进程被SIGCHILD信号唤醒然后去回收僵尸子进程
(4)父子进程之间是异步的,SIGCHILD信号机制就是为了解决父子进程之间的异步通信问题,让父进程可以及时的去回收僵尸子进程。
1)发送信号:kill()函数与raise()函数
kill()函数与我们之前学习过的kill命令一样,都可以发送信号给一个进程或进程组(实际上,Shell命令的kill命令就是内核通过kill()函数实现的)。需要注意的是,kill()函数不仅可以发送终止进程的信号,它也可以发送其他信号。
raise()函数也可以发送一个信号,不过与kill()函数不同的是,raise()函数只能让进程向自身发送信号。
函数kill()
所需头文件:#include
#include
函数原型:int kill(pid_t pid, int sig)
函数参数:
pid:
正数 发送信号给进程标识符为pid的进程
0 信号被发送到所有和当前进程在同一个进程组的进程
-1 信号被发送给所有的有权给其发送信号的进程(除了1号init进程)
<-1 信号发送给进程组号为-pid的每个进程
sig : 需要发送的信号。
若为sig为0则不会送出信号,但是系统会执行错误检查。通常使用0来检测某个进 程是否正在运行如果返回0 既该进程已经结束 返回-1就是未结束
函数返回值:
成功:0
失败:-1
函数raise()
该函数是 给调用raise()的进程发送信号
所需头文件:#include
函数原型:int raise(int sig)
函数参数:
sig 需要发送的信号。
若为sig为0则不会送出信号,但是系统会执行错误检查。通常使用0来检测某个进程是否正在运行如果返回0 既该进程已经结束 返回-1就是未结束
函数返回值:
成功:0
失败:-1
//函数raise()等价于kill(getpid(),sig)或pthread_kill(pthread_self(),sig)
示例:演示kill()函数与raise()函数。首先创建子进程,在子进程内调用raise()函数发送一个SIGSTOP信号使自身暂停;父进程中调用kill()函数向子进程发送信号SIGKILL
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
pid_t pid;
if((pid=fork())<0)
{
perror("cannot fork");
exit(0);
}
else if(pid==0)//子进程
{
printf("这是子进程:%d, 等待信号。\n",getpid()); //主函数的sleep(5);
raise(SIGSTOP); //子进程被暂停
printf("子进程已被暂停\n"); //注意这句话并不会输出
exit(0);
}
else //父进程
{
int ret;
sleep(5); //让子进程先运行,等待时间
ret = waitpid(pid,NULL,WNOHANG); //wait();第一个参数:在else这里pid为子进程的pid;第二个参数:子进程退出时的状态;第三个参数:WNHANG:表示若指定的进程未结束,则立即返回0
if(ret==0) //如果子进程在运行
{
kill(pid,SIGKILL); //向子进程发送SIGKILL,杀死子进程
printf("父进程杀死了子进程。 %d\n",pid);
}
waitpid(pid,NULL,0);//回收子进程
exit(0);
}
return 0;
}
示例2:在示例1的基础上,将父进程发送的信号换成SIGCONT,观察效果
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
pid_t pid;
if((pid=fork())<0)
{
perror("cannot fork");
exit(0);
}
else if(pid==0)//子进程
{
printf("这是子进程: %d, 等待信号。\n",getpid());
raise(SIGSTOP);//子进程被暂停
printf("子进程已被暂停\n");//注意这句话会输出
exit(0);
}
else//父进程
{
int ret;
sleep(5);//让子进程先运行
ret = waitpid(pid,NULL,WNOHANG);
if(ret==0)//如果子进程在运行
{
kill(pid,SIGCONT);//向子进程发送SIGCONT,子进程恢复运行
}
waitpid(pid,NULL,0);//回收子进程
exit(0);
}
return 0;
}
1. alarm()函数也称为闹钟函数,它可以在进程中设定一个定时器.
当定时器计时结束时,它就会向进程发送SIGALRM信号,SIGALRM信号默认的处理方 式是终止程序,并且在终端输出"Alarm clock"表示计时结束,如果想要接受到SIGALRM信号后不终止程序 就要使用signal(SIGALRM,SIGIGN)。
注意:一个进程只能有一个闹钟时间,如果在调用alarm()函数前已经设定过闹钟时间,则旧的闹钟时间会被新的闹钟时间替代。
2. pause()函数 用于将该进程挂起直至接收到某个信号为止。
函数alarm()
所需头文件:#include
函数原型:unsigned int alarm(unsigned int second)
函数参数:
second : 指定倒计时秒数,在second秒后发送SIGALRM信号 SIGALRM信号默认的处理方 式是终止程序
函数返回值:
成功:0(未设置过闹钟时间) 或 上个闹钟时间的剩余时间(设置过闹钟时间)
失败:-1
函数pause()
所需头文件:#include
函数原型:int pause()
函数参数:无
函数返回值:-1,并且把errno设定为EINTR(仅会在接收到信号后返回)
示例:演示alarm()函数与pause()函数
#include
#include
#include
int main()
{
alarm(5);//设定闹钟时间5s
pause();//将进程挂起,等待闹钟
printf("I should wake up\n");//注意此语句不会执行
return 0;
}
如果没有pause();挂起操作,程序的运行时间要超过alarm();所设定的时间,否则alarm();就不会返回"Alarm clock",就是因为已经提前结束了
#include
#include
#include
int main(int argc, char *argv[])
{
int i = 1;
alarm(5);
while(i <= 6)
{
printf("这是%d第次循环\n",i++);
sleep(1);
}
printf("I will exit\n");
return 0;
}
执行程序,我们会发现printf()内的字符串不会被打印,而是会打印"Alarm clock"。这是因为SIGALRM信号默认的处理方式是终止程序,因此程序在printf()执行前就已经退出了。
#include
#include
#include
int main(int argc, char *argv[])
{
int i = 1;
alarm(5);
while(i <= 3)
{
printf("这是%d第次循环\n",i++);
sleep(1);
}
printf("I will exit\n");
return 0;
}
在刚才的学习过程中,我们发现绝大多数的信号的默认处理都是终止进程。如果想要让进程接收信号后做出不同的响应,则需要设置信号处理函数。
信号处理函数主要有两个:signal()函数和sigaction()函数。signal()函数比较简单,只需指定信号类型与信号处理函数即可,但是它只能用于编号前31种信号处理,且不能通过信号传递信息。而sigaction()函数可以看做是signal()函数的升级版,功能比signal()函数更加健全强大。
函数signal()
所需头文件:#include
函数原型:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数参数:
signum : 指定信号
handler : 指定接收信号后的处理方式
SIG_IGN:忽略信号
SIG_DFL:采用默认方式处理信号
其他:自定义信号处理函数
函数返回值:
成功:以前的信号处理函数
失败:SIG_ERR
示例1:使用signal()函数捕捉信号,并执行相应的信号处理函数。其中SIGINT代表ctrl+c组合键,SIGQUIT代表ctrl+\组合键。
#include
#include
#include
void handler(int sig_no) //自定义信号处理函数
{
if(sig_no == SIGINT)
{
printf("Got a signal: SIGINT(ctrl+c)\n");
}
else if(sig_no == SIGQUIT)
{
printf("Got a signal: SIGQUIT(ctrl+\\)\n");
}
}
int main()
{
signal(SIGINT,handler);
signal(SIGQUIT,handler);
printf("Waiting for signal SIGINT or SIGQUIT……\n");
pause(); //等待接收信号
return 0;
}
练习:若将程序内主函数的"signal(信号,handler);"改为"signal(信号,SIG_IGN);",则该程序会出现什么效果?
共享内存、消息队列、信号量都属于SystemV型的进程间通信方法,三者在使用函数方面有相似之处,我们在学习过程中要仔细观察三者的相同点与不同点。
首先要注意一个概念:IPC结构都是内核的结构。也就是说IPC结构由内核维护,对于每个进程都是公共的,不属于某个特定进程。只有这样,IPC结构才能支持它们“进程间通信”的功能。
在shell环境下可以使用ipcs
查看当前系统IPC中的状态,例如当前的电脑中:
ipcs -l 查看当前电脑的ipc通信容量限制
ipcs -m查看ipc的存储空间
ipcrm +ipcid删除该ipc对象
linux进程间通信:IPC对象 概念介绍(扫盲)_天行健,地势坤-CSDN博客_ipc撖寡情什么是IPC对象IPC:inter-process communication,进程间通信对象;包括如下组件:管道通信 :FIFO,PIPE,流式数据消息队列:message queue信号量:semaphore共享内存:share memory…IPC分类System V IPC当前系统调用版本主要是类unix和linux操作系统之中包含,它是在linux操作系统出现之前发...https://blog.csdn.net/Z_Stand/article/details/101356382
进程间通信(IPC)
有两个东西可以标识一个IPC结构:标识符(ID)和键(key)。
IPC对象的基本概念
-支持不同的进程通过ipc对象通信,IPC对象是存储在内核之中,且全局可见。每个IPC对象在内核之中有自己的数据结构,定义在各自头文件
如何引用IPC对象
类似于普通文件是通过文件名(文件描述符)进行读写操作,通过IPC对象:IPC key和 IPC标识符(ID)进行IPC对象的读写操作。
标识符(ID)(类似于文件描述符)
ID是IPC结构的内部名,用来确保使用同一个通讯通道(比如说这个通讯通道就是消息队列)。内部即在进程内部使用,这样的标识方法是不能支持进程间通信的。当一个IPC结构被创建,然后又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大正值,然后又回转到0. 就像是进程的文件描述符从0 当陆陆续续有文件打开 文件描述符会一直陆陆续续+1 直到达到一个最大值
- 类似于文件描述符,可以用一个IPC标示符来引用一个IPC对象
- IPC对象描述符类似于文件描述符,是一个整数,是IPC对象的内部名字
当多个进程引用同一个IPC对象时,因为多个进程是不知道这个IPC对象的标识符(ID)的,就比如,其他进程是不知道这个进程的文件的文件描述符的;此时就需要一个统一的外部名(key)类似于文件名,这样不同进程就可以访问这个IPC对象了
键值(key)(类似于文件名)
key就是IPC结构的外部名。当多个进程,针对同一个key调用get函数(msgget等),这些进程得到的ID其实是标识了同一个IPC结构。多个进程间就可以通过这个IPC结构通信。
- 类似于文件名,每个IPC对象与一个key相关联
IPC key,ipc对象的外部名,是一个独一无二的整数,用来确保ipc对象的唯一性
该整数类型为key_t,在sys/types.h中被定义为长整型
普通文件是通过open打开一个文件名,获得文件描述符;IPC队形是通过get可根据给定的key 去创建一个IPC对象,并返回IPC标识符
每个内核中的IPC结构(消息队列、信号量或者共享存储段)都用一个非负整数的标识符(ID——identifier)加以引用。例如,要向一个消息队列发送消息或者从一个消息队列取消息,只需要知道其队列标识符。与文件描述符不同,IPC标识符(ID)不是小的整数。当一个IPC结构被创建,然后又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大正值,然后又回转到0. 就像是进程的文件描述符从0 当陆陆续续有文件打开 文件描述符会一直陆陆续续+1 直到达到一个最大值
标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。为此,每个IPC对象都与一个键(key)相关联,将这个操作为该对象的外部名。
已知一个key,当希望利用这个key创建一个新的IPC时,可以使用get函数,并在flag中指定IPC_CREAT位,例如队列的情况,就是qid = msgget(key, IPC_CREAT)。apue15.6.1节提到:若随后另一个进程希望访问该已创建的队列,同样可以使用get函数,但此时不应再指定IPC_CREAT位。但在实际使用中,即使第二个进程在msgget函数中指定了IPC_CREAT位,函数也不会返回错误,且两个进程可以通过该队列(IPC)传递消息。
若在flag中同时指定了IPC_CREAT和IPC_EXCL位,则针对一个已存在的IPC结构再调用get函数时,则会返回错误。
为什么要有键值?
因为是进程间的通讯,所以必须有个公共的标识来确保使用同一个通讯通道(比如说这个通讯通道就是消息队列),然后再把这个标识与某个消息队列进行绑定,任何一个进程如果使用同一个标识,则内核就可以通过该标识找到对应的那个队列,这个标识就是键值。如果没有键值的话,进程A打开或者创建一个队列并返回这个队列的描述符,但其他进程不知道这个队列的描述符是多少,因此就不能通讯了。
在创建SystemV进程间通信(共享内存/消息队列/信号量)的函数中,都出现了key_t类型的key参数。该参数表示指定的键值,其他进程可以通过该键值访问该结构。不过有些情况下参数key的值是不能直接指定的(例如内核中已有该键值或键值非法),此时就需要使用ftok()函数生成一个符合要求的键值。
函数ftok()用于将路径名和当前进程标识符转换成符合SystemV的key值,该值是系统生成的,具有唯一性。因此在不能自定义指定参数key的时候,我们可以使用ftok()函数生成一个符合标准的key值。
键值 = 文件标示符(其实就是inode节点号) + 项目ID
这里所谓的文件标示符其实下面提到的 内核的物理编号,也就是索引节点 inode。
在一般的UNIX实现中,是将文档的索引节点号(其实就是inode节点号)取出,前面加上子序号得到key_t的返回值。 如指定文档的索引节点号(其实就是inode节点号)为65538,换算成16进制为0x010002,而您指定的ID值为38,换算成16进制 为 0x26,则最后的key_t返回值为0x26010002。
查询文档索引节点号(其实就是inode节点号)的方法是: ls -i我们可以用 ls - i查看输出每个文件的inode号
当删除重建文档后,索引节点号由操作系统根据当时文档系统的使用情况分配,因此和原来不同,所以得到的索引节点号也不同。
由于系统中的每一个文件都有一个唯一的inode节点,都有唯一的一个值,所以利用它来生成一个唯一的key值。注意它和文件的内容是无关的。ftok根据路径名,提取inode号,再根据proj_id,合成key。因此,该路径是必须存在的,但proj_id是可以根据自己的约定,随意设置。
比如:我们在开发一个项目的时候,有可能不同人需要在同一个路径下编写代码,防止大家不小心使用了相同的key,一般项目经理会分配给每个人不同的proj_t ,这个时候就可以用当前路径pathname和proj_t生成所需的key。
当用到进程间的通信时, 必须要注意到的是键值是怎么产生的,我们知道任何一个文件时通过文件名来访问的, 而内核在对应的给其一个值,也就是文件标示符(inode节点号)。
系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值 。通常情况下,该id值通过ftok函数得到 。 ftok原型如下:
key_t ftok( char * fname, int id )
此时这个 id也就是项目 ID,最后将两者结合生成了键值!这里所谓的文件标示符其实下面提到的 内核的物理编号,也就是索引节点 inode。
参数说明: fname就时您指定的文档名, id是子序号。
ftok()函数生成键值(这也是ipc都会用到的函数)
每一个共享存储段都有一个对应的键值(key)相关联(消息队列、信号量也同样需要)。
所需头文件:#include
#include
函数原型:key_t ftok(const char *pathname, int proj_id)
函数参数:
pathname 指定的目录
proj_id 指定的子序号,代表项目ID,自己取 取值范围为0~255
函数返回值:
成功:生成的key值
失败:-1
使用ftok()生成键值的方式如下:
if((key=ftok(".",'a'))==-1)//第一个参数表示当前目录,第二个参数可以随意指定
{
perror("cannot ftok");
exit(0);
}
若ftok()执行成功,则变量key内存储的就是符合标准的key值。
例如:key_t key = ftok( “/tmp”, 66);
共享内存,顾名思义,就是两个或多个进程都可以访问的同一块内存空间,一个进程对这块空间内容的修改可为其他参与通信的进程所看到的。
显然,为了达到这个目的,就需要做两件事:
一件是在内存划出一块区域来作为共享区;另一件是把这个区域映射到参与通信的各个进程空间。
通常在内存划出一个区域的方法是,在内存中打开一个文件,若通过系统调用mmap()把这个文件所占用的内存空间映射到参与通信的各个进程地址空间,则这些进程就都可以看到这个共享区域,进而实现进程间的通信。
顾名思义,共享内存就是允许两个不相关的进程访问同一个内存。共享内存是进程间最为高效的一种通信方式,进程间可以直接读写内存而无需使用其他的手段。
在Linux系统内,内核专门预留了一块内存区域(这块内存区域是物理地址空间的内存区域)用于进程间交换信息。
这段内存区可以由任何需要访问的进程将其映射到自己的私有地址空间(既进程的虚拟地址空间),进程可以直接读写该区域而无需数据拷贝,从而大大提高了效率。所有进程都有权访问共享内存地址,就好像使用malloc()函数分配的内存一样。
但是,由于多个进程共享一段内存,因此一个进程改变共享内存内的数据可能会影响其他进程。既内存映射区的读写会改变共享内存!!!!!!!!!!!!!
内存映射区的读写会改变共享内存!!!!!!!!!!!!!
由于共享内存本身并没有提供同步与互斥机制,因此需要依靠某种同步与互斥机制来保证数据的独立性,常见的手段是使用信号量来实现同步与互斥机制。
注:这里的进程12的地址空间是虚拟地址空间!分配的共享内存是物理地址空间的内存
看懂这张图!
内核在 物理地址空间 预留了一块空间用来做 内核共享内存区,我们使用shmget申请的共享内存其实就是在申请这个 内核共享内存区 里的一块内存
我们下面把 在物理地址空间的内核共享内存区申请得到的共享内存 叫共享内存
最简单的共享内存的使用流程:
①ftok函数生成键值
②shmget函数创建共享内存空间(共享内存空间是物理地址空间中的内存)
③shmat函数获取第一个可用共享内存空间的地址
④shmdt函数进行分离(对共享存储段操作结束时的步骤,并不是从系统中删除共享内存和结构)
⑤shmctl函数进行删除共享存储空间
我们可以使用ipcs命令查看进程间通信的状态。
使用共享内存编程通常需要调用shmget()、shmat()、shmdt()和shmctl()几个函数。(函数名中的shm其实就是share memory 后面的字母就是函数的具体功能了)
1)ftok()函数生成键值(这也是ipc都会用到的函数)
每一个共享存储段都有一个对应的键值(key)相关联(消息队列、信号量也同样需要)。
所需头文件:#include
#include
函数原型:key_t ftok(const char *pathname, int proj_id)
函数参数:
pathname 指定的目录
proj_id 指定的子序号,代表项目ID,自己取 取值范围为0~255
函数返回值:
成功:生成的key值
失败:-1
使用ftok()生成键值的方式如下:
if((key=ftok(".",'a'))==-1)//第一个参数表示当前目录,第二个参数可以随意指定
{
perror("cannot ftok");
exit(0);
}
若ftok()执行成功,则变量key内存储的就是符合标准的key值。
例如:key_t key = ftok( “/tmp”, 66);
2)函数shmget()用于创建共享内存(既在内核共享内存区 里申请一块内存)
函数shmget()
所需头文件:#include
#include
函数原型:int shmget(key_t key, size_t size, int shmflg)
函数参数:
key : 共享内存的键值,其他进程通过该值访问该共享内存,其中有个特殊值IPC_PRIVATE,表示创建当前进程的私有共享内存
size : 申请的共享内存段的大小
shmflg : 同open()函数的第三个参数,为共享内存设定权限,通常使用八进制表示。若共享 内存不存在想创建一块全新的共享内存时,需要按位或IPC_CREAT
函数返回值:
成功:共享内存段的标识符 shmid (非负整数)(既ID)
失败:-1
例如:int id = shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);创建一个大小为4096个字节的权限为0666(所有用户可读可写,具体查询linux权限相关内容)的共享存储空间,并返回一个整形共享存储标识符 shmid (ID),如果key值已经存在有共享存储空间了,则出错返回-1。
3)函数shmat()用于将共享内存映射到进程中(既将在内核中申请的共享内存映射到进程的虚拟 地址空间中)
函数shmat()
所需头文件:#include
#include
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg)
函数参数:
shmid : 要映射的共享内存区标识符(即shmget()函数的返回值)
shmaddr : 将共享内存映射到 进程指定内存地址addr,既将共享内存映射到进程的虚拟地址空 间的addr地址 如果为NULL则会自动分配到一块合适的虚拟内存空间地址 ,推荐使用NULL 既由内核为我们分配共享内存映射的虚拟地址空间的地址
shmflg : SHM_RDONLY表示共享内存为只读,0(默认值)表示共享内存可读可写
函数返回值:
成功:返回shmaddr(既共享内存映射到虚拟地址空间的首地址)如果shmat()函数的 shmaddr参数是NULL,那么返回的就是内核自动分配的虚拟地址空间的某一个 地址
失败:-1(打印出指针的值为全F)
函数shmdt()用于将进程与共享内存分离。注意“分离”并不是删除共享内存,而是表示该进程不再使用该共享内存。
例如:char *addr = shmat(id, NULL, 0);就会返回第一个可用的共享内存地址的指针的值给addr
4)函数shmdt()用于将进程与共享内存分离。注意“分离”并不是删除共享内存,而是表示该进程 不再使用该共享内存。
函数shmdt()
所需头文件:#include
#include
函数原型:int shmdt(const void *shmaddr)
函数参数:
shmaddr : 函数shmat()返回的地址 也既共享内存在虚拟地址空间映射的地址
函数返回值:
成功:0
失败:-1
例如:int ret = shmdt(addr);
5)函数shmctl()用于控制共享内存,包括获得属性、删除共享内存等
函数shmctl()
所需头文件:#include
#include
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf)
函数参数:
shmid 共享内存区标识符(ID)(即shmget()函数的返回值 既ID)
cmd 需要对共享内存采取的操作。可取值有很多,常用的有:
IPC_STAT : 将shmid_ds结构体中的数据设置为共享内存的当前关联值,即用shmid覆 盖 shmid_ds内的值
IPC_SET : 如果进程权限允许,将共享内存的当前关联值设置为shmid_ds中给出的值
IPC_RMID : 删除共享内存
buf 该参数是一个shmid_ds类型的结构体指针,使用时必须使用地址传递的方式。结构体成员很多,常用的有:
struct shmid_ds
{
uid_t shm_perm.uid; /* Effective UID of owner */
uid_t shm_perm.gid; /* Effective GID of owner */
mode_t shm_perm.mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */
……
};
函数返回值:
成功:
IPC_INFO或SHM_INFO操作:内核内部记录的有关共享内存段的使用条目
SHM_STAT操作:shmid中指定的共享内存标识符
其他操作:0
失败:-1
ps:在Linux下,比如你申请24字节大小的共享存储空间,系统还是会默认给你分配一页的大小,但你还是只能使用这一页上24字节的空间。使用getconf PAGE_SIZE 命令就能显示出一页的大小
使用ipcs -m可以查看当前系统所有的共享内存空间信息
如果你的程序创建了一个共享内存段,但没有销毁,可以使用命令ipcrm -m shmid命令删除共享内存段,不然程序再运行有可能出错。
在程序运行期间,多次调用"system("ipcs -m")"命令报告共享内存情况,注意每次报告的情况的异同。
示例:建立两个进程间的共享内存通信。首先创建两个文件分别执行两个不同的程序,文件shmread.c负责读取共享内存数据,文件shmwrite.c负责写入共享内存数据。为了方便操作,两个文件使用相同的结构体保存数据,结构体定义在头文件shmdata.h中。(这里没有使用信号量(因为还没学到)而是使用一个int来实现互斥)
在shmwrite.c内写入数据,让shmread.c读取,当写入"end"时程序结束。
由于共享内存通信需要互斥,我们在头文件内使用written变量控制共享内存的读写,非0值表示可读,0表示可写。
//文件shmdata.h
#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
#include
#include
#include
#include
#include
#include
#include
#define TEXT_SZ 2048
struct shared_use_st
{
int written;//作为一个标志,非0:表示可读,0表示可写
char text[TEXT_SZ];//记录写入和读取的文本
};
#endif
//文件shmread.c
#include "shmdata.h"
int main()
{
int running = 1;//程序是否继续运行的标志
void *shm = NULL;//分配的共享内存的原始首地址
struct shared_use_st *shared;
int shmid;//共享内存标识符
//创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
perror("shmget failed");
exit(0);
}
//将共享内存连接到当前进程的地址空间
shm = shmat(shmid, NULL, 0);
if(shm == (void*)-1)
{
perror("shmat failed");
exit(0);
}
printf("\nMemory attached at %p\n", shm);
//设置共享内存
shared = (struct shared_use_st*)shm;
shared->written = 0;
while(running)//读取共享内存中的数据
{
//没有进程向共享内存定数据有数据可读取
if(shared->written != 0)
{
printf("You wrote: %s\n", shared->text);
//读取完数据,设置written使共享内存段可写
shared->written = 0;
//输入了end,退出程序
if(strncmp(shared->text, "end", 3) == 0)
running = 0;
}
else//有其他进程在写数据,不能读取数据
sleep(1);
}
//把共享内存从当前进程中分离
if(shmdt(shm) == -1)
{
perror("shmdt failed");
exit(0);
}
//删除共享内存
if(shmctl(shmid, IPC_RMID, 0) == -1)
{
perror("shmctl(IPC_RMID) failed");
exit(0);
}
return 0;
}
//文件shmwrite.c
#include "shmdata.h"
int main()
{
int running = 1;
void *shm = NULL;
struct shared_use_st *shared = NULL;
char buffer[TEXT_SZ + 1];//用于保存输入的文本
int shmid;
//创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
perror("shmget failed");
exit(0);
}
//将共享内存连接到当前进程的地址空间
shm = shmat(shmid, NULL, 0);
if(shm == (void*)-1)
{
perror("shmat failed");
exit(0);
}
printf("Memory attached at %p\n", shm);
//设置共享内存
shared = (struct shared_use_st*)shm;
while(running)//向共享内存中写数据
{
//数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本
while(shared->written == 1)
{
sleep(1);
printf("Waiting...\n");
}
//向共享内存中写入数据
printf("Enter some text: ");
fgets(buffer,TEXT_SZ,stdin);
strcpy(shared->text, buffer);
//写完数据,设置written使共享内存段可读
shared->written = 1;
//输入了end,退出循环(程序)
if(strncmp(buffer, "end", 3) == 0)
running = 0;
}
//把共享内存从当前进程中分离
if(shmdt(shm) == -1)
{
perror("shmdt failed");
exit(0);
}
sleep(2);
return 0;
}
其实,示例2的程序实际上是有风险的,因为我们并未对这块共享内存实施任何的互斥操作(虽然使用了written变量但功能不足)。使用信号量实现共享内存的互斥操作见本文最后的程序。
消息队列其实就是一个先入先出的链表队列,其中msqid_ds就是链表头 里面包含着链表的首节点和尾节点的以及其他信息 就相当于一个管理模块 我们以后在写链表的时候可以模仿这种写法 既:通过一个链表头来管理链表
顾名思义,消息队列就是一些消息构成的列表,准确来说,消息队列是在消息的传输过程中保存消息的容器。如果从功效方面说,消息队列可以看做是消息构成的链表。用户可以在消息队列中添加和读取消息等。消息队列具有一定的管道的特点,但是消息队列可以实现消息的随机查询,比管道具有更加明显的优势。
消息队列存在于内核中,使用消息队列的“队列ID”来唯一标识。
消息队列是消息的链表,存放在内核中并由消息队列标识符表示。
消息队列提供了一个从一个进程向另一个进程发送数据块的方法,每个数据块都可以被认为是有一个类型,接受者接受的数据块可以有不同的类型。
但是同管道类似,它有一个不足就是每个消息的最大长度是有上限的(MSGMAX),每个消息队列的总的字节数(MSGMNB),系统上消息队列的总数上限(MSGMNI)。可以用cat /proc/sys/kernel/msgmax查看具体的数据。
内核为每个IPC对象维护了一个数据结构struct ipc_perm,用于标识消息队列,让进程知道当前操作的是哪个消息队列。每一个msqid_ds表示一个消息队列(),并通过msqid_ds.msg_first、msg_last维护一个先进先出的msg链表队列,当发送一个消息到该消息队列时,把发送的消息构造成一个msg的结构对象,并添加到msqid_ds.msg_first、msg_last维护的链表队列。在内核中的表示如下:
这是通过链表管理消息队列
通过msqid_ds这个数据结构既 链表头 保存着链表的 首节点和尾节点 以及其他信息
以后写链表也可以这样写
1)使用消息队列有以下的优点:
1.解耦:
使用消息队列通信的每个成员不会受到其他成员的干扰,成员与成员间只使用消息队列互相通信,大大降低了成员间的耦合度。
2.提速:
使用消息队列后,消息的发送者无需监视消息消息的接收状态,这样就有更多的时间处理其他事务。
3.广播:
一个成员向消息队列提供了消息后,其他成员都可以看到,降低了消息广播的成本。
4.削峰:
若遇到瞬时消息量暴涨,消息队列可以起到一定的缓冲作用,使用消息队列的成员不会受到大量消息的干扰。
2)但是使用消息队列也是存在一定的弊端:
1.引入了额外的空间:
毫无疑问,消息队列是需要额外存储空间的。
2.无法实时通信:
在一个成员送出消息后,它无法保证也无法获知目标成员什么时候会接收到消息,也无法知道目标成员是否已经处理该消息。
消息队列不仅可以用于同一个系统的进程间通信,也可以应用于网络通信。例如访问网络数据库、大型服务器(主要用于削峰)、大型网站、在线聊天室、P2P数据传输等领域都有消息队列的应用。
特点:
- 生命周期随内核,消息队列会一直存在,需要我们显示的调用接口删除或使用命令删除
- 消息队列可以双向通信
- 克服了管道只能承载无格式字节流的缺点
2、消息队列编程
首先使用ftok获取键值
使用消息队列通常需要创建/打开消息队列、添加消息、读取消息、控制消息队列四种操作。
1)函数msgget()用于创建/打开消息队列
函数msgget()
所需头文件:#include
#include
#include
函数原型:int msgget(key_t key,int msgflg)
函数参数:
key 消息队列的键值,其他进程通过该值访问该消息队列,其中有个特殊值 IPC_PRIVATE,表示创建当前进程的私有消息队列
msgflg 同open()函数的第三个参数,为消息队列设定权限,通常使用八进制表示。若消息队列不存在想创建一个全新的消息队列时,需要按位或IPC_CREAT
函数返回值:
成功:消息队列ID(既消息队列的标识符)
失败:-1
2) 函数msgsnd()用于向消息队列中添加一个消息,该函数可以将消息添加到消息队列的末尾
函数msgsnd()
所需头文件:#include
#include
#include
函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
函数参数:
msqid : 消息队列标识符(即msgget()函数的返回值)
msgp : 指向消息队列的结构体指针(既指针指向准备发送的消息),必须使用地址传递的方式传参。该结构体的类型如 下:(这个结构体相当于链表的节点 保存着内容)
struct msgbuf
{
long mtype;//消息类型,必须大于0 相当于是给消息编号
char mtext[n];//消息正文
};
其中:Long mtype 是消息类型 必须大于0相当于是给消息编号 这样方便我们取消息
char mtext[n] 是消息正文 就是这个消息的字节数
消息正文就是这个消息的数据内容
msgsz : 消息正文的字节数,必须与第二个参数的消息正文数据长度一致
既为消息结构体mymesg里的字符数组mtext大小,sizeof(mtext)
msgflg:
IPC_NOWAIT 若消息无法立即发送则立即返回
0 若消息无法立即发送(这是消息队列满的情况)则阻塞等待消息发送成功
函数返回值:
成功:0
失败:-1
3)函数msgrcv()用于从消息队列中取出指定消息类型的消息(相当于取出指定编号(编号必须大于1)的消息),取出后 消息队列中就没有这个消息了,它与msgsnd()经常一起使用。与管道不同的是,消息队列可以指定取走任意消息
函数msgrcv()
所需头文件:#include
#include
#include
函数原型:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
函数参数:
msqid : 消息队列标识符(即msgget()函数的返回值)
msgp : 指向消息队列的结构体指针,必须使用地址传递的方式传参。(相当于通过指定msgid和消息类型(既相当于消息的编号)取出指定消息 赋值给 这个结构体指针指向的消息结构体 这样 这个结构体指针指向的消息结构体就等于我们要取出的消息了)该结构体的类型如下:
struct msgbuf
{
long mtype;//消息类型,必须大于0
char mtext[n];//消息正文
};
msgsz: 消息正文的字节数,必须与第二个参数的消息正文数据长度一致
msgtyp :
0 接收消息队列中第一个消息
大于0 接收消息队列中第一个值为msgtyp的消息
小于0 接收消息队列中具有小于或等于msgtyp绝对值的最小mtype值的第一条消息
msgflg:
MSG_NOERROR 若返回的消息比msgsz字节多,则消息会截断到msgsz字节,且不通知消息发送进程
IPC_NOWAIT 若消息队列中无对应类型的消息接收则立即返回
0 阻塞等待直至接收到一条相应类型的消息为止
函数返回值:
成功:0
失败:-1
4)函数msgctl()用于消息队列控制,包括获得属性、删除消息队列等
函数msgctl()
所需头文件:#include
#include
#include
函数原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf)
函数参数:
msqid : 消息队列标识符(即msgget()函数的返回值)
cmd : 需要对消息队列采取的操作。可取值有很多,常用的有:
IPC_STAT 读取消息队列的数据结构msqid_ds并将其存储在buf指定的地址中
IPC_SET 设置消息队列中的数据结构msqid_ds中的pic_perm元素的值。这个值来自buf参数
IPC_RMID 从内核中删除消息队列 如果没有使用这个删除消息队列或者在命令行删除消息队列(ip) 则这个消息队列一直存在 我们可以随时写消息或者取消息
buf : 该参数是一个msqid_ds类型的结构体指针,使用时必须使用地址传递的方式。结构体成员很多,常用的有
struct msqid_ds
{
uid_t msg_perm.uid; /* Effective UID of owner */
gid_t msg_perm.gid; /* Effective GID of owner */
……
};
函数返回值:
成功:0
失败:-1
此外,我们还需要学习两个重要的命令
前面我们说过,消息队列需要手动删除IPC资源
ipcs:显示IPC资源
ipcrm:ipcrm + IPCid 手动删除指定的IPC资源
示例:建立两个进程间的共享内存通信。文件msgsend.c负责读取用户输入信息并发送信息,若输入"quit"则程序停止;文件msgreceive.c负责接收消息队列的信息,若接收到"quit"则表示程序结束,删除消息队列。两个文件共用一个同文件msgdata.h。
//文件msgdata.h
#ifndef _MSGDATA_H_INCLUDE_
#define _MSGDATA_H_INCLUDE_
#include
#include
#include
#include
#include
#include
#include
#define BUFFERSIZE 512
struct message//需要发送/接收的消息的数据类型
{
long msg_type;
char msg_text[BUFFERSIZE];
};
#endif
//文件msgsend.c
#include"msgdata.h"
int main()
{
int qid;
key_t key = 1234;
struct message msg;
if((qid = msgget(key,IPC_CREAT|0664))==-1)//打开/创建消息队列
{
perror("cannot msgget");
exit(0);
}
printf("Open MessageQueue %d\n",qid);
while(1)
{
printf("Please input string, input 'quit' to stop:");
fgets(msg.msg_text,BUFFERSIZE,stdin);
msg.msg_type = getpid();//将自己的进程ID作为type发送给msgreceive
if((msgsnd(qid,&msg,strlen(msg.msg_text),0))==-1)//发送数据
{
perror("cannot msgsnd");
exit(0);
}
if(strncmp(msg.msg_text,"quit",4)==0)
{
break;
}
}
return 0;
}
//文件msgreceive.c
#include"msgdata.h"
int main()
{
int qid;
key_t key = 1234;
struct message msg;
if((qid = msgget(key,IPC_CREAT|0664))==-1)//打开/创建消息队列
{
perror("cannot msgget");
exit(0);
}
printf("Open MessageQueue %d\n",qid);
while(1)
{
bzero(msg.msg_text,BUFFERSIZE);
if(msgrcv(qid,(void*)&msg,BUFFERSIZE,0,0)==-1)//接收数据,第4个参数0表示接收第一个消息
{
perror("cannot msgrcv");
exit(0);
}
printf("receive message from %ld:%s\n",msg.msg_type,msg.msg_text);
if(strncmp(msg.msg_text,"quit",4)==0)
{
break;
}
}
if((msgctl(qid,IPC_RMID,NULL))<0)//如果接收到quit则删除消息队列
{
perror("cannot msgctl");
exit(0);
}
printf("Remove MessageQueue %d success\n",qid);
return 0;
}
Linux_信号与信号量_逝去的浪花-CSDN博客_信号量信号:信号机制是类UNIX系统中的一种重要的进程间通信手段之一。我们经常使用信号来向一个进程发送一个简短的消息。例如:假设我们启动一个进程通过socket读取远程主机发送过来的网络数据包,此时由于网络因素当前主机还没有收到相应的数据,当前进程被设置为可中断等待状态(TASK_INTERRUPTIBLE),此时我们已经失去耐心,想提前结束这个进程,于是可以通过kill命令想这个进程发送KILL信https://blog.csdn.net/sty23122555/article/details/51470949
信号和信号量:
Linux信号(signal) 机制
signal,又简称为信号(软中断信号 )用来通知进程发生了异步事件。
原理:
一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。
分类:从两个不同的分类角度对信号进行:
可靠性方面:可靠信号与不可靠信号;
与时间的关系上:实时信号与非实时信号。
Linux信号量(semaphore)机制
Linux内核的信号量用来操作系统进程间同步访问共享资源。原理:信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。
一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。
当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。
最全面的linux信号量解析_qinxiongxu的专栏-CSDN博客_linux 信号量2012-06-28 15:08 285人阅读 评论(0)收藏编辑删除信号量一.什么是信号量信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。二.信号量的分类在学习信号量之前,我们必须先知道https://blog.csdn.net/qinxiongxu/article/details/7830537
一.什么是信号量
信号量的使用主要是用来保护共享资源。
信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为0,说明 它被占用,测试的线程要进入睡眠队列中,等待被唤醒。
二.信号量的分类
在学习信号量之前,我们必须先知道—— Linux提供两种信号量 :
(1) 内核信号量,由内核控制路径使用
(2) 用户态进程使用的信号量 ,这种信号量又分为POSIX信号量和SYSTEM V信号量。
POSIX信号量 又分为有名信号量和无名信号量:
(1)有名信号量(常用于进程通信,当然线程也可以),其值保存在文件中,所以它既可以用于线程,也可以用于相关进程间,甚至是不相关进程。有名信号量以文件的形式存在,即时是不同进程间的线程也可以访问该信号量,因此可以用于不同进程间的多线程间的互斥与同步。(2)无名信号量(常用于线程同步),其值保存在内存中。无名信号量常用于多线程间的同步,同时也用于相关进程间的同步。也就是说,无名信号量必须是多个进程(线程)的共享变量,无名信号量要保护的变量也必须是多个进程(线程)的共享变量,这两个条件是缺一不可的。无名信号量存在于进程内的虚拟空间中,对于其他进程是不可见的,因此无名信号量用于一个进程体内各线程间的互斥和同步
倘若对信号量没有以上的全面认识的话,你就会很快发现自己在信号量的森林里迷失了方向。
POSIX 信号量与SYSTEM V信号量的比较:
1. 对POSIX来说,信号量是个非负整数。常用于线程间同步。而SYSTEM V信号量则是一个或多个信号量的集合,它对应的是一个信号量结构体,这个结构体是为SYSTEM V IPC服务的,信号量只不过是它的一部分。常用于进程间同步。
2.POSIX信号量的引用头文件是“”,而SYSTEM V信号量的引用头文件是“ ”。
3.从使用的角度,System V信号量是复杂的,而Posix信号量是简单。比如,POSIX信号量的创建和初始化或PV操作就很非常方便。
在多进程/多线程系统内,多个进程/线程会同时运行,多个进程/线程可能会为了完成同一个任务共同协作,这时进程/线程间就出现了同步关系。同样,在多进程/多线程系统内,不同任务之间也可能会争夺有限的系统资源,多个进程/线程可能进入争夺状态,这时进程/线程间就出现了互斥关系。
任务之间的同步与互斥存在的根源主要是临界资源。临界资源是指在同一时刻只允许有限个(通常是一个)任务可以访问资源。例如各种硬件资源(CPU、内存空间、存储设备、打印机等外部设备等)和软件资源(共享内存段、共享变量等)。临界资源存放的区域称为临界区。
信号量是用来解决进程/线程间同步与互斥问题的一种通信机制,包括一个信号量变量,以及对该信号量进行的原子操作(PV操作)。
原子操作(PV操作)的具体定义如下:
P操作(通过):对信号量减1,若结果大于等于0,则进程继续,否则执行P操作的进程被阻塞等待释放
V操作(释放):对信号量加1,若结果小于等于0,则唤醒队列中一个因为P操作而阻塞的进程,否则不必唤醒进程
最简单的信号量只有0和1两个值,称为二值信号量;如果一个信号量有多个值(0~n)则称为计数信号量,计数信号量表示可用的资源数。
Linux多进程访问共享资源时,需要按下列步骤进行操作:
(1)检测控制这个资源的信号量的值。
(2)如果信号量是正数,就可以使用这个资源。进程将信号量的值减一,表示当前进程占用了一份资源。
(3)如果信号量是0,那么进程进入睡眠状态,直到信号量的值重新大于0时被唤醒,转入第一步操作。
上述过程也被称为PV操作。为了正确实现信号量机制,检测和增减信号量的值都应该是原子操作,因此信号量一般是在内核中实现的。
在信号量的实际应用中,是不能单独定义一个信号量的,只能定义一个信号量集,其中包含一组信号量。同一信号量集中的信号量使用同一个引用ID,这样的设置是为了多个资源或同步操作的需要。每个信号量集都有一个与之对应的结构,其中记录了信号量集的各种信息,该结构的定义如下:
#include
struct semid_ds
{
struct ipc_perm sem_perm; //指向与信号量集相对应的ipc_perm结构的指针
struct sem *sem_base; //指向这个集合中第一个信号量的指针
ushort sem_nsems; //集合中信号量的数量
time_t sem_otime; //最近一次调用semop函数的时间
time_t sem_ctime; //最近一次改变的时间
};
semid_ds结构中的sem结构记录了单一信号量的一些信息:
struct sem
{
ushort semval; //信号量的值
pid_t sempid; //最近一次执行操作的进程的进程号
ushort semncnt; //等待信号值增长,即等待可利用资源出现的进程数
ushort semzcnt; //等待信号值减少,即等待全部资源可被独占的进程数
};
我们在编程的时候
一般在semctl函数里面有一个联合体union semun里面包含了一个指向semid_ds结构体的指针和信号量的值以及一些其他信息 相当于是封装了一下semid_ds方便使用
其实信号量就像是一个开关 只不过这个开关 能够被多少个人使用 是由我们决定的
在Linux系统中,使用信号量通常需要创建信号量、初始化信号量、信号量PV操作以及信号量删除四种操作。
函数semget()用于创建一个(或多个)信号量
函数semget()
所需头文件:#include
#include
#include
函数原型:int semget(key_t key, int nsems, int semflg)
函数参数:
key 信号量的键值,其他进程通过该值访问该信号量,其中有个特殊值IPC_PRIVATE,表示创建当前进程的私有信号量
nsems 需要创建的信号量数目,通常为1。若创建多个信号量则称为信号量集
semflg 同open()函数的第三个参数,为信号量设定权限,通常使用八进制表示。若按位或IPC_CREAT表示创建一个全新的信号量,即使该信号量已经存在也不会报错;若按位或IPC_EXCL,既IPC_CREAT|IPC_EXCL,则信号量存在时该函数会返回报错。
函数返回值:
成功:信号量的标识符(非负整数)
失败:-1
函数semctl()用于对信号量进行相应的控制,包括获取信息、设置属性、删除信号量(集)等操作
函数semctl()
所需头文件:#include
#include
#include
函数原型:int semctl(int semid, int semnum, int cmd, union semun arg)
函数参数:
semid : 信号量标识符(即semget()函数的返回值)
semnum 信号量编号,通常存在多个信号量时才会使用。通常取值为0,即第一个信号量。
cmd : 需要对信号量采取的操作。可取值有很多,常用的有:
cmd参数说明
参数值 说明
SETVAL 设置单个信号量的值
GETALL 返回信号量集中所有信号量的值
SETALL 设置信号量集中所有信号量的值
IPC_STAT 放置与信号量集相连的semid_ds结构当前值于arg.buf指定的缓冲区
IPC_SET 用arg.buf指定结构值替代与信号量集合相连的semid_ds结构的值
GETVAL 返回单个信号量的值
GETPID 返回最后一个操作该信号量集的进程ID
GETNCNT 返回semncnt的值
GETZCNT 返回semzcnt的值IPC_RMID 删除指定的信号
sem
arg : 是一个union semun结构的共用体(每次创建或者使用信号量集都要使用到这个结构体 使用这个结构体是因为 配合cmd参数的使用)其中 int val 是信号量(信号量集)的值,具体类型如下:
union semun
{
int val; /* 设置的信号量值*/
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET指向semid_ds的结构体
指针 */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
注意:某些系统内未给出union semun的定义,需要程序员自己定义该共用体。
其中buf参数是一个semid_ds类型的结构体指针,使用时必须使用地址传递的方式。结构体成员很多,常用的有:
struct semid_ds
{
uid_t sem_perm.uid; /* Effective UID of owner */
gid_t sem_perm.gid; /* Effective GID of owner */
……
};
函数返回值:
成功:
IPC_STAT、SETVAL或IPC_RMID操作:0
GETVAL操作:返回当前信号量的值
失败:-1
函数semop()用于对信号量进行PV操作
函数semop()
所需头文件:#include
#include
#include
函数原型:int semop(int semid, struct sembuf *sops, size_t nsops)
函数参数:
semid 信号量标识符(即semget()函数的返回值)
sops 是一个sembuf类型的结构体指针,使用时必须使用地址传递的方式。结构体类型如下:
struct sembuf
{
unsigned short sem_num;//信号量编号,若是单个信号量则取值0
short sem_op;//取值-1为P操作,取值1为V操作
short sem_flg;//通常取值SEM_UNDO,表示进程结束后系统自动释放该进程中未释放的信号量
};
nsops 需要操作的信号量数目,通常取值1(一个操作)
函数返回值:
成功:信号量的标识符
失败:-1
示例1:演示3个信号量处理函数。在该示例中,为了使用方便,我们将初始化信号量操作、删除信号量操作、PV操作分别封装成子函数。
#include
#include
#include
#include
#include
#include
union semun //若该共用体未定义,则需要手动定义
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_union.val = init_value;
if(semctl(sem_id,0,SETVAL,sem_union)==-1)
{
perror("Initialize semaphore");
return -1;
}
return 0;
}
int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id,0,IPC_RMID,sem_union)==-1)
{
perror("Delete semaphore");
return -1;
}
return 0;
}
int sem_p_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0; //表示单个信号量
sem_b.sem_op=-1; //表示P操作
sem_b.sem_flg=SEM_UNDO; //表示系统会自动回收系统内残余的信号量
if(semop(sem_id,&sem_b,1)==-1)
{
perror("P opreate");
return -1;
}
return 0;
}
int sem_v_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0; //表示单个信号量
sem_b.sem_op=1; //表示V操作
sem_b.sem_flg=SEM_UNDO; //表示系统会自动回收系统内残余的信号量
if(semop(sem_id,&sem_b,1)==-1)
{
perror("V opreate");
return -1;
}
return 0;
}
int main()
{
pid_t pid;
int sem_id;
sem_id = semget(1234,1,0664|IPC_CREAT); //创建1个信号量
init_sem(sem_id,0); //将信号量初值设定为0
pid = fork();
if(pid<0)
{
perror("cannot fork");
exit(0);
}
else if(pid==0) //子进程
{
printf("Child process %d will ENTER to wakeup Parent……\n",getpid());
getchar(); //输入回车,释放父进程
printf("Child %d will V-operate\n",getpid());
sem_v_operate(sem_id);
}
else //父进程
{
printf("Parent process %d will P-operate\n",getpid());
sem_p_operate(sem_id); //父进程被阻塞等待释放
printf("Parent process %d go on\n",getpid());
del_sem(sem_id);
}
return 0;
}
示例2:使用信号量完成公交车司机——公交车售票员模型
司机的流程如下:
P(S1)
启动车辆
正常行驶
到站停车
V(S2)
售票员的流程如下:
关车门
V(S1)
售票
P(S2)
开车门
上下乘客
#include
#include
#include
#include
#include
#include
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_union.val = init_value;
if(semctl(sem_id,0,SETVAL,sem_union)==-1)
{
perror("Initialize semaphore");
return -1;
}
return 0;
}
int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id,0,IPC_RMID,sem_union)==-1)
{
perror("Delete semaphore");
return -1;
}
return 0;
}
int sem_p_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=-1;
sem_b.sem_flg=SEM_UNDO;
if(semop(sem_id,&sem_b,1)==-1)
{
perror("P opreate");
return -1;
}
return 0;
}
int sem_v_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=1;
sem_b.sem_flg=SEM_UNDO;
if(semop(sem_id,&sem_b,1)==-1)
{
perror("V opreate");
return -1;
}
return 0;
}
int main()
{
pid_t pid;
int sem_ids1,sem_ids2;
sem_ids1 = semget(1234,1,0664|IPC_CREAT);
sem_ids2 = semget(5678,1,0664|IPC_CREAT);
init_sem(sem_ids1,0);
init_sem(sem_ids2,0);
pid = fork();
if(pid<0)
{
perror("cannot fork");
exit(0);
}
else if(pid==0)//子进程表示司机进程
{
while(1)
{
sem_p_operate(sem_ids1);
printf("司机发现关闭车门\n");
printf("司机启动车辆\n");
sleep(1);
printf("司机驾驶车辆\n");
sleep(10);
printf("车辆到站,司机停车\n");
sem_v_operate(sem_ids2);
}
}
else//父进程表示售票员进程
{
while(1)
{
printf("售票员关闭车门\n");
sleep(1);
sem_v_operate(sem_ids1);
printf("售票员开始售票\n");
sleep(5);
printf("售票员售票完毕\n");
sem_p_operate(sem_ids2);
printf("车辆到站,售票员开启车门\n");
printf("乘客上下车\n");
sleep(1);
}
del_sem(sem_ids1);
del_sem(sem_ids2);
}
return 0;
}
示例3:使用信号量实现共享内存的互斥操作该程序由4个文件组成
shmdata.h其余三个.c文件需要的头文件
semPV.c信号量的操作
shmwrite.c向共享内存内写数据操作
shmread.c读取共享内存的数据操作
编译时shmwrite.c和shmread.c都需要与semPV.c一起编译,运行时,在两个不同的终端分别运行两个程序,先运行写程序,再运行读程序
//文件shmdata.h
#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
#include
#include
#include
#include
#include
#include
#include
#include
#define TEXT_SZ 2048
struct shared_use_st
{
pid_t pid;//写入数据的进程ID
char buffer[TEXT_SZ];//记录写入和读取的文本
};
#endif
//文件semPV.c
#include "shmdata.h"
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_union.val = init_value;
if(semctl(sem_id,0,SETVAL,sem_union)==-1)
{
perror("Initialize semaphore");
return -1;
}
return 0;
}
int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id,0,IPC_RMID,sem_union)==-1)
{
perror("Delete semaphore");
return -1;
}
return 0;
}
int sem_p_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=-1;
sem_b.sem_flg=SEM_UNDO;
if(semop(sem_id,&sem_b,1)==-1)
{
perror("P opreate");
return -1;
}
return 0;
}
int sem_v_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=1;
sem_b.sem_flg=SEM_UNDO;
if(semop(sem_id,&sem_b,1)==-1)
{
perror("V opreate");
return -1;
}
return 0;
}
//文件shmwrite.c
#include "shmdata.h"
int main()
{
void *shared_memory = NULL;
struct shared_use_st *shm_buff_insert = NULL;
char buffer[BUFSIZ + 1];//用于保存输入的文本
int shmid,semid;
semid = semget(ftok(".",'a'), 1, 0666|IPC_CREAT);//创建信号量
init_sem(semid,1);//设置信号量的初值为1
//创建共享内存
shmid = shmget(ftok(".",'b'), sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
perror("shmget failed");
exit(0);
}
//将共享内存连接到当前进程的地址空间
shared_memory = shmat(shmid, (void*)0, 0);
if(shared_memory == (void*)-1)
{
perror("shmat failed");
exit(0);
}
printf("Write Process ID is %d\n",getpid());
printf("Memory attached at %p\n", shared_memory);
//设置共享内存
shm_buff_insert = (struct shared_use_st*)shared_memory;
do
{
sem_p_operate(semid);//P操作
printf("Input string to shm(enter 'quit' to exit):");
if(fgets(shm_buff_insert->buffer,TEXT_SZ,stdin)==NULL)//读取用户输入
{
perror("cannot write shm");
sem_v_operate(semid);
break;
}
shm_buff_insert->pid = getpid();//将自己的进程ID写入共享内存
sem_v_operate(semid);//V操作
}while(strncmp(shm_buff_insert->buffer,"quit",4)!=0);
//把共享内存从当前进程中分离
if(shmdt(shared_memory) == -1)
{
perror("shmdt failed");
exit(0);
}
del_sem(semid);//删除信号量
return 0;
}
//文件shmread.c
#include "shmdata.h"
int main()
{
void *shared_memory = NULL;
struct shared_use_st *shm_buff_insert = NULL;
char buffer[BUFSIZ + 1];//用于保存输入的文本
int shmid,semid;
if((semid = semget(ftok(".",'a'), 1, 0666|IPC_CREAT))<0)//获取信号量
{
perror("cannot semget");
exit(0);
}
//创建共享内存
shmid = shmget(ftok(".",'b'), sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
perror("shmget failed");
exit(0);
}
//将共享内存连接到当前进程的地址空间
shared_memory = shmat(shmid, (void*)0, 0);
if(shared_memory == (void*)-1)
{
perror("shmat failed");
exit(0);
}
printf("Read Process ID is %d\n",getpid());
printf("Memory attached at %p\n", shared_memory);
//设置共享内存
shm_buff_insert = (struct shared_use_st*)shared_memory;
while(1)
{
sem_p_operate(semid);//P操作
printf("Shared-Memory was written by process %d: %s",shm_buff_insert->pid,shm_buff_insert->buffer);
if(strncmp(shm_buff_insert->buffer,"quit",4)==0)
break;
shm_buff_insert->pid = 0;
bzero(shm_buff_insert->buffer,TEXT_SZ);
//sleep(10);
sem_v_operate(semid);//V操作
}
//把共享内存从当前进程中分离
if(shmdt(shared_memory) == -1)
{
perror("shmdt failed");
exit(0);
}
if(shmctl(shmid,IPC_RMID,NULL)==-1)//删除共享内存
{
perror("cannot shmctl(IPC_RMID)");
exit(0);
}
return 0;
}
我们可以在阅读程序的V操作之前添加一个延时,此时可以发现写程序暂时无法向共享内存中写入数据。
这个例子的主要注意点 PV操作就相当与排队 先到先得
在写文件中:
要想写文件可以写数据到共享内存 那么信号量在执行do-while中的P操作后 信号量的值要大于0 否则阻塞 当执行一次循环体 后执行V操作让 信号量+1 此时再次来到循环体的开头P操作 因为读文件也使用了P操作 所以中执行了写文件中的循环体一次后 先去执行一次读文件的循环体 再回来执行一次写文件的循环体 以此类推
读文件也是类似的
所以通过这样的交叉执行 达到了写一次数据就读取一次数据的功能
不过上面代码有个问题 :因为我们是将一个结构体指针指向了共享内存的首地址
struct shared_use_st
{
pid_t pid;//写入数据的进程ID
char buffer[TEXT_SZ];//记录写入和读取的文本
}; 这是结构体
所以我们实际上是在共享内存中的一个结构体大小的区域读写 且因为每次读写大小都是TEXT_SZ所以每次写入都会覆盖之前写入的内容 所以要写一次读一次这样我们写入的内容才不会丢失
但是如果我们只执行了写文件没有执行读文件 这样我们在执行读文件之前 一直在执行写文件的循环体 既在写文件中执行完写文件的V操作后 没有其他的比写文件的P操作的P操作在等待 所以直接
执行P操作 既一直写数据 一直覆盖之前写入的数据 这样就会丢失之前的数据
所以我们使用这个代码要都执行读写程序