一、实验目的与要求
综合利用进程控制的相关知识,结合对shell功能的和进程间通信手段的认知,编写简易shell程序,加深操作系统的进程控制和shell接口的认识。
二、实验内容
使用Linux操作系统;学习使用Linux进程间控制,进程间通信,管道,消息队列,共享内存等手段以及处理机调度
学会使用POSIX信号量实现生产者与消费者间同步关系
编写简易shell程序
三、实验步骤与过程
1 Linux学习与实践
1.1 管道
进程间的管道通信有两种形式,无名管道用于父子进程间,命名管可以用于任意进程间— —命名管道在文件系统中有可访问的路径名。管道通信方式主要用于单向通信,如果需要双向 通信则建立两条相反方向的管道。管道实质是由内核管理的一个缓冲区(一边由进程写入,另 一边由进程读出),因此要注意,如果缓冲区满了则写管道的进程将会阻塞。另外管道内部没 有显式的格式和边界,需要自行处理消息边界,如果多进程间共享还需要处理传送目标等工作。
无名管道
管道(pipe),或称无名管道,是所有 Unix 都提供的一种进程间通信机制。管道是单向的 信道,进程从管道的写端口写入数据,需要数据的进程从读端口中获取数据,数据在管道中按 到达顺序流动。Unix 命令中使用“|”来连接两个命令时使用的就是管道,例如“ls | more”将 ls命令的标准输出内容写入到管道中,管道的输出内容作为 more 命令的标准输入。注意,重定向技术虽然看起来和管道很相似,例如“ls > temp”,但重定向并不使用管道。
如下面图 1,以 pipe()为例展示父子进程间使用管道进行通信的方法,pipe()将通过两个文件描述符(整数)来指代管道缓冲区的读端和写端(代码中用 fds[] 变量记录)。其中父进程关闭管道的读端 fds[0]并往管道的写端 fds[1]写出信息,子进程关闭了管道的写端 fds[1]并从管道的读端 fds[0]读回信息。
图 1 pipe-demo.c 代码
图 2 pipe-demo.c输出
图 2是图 1中pipe-demo运行的输出,其表明父进程发送了消息到管道,子进程成功接收到了“Message from parent”。
命名管道
前面提到的无名管道有一个主要缺点,只能通过父子进程之间(及其后代)使用文件描述符的继承来访问,无法在任意的进程之间使用。命名管道(named pipe)或者叫 FIFO 则突破了这个限制。可以说 FIFO 就是无名管道的升级版——有可访问的磁盘索引节点,即 FIFO 文件将出现在目录树中(不像无名管道那样只存在于 pipefs 特殊文件系统中)。
下面图 3我们用 mkfilo 命令来创建命名管道 os-exp-fifo,如屏显 4-2 所示,其中 ls 命令查看时可以看出其类型是管道“p”。
图 3 mkfifo 创建命名管道
此时可以用 cat os-exp-fifo 命令尝试从管道中读入数据,但是此时管道中还没有写入任何数据,因此 cat 将进入阻塞状态,如图 4:
图 4 用 cat 尝试读取空的管道文件(阻塞)
如果此时在另一个终端上用“echo Hello, Named PIPE! >os-exp-fifo”则cat 会被唤醒并读入管道数据回显字符串“Hello, Named PIPE!”,如图 5
图 5 用 echo 向管道写入数据
1.2 System V IPC
Linux 的进程通信继承了 System V IPC。System V IPC 指的是 AT&T 在 System V.2 发行版中 引入的三种进程间通信工具:
信号量,用来管理对共享资源的访问
共享内存,用来高效地实现进程间的数据共享
消息队列,用来实现进程间数据的传递。
我们把这三种工具统称为 System V IPC 的对象,每个对象都具有一个唯一的IPC标识符 ID。为了使不同的进程能够获取同一个 IPC 对象,必须提供一个IPC 关键字(IPCkey),内核负责把 IPC 关键字转换成 IPC 标识符 ID。下面我们观察这三种IPC 工具。
在Linux中执行ipcs命令可以查看到当前系统中所有的System V IPC对象,如图 6所示。
图 6 ipcs 命令的输出
查看这些 IPC 对象时还可以带上参数,ipcs -a 是默认的输出全部信息、ipcs -m 显示共享内 存的信息、ipcs -q 显示消息队列的信息、ipcs -s 显示信号量集的信息。另外用还有一些格式控 制的参数,–t 将会输出带时间信息、-p 将输出进程 PID 信息、-c 将输出创建者/拥有者的 PID、 -l 输出相关的限制条件。例如用 ipcs -ql 将显示消息队列的限制条件,如图 7所示。
图 7 ipcs -ql 的输出
删除这些 IPC 对象的命令是 ipcrm,它会将与 IPC 对象及其相关联的数据也一起删除, 管 理员或者 IPC 对象的创建者才能执行删除操作。该命令可以使用 IPC 键或者 IPC 的 ID 来指定 IPC 对象:ipcrm -M shmkey 删除用 shmkey 创建的共享内存段而 ipcrm -m shmid 删除用 shmid 标识的共享内存段、ipcrm -Q msgkey 删除用 msqkey 创建的消息队列而 ipcrm -q msqid 删除用 msqid 标识的消息队列、ipcrm -S semkey 删除用 semkey 创建的信号而 ipcrm -s semid 删除用 semid 标识的信号。
1.2.1 消息队列
消息队列有些项邮政中的邮箱,里面的消息有点像信件——有信封以及写有内容的信纸。 由于各条消息可以通过类型(type)进行区分,因此可以用于多个进程间通信。比如一个任务 分派进程,创建了若干个执行子进程,不管是父进程发送分派任务的消息,还是子进程发送任务执行的消息,都将 type 设置为目标进程的 PID,目标进程只接收消息类型为 type 的消息就 实现了子进程只接收自己的任务,父进程只接收任务结果。
下面图 8图 9给出代码 msgtool.c,其每次启动都是以新的进程形式运行,因此各次运行都是相互独立的。其中发送消息的核心函数是 msgsnd(),第一个参数是消息队列的 ID,第二个参数时被发送消息的起始地址(消息的第一个成员是一个整数用于指出消息类型),第三个参数时消息长度,第四个参数指定写消息时的一些行为(此例子用 0);接受消息的函数是 msgrcv(),第一个参数用于指定消息队列的ID,第二个参数是接受缓冲区地址,第三个参数指出希望接受的消息类型(0 表示接受任意类型的一条消息,>0表示接受指定类型的消息,
图 8 msgtool.c 代码
图 9 msgtool.c 代码
执行 msgtool s 1 Hello,my_msg_queue!发送类型为 1 的消息,然后用ipcs -q 查看新创建的消息队列,里面有 20 个字节的一条消息。此时再执行 msgtool -r 1(另一个进程了)读走类型为 1 的消息,然后再用 ipcs -q 可以看到该消息队列为空(0 字节)了。上述操作的输出如图 10:
图 10 msgtool 的执行结果
1.2.2 共享内存
System V IPC 的共享内存是由内核提供的一段内存,可以映射到多个进程的续存空间上, 从而通过内存上的读写操作而完成进程间的数据共享。我们首先来看看如何创建共享内存的, 示例代码如图 11所示,它创建了一个 4096 字节的共享内存区。shmget()的第一个参数 IPC_PRIVATE(=0,表示创建新的共享内存),第二个参数是共享内存区的大小,第三个是访问 模式。虽然也可以像前面的消息队列的例子那样通过 ftok()将键值转换成 ID,但这里没有指定 ID,而是创建共享内存后由系统返回一个 ID 值(后面的进程要使用该共享内存时需要指定该 ID)。
图 11 shmget-demo.c代码
执行该程序,其输出如图 12所示。输出结果表明新创建的共享内存的 ID为24,长度为 4096 字节,当前还没有进程将他映射到自己的进程空间(连接数列为 0)。
图 12 shmget-demo.c的运行结果
下面展示另一个进程通过影射该共享内存而使用它的过程,具体如图 13所示
图 13 shmatt-write-demo.c 代码
我们运行 shatt-demo 24(命令行参数中指出共享内存的 ID 为 24),其第一段输出结果如图 14所示。完成共享内存的映射后,shmatt-write-demo 往共享内存中写入一个 字符串“Hello shared memory!”。shmatt-write-demo 还通过 system()执行了“ipcs -m”,因此 也输出了当前的共享内存信息,可以看到 ID 为 24 的共享内存已经有被映射了一次 (nattach 列为 1)。
图 14 shmatt-write-demo.c运行结果
接下来使用 ps -a 指令,可以看到 shmatt-write-de 的 PID为4898,如图 15
图 15 ps -a 指令查看matt-write-de 的 PID
使用 cat /proc/4898/maps 查看进程的进程空间,可以看到进程布局如图 16所示
图 16 运行之后的进程布局
在 shmatt-write-demo击键回车后将解除共享内存的映射,此时ipcs -m显示对应的共享内存区没有人使用(连接数为 0),如图 17所示。此时如果检查内存布局可以发现原来区间的虚存已经消失。
图 17 运行write后的共享内存
此时再尝试用另一个程序去映射该共享内存并从中读取数据,shmatt-read-demo 代码如图 18所示。
图 18 shmatt-read-demo.c代码
如图 19,可以看到虽然创建该共享内存的进程已经结束了,可是shmatt-read-demo映射 ID 为 24 的共享内存后仍读出了原来写入的字符串。
图 19 运行read后的共享内存
从上面实验看出共享内存是比较灵活的通信方式,不需要像管道一样用文件接口read()、write()等函数,也不需要像消息队列那样用 msgsend()/msgrcv()等函数来操作,直接用内存指针的方式就可以操作。虽然实验中没有验证其容量,但是共享内存的容量远比管道和消息队列大。
1.2.3 信号量数组/信号量集
在操作系统原理性课程中我们以及学习过信号量和信号量集机制。Linux 支持的System V IPC 中的信号量实际上是信号量数组(信号量集),一次可以创建多个信号量。创建或者获得信号量集之后,可以对各个信号量进行 P/V 操作(或者称up/down 操作),进程进行 P/V 操作时遵循信号的同步约束关系——由操作系统完成进程的阻塞或唤醒。
1.3 进程间同步
Linux 同时支持 System V IPC 中的信号量集和 POSIX 信号量。前者常用于进程间通信、是基于内核实现的(不随进程结束而消失);而后者是常用于线程间同步、方便使用且仅含一个信号量。POSIX 信号量分成有名信号量和无名信号量,前者和一个文件的路径名相关联,创建 后不随进程结束而消失(可用于进程间通信),反之无名信号量则只在进程生命周期内存在且 只能在该进程创建的线程间使用。 上述两种信号量的编程接口函数是很容易被区分:对于所有 System V信号量函数,在它们的名字里面没有下划线(例如,有semget()而不是sem_get()),然而所有的 POSIX 信号量 函数都有一个下划线(例如,有sem_post()而不是sempost())。Linux操作系统内核内部也有多个并发的执行流,它们之间使用内核的信号量,和这里讨论的用户态信号量又不相同。
1.3.1 System V IPC 信号量集
进程间的 System V IPC 信号量集的同步机制已经在前面 System V IPC 中和进程间通信主题一并讨论过,这里不再重复。
1.3.2 POSIX 信号量
POSIX 信号量又分成有名信号量和无名信号量,前者可以用于在多个进程间或多个线程间的同步,无名信号量只能用于线程间同步。两者的创建函数不同,但是响应的P/V操作函数是一样的。有名信号量由于可以通过标识来访问,因此可以同时用于进程间同步和线程间同步。有名信号量的创建使用 sem_open()完成,代码 psem-named-open.c 如图 20所示,其先用 sem_open()创建了一个信号量,该信号量由一个字符串所标识。
图 20 psem-named-open.c代码
然后用 gcc psem-named-open.c -o psem-named-open -lpthread(参数-lpthread 用于指出链接时所用的线程库)完成编译,然后运行 psem-named-open。如果没有输入作为标识的文件名字符串,则给出提示要求用户输入;如果输入一个文件名字符串,正常情况将完成创建过程,如图 21所示。
图 21 psem-named-open 的输出
之后尝试执行 P/V 操作中的 V 操作(即对信号量进行减 1 操作,可能引发阻塞),程序psem-named-wait-demo.c如图 22所示,它通过 sem_wait()来执行V 操作(减1 操作),并且通过 sem_getvalue()来查看信号量的值。同样出于代码简洁的考虑,这里的代码也是没有检查 sem_open()是否成功获得了信号量。因此,如果输入错误的标识字符串,则无法成功获得所指定的信号量,sem_wait()引用无效的信号量而引发段错误。
图 22 psem-named-wait-demo.c 代码