众所周知,操作系统是连接计算机硬件与上层软件及用户的桥梁,它的安全性是至关重要的。虽然我们不能说Linux一定比Windows更安全,但与封闭源代码的Windows相比,开放源代码的Linux系统可以让我们方便地分析其源代码,找出其中的不足并加以修改,而不是像Windows那样,用户只能被动地接收微软公司的安全补丁。另外,从国家安全的角度来讲,中国显然不能过多地依赖西方国家,能拥有自己的操作系统一直是国人努力的方向,而开放源代码的Linux给了我们这个机会。有迹象表明,开放源代码的Linux操作系统将会在中国大有作为,所以我们需要向人们推介它,使更多的人了解Linux。对于计算机专业人员来说,更要了解和掌握Linux的内核源代码,这样才能使我们的系统开发以及提高操作系统的效率和安全性等工作更具针对性、更加有效。
(一)Linux中的进程分析
1.进程的定义
Linux系统中的进程具备下列各要素:有一段程序供其执行,这段程序不一定是进程所专有,可以与其他进程共用;程序数据,包含临时数据(如参数、返回地址、临时变量等)的进程堆栈,处理器各寄存器的状态等;具有进程专用的系统堆栈空间;在内核中具有一个称为进程控制块的task-struct数据结构。有了进程控制块,进程才能成为内核调度的一个基本单位接受内核的调度。同时,进程控制块也记录着进程所占用的各项资源,有独立的存储空间,它意味着除前述的系统空间堆栈外还有其专用的用户空间堆栈。系统空间堆栈是不能独立的,任何,进程都不能直接(不通过系统调用)改变系统空间的内容(除其本身的系统空间堆栈以外)。
所有这些要素缺一不可,共同构成一个进程的上下文。如果只具备前四条,则称为“线程”。如果完全没有用户空间,就称为“内核线程”;如果共享用户空间,则称为“用户线程”。事实上,在Linux系统中,许多进程在刚被创建时就与其父进程共用同一个存储空间,所以严格来说还是线程。但是,子进程可以建立自己的存储空间,并与父进程分开,成为真正意义上的进程。Linux系统包括下面几种类型的进程。
交互进程:是由shell控制和运行的。它既可以在前台运行,也可以在后台运行。
批处理进程:不属于某个终端,它被提交到一个队列中后便能按顺序执行。
守护进程:只有在需要时,才被唤起在后台运行。它一般在Linux启动时开始执行。
2.进程控制块的数据结构
在Linux系统中,每一个进程都有一个进程控制块task-struct结构与其相对应。Limix就是利用该结构中保存的信息来管理系统中的每个进程的。所有指向这些task-struct数据结构的指针,组成了系统中的一个进程向量数组。缺省情况下,系统的进程向量数组大小为512,它表示系统中同时最多容纳的进程为512个。每当一个新的进程创建时,一个新的task-struct结构就将分配给该进程,并同时把它添加到进程向量数组中。系统还有一个当前进程指针,用来指向正在运行的进程。
3.进程管理
每个进程都有被创建、执行某段程序以及最后消亡的过程。在Linux系统中,第一个进程是系统本身所固有的。内核在引导并完成基本的初始化以后,就有了系统的第一个进程,即第一个内核线程。除此之外,所有其他进程和内核线程都由这个原始进程或其子孙进程所创建,都是这个原始进程的子孙。
(1)进程的创建与执行
Linux将进程的创建与执行分为两步:
第一步,是从已经存在的父进程中复制出一个子进程。该子进程有自己的task_struct结构和系统空间堆栈,并与父进程共享其他所有资源。Linux为此提供了两个系统调用,一个是fork(),另一个是done()。系统调用结束时,如果被调度算法选择,新进程就可以准备运行。系统从物理内存中分配给新进程一个taskstruct数据结构和进程堆栈。新的tast-struct结构加入进程向量表中。进程还得到一个和系统中的其他进程不同的唯一的标识符。在复制进程时,Linux允许两个进程共享系统资源,包括进程要用到的文件、信号处理程序以及虚拟内存等。当资源共享时,共享资源的计数器将会增加。这样,除非所有使用该资源的进程都停止运行,否则该资源不会被删除。fork()与done()的区别在于,fork()是复制,而done()是将资源有选择地复制。另外,还有一个系统调用vfork(),它只是复制task-struct结构和系统空间堆栈。
第二步,目标程序的执行。一般来说,创建一个新的进程是因为有不同的目标程序要让新的程序去执行,所以在复制完成之后,子进程通常要与父进程分开,走自己的执行路线。Linux为此提供了一个系统调用execve(),让一个进程执行以文件形式存在的可执行程序的映像。
(2)进程的调度
Linux是一个多任务、多用户操作系统,所以,在同一时间里,尽管系统中会有很多进程存在,但却只能由一个进程处于活动状态,即占用CPU及相关资源。进程调度就是根据系统及所有进程的状态,从CPU上调出一个受阻进程,然后再从可运行进程队列中选择一个进程调入CPU,让其占有相关系统资源并开始运行。
(3)进程的终止
可以通过向进程发送信号SIGXCPU来强行终止进程,但更普通的情况是进程运行结束而自动退出。在Linux系统中,进程都是由其父进程创建的,因此也应该由其父进程将其撤销。所以,当一个进程要终止时,会利用信号通信机制,向父进程发送信号SIGCHLD(17),通知其父进程自己将要终止,父进程响应子进程发来的终止信号,并执行相应的信号处理函数。如果父进程先于其子进程终止,则其所有子进程都会自动变为init的子进程,init进程是Linux系统启动后自动生成的第一个进程。
因为进程自己不能消亡,所以它不能立刻释放自己的taskstruct结构。但是,在终止以后,进程实际上已经不能再运行,因此必须给这种既不活动又没有真正死亡的进程一种状态。为此,Linux定义了僵死状态TASK-ZOMBIE。进程终止时,自己调用exit函数,将自己的状态改为TASK-ZOMBIE,然后向父进程发送终止信号。父进程调用wait函数,回收僵死的子进程,将其从内存中彻底清除。
4.进程间通信
对于Linux这样多任务、多用户的操作系统,系统中的进程和系统内核之间,以及各个进程之间需要相互通信,以便协调它们的运行。所以,进程间通信(IPC)是一项非常重要且不可缺少的手段和机制。Linux提供了多种进程间通信机制,信号是最基本的一种,管道则是较常用的一种。
(1)信号
信号(signal)机制是Unix系统中最为传统的进程之间的通信机制。它用于在一个或多个进程之间传递异步信号。信号可以由各种异步事件产生,如键盘中断等。Shell也可以使用信号将作业控制命令传递给它的子进程。严格来说,信号这种机制并不是专门为进程间通信而设置的,它也可以用于内核与进程之间的通信(不过内核只能向进程发送信号,而不能接收信号)。一般来说,信号是对中断这个概念在软件层次上的模拟,其中信号的发送者相当于中断源,而接收者相当于处理器,所以必须是一个进程。就像在多处理器系统中,一个处理器通常能向另一个处理器发出中断请求一样,一个进程也可以向其他进程发出信号,此时信号就成了一种进程间通信的手段。
(2)管道
管道(pipe)又称为无名管道,父进程和子进程之间,或两个兄弟进程之间,可以通过系统调用建起一个单向的通信管道。但是,这种管道只能由父进程来建立,所以对子进程来说是静态的,与生俱来的。在Linux系统中,管道是通过指向同一个临时VF Sinode的两个file数据结构来实现的,此VF Sinode指向内存中的一个物理页面。每个file数据结构指向不同的文件操作进程向量,一个是实现对管道的写,另一个从管道中读。这样就隐藏了读写管道和读写普通的文件时系统调用的差别。当写入进程对管道写时,字节被拷贝到共享数据页面中;当读取进程从管道中读时,字节从共享数据页面中拷贝出来。Linux必须同步对管道的访问。它必须保证读者和写者以确定的步骤执行,为此需要使用锁、等待队列和信号等同步机制。
(3)共享内存(www.xing528.com)
共享内存机制是最有用的进程间通信方式,同时也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。采用共享内存通信的一个好处是效率高,因为进程可以直接读写内存,而不需要任何数据拷贝。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时再重新建立共享内存区域,而是保持共享区域,直到通信完毕为止。这样,数据内容一直保存在共享内存中,并没有写回文件,所以说这种机制的效率是比较高的。
(4)消息队列
一个进程可以通过系统调用建立一个消息队列,然后任何进程都可以通过系统调用向这个队列发送“消息”或从队列中接收“消息”,从而以进程间消息传递的形式实现通信。Linux维护着一个msgque消息队列链表,其中每个元素指向一个描述消息队列的msqid_ds结构。当创建新的消息队列时,系统将从系统内存中分配一个msqid_ds结构,同时将其插入msgque消息队列链表中。每个msqid_ds结构包含一个ipc-perm结构和指向已经进入此队列消息的指针。msqid_ds包含两个等待队列:一个为队列写入进程使用,而另一个由队列读取进程使用。每次进程试图向写入队列写入消息时,系统将把其有效用户和组标志符与此队列的ipc-perm结构中的模式进行比较。如果允许写入操作,则把此消息从此进程的地址空间拷贝到msg数据结构中,并放置到此消息队列尾部。由于Linux严格限制可写入消息的个数和长度,队列中可能容纳不下这个消息。此时,此写入进程将被添加到这个消息队列的等待队列中,同时调用调度管理器选择新进程运行。当有消息从此队列中释放时,该进程将被唤醒。从队列中读的过程与之类似。进程对这个写入队列的访问权限将被再次检验。读取进程将选择队列中第一个消息或者第一个某特定类型的消息。如果没有消息可以满足此要求,读取进程将被添加到消息队列的读取等待队列中,然后系统运行调度管理器。当有新消息写入队列时,进程将被唤醒继续执行。
(5)信号量
信号量与其他进程间通信方式有些不同,它主要提供对进程间共享资源访问的控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时进程也可以修改该 标志。
信号量最简单的形式是某个可以被多个进程检验和设置的内存单元。这个检验与设置操作对每个进程而言是不可中断或者说是一个原子性操作,一旦启动,谁也终止不了。检验与设置操作的结果是信号量当前值加1,这个值可以是正数,也可以是负数。根据这个操作结果,进程可能可以一直睡眠到此信号量的值被另一个进程更改为止。信号量可用来实现临界区(critical region),某一时刻在此区域内的代码只能被一个进程执行。
(6)Socket套接字
Socket与有名管道是很相似的,但其重要之处在于Socket不仅可以用来实现同一台计算机上的进程间通信,还可以用来实现分布于不同计算机中的进程通过网络进行通信。这样,就提供了一种统一的、更为一般的进程间通信模式。如果说有名管道把管道这种原来只适用近亲之间的通信手段推广到了同一台计算机中任意进程之间,则Socket又进一步将其推广到计算机网络中任意进程之间。从这个意义上讲,Socket成了最一般、最普遍适用的进程间通信手段。
(二)Linux进程间信号通信机制的分析
Linux提供了多种IPC机制,包括信号、管道、消息队列、共享内存、信号量和套接字。信号是Linux内核不可分割的一部分,在整个Linux系统中到处可见信号。如果没有信号,Linux将无法运行。
1.信号的定义
信号是进程间通信机制中的一种异步通信机制,可以看作异步通知,即告诉接收信号的进程有哪些事情发生,该进程根据所接收到的信号的值以及相关参数将会做出相应动作。进程之间可以相互发送信号,内核也可以单方向给进程发送信号。从某种角度来说,信号机制是对中断机制在软件层上的模拟,所以这时的信号也称“软中断”。
发出信号的事件被称为信号源,主要的信号源有:
进程:进程之间可以互相发送信号,用来通知事件和控制作业;
内核:内核通过信号告知进程其中发生的事件,如进程使用资源超限等;
中断:系统中的硬件设备向进程发送信号;
异常:进程运行时出现非法操作。
对信号的处理必须由接收信号的进程完成,接收进程在收到信号后允许采取以下操作:
忽略信号:接收进程而不对该信号做出反应,但信号SIGKILL和SIGSTOP除外;
阻塞信号:接收进程先把信号记录下来,等到解除阻塞以后再进行处理;
缺省处理:接收进程把信号交给内核中的缺省处理程序进行处理;
自己处理:接收进程把信号交给自己注册的处理程序进行 处理。
2.信号的种类
Linux系统中定义了一系列的信号,这些信号可以由内核产生,也可以由系统中的其他进程产生,只要这些进程有足够的 权限。
不可靠信号:Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题。因此,把那些建立在早期机制上的信号叫作不可靠信号,信号值小于SIGRTMIN(Redhat9.0中,SIGRTMIN=32,SlGRTMAX=63)的信号都是不可靠信号。
可靠信号:随着时间的推移,实践证明有必要对信号的原始机制加以改进和扩充。因此,后来出现的各种Unix版本分别在这方面进行了研究,力图实现“可靠信号”。原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。同时,信号的发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction()。POSIX.4对可靠信号机制做了标准化。但是,POSIX只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,对信号机制的实现没有作具体规定。
信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。