Linux多任务编程

Linux下多任务介绍

首先,先简单的介绍一下什么叫多任务系统?任务、进程、线程分别是什么?它们之间的区别是什么?,从而可以宏观的了解一下这三者,然后再针对每一个仔细的讲解。

什么叫多任务系统?多任务系统指可以同一时间内运行多个应用程序,每个应用程序被称作一个任务

任务定义:任务是一个逻辑概念,指由一个软件完成的任务,或者是一系列共同达到某一目的的操作。

进程定义:进程是指一个具有独立功能的程序在某个数据集上的一次动态执行过程,它是系统进行资源分配和调度的最小单元。

线程定义:线程是进程内独立的一条运行路线,是处理器调度的最小单元,也可以成为轻量级进程。

看了定义,有点晕,还是通俗的说一下它们的区别吧。①通常一个任务是一个程序的一次执行,一个任务包含一个或多个完成独立功能的子任务,这个独立的子任务就是进程或线程。②一个进程可以拥有多个线程,每个线程必须有一个父进程。

任务

任务是一个逻辑概念,指由一个软件完成的任务,或者是一系列共同达到某一目的的操作。通常一个任务是一个程序的一次执行,一个任务包含一个或多个完成独立功能的子任务,这个独立的子任务就是进程或线程。例如,一个杀毒软件的一次运行是一个任务,目的是从各种病毒的侵害中保护计算机系统,这个任务包含多个独立功能的子任务(进程或线程),包括实时监控功能、定时查杀功能、防火墙功能及用户交互功能等。任务、进程和线程之间的关系如图1所示

进程

进程的基本概念

进程是指一个具有独立功能的程序在某个数据集上的一次动态执行过程,它是系统进行资源分配和调度的基本单元。一次任务的运行可以并发激活多个进程,这些进程相互合作来完成该任务的一个最终目标。

进程具有并发性、动态性、交互性、独立性和异步性等主要特性。

进程和程序是有本质区别的:程序是静态的一段代码,是一些保存在非易失性存储器的指令的有序集合,没有任何执行的概念;而进程是一个动态的概念,它是程序执行的过程,包括动态创建、调度和消亡的整个过程,它是程序执行和资源管理的最小单位。

Linux下的进程结构

进程不但包括程序的指令和数据,而且包括程序计数器和处理器的所有寄存器及存储临时数据的进程堆栈,因此,正在执行的进程包括处理器当前的一切活动。

因为linux是一个多任务多进程的操作系统,所以其他的进程必须等到系统将处理器使用权分配给自己之后才能运行。当正在运行的进程等待其他的系统资源时,linux内核将取得处理器的控制权,并将处理器分配给其他正在等待的进程,他按照内核中的调度算法决定将处理器分配给哪一个进程,也就是说,内核不会让处理器闲着。

内核将所有进程存放在双向循环链表(进程链表)中,其中链表的头是 init_task 描述符。链表的每一项都是类型为 task_struct,称为进程描述符的结构,该结构包含了一个进程相关的所有信息,定义在<include/linux/sched.h>文件中。task_struct内核结构比较大,它能完整的描述一个进程,如进程的状态、进程的基本信息、进程标识符、内存相关信息、父进程相关信息、与进程相关的终端信息、当前工作目录、打开的文件信息、所接收的信号信息等。

下面详细讲解task_struct结构中最为重要的两个域:state(进程状态)和pid(进程标识符)。如果想具体了解task_struct,请点这里。

(1)进程状态

Linux中的进程有以下几种状态。

● 运行状态(TASK_RUNNING):进程当前正在运行,或者正在运行队列中等待调度。

● 可中断的阻塞状态(TASK_INTERRUPTIBLE):进程处于阻塞(睡眠)状态,正在等待某些事件发生或能够占用某些资源。处在这种状态下的进程可以被信号中断。接收到信号或被显式的唤醒呼叫(如调用 wake_up 系列宏:wake_up、wake_up_interruptible等)唤醒之后,进程将转变为 TASK_RUNNING 状态。

