第十章 实例研究1:Linux
1.一个目录包含以下的文件:
aardvark feret koala porpoise unicorn bonefish grunion llama quacker vicuna capybara hyena marmot rabbit weasel dingo ibex nuthatch seahorse yak jellyfish ostrich
哪些文件能通过命令ls [abc]*e*被罗列出来?
答:将列出的文件是:bonefish、quacker、seahorse和weasel。
2.下面的Linux shell管线的功能是什么?
grep nd xyz | wc -|
答:它打印xyz文件中包含字符串“nd”的行号。
3.写一个能够在标准输出上打印文件z的第八行的Linux管线。
答:管线如下:head -8 z | tail -1。第一部分选出文件z的前八行,并将它们传递给tail,tail将最后一行写入屏幕。
4.Linux在标准输出和标准错误对于终端都是默认的情况下是怎么区分标准输出和标准错误的?
答:它们是分开的,因此标准输出可以重定向,而不会影响标准错误。在管线中,标准输出可能会转到另一个进程,但标准错误仍然在终端上输出。
5.一个用户在终端键入了如下的命令:
a|b|c&
d|e|f&
在shell处理完这些命令后,有多少新的进程在运行?
答:每个程序都运行在自己的进程中,因此将启动六个新进程。
6.当Linux shell启动一个进程,它把它的环境变量,如HOME放到进程栈中,使得进程可以找到它的home目录是哪个。如果这个进程之后进行派生,那么它的子进程也能自动地得到这些变量吗?
答:是的。子进程的内存是父进程的完整副本,包括堆栈。因此,如果环境变量在父进程的堆栈上,它们也将在子进程堆栈上。
7.在如下的条件下:文本大小=l00KB,数据大小=20KB,栈大小=10KB,任务结构=1KB,用户结构=5KB,一个传统的UINX系统要花多长的时间派生一个子进程?内核陷阱和返回的时间用lms,机器每50ns就可以复制一个32位的字。共享文本段,但是不共享数据段和堆栈段。
答:由于文本段是共享的,因此只需要复制36KB。该机器每微秒可以处理80个字节,因此36KB需要0.46毫秒。进出内核需要另外的1毫秒,整个过程大概需要1.46毫秒。
8.当多兆字节程序变得越来越普遍,花在执行fork系统调用以及复制调用进程的数据段和堆栈段的时间也成比例地增长。当在Linux中执行fork,父进程的地址空间是没有被复制的,不像传统的fork语义那样。Linux是怎样防止子进程做一些会彻底改变fork语义的行动的?
答:Linux依赖于写时复制。让子进程指向父进程的地址空间,但标记父进程的页面被写保护。当子进程尝试写父进程的地址空间时,会发生陷入,这时创建父进程的页面的副本并将其分配到子进程的地址空间中。
9.当一个进程进入僵死状态后,取走它的内存有意义吗?为什么?
答:有意义。它不再运行了,所以它的内存越早在空闲列表中越好。
10.你认为为什么Linux的设计者禁止一个进程向不属于它的进程组的另一个进程发信号呢?
答:如果恶意用户可以将信号发送到任意无关的进程,可能会对系统造成严重破坏。没有什么会阻止用户编写一个程序,该程序包含一个循环,该循环向PID i发送一个信号,i从1到PID的最大值。许多这些进程对于信号将是无准备的,并且将被杀死。如果你想杀死你自己的进程,那没关系,但是杀死邻居的进程是不能接受的。
11.一个系统调用常用一个软件中断(陷阱)指令实现。一个普通的过程调用在Pentium的硬件上也能使用吗?如果能使用,在哪种条件下?如何使用?如果不能,请说明原因。
答:使用Linux或Windows Vista是不可能的,但Pentium硬件确实可以实现。需要的是使用硬件的分段功能,这不是Linux或Windows Vista所支持的。操作系统可以放在一个或多个全局段中,通过受保护的过程调用来进行系统调用而不是陷阱。OS/2以这种方式工作。
12.通常情况下,你认为守护进程比交互进程具有更高的优先级还是更低的优先级?为什么?
答:通常,守护进程在后台运行,如打印和发送电子邮件。由于人们通常不坐在它们椅子的边缘等待它们完成,所以它们被赋予了低优先级,吸收了交互进程所不需要的多余的CPU时间。
13.当一个新进程被创建,它一定会被分配一个惟一的整型数作为它的PID。在内核里有一个每个进程创建时就递增的计数器够用么?其中计数器作为新的PID,讨论你的结论。
答:PID必须是唯一的。迟早计数器会溢出从而返回到0,然后它会重新开始计数,例如15分配给了一个几个月前开始执行直到现在仍在运行的进程,则15不能被分配给一个新创建的进程。因此,通过计数器选出PID之后,必须对进程表进行搜索以查看PID是否已在使用。
14.在每个任务结构中的进程项中,父进程的PID被储存。为什么?
答:当进程退出时,父进程将被赋予其子进程的退出状态。PID需要能够识别父进程,使得退出状态可以返回到正确的父进程中。
15.当响应一个传统的UNIX fork调用时,Linux的clone命令会使用什么样的sharing_flags位的组合?
答:如果所有sharing_flags位都被设置,则clone命令启动一个常规的线程。如果所有的位被清除,这个调用本质上就是一个fork。
16.Linux调度器在2.4版本和2.6版本的内核间经历了一个大整修。现在的调度器可以在O(1)时间做出调度决定。请解释为什么会这样?
答:每个调度决策需要查找活动阵列的位图并搜索阵列中的第一组位,这可以在恒定时间内完成,从所选择的队列中再次执行单个任务,再次执行常数时间操作,或者如果位图 值为零,交换活动和过期列表的值,再次进行恒定时间操作。
17.当引导Limix(或者大多数其他操作系统在引导时)时,在0号扇区的引导加载程序首先加载一个引导程序,这个程序之后会加载操作系统。这多余的一步为什么是必不可少的?0号扇区的引导加载程序直接加载操作系统会更简单的。
答:从块0加载的程序最大长度为512字节,因此不能很复杂。加载操作系统需要了解文件系统布局才能查找和加载操作系统。不同的操作系统有不同的文件系统,512字节的程序无法满足要求。相反,块0引导加载程序仅从磁盘分区上的固定位置获取另一个加载程序。该程序可以更长,而且和系统相关,以便它可以找到并加载操作系统。
18.某个编辑器有100KB的程序文本,30KB的初始化数据和50KB的BSS,初始堆栈是10KB。假设这个编辑器的三个复制是同时开始的。
(a)如果使用共享文本,需要多少物理内存呢?
(b)如果不使用共享文本又需多少物理内存呢?
答:使用共享文本,文本需要100 KB。这三个进程中的每个进程对于其数据段需要80 KB,对于其堆栈需要10 KB,因此所需的总内存为370 KB。不使用共享文本,每个程序需要190 KB,因此三个进程共需要570 KB。
19.在Linux中打开文件描述符表为什么是必要的呢?
答:进程可以通过共享一个打开的文件描述符来共享文件,包括当前共享文件的指针位置,而不必更新对方的私有文件描述符表中的任何东西。同时,另一个进程可以通过单独的打开的文件描述符访问同一个文件,获取不同的文件指针,并以自己的意愿移动文件。
20.在Linux中,数据段和堆栈段被分页并交换到一个特别的分页磁盘或分区的暂时副本上,但是代码段却使用了可执行二进制文件。为什么?
答:代码段不能更改,所以它不需要被分页。如果需要它的页框,可以直接释放。并始终可以从文件系统检索到它页面。数据段不能被分页后返回可执行文件,因为它可能已经被改变。将其分页后返回会破坏可执行文件。堆栈段甚至不存在于可执行文件中。
21.描述一种使用mmap和信号量来构造一个进程内部间通信机制的方法。
答:两个进程可以同时将同一个文件映射到它们的地址空间。这给了他们一种共享内存的方法。共享内存的一半可以用作从A到B的缓冲区,另一半用作从B到A的缓冲区。为了通信,一个进程向它的一半共享内存写入消息,然后向另一个进程发送一个信号以指示有一个消息等待它。对消息的回复可以使用另一半共享内存缓冲区。
22.一个文件使用如下的mmap系统调用映射:
mmap(65536,32768,READ,FLAGS,fd,0)
每页有8KB。当在内存地址72000处读一个字节时,访问的是文件中的哪个字节?
答:存储器地址65,536是文件0字节,所以存储器地址72,000是文件的6464字节。
23.在前一个问题的系统调用执行后,执行munmap(65535,8192)调用会成功吗?如果成功,文件的哪些字节会保持映射?如果不成功,为什么会失败?
答:会成功。最初,文件的四页被映射:0、1、2和3。调用完成后,只有第2页和第3页仍然被映射,即字节16,384到32,767。
24.一个页面故障会导致错误进程终止吗?如果会,举一个例子。如果不会,请解释原因。
答:有可能。例如,当堆栈超出底部页面时,会发生页面错误,并且操作系统通常会为其分配最下一页。但是,堆栈已经碰到数据段,下一页不能分配给堆栈,所以由于虚拟地址空间已经用完,进程必须被终止。此外,即使虚拟内存中有其他页面可用,磁盘的页面交换区域可能已满,因此无法为新页面分配后备存储,这也将终止进程。
25.在内存管理的伙伴系统中,两个相邻的同样大小的空闲内存块有没有可能同时存在而不会被合并到一个块中?如果有解释是怎么样的情况。如果没有可能,说明为什么不可能。
答:如果两个块不是伙伴,这是可能的。考虑图10-17(e)中的情况。两个新的8个页面的请求到来。在这一点上,底部32个页面的内存由4个不同的用户拥有,每个拥有8个页面。现在用户1和用户2释放它们的页面,但用户0和用户3保留它们的页面。这产生了8个页面被占用,8个页面空闲,8个页面空闲,8个页面被占用的情况。有两个相同大小的相邻块,但不能合并,因为它们不是伙伴。
26.据说在代码段中分页分区要比分页文件性能更好。为什么呢?
答:分页分区允许使用原始设备,而不需要文件系统数据结构的开销。要访问块n,操作系统可以通过将其添加到分区的起始块来计算其磁盘位置,而没有必要通过所有的间接区块。
27.举两个例子说明相对路径名比绝对路径名有优势。
答:通过相对于工作目录的路径打开文件通常对程序员或用户来说更为方便,因为需要较短的路径名。它通常也比较简单,需要更少的磁盘访问。
28.以下的加锁调用是由一个进程集合产生的,对于每个调用,说明会发生什么事情。如果一个进程没能够得到锁,它就被阻塞。
a)A想要0到10字节处的一把共享锁。
b)B想要20到30字节处的一把互斥锁。
c)C想要8到40字节处的一把共享锁。
d)A想要25到35字节处的一把共享锁。
e)B想要8字节处的一把互斥锁。
答:结果如下:
(a)锁被授予。
(b)锁被授予。
(c)C被阻塞,因为20字节到30字节不可用。
(d)A被阻塞,因为20字节到25字节不可用。
(e)B被阻塞,因为8字节不可用于独占锁定。
这时,发生了一个死锁。没有一个进程可以再次运行。
29.考虑图10-26c中的加锁文件。假设一个进程尝试对10和11字节加锁然后阻塞。那么,在C释放它的锁前,还有另一个进程尝试对10和11字节加锁然后阻塞。在这种情况下语义方面会产生什么问题?提出两种解决方法并证明。
答:问题出现在哪个进程可用时可以获得锁。最简单的解决方案是将其设为未定义。这是POSIX所做的,因为它是最简单的实现。另一个是要求按照请求的顺序授予锁。这种方法需要更多的工作去实现,但可以防止饥饿。另一种可能是让进程在请求锁时提供优先权,并使用这些优先级做出选择。
30.假设lseek系统调用在一个文件中寻找一个负的偏移量。给出两种可能的处理方法。
答:一种方法是给出错误,拒绝执行lseek。另一个是使偏移变为负值。只要不使用,就没有任何伤害。只有尝试读取或写入文件时,才能给出错误信息。如果lseek跟随另一个使偏移为正的lseek,则不会给出错误。
31.如果一个Linux文件拥有保护模式755(八进制),文件所有者、所有者所在组以及其他每个用户都能对这个文件做什么?
答:文件所有者可以读取,写入和执行它,其他所有用户(包括文件所有者的组)只能读取并执行它,但不能写入。
32.一些磁带驱动拥有编号的块,能够在原地重写—个特定块同时不会影响它之前和之后的块。这样一个设备能持有一个已加载的Linux文件系统吗?
答:能。能够任意读和写的块设备可以用来保存文件系统。即使没有办法寻求特定的块,总是可以倒带,然后计数到所请求的块。这样的文件系统不是一个高性能的文件系统,但它能工作。作者在PDP-11上使用DECtapes实现了该功能,它能够工作。
33.在图10-24中链接之后Fred和Lisa在他们各自的目录中都能够访问文件X。这个访问是完全对称的吗,也就是说其中一个人能对文件做的事情另一个人也可以做?
答:不是,该文件仍然只有一个所有者。例如,如果所有者只能在文件上写,则对方不能这样做。将文件链接到您的目录不会突然给您以前没有的任何权限。它只是创建一个新的访问文件的路径。
34.正如我们看到的,绝对路径名从根目录开始查找,而相对路径名从工作目录开始查找。提供一种有效的方法实现这两种查找。
答:当工作目录被更改时,使用chdir系统调用,新工作目录的i-node将被取出并保存在内存的i-node表中。根目录的i-node也在那里。在用户结构中,维护这两者的指针。当路径名解析时,检查第一个字符。如果是“/”,则使用指向根i-node的指针作为起始位置,否则将使用指向工作目录的i-node的指针。
35.当文件/usr/ast/work/f被打开,读i节点和目录块时需要一些磁盘访问。在根节点的i节点始终在内存中以及所有的目录都是一个块的大小这样的假设下计算需要的磁盘访问数量。
答:访问根目录的i节点不需要磁盘访问,因此具有以下过程:
1.读取/目录查找“usr”。
2.读取/usr的i节点。
3.读取/usr目录以查找“ast”。
4.读取/usr/ast的i节点。
5.读取/usr/ast目录查找“work”。
6.读取/usr/ast/work的i节点。
7.读取/usr/ast/work目录以查找“f”。
8.读取/usr/ast/work/f的i节点。
36.一个Linux i节点有12个磁盘地址放数据块,还有一级、二级和三级间接块。如果每一个块能放256个磁盘地址,假设一个磁盘块的大小是1KB,能处理的最大文件的大小是多少?
答:i节点拥有12个地址。一级间接块持有256个磁盘地址,二级双重间接块持有65,536个磁盘地址,三级间接块持有16,777,216个磁盘地址,总共16,843,018个块。这将最大文件大小限制为12 + 256 + 65,536 + 16,777,218个块,大约为16G字节。
37.在打开文件的过程中,i节点从磁盘中被读出,然后放人内存中的i节点表里。这个表中有些域在磁盘中没有。其中一个是计数器,用来记录i节点已经被打开的次数。为什么需要这个域?
答:当文件关闭时,内存中i节点的计数器递减。如果大于零,则无法从表中删除i节点,因为该文件在某些进程中仍然处于打开状态。只有当计数器变为0时才能删除i节点。没有这个域,系统将不知道何时从表中删除i节点。每次打开文件时,单独复制一个i节点将不起作用,因为在一个副本中所做的更改在其他副本中不可见。
38.在多CPU平台上,Linux为每个CPU维护一个runqueue,这是个好想法吗? 请解释你的答案。
答:通过维护每个CPU运行队列,可以在本地进行调度决策,而无需执行昂贵的同步机制来始终访问和更新共享的运行队列。而且,如果我们在已经执行的同一个CPU上安排一个线程,那么所有相关的内存页面更有可能仍然在cache1中。
39.pdflush线程可以被周期性地唤醒,把多于30秒的旧页面写回到磁盘。这个为什么是必要的?
答:通过每30秒将修改后的文件的内容强制写回到磁盘上,由碰撞造成的损失限制在30秒。如果pdflush没有运行,进程可能会写一个文件,然后退出,文件的完整内容仍然在缓存中。事实上,用户可能会退出系统并回家,而文件仍然在缓存中。一小时后,系统可能会崩溃并丢失文件,而文件内容仍然只在缓存中而不是磁盘上。第二天我们不会遇到一个愉快的用户。
40.在系统崩溃并重启后,通常一个恢复程序将运行。假设这个程序发现一个磁盘i节点的连接数是2,但是只有一个目录项引用了这个i节点。它能够解决这个问题吗?如果能,该怎么做?
答:它所要做的就是将链接数设置为1,因为只有一个目录项引用了i节点。
41.猜一下哪个Linux系统调用是最快的?
答:通常是getpid,getuid,getgid或类似的东西。他们所做的只是从已知的地方获取一个整数并返回。其他的系统调用都会做更多的工作。
42.对一个从来没有被连接的文件取消连接可能吗?会发生什么?
答:该文件被删除。这是删除文件的正常方式(实际上是唯一的方法)。
43.基于本章提供的信息,如果一个Linux ext2文件系统放在一个1.44MB的软盘上,用户文件数据最大能有多少可以储存在这个盘上?假设磁盘块的大小是1KB。
答:一个1.44MB的软盘可容纳1440个原始数据块。ext2文件系统的引导块,超块,组描述符块,块位图和i节点位图都使用一个块。如果创建了8192个128字节的i节点,这些i节点将占用另外1024个块,只剩下411个块。根目录至少需要一个块,所以只有410个文件数据块空闲。实际上,Linux的mkfs程序很聪明,不会使用超过必须的i节点,所以效率不是很差。默认情况下,将创建占用23个块的184个i节点。然而,由于ext2文件系统的开销,Linux通常在软盘和其他小型设备上使用MINIX 1文件系统。
44.考虑到如果学生成为超级用户会造成的所有麻烦,为什么这个概念还会出现?
答:有人会做一些通常被禁止的事情。例如,用户启动生成无限量输出的作业。然后,用户注销并在伦敦度过了为期三周的假期。磁盘迟早会被填满,超级用户将不得不手动杀死进程并删除输出文件。还有其他这样的例子。
45.一个教授通过把文件放在计算机科学学院的Linux系统中的一个公共可问的目录下来与他的学生共享文件。一天他意识到前一天放在那的一个文件变成全局可写的了。他改变了权限并验证了这个文件与他的原件是一样的。第二天他发现文件已经被修改了。这种情况为什么会发生,又如何能预防呢?
答:当教授更改权限时,可能已经有人将文件打开。教授应该删除该文件,然后将另一个副本放入公共目录。此外,他应该使用更好的方法来分发文件,例如网页,但这超出了本练习的范围。
46.Linux支持一个系统调用fsuid。setuid准许使用者拥有与他运行的程序相关的有效id的所有权利。与setuid不同,fsuid准许正在运行程序的使用者拥有特殊的权利,只能够访问文件。这个特性为什么有用?
答:如果使用fsuid系统调用给其他用户授予超级用户权限,则该用户可以访问超级用户文件,但无法发送信号,终止进程或执行需要超级用户权限的其他操作。
47.写一个允许简单命令执行的最小的shell,也要使这些命令能在后台执行。
答:略。
48.使用汇编语言和BIOS调用,写一个在Pentinum类计算机上从软盘上引导自己的程序。这个程序应该使用BIOS调用来读取键盘以及回应键入的字符,只是证明这个程序确实在运行。
答:略。
49.写一个能通过串口连接两台Linux计算机的哑(dumb)中断程序。使用POSIX终端管理调用来配置端口。
答:略。
50.写一个客户-服务器应用程序,应答请求时能通过套接字传输一个大文件。使用共享内存的方法重新实现相同的应用程序。你觉得哪个版本性能更好?为什么?使用你写好的代码和不同的文件大小进行性能的测试,你观察到了什么?你认为在Linux内核中发生了什么导致这样的行为?
答:略。
51.实现一个基本的用户级线程库,该线程在Linux的上层运行。库的API应该包含函数调用,如mythreads_init、mythreads_create、mythreads_join、mythreads_exit、mythreads_yield、mythreads_self,可能还有一些其他的。进一步实现这些同步变量,以便用户能使用安全的并发操作:mythreads_mutex_init,ythreads_mutex_lock,mythreads_mutex_unlock。在开始前,清晰地定义API并说明每个调用的语义。接着使用简单的轮转抢占调度器实现用户级的库,还需要利用该库编写一个或更多的多线程应用程序,用来测试线程库。最后,用另一个像本章描述的Linux2.6 O(1)的调度策略替换简单的调度策略。使用每种调度器时比较你的应用程序的性能。
答:略。