图 23显示该程序第二次运行后并没有返回到 shell 提示符,如果此时用另一个终端执行 ps 命令可以看到该进程处于S+状态,如图 24所示。
图 24 查看 psem-named-wait-demo 的运行状态
再接下来看 P 操作,使得前面的 psem-named-wait-demo 进程从原来的阻塞状态唤醒并执行结束。程序如图 25所示,这里也要注意实际上代码并没有对sem_open()的返回值进行判断。
图 25 psem-named-post-demo.c 代码
编译并执行 psem-named-post-demo(与前面不在同一个终端 shell 上),可以看到此时信号量的值增加到 1,并使得原来阻塞的psem-named-wait-demo被唤醒并执行完毕,如图 26所示。
图 26 psem-named-post-demo 的运行输出
同时可以看到,原来阻塞的 psem-named-wait-demo 被唤醒并执行完毕,如图 27所示。
图 27 唤醒阻塞的 psem-named-wait-demo 进程
最后,如果不希望使用这个信号量可以通过 sem_unlink()撤销该信号量,如图 28所示。
图 28 psem-named-unlink-demo.c 代码
而 POSIX 无名信号量适用于线程间通信,如果无名信号量要用于进程间同步,信号量要放在共享内存中(只要该共享内存区存在,该信号灯就可用)。无名信号量使用sem_init()创建。
互斥量是信号量的一个退化版本,仅用于并发任务间的互斥访问。下面先用一个代码来展示多线程并发且没有用互斥量保护共享变量的情形,如代码4-10 所示,此时结果可能会出现错误。该程序对一个缓冲区(缓冲区内是数值为 3、4、3、4......交织的整数)内的每个整数进行检查,并对数值为 3 的整数进行计数统计,统计工作由16 个线程并发完成(每个线程负责缓冲区的 1/16 的数据)。
下面的代码 no-mutex-demo.c 展示多线程并发且没有使用互斥量保护共享变量的状况,代码如图 29所示,编译后运行 no-mutex-demo,所得结果如图 30所示。可以看到,每次运行的结果并不唯一,共享变量没有被互斥访问。
图 29 no-mutex-demo.c 代码
图 30 no-mutex-demo 的运行结果
如果对 count++这个临界区加以保护,即增加一个互斥量 mutex m;就能避免出现这个问题。编译运行no-mutex-demo1,每次运行都能获得相同的结果。代码如图 31所示,结果如所示。由于实现了共享变量的互斥访问,因此每次运行的结果都是确定的值。
图 31 no-mutex-demo1.c 代码
图 32 no-mutex-demo1.c 结果
至此,自行完成的Linux学习与实践全部结束。
2 使用POSIX信号量完成生产者与消费者的同步关系
根据前面的学习与本地的题目要求,我们可以得到设计的思路:
(1)要创建一个生产者消费者共用的缓冲区;
(2)对于生产者程序,首先要获取共享内存区并且挂入内存,之后创建三个信号量,将读取的行写入缓冲区(信号量在过程中要有对应的操作),然后释放信号量,结束内存映射并删除共享内存区域;
(3)对于消费者程序,同样要获取共享内存区并挂入内存,之后获取三个信号量,将缓冲区中的行字符串打印(信号量在过程中要有对应的操作),然后释放信号量。这里需要创建两个进程并发的进行上述操作。
头文件:
首先,定义如图 33的头文件,定义NUM_LINE=16作为共享内存的行数(可存储的行数),每行的内存大小为256;并且定义三个信号量分别用来判断是否互斥以及共享内存的空、满。
图 33 头文件
生产者代码:
由于本次实验已经给出了代码框架,只需要补充框架中对应的内容即可。补全代码如图 34,定义共享内存指针、共享内存id以及访问共享内存的信号量指针,然后获取共享内存区,并将共享内存映射到内存空间。
图 34 获取共享内存区并放入内存
之后需要创建三个信号量,sem_queue、sem_queue_empty、sem_queue_full 的信号量初始值分别为 1、NUM_LINE 和 0,具体如图 35:
图 35 创建信号量
接下来如图 36,将读写指针初始化,开始时都指向第0行,将输入的行写入缓冲区,并且要有信号量操作。对共享区域加锁,输出信号量的值,把输入的内容存入共享区域,更新写操作的行并判断:如果是quit 则跳出循环。
图 36 将输入的行写入缓冲区
最后一部分如图 37,为释放信号量,结束共享内存在本进程的挂载映象,删除共享内存区域。该部分仿照前面的实验指导部分完成。
图 37 释放信号量
生产者代码:
如图 38,消费者代码与生产者代码相似,首先获得生产者代码中的已创建的共享内存,然后将共享内存区映射到本进程的进程空间。
图 38 生产者获取共享内存区
如图 39,然后要获取 生产者创建的 3 个信号量。创建两个进程,当进入子进程时先等待信号量,进行 P 操作,当成功后打印消费内容及进程号,发现 quit 后退出,释放信号量。父子进程都采用相同的处理方式。
图 39 生产者信号量操作
如图 40,同图 39,父子进程采用相同的处理方式。此处要注意的是,父进程释放信号量时,最后要加上 waitpid(fork_result,NULL,0); 用以等待所有的子进程结束后再退出,防止有孤儿进程生成。
图 40 生产者父进程信号量操作
完成代码的编写后,将进行测试。如图 41,左侧为producer的运行,右侧为customer的运行,可以看到,程序完成了同步与通信功能,producer的输入通过共享内存,customer能够进行读取,producer输入的产品内容与customer输出的消费信息一一对应,其中产品号为奇数的内容对应父进程的运行,产品号为偶数的内容对应子进程的运行。当producer输入 quit 之后,两边都正常退出。
图 41 生产者/消费者问题的代码运行结果
3 设计简单Shell程序
根据前面的学习,我们可以知道,首先需要将输入的命令进行读入,并进行内部命令与外部命令的解析,如果成功解析出对应的命令则进行执行,否则视为无效命令。
首先,如图 42,引入必须的头文件,借助宏定义完成几个命令的分类并预先定义好几个函数。具体作用将在下面介绍。
图 42 shell程序的头文件以及函数
然后,定义如图 43的主函数。主函数的主要作用为申请存命令的空间,循环读入命令,并根据命令的不同做出对应的执行。在此,如“help”,“exit”等的内部命令将使用字符串比较的形式进行解析,并直接进行执行。
图 43 shell程序的主函数
由于shell需要打印提示符信息,在图 44中,我定义了一个函数获取当前目录并输出提示符。
图 44 shell程序的提示信息
对于帮助信息,将调用图 45中的函数直接输出帮助信息即可。
图 45 shell程序的帮助信息函数
由于要完成命令行的输入,我定义了如图 46的输入函数。利用循环将字符一个一个读入,当读入换行符或者超过长度时则终止循环。每次读入的时候都将读到的字符存入命令数组中。
图 46 shell程序的读入函数
由于需要对命令进行解析,我定义了如图 47的函数。他将利用空格,对输入的命令进行拆分,分别存入数组中,直到遇到了换行符。
图 47 shell程序的命令解析函数
Shell中最重要的是执行命令。执行命令中最重要的是判断命令是否合法。并根据命令的类型(重定向命令,管道命令等)对命令进行分类,代码如图 48,首先需要将命令取出,并判断是否含有后台运行符。
图 48 shell程序的命令判断
接着,判断是否为重定向或者管道命令。如果非法则flag++,如果合法将进行分类。首先,对重定向符号以及管道符号进行判断,具体如图 49。在此将判断对重定向符号以及管道符号的个数,并对命令进行分类。然后对于重定向命令,将取出重定向命令的目标,并存入file变量中。如果是管道命令,则将管道符号后的可执行shell命令取出,以便进行执行。
图 49 shell程序的重定向与管道命令的判断
完成了命令的分类,将进行命令的执行。首先,如图 50,如果不含有重定向及管道符号的最常规命令,直接调用execvp函数进行执行即可。
图 50 shell程序的常规命令执行
对于有输出重定向符号的命令,如图 51,利用dup2函数进行重定向即可。
图 51 shell 程序的输出重定向命令执行
对于有输入重定向符号的命令,如图 52,利用dup2函数进行重定向即可,与输出重定向类似。
图 52 shell程序的输入重定向命令执行
对于管道命令,则相对复杂,需要利用子进程将管道符前面的命令执行完毕后再调用父进程完成管道符右侧命令的执行。具体代码如图 53,首先,使用fork开辟子进程,并利用子进程将管道符左边的命令的输出写入到借助中间文件里。这个过程中父进程需要等待子进程完成执行后再行执行。接着,父进程将由子进程写入的中间文件作为输入,运行管道符右侧的命令。最后删除暂存的文件即可。
图 53 shell程序的管道命令执行
此外,如果有后台运行符,则父进程直接返回而不需等待子进程。此时代码如图 54
图 54 shell程序的后台运行符处理
此外,在每次执行的过程中都需要对命令进行查找,因此使用了如图 55的函数对命令进行查找。将分别在当前目录,bin目录,以及user下的bin目录进行查找。
图 55 shell程序的命令查找
所有代码如上,接下来进行测试如下。首先,测试内部命令“help”,效果如图 56,将打印帮助信息
图 56 shell程序输出帮助信息
如果输入非法命令,则也会进行提示,具体如图 57
图 57 shell程序非法命令
对外部命令的支持,首先,对“ls”进行测试,结果如图 58
图 58 shell程序ls运行结果
接着,对“ps”进行测试,结果如图 59
图 59 shell程序ps运行结果
接着,对“cp”命令进行测试,在此,我测试使用cp命令复制“helloworld.c”文件,结果如图 60和图 61
图 60 shell 程序的cp命令
图 61 shell 程序的cp命令结果
此外,还可以在我的shell里对c程序进行编译并运行,结果如图 62
图 62 shell程序对c代码的编译与运行
接着,测试管道功能,使用 ls | grep helloworld 和 ls | grep shell 作为指令。grep 指令是用于查找文件里符合条件的字符串,ls | grep helloworld 就是找出当下文件夹里含有helloworld的文件名并打印出来。在 myshell 下的运行结果如图 63所示,可以看到管道命令可以正常执行。
图 63 shell程序的管道命令测试
接着,测试重定向功能。首先,对输入重定向进行测试。为了完成输入重定向的测试,我编写了一个简单的a+b程序,代码如图 64,并创建了用于重定向的文件,如图 65所示。
图 64 a+b程序
图 65 输入重定向的文件
接着,在我的shell中,进行输入重定向的测试,结果如图 66,可以看到程序执行正确
图 66 shell的输入重定向测试
最后,对输出重定向进行测试,如图 67,使用ps命令,将结果重定向到“log.txt”中,结果如图 68。
图 67 shell的输出重定向测试
图 68 shell输出重定向测试的结果
可以看到输出重定向的结果也符合预期。至此,shell编写完毕,我的shell可以接受内部,外部命令并以包含路径的信息作为提示符,可以在shell内部循环读取执行命令。此外,我的shell也实现了输入输出的重定向以及管道命令。