● 不可中断的阻塞状态(TASK_UNINTERRUPTIBLE):此进程状态类似于可中断的阻塞状态(TASK_INTERRUPTIBLE),只是它不会处理信号,把信号传递到这种状态下的进程不能改变它的状态。在一些特定的情况下(进程必须等待,直到某些不能被中断的事件发生),这种状态是很有用的。只有在它所等待的事件发生时,进程才被显示的唤醒呼叫唤醒。

● 可终止的阻塞状态(TASK_KILLABLE):该状态的运行机制类似于TASK_UNINTERRUPTIBLE,只不过处在该状态下的进程可以响应致命信号。它可以替代有效但可能无法终止的不可中断的阻塞状态(TASK_UNINTERRUPTIBLE),以及易于唤醒但安全性欠佳的可中断的阻塞状态TASK_INTERRUPTIBLE)。

● 暂停状态(TASK_STOPPED):进程的执行被暂停,当进程收到 SIGSTOP、SIGSTP、SIGTTIN、SIGTTOU等信号时,就会进入暂停状态。

● 跟踪状态(TASK_TRACED):进程的执行被调试器暂停。当一个进程被另一个监控时(如调试器使用ptrace()系统调用监控测试程序),任何信号都可以把这个进程置于跟踪状态。

● 僵尸状态(EXIT_ZOMBIE):进程运行结束,父进程尚未使用 wait 函数族(如调用 waitpid()函数)等系统调用来“收尸”,即等待父进程销毁它。处在该状态下的进程“尸体”已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的推出状态等信息供其他进程收集。

● 僵尸撤销状态(EXIT_DEAD):这是最终状态,父进程调用 wait 函数族“收尸”后,进程彻底由系统删除。

它们之间的转换关系如图2所示:

进程可以使用 set_task_state 和 set_current_state 宏来改变指定进程的状态信息和当前进程的状态。

(2)进程标识符

Linux内核通过唯一的进程标识符 PID 来标识每个进程(就和文件描述符一样)。PID存放在进程描述符的 pid 字段中,新创建的 PID 通常是前一个进程的 PID 加1,不过PID的值有上限(最大值=PID_MAX_DEFAULT-1,通常为32767),读者可以查看/proc/sys/kernel/pid_max 来确定该系统的进程数上限。

当系统启动后,内核通常作为某一个进程的代表。一个指向task_struct的宏current用来记录正在运行的进程。current经常作为进程描述符结构指针的形式出现在内核代码中,例如,current->pid 表示处理器正在执行的进程的PID。当系统需要查看所有的进程时,则调用for_each_process()宏,这将比系统搜索数组的速度要快的多。

在Linux中获得当前进程号的(PID)和父进程号(PPID)的系统调用函数分别为 getpid() 和 getppid()。

进程的创建、执行、终止

(1)进程的创建和执行

咱们首先得知道啥是创建,啥是执行哈!我刚开始看的时候没懂。创建进程就是产生一个新的进程,这个大家都知道。而进程的执行,前边讲进程的的定义的时候,就说了正在运行的子任务,说白了,进程执行也就是让产生的这个进程干点什么事,别占着那啥不拉那啥。

许多操作系统提供的都是产生进程的机制,也就是说,首先在新的地址空间里创建进程、读入可执行文件,最后再开始执行。Linux 中进程的创建很特别,它把上述的步骤分解到两个单独的函数中去执行:fork()函数和exec函数族。首先,fork()函数通过复制当前进程创建一个子进程(注意此时资源还没有被复制过来,去了解一下写时复制页技术吧),子进程于父进程的区别仅仅在于不同的PID、PPID和某些资源及统计量。exec函数族负责读取可执行文件并将其载入地址空间开始运行。

(2)进程的终止

进程终结也需要很多繁琐的工作,系统必须保证回收进程所占用的资源,并通知父进程。Linux首先把终止的进程设置为僵尸状态,这时,进程无法投入运行,它的存在只为父进程提供信息,申请死亡。父进程得到信息后,开始调用 wait 函数族,最后终止子进程,子进程占用的所有资源被全部释放。

