浅显理解*nix下的守护进程机制及fork函数

简介:

最近空闲时间重新仔细看了一下memcached的使用说明文档,硬着头皮看了一点源码,有时候看到一些晦涩的c函数感觉实在恍惚只能跳过。不过也不算是全无收获,终于LZ还敢再看c语言,终于LZ又看起了c语言,终于近期的睡眠质量明显好了很多。扯淡到此结束,下面记录一下自己的学习心得。

一、Unix Daemon Process

memcached的守护进程机制使用经典的Unix daemon模式(daemon.c),它的实现部分源码如下:

memcached daemon.c

具体工作流程处理如下:

memcachedprocessguard

看源码和处理流程图感觉好像也是平淡无奇。学校里老师讲操作系统,必然提到我们所熟知的单用户单任务操作系统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,如需转载请自行联系原作者


目录
相关文章
|
10月前
|
Shell Linux C语言
函数和进程之间的相似性
在一个C程序可以fork/exec另一个程序,其过程是先fork一个子进程,然后让子进程使用exec系列函数将子进程的代码和数据替换为另一个程序的代码和数据,之后子进程就用该程序的数据执行该程序的代码,从而达到程序之间相互调用的效果。在学了C语言、C++或是JAVA等高级语言,你会知道,在这些语言中的函数是可以相互进行见调用的,但是在学习了Linux的前面的知识后,你就会有意无意的认识到其实进程也是与函数有相同之处的,进程之间也是可以相互调用的。程序之间相互调用带来的好处之一。那么下面就将这部分内容扩展。
166 0
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
711 13
|
Linux C语言
C语言 多进程编程(三)信号处理方式和自定义处理函数
本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。
|
Linux API
Linux源码阅读笔记07-进程管理4大常用API函数
Linux源码阅读笔记07-进程管理4大常用API函数
|
编译器
【收藏】内核级利用通用Hook函数方法检测进程
【收藏】内核级利用通用Hook函数方法检测进程
|
小程序 Linux
【编程小实验】利用Linux fork()与文件I/O:父进程与子进程协同实现高效cp命令(前半文件与后半文件并行复制)
这个小程序是在文件IO的基础上去结合父子进程的一个使用,利用父子进程相互独立的特点实现对数据不同的操作
375 2
|
算法 Linux 调度
Linux进程——进程的创建(fork的原理)
Linux进程——进程的创建(fork的原理)
|
NoSQL Linux Redis
c++开发redis module问题之避免在fork后子进程中发生死锁,如何解决
c++开发redis module问题之避免在fork后子进程中发生死锁,如何解决
|
运维 JavaScript Serverless
Serverless 应用引擎产品使用合集之函数计算里中FC出现函数还没有执行完进程就关闭了是什么导致的
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。
下一篇
开通oss服务