最近空闲时间重新仔细看了一下memcached的使用说明文档,硬着头皮看了一点源码,有时候看到一些晦涩的c函数感觉实在恍惚只能跳过。不过也不算是全无收获,终于LZ还敢再看c语言,终于LZ又看起了c语言,终于近期的睡眠质量明显好了很多。扯淡到此结束,下面记录一下自己的学习心得。
一、Unix Daemon Process
memcached的守护进程机制使用经典的Unix daemon模式(daemon.c),它的实现部分源码如下:
memcached daemon.c
具体工作流程处理如下:
看源码和处理流程图感觉好像也是平淡无奇。学校里老师讲操作系统,必然提到我们所熟知的单用户单任务操作系统PC DOS(Disk Operating System), 单用户单任务操作系统是指一台计算机同时只能有一个用户在使用,该用户一次只能提交一个作业,一个用户独自享用系统的全部硬件和软件资源。和DOS截然不同的是...哎,说来话长,Unix在二十世纪七十年代就发明了fork函数,真正实现多任务的操作系统,这样Unix系统可以多人多任务地并行处理。所以站在操作系统发展的历史角度,每个看上去很简单的逻辑都是非常牛的跨越,还是觉得很玄乎,这里有必要抄一段daemon的概念来给自己解惑一下。
1、什么是Daemon Process(守护进程)
Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。*nix系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等(看到这里会不会产生“这tmd不就是windows服务”的错觉?)。守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
2、工作原理
*nix守护进程的工作模式是服务器/客户机(Server/Client),服务器在一个特定的端口上监听(Listen)等待客户连接,连接成功后服务器和客户端通过端口进行数据通信。守护进程的工作就是打开一个端口,并且监听(Listen)等待客户连接。如果客户端产生一个连接请求,守护进程就创建(fork)一个子服务器响应这个连接,而主服务器继续监听其他的服务请求。
看过上面的两点阐述和说明,熟悉windows的朋友一定会联想到windows服务。虽然windows下子进程的概念很少有人提,但它也确实是存在的,至于windows内部是不是通过fork实现子进程的,只能恕我孤陋寡闻了。
二、fork函数
从memcached的处理流程上可以看到实现守护进程机制的第一步是先fork一个子进程。看过园子里T2写的一篇文章讲fork的一道编程题,印象非常深刻。下面可以看一个广为流传的更简单直接的讲解fork函数的例子:
aboutfork
这个函数最牛的地方是,表面上它的条件判断只能有一个为真(即只能打印(printf)一次),实际的输出让不熟悉*nix的普通开发人员如区区在下感到非常不解,在linux下运行,它的实际输出为:
i am the child process, my process id is 3279
i am the parent process, my process id is 3278
上面的输出可能在不同的linux内核(kernel)实现上输出顺序也不一样,但是总是输出两行。开始笔者一下子也是想不到为什么两行都打印出来,因为从常规程序运行角度理解,一个进程顺序执行,不管pid是多少,都应该只打印一行才对,可是fork(分叉)函数却可以打破我们这种思维定势。下面摘抄一段fork函数及子进程工作原理:
由fork 创建的新进程被称为子进程(该进程几乎是当前进程的一个完全拷贝),fork 函数被调用一次,但是返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的ID。将子进程的ID 返回给父进程的理由是:一个进程可以有多个子进程,并且没有该进程几乎是当前进程的一个完全拷贝函数是一个进程获得其子进程的进程ID。fork 在子进程中返回0的理由是:一个进程只能有一个父进程,并且可以通过getppid 函数获得其父进程的ID。子进程和父进程继续执行fork 调用后的指令,子进程是父进程的副本。例如:子进程可获得父进程的数据空间、堆和栈的副本。
上面这段话看上去好像比较深奥,实际上,如果我们学习过操作系统和c语言,应该能够读懂它的大致意思,当然这里必须要正确理解父进程和子进程也就是进程的概念。
那么什么是进程呢?
我们可能已经看到过很多面试题和参考书在讨论什么是进程和线程以及二者之间的关系。这里也不能免俗,顺带提一下进程的概念加深大家的理解:一个进程在内存里有三部分数据,即"代码段"、"堆栈段"和"数据段",这三个部分是构成一个完整的可执行单元的必要的组成。"代码段"就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用相同的代码段。"堆栈段"存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(如用malloc之类的函数取得的空间)。
随着开发语言的发展,现在有很多的应用都是通过有比较完善的内存管理机制(比如.NET的托管环境(CLR)、Java虚拟机等)的高级语言开发的,所以普通开发者平时对内存分配和管理几乎不用怎么费心,所以有些底层基本的东西理解的也不是很清楚。但是实际上高级语言最终还是必须要经过类似IL(MS中间语言)或者二进制字节码(bytecode)“翻译”成原生代码(native code)通过CPU执行,所以本质上我们通过高级语言编写的程序执行时还是需要在内存里维护"代码段"、"堆栈段"和"数据段"这三部分。
哪位就要说了,既然进程需要内存维护这三部分,创建子进程不就需要多分配内存多维护数据了吗?fork的系统开销不会非常大吗?哈哈,这个也正是我的疑问,这里由于本人非常不熟悉*nix,虽然道听途说看到了一些解释,但是自我感觉理解的还不是太透彻,这里就暂时保留自己的看法,希望有心的你能够查阅一些资料深入学习一下。
下面来看点看上去及其简单的据传说可以很快搞死操作系统的c函数:
void main() { for( ; ; ) fork(); }
系统是怎么被搞死的呢?据说原理就是这个程序什么也不做,就是死循环地fork,让程序不断产生进程,而这些进程又不断产生新的进程…其结果是系统产生了很多的进程,直到资源不足而崩溃。
其实这个死循环在我看来还不是最牛的。分析上面的代码,for循环一次产生一个子进程,循环两次产生两个子进程…其实进程个数还是按照代数级数增长。想象一下如果有个函数执行一次,然后能够让进程产生子进程,子进程产生孙子进程……如此递归执行,进程按照指数规模膨胀,这个难道就是传说中的fork炸弹吗(这个函数应该怎么编写呢?网上看到一个,很简短,欢迎您的解答)?
总体感觉fork的运行机制还是非常好玩的。实际上现在最让我感兴趣的是多进程之间如何像多线程编程一样实现数据共享和同步。不管是多进程还是多线程,数据共享和同步(通信)一直是个头等难题,有时间一定抽空再好好整理总结一下。
本文转自JeffWong博客园博客,原文链接:http://www.cnblogs.com/jeffwongishandsome/archive/2011/10/26/2224464.html,如需转载请自行联系原作者