进程的内存结构

Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该地址空间是大小为 4GB的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理地址),而且,更重要的是,用户程序可以使用比实际物理内存更大的地址空间。

4GB的进程地址空间会被分成两个部分:用户空间与内核空间。用户地址空间是从0到3GB(0xC000 0000),内核地址空间占据3GB到4GB。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。只有用户进程使用系统调用(代表用户进程在内核态执行)时可以访问到内核空间。每当进程切换时,用户空间就跟着变化;而内核空间由内核负责映射,它不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,用户进程各自有不同的页表。每个进程的用户空间都是完全独立、互不相干的。进程的虚拟内存空间如图3所示,其中用户空间包括以下几个功能区域:

● 只读段: 包含程序代码(.init和.text)和只读数据(.rodata)。

● 数据段: 存放的是全局变量和静态变量。其中可读可写数据段(.data)存放已初始化的全局变量和静态变量,BSS数据段(.bss)存放未初始化的��局变量和静态变量。

● 堆: 由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等。

● 堆栈: 存放动态分配的数据,一般由程序员动态分配和释放。若程序员不释放,程序结束时可能由操作系统回收。

● 共享库的内存映射区域: 这是Linux动态链接器和其他共享代码库代码的映射区域。

 

由于在Linux系统中每一个进程都会有/proc文件系统下与之对应的一个目录(如将init进程的相关信息在/proc/1 目录下的文件中描述),因此通过 proc 文件系统可以查看某个进程的地址空间的映射情况。例如,运行一个应用程序,如果它的进程号为13703,则输入“ cat /proc/13703/maps”命令,可以查看该进程的内存映射情况。

线程

前面已经讲到,进程是系统中程序执行和资源分配的基本单位。每个进程都拥有自己的数据段、代码段和堆栈段,这就造成了进程在进行切换等动作时需要较复杂的上下文切换等动作。为了进一步减少处理机的空闲时间,支持多处理器及减少上下文切换开销,进程在演化中出现了另一个概念---线程。它是进程内独立的一条运行路线,是处理器调度的最小单元,也可以称为轻量级线程。线程可以对进程的内存空间和资源分配进行访问,并与同一进程中的其他线程共享。因此,线程的上下文切换的开销比创建进程小得多。

一个进程可以拥有多个线程,每个线程必须有一个父进程。线程不拥有系统资源,它只具有运行时所必须的一些数据结构,如堆栈/寄存器与线程控制块(TCB),线程与其父进程的其他进程共享该进程所拥有的全部资源。要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他进程带来影响。由此可知,多线程中的同步是一个非常重要的问题。在多线程系统中,进程与线程的关系如图4所示

在Linux系统中,线程分为3种:①用户线程 ②轻量级线程 ③内核线程



fork()函数

在 Linux 中创建一个新进程的唯一方法是使用fork()函数。fork()函数是 Linux 系统中一个非常重要的函数,和咱们以前遇到过的函数由一些区别,因为它看起来执行一次却返回两个值,这又作何解释?不着急,慢慢看。

函数说明

fork()函数用于从已存在的一个进程中创建一个新的进程,新进程称为子进程,而原进程称为父进程。使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。

因为子进程几乎是父进程的完全复制,所以父子进程会运行同一个程序。这就需要用一种方式来区分它们,并使它们照此运行,否则,这两个进程不可能做不同的事。

实际上是在父进程中执行fork()函数时,父进程会复制出一个子进程,而且父子进程的代码从fork()函数的返回开始分别在两个地址空间中同时运行,从而使两个进程分别获得其所属fork()函数的返回值,其中在父进程中的返回值是子进程的进程号,而在子进程中返回0。因此,可以通过返回值来判定该进程是父进程还是子进程。

同时可以看出,使用fork()函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部分内容,使得 fork()函数的系统开销比较大,而且执行速度页不是很快。

函数语法

下表列出了 fork() 函数的语法要点

 

 

基础实验

