操作系统:进程介绍 很久前我就想写这篇文章了,但总是以各种理由来拖延。操作系统是我日常工作的主要部分,特别是GNU/Linux,这篇文章主要关注GUN/Linux。 进程是个大话题,我不确定如何才能覆盖进程的所有知识点。这篇文章将会包含足够多的代码让你学会如何与进程交互。这些代码例子将会侧重于GNU/Linux系统,因为这是我最熟悉的系统。 那么,什么是进程呢?Linux信息项目(The Linux Information)把进程定义为“程序的一个执行(即,运行)实例”。所以,要定义进程我们先要定义什么是程序。再次根据Linux信息项目里的定义,“程序是内存里的一个可执行文件。” 所以,我们知道进程是正在运行的程序的一部分。这是否意味着进程一定是在运行中的?不一定。 进程状态 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。 下面的状态在 fs/proc/array.c 文件里定义:
运行状态(running)并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。睡眠状态(sleeping)意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT信号让进程继续运行。 例如,可以用下面的方法来停止或继续运行进程:
可以使用gdb终止进程来实现跟踪终止状态。如果我没有记错的话,这个状态和终止状态基本上是一样的。 死亡状态是内核运行 kernel/exit.c 里的 do_exit() 函数返回的状态。这个状态只是一个返回状态,你不会在任务列表里看到这个状态。 僵死状态(Zombies)是一个比较特殊的状态。有些人认为这个状态是在父进程死亡而子进程存活时产生的。实际上不是这样的。父进程可能已经死了但子进程依然存活着,那个子进程的父进程将会成为init进程,pid 1。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。 这里有一个创建维持30秒的僵死进程例子:
Linux进程状态是一篇非常棒的文章,它使用代码例子来讲述进程状态并使用 ptrace 来控制它。 进程包含了什么信息? 我简要地提过进程表,我将会在这解释什么是进程表。进程表是Linux内核的一种数据结构,它会被装载到RAM里并且包含着进程的信息。 每个进程都把它的信息放在 task_struct 这个数据结构里,task_struct 包含了这些内容:
保存进程信息的数据结构叫做 task_struct,并且可以在 include/linux/sched.h 里找到它。所有运行在系统里的进程都以 task_struct 链表的形式存在内核里。 进程的信息可以通过 /proc 系统文件夹查看。要获取PID为400的进程信息,你需要查看 /proc/400 这个文件夹。大多数进程信息同样可以使用top和ps这些用户级工具来获取。 进程执行 当进程执行时,它会被装载进虚拟内存,为程序变量分配空间,并把相关信息添到task_struct里。 进程内存布局分为四个不同的段:
这里有两种创建进程的方法,fork()和execve()。它们都是系统调用,但它们的运行方式有点不同。 要创建一个子进程可以执行fork()系统调用。然后子进程会得到父进程中数据段,栈段和堆区域的一份拷贝。子进程独立可以修改这些内存段。但是文本段是父进程和子进程共享的内存段,不能被子进程修改。 如果使用execve()创建一个新进程。这个系统调用会销毁所有的内存段去重新创建一个新的内存段。然而,execve()需要一个可执行文件或者脚本作为参数,这和fork()有所不同。 注意,execve()和fork()创建的进程都是运行进程的子进程。 进程执行还有很多其他的内容,比如进程调度,权限许可,资源限制,库链接,内存映射… 然而这篇文章由于篇幅限制不可能都讲述,以后访问可能会加上 进程间通信(IPC) 为了进程间的通信,存在两个解决方法,共享内存,消息传递。 在共享内存的方案里,为了几个进程间能够通信创建了一个共享的区域。这个区域能被多个进程同时访问。这种方法通常在使用线程时使用。这是实现IPC最快的形式,因为这种形式只涉及到内存的读写。 但是,这需要进程在访问共享内存时受到的限制和访问内核实现的其他进程内存一样。 共享内存段的使用情况可以使用ipcs -m命令查看。 实现一个共享内存的服务器程序,代码如下:
通过把 *shmaddr = ‘a’; 替换为 printf(“Segment: %sn”, shmaddr) ,你将会得到一个客户端程序并且能够读取共享内存段的数据。 运行 ipcs -m 将会输出服务共享内存段的信息:
共享内存段可以使用 ipcrm 命令移除。要了解更多的共享内存实现IPC,可以阅读Beej的共享内存段教程。 其他实现IPC的方法有文件,信号,套接字,消息队列,管道,信号灯和消息传递。这些方法我不可能全部都深入讲解,但我觉得信号和管道的方法我需要提供一些有趣的例子。 信号 介绍进程状态时,我们已经看了一个使用kill命令的信号示例。信号是把事件或者异常的发生通知进程的软件中断。 每个信号都有一个整型标识,但通常使用 SIGXXX 来描述信号,例如 SIGSTOP 或者 SIGCONT 。内核使用信号来通知进程事件的发生,进程也可以使用kill()系统调用发送信号给进程。接收信号的进程可以忽略信号,被杀死,或者被挂起。可以使用信号处理器来处理信号并且在信号出现时任意处理信号。SIGKILL 这个特殊的信号不能被捕获(处理器处理),要杀死一个挂起的进程时可以使用这个信号。不要把 SIGKILL 和 SIGTERM 混淆了,当使用 Ctrl+C 或者 kill <PID> 杀死进程时默认会发送SIGKILL 信号。 SIGTERM 不会强制杀死进程并且它可以被捕获,使用 SIGTERM 的进程通常可以被清理。 管道 管道用来把一个进程的输出连接到另外一个进程的输入。这是实现IPC最古老的方法之一。普通管道是单向通信的, 它有一个单向流。可以使用pipe() 创建一个管道,管道和Linux的其他对象一样,都被看成文件对象。 命名管道是普通管道的增强版,它是双向通信的并且可以实现管道的多进程读写。这都是普通管道不能实现的。无论有没有进程对命名管道进行读写,它都会实际存在。命名管道在文件系统里以特殊设备文件存在。在GNU/Linux里,命名管道也被称为FIFOs(先进先出,First In First Out)。 这里有一个创建命名管道的例子:
在运行目录里,我们会看到myfifo文件。它的信息和下面的类似:
以上就是进程的基本介绍。写得越多我就越意识到进程有太多东西要讲了。从哪里开始讲进程和把不需要覆盖的知识划分出来,这是个很艰难的决定。共享内存段是我没有很好地规划好的一部分。回看进程间通信那部分是很有趣的。此外,因为有大量诸如Linux编程接口和操作系统概念的好资源,使我们更容易回归概念思考。 参考 下面的资源用来加深对这个领域知识的理解。如果你想学习关于操作系统的更多内容,一定要看看这些书,虽然书很厚但是值得你阅读。 本文由 伯乐在线 - 进林 翻译,sunbiaobiao 校稿。 |