如下为fork()函数的基础实验程序

程序代码我上传到资源,可以自己下载,点此下载

 

将它编写成C文件,然后输入命令:gcc fork.c -o fork

接着执行命令:./fork,就可以看到如下图的成功执行结果

当然,你也可以使用命令:arm-linux-gcc fork.c -o fork 交叉编译后下载到板子上运行也可。



exec函数族

函数族说明

fork() 函数用于创建一个新的子进程,该子进程几乎复制了父进程的全部内容,但是,这个新创建的子进程如何执行呢?exec 函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行的脚本文件。

在 Linux 中使用exec函数族主要有两种情况:

● 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用 exec 函数族中的任意一个函数让自己重生。

● 如果一个进程想执行另一个程序,那么它就可以调用 fork() 函数新建一个进程,然后调用 exec 函数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常普遍)。

函数族语法

实际上,在Linux中并没有exec()函数,而是由6个以 exec 开头的函数,它们之间的语法有细微差别。下表列出了 exec 函数族的6个成员函数的语法:

这6个函数在函数名和使用语法的规则上都有细微的区别,下面就从可执行文件查找方式、参数传递方式和环境变量这几个方面进行比较。

● 查找方式:表1中的前4个函数的查找方式都是完整的文件目录路径,而最后两个函数(也就是以 p 结尾的两个函数)可以只给出文件名,系统就会自动按照环境变量“$PATH” 所指定的路径进行查找。

● 参数传递方式:exec函数族的参数传递有两种:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。在这里是以函数名的第5位字母来区分的,字母为 "l"(list)的表示逐个列举参数的方式,其语法为const char *arg;字母为“v”(vector)的表示将所有参数整体构造指针数组传递,其语法为 char *const argv[]。这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的是,这些参数必须以NULL结束。

● 环境变量: exec函数族可以默认系统的环境变量,也可以传入指定的环境变量。这里以 “e”(environment)结尾的两个函数 execle()和 execve()就可以在 envp[]中指定当前进程所使用的环境变量。

表2再对这6个函数中的函数名和对应语法做了一个小结,主要指出了函数名中每一位对应所表明的含义,以此表加以记住这6个函数。

事实上,这6个函数中真正的系统调用只有execve(),其他5个都是库函数,它们最终都会调用execve()这个系统调用。在使用exec函数族时,一定要加上错误判断语句。exec 很容易执行失败,其中最常见的原因有:

① 找不到文件或路径,此时 errno 被设置为 ENOENT。

② 数组argv 和envp 忘记用NULL结束,此时,errno被设置为 EFAUL。

③ 没有对应可执行文件的运行权限,此时 errno 被设置为EACCES。

基础实验

实验1

本实验是为了说明如何使用文件名来查找可执行文件,同时使用参数列表的方式。这里用的函数是 execlp()。程序代码如下:

在该程序中,首先使用 fork()函数创建一个子进程,然后在子进程中使用 execlp()函数。可以看到,这里的参数列表列出了在 shell 中使用的命令名和选项,并且当使用文件名进行查找时,系统会在默认的环境变量PATH中寻找该可执行文件。

使用命令:gcc execlp.c -o execlp编译后,然后再执行,结果如下图:

使用env命令,可以查看到环境变量的路径名

此程序的执行结果与在shell中直接输入命令“ps -ef”是一样的,当然,在不同系统的不同时刻可能会有不同的结果。

实验2

本实验实现的功能和实验1的一样,不同的是使用的函数不同。本实验将使用完整的文件目录来查找对应的可执行文件。注意,目录必须以“/”开头,否则将其视为文件名。程序代码如下:

编写保存源文件,然后使用命令:gcc execl.c -o execl编译,接着执行命令:./execl,可以看到实验结果和实验1一样

实验3

本实验是利用execle()函数将环境变量添加到新建的子进程中,这里的“env”是查看当前进程环境变量的命令,实验代码如下:

编写保存源文件后,使用命令:gcc execle.c -o execle,再执行命令:./execle,执行结果如下图

实验4

本实验实现功能和实验3一样,不同的是使用的execve()函数,通过构造指针数组的方式来传递参数,注意参数列表一定要以NULL作为结尾标识符,实验代码如下:

编写保存源文件,使用命令:gcc execve.c -o execve,再执行命令:./execve,结果如下:

 

到此关于进程的执行实验完毕,下一节学习进程的终止



exit()和_exit()函数

函数说明

创建进程使用fork()函数,执行进程使用exec函数族,终止进程则使用exit()和_exit()函数。当进程执行到exit()或_exit()函数时,进程会无条件的停止剩下的所有操作,清除各种数据结构,并终止本进程的运行。但是,这两个函数还是有区别的,其调用过程如图1所示:

从图1可以看出,_exit()函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构;而exit()函数则在这些基础上做了一些包装,在执行退出之前加了若干道工序。exit()函数和_exit()函数的最大区别就在于exit()函数在终止当前进程之前要检查该进程打开过哪些文件,把文件缓冲区中的内容写回文件,也就是图1中的“清理I/O缓冲”一项。

在Linux的标准函数库中,有一种被称作“缓冲I/O(buffered I/O)”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。

每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件时,也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等,最典型的就是咱们的vim中使用的:w命令),再将缓冲区中的内容一次性写入文件。

这种技术大大增加了文件读写的速度,但也给咱们的编程带来了一些麻烦。比如有些数据你认为已经被写入到文件中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内,这时用_exit()函数直接将进程关闭掉,缓冲区中的数据就会丢失。因此,若想保证数据的完整性,最好使用exit()函数。

函数语法

下表列出了exit()和_exit()函数的语法要点:

基础实验

以下两个基础实验1比较了exit()和_exit()函数的区别。由于 printf()函数使用的是缓冲I/O方式,该函数在遇到“\n”换行符时自动从缓冲区中将记录读出,以下两个基础实验就是利用这个性质来进行比较的。以下为实验1的代码:

执行结果如下图

从输出的结果可以看到,调用exit()函数时,缓冲区中的记录也能正常输出。

实验2的代码如下:

执行结果如下图:

从最后的结果可以看到,调用_exit()函数无法输出缓冲区中的记录。

如果在实验2中的代码中的 第二个代码加上回车符,那么结果会有不同了。自己试试哈!



wait()和waitpid()

函数说明

wait()函数用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或者该进程接收到了一个指定的信号为止。如果该父进程没有子进程或者它的子进程已经结束,则wait()函数就会立即返回。

waitpid()的作用和wait()一样,但它并不一定要等待第一个终止的子进程(它可以指定需要等待终止的子进程),它还有若干选项,如可提供一个非阻塞版本的 wait()功能,也能支持作业控制。实际上,wait()函数只是 waitpid()函数的一个特例,在Linux 内部实现 wait()函数时直接调用的就是waitpid()函数。

函数格式

下图为wait()函数的格式

下图为waitpid()函数的格式

 

基础实验

实验1

本实验中首先使用fork()创建一个子进程,然后让其子进程暂停5s(使用了sleep()函数)。接下来对原有的父进程使用waitpid()函数,并使用参数WNOHANG是该父进程不会阻塞。若有子进程退出,则waitpid()返回子进程号;若没有子进程退出,则waitpid()返回0,并且父进程每隔1s循环判断一次。该程序的流程图如下:

程序源代码我上传到网站,可以免费下载waitpid.c文件,点此下载

下载文件后,使用命令:gcc waitpid.c -o waitpid

然后执行命令:./waitpid 结果如下图;

从输出结果就可以看出程序的执行流程。先执行一次父进程,父进程睡眠1s,此时执行子进程,然后子进程睡眠5秒;接着再执行父进程监听。哎哟我去我不分析流程了,怕再说迷糊了。

实验2

本实验使用函数wait(),同实验2一样,也是先用fork()新建一个子进程,然后让子进程暂停5s。接下来对原有的父进程使用wait()函数。不同的是,wait()函数会使得父进程阻塞,通过本实验的结果就可以看出。代码如下:

执行结果如下图

我建议你亲自实验一下,能很明显的看出不同。



实验目的

通过编写多进程程序,熟练掌握fork()、exec()、wait()和waitpid()等函数的使用,进一步理解在Linux中多进程编程的步骤。

实验内容

该实验有3个进程,其中一个为父进程,其余两个是该父进程创建的子进程,其中一个子进程运行“ls -l”指令,另一个子进程在暂停5s后异常退出。父进程先用阻塞方式等待第一个进程的结束,然后用非阻塞方式等待另一个子进程的退出,待收集到第2个子进程结束的消息后,父进程就返回。

实验步骤

该实验的流程图如下

实验源代码

先看一下下面的代码,这个程序能得到我们所希望的结果吗?它的运行会产生几个进程?

执行结果如下图

也有可能会出现下面的结果

分析执行结果可以指知道,这里其实是产生了3个子进程,在子进程2中又产生了一个子进程1。

下面咱们贴出正确的实验代码:

执行结果如下图

不论程序执行多少次,结果都是一样的。对比上一个结果,咱们就可以理解多进程该怎么创建了。一定要明确创建子进程的父进程是哪一个。



守护进程概述

守护进程,又叫daemon进程(不知怎的,我突然想起来吸血鬼日记中的达蒙了,很好看的美剧),是Linux中的后台服务进程。他是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或者等待处理某些发生的事件。守护进程常常在系统引导载入时启动,在系统关闭时终止。Linux有很多系统哦服务,大多数服务都是通过守护进程实现的。同时,守护进程还能完成许多系统任务,例如,作业规划进程cronf、打印进程lqd等(这里的结尾字母 d 就是 daemon的意思)。

在Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端称为这些进程的控制终端,当控制终端关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到接收到某种信号或者整个系统关闭时才退出。如果想让某个进程不因为用户、终端或者其它的变化而受到影响,那么就必须把这个进程变成一个守护进程。可见,守护进程是非常重要的。

编写守护进程步骤

编写守护进程遵循一个特定的流程,下面就说一下守护进程的创建步骤。

1、创建子进程,父进程退出。

这是编写守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在shell终端造成一种程序已经运行完毕的假象,之后的所有工作都在子进程中完成,而用户在shell终端则可以执行其他的命令,从而在形式上做到与控制终端的脱离。

但是,父进程创建了子进程后退出,此时该子进程不就没有父进程了吗?守护进程中确实会出现这么一个有趣的现象:由于父进程已经先于子进程退出,就会造成子进程没有父进程,从而变成一个孤儿进程。在Linux中,每当系统发现一个孤儿进程时,就会自动由1号进程(也就是 init 进程)收养它,这样原先的子进程就会变成 init 进程的子进程。其关键代码如下;

2、在子进程中创建新会话

这个步骤是创建守护进程最重要的一步,虽然实现非常简单,但意义却非常重大。在这里使用的是系统函数 setsid(),在具体介绍 setsid()之前,先了解以下两个概念:进程组和会话期。

● 进程组。进程组是一个或多个进程的集合。进程组由进程组ID来唯一标识。除了进程号PID之外,进程组ID也是一个进程的必备属性。每隔进程组都有一个组长进程,其组长进程的进程号PID等于进程组ID,且该进程组ID不会因为组长进程的退出而受到影响。(组长没了,再找个组员来担任组长呗)

● 会话期。会话组是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。进程组和会话期之间的关系如图1所示:

接下来具体介绍 setsid()的相关内容。

① setsid()函数的作用。setsid()函数用于创建一个新的会话组,并让执行此函数的进程担任该会话组的组长。调用setsid()有以下3个作用:

● 让进程摆脱原会话的控制

● 让进程摆脱原进程组的控制

● 让进程摆脱原控制终端的控制

那么,回过头来想想,在创建守护进程时为什么要调用 setsid()函数呢?是这样的,在创建守护进程的第一步中,调用了fork()函数创建子进程再令父进程退出。由于在调用 fork()函数时,子进程全盘复制了父进程的会话期、进程组和控制终端等,虽然父进程退出了,但原先的会话期、进程组和控制终端等并没有改变,因此,还不是真正意义上的独立。而setsid()函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

② setsid函数格式

3、改变当前目录为根目录

这一步也是必要的步骤。使用fork()创建的子进程继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统(如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦(如系统由于某种原因需要进入单用户模式)。因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述问题。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir()。

4、重设文件权限掩码

文件权限掩码是指屏蔽掉文件权限中的对应位。例如,有一个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork()函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask()。通常的使用方法为umask(0)。

5、关闭文件描述符

同文件权限掩码一样,用fork()函数新建的子进程会从父进程那里继承一些已经打开的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法被卸载。

事实上,在上面的第2步之后,守护进程已经与所属的控制终端失去了联系,因此,从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf())输出的字符也不可能在终端上显示出来。所以文件描述符为0,1和2的3个文件(常说的输入/输出和报错这3个文件)已经失去了存在的价值,也应该被关闭。通常了,按如下方式关闭文件描述符:

有关getdtables()的作用请看博客:

到这里,一个简单的守护进程就建立起来了。创建守护进程的流程图如图所示:

基础实验

本实验按照以上的创建流程建立了一个守护进程,然后让守护进程每隔10s向日志文件/home/song/tmp/daemon.log 写入一句话。程序代码如下,我也上传到网站,点此下载

我们先看一下 /tmp文件夹下是没有daemon.log的

下载文件后,使用命令编译:gcc dameon.c -o daemon

然后执行命令: ./daemon 你可以看到此时没有看到有什么变化

使用命令:ps -ef|grep ./daemon 利用ps中的关键字来查看系统当前正在运行的进程中,有没有咱们的daemon进程

可以看到咱们的守护进程已经在运行了,再来看看/tmp目录下的内容

可以看到,已经有daemon.log日志文件了。

然后使用命令:tail -f /tmp/daemonl.log ,可以看到该程序每隔10s就会在对应的文件中输入相关的内容

到这里,这个实验就已经结束了,通过前边使用命令:ps -ef|grep ./daemon可以看到咱们这个进程的进程号是3346,现在使用命令:kill -9 3346将这个进程杀死,同时也把/tmp中的daemon.log文件页删除,方便咱们下边的实验。

守护进程的出错处理

在编写守护进程的具体调试过程中会发现,由于守护进程完全脱离了控制终端,因此,不能像其他普通进程一样,将错误信息输出到控制终端来通知程序员,即使使用gdb也无法正常调试。那么,守护进程的进程要如何调试呢?一种通用的方法是使用 syslog 服务,将程序中的出错信息输入到系统日志文件中(如“/var/log/messages”),从而可以直观地看到程序的问题所在(“/var/log/message”系统日志文件只能由拥有root权限的超级用户查看。在不同的Linux发行版本中,系统日志文件路径全名可能有所不同,例如,我的Ubuntu中路径就是“/var/log/syslog”)。

syslog 是Linux中的系统日志管理服务,通过守护进程 syslogd 来维护。该守护进程在启动时会读一个配置文件“/etc/syslog.conf”,该文件决定了不同种类的消息会发送到何处。例如,紧急消息可被送到系统管理员并在控制台上显示,而警告消息则可被记录到一个文件中。

该机制提供了3个syslog相关函数,分别为 openlog()、syslog()和closelog(),下面就分别介绍这3个函数。

函数说明

openlog()函数用于打开系统日志服务的一个链接;syslog()函数用于向日志文件中写入信息,在这里可以设定消息的优先级、消息输出格式等;closelog()函数用于关闭系统日志服务的链接。

函数格式

 

基础实验

咱们可以尝试用普通身份执行程序(RedHat中不要用root,ubuntu正常运行就可以)。由于这里的open()函数必须具有root权限,因此,syslog 会将错误信息写入到系统日志文件("如/var/log/syslog")中,结果如下图


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值