在上一篇文章中,我们在Linux系统中编写了一个守护进程。
Linux下编写守护进程(C语言程序示例)
不知道大家是否明白守护进程的特点以及实现思路。
我们先思考下面几个问题:
Linux下terminal、shell、bash是什么,他们之间的关系是什么?
操作系统中进程与程序的关系?
进程控制块(PCB)包括哪些内容,进程控制块的作用是什么?
进程与进程之间有什么关系(父子关系、兄弟关系.....),我们在终端执行hello world程序时,他的父进程是谁?
什么是孤儿进程,什么是僵尸进程?
程序代码中的fork函数是什么功能
信号是什么?如何杀死一个进程
接下来我们对进程的基础知识点进行分析,通过下面的分析,我们再次回顾一下前面守护进程的实现思路和方法。
终端(terminal)和shell
终端(terminal)作用是提供一个命令的输入输出环境。对于普通 Linux 来说,终端的作用是一个字符(或者模拟字符)的命令交互界面,实现对计算机的控制。
shell是一个命令行解释器,是linux内核的一个外壳,负责外界与linux内核的交互。shell接收命令, 然后将这些命令转化成内核能理解的语言并传给内核, 内核执行命令完成后将结果返回给用户或者应用程序。
当打开一个terminal时,操作系统会将terminal和shell关联起来,当在terminal中输入命令后,shell就负责解释命令。
linux终端分为物理终端,伪终端,串行终端,虚拟终端
物理终端:/dev/console
伪终端(远程网络终端 、图形下的终端):/dev/pts/#(数字)
虚拟终端:/dev/tty#
串行终端:/dev/ttys#
bash的全称叫做Bourne Again shell,从名字上可以看出bash是Bourne shell的扩展,bash 与 Bourne shell 完全向后兼容,并且在 Bourne shell 的基础上增加和增强了很多特性,如命令补全、命令编辑和命令历史表等功能,它还包含了很多 C shell 和 Korn shell 中的优点,有灵活和强大的编程接口,同时又有很友好的用户界面。总而言之,bash是shell的一种,是增强的shell。
进程的组成部分
进程的静态描述由三部分组成:
进程控制块(PCB):用于描述进程情况及控制进程运行所需的全部信息,是操作系统用来感知进存在的一个重要数据结构。
代码段:是进程中能被进程调度程序在CPU上执行的程序代码段。
数据段:一个进程的数据段,可以是进程对应的程序加工处理的原始数据,也可以是程序执行后产生的中间或最终数据
一个进程进程=代码段(编译后形成的一些指令)+数据段(程序运行时需要的数据)+堆栈段(程序运行时动态分配的一些内存)+PCB(进程信息,状态标识等)
数据段包括:
只读数据段:常量
已初始化数据段:全局变量,静态变量
位初始化数据段(bss)(0初始化段):未初始化的全局变量和静态变量(实际上不分配内存,因为都为0,只有一些标记信息)
关于代码段、数据端、堆栈段,我们在之后的文章中再详细分析。
程序与进程的关系
可阅读《深入理解计算机系统》(第三版),中文版 P523的描述。
程序是完成特定任务的一系列指令集合。
从用户的角度来看进程是程序的一次动态执行过程。
从操作系统的核心来看,进程是操作系统分配的内存、CPU时间片等资源的基本单位。
每一个进程都有自己独立的地址空间与执行状态。
区别和联系
进程是动态的,程序是静态的
进程的生命周期是相对短暂的,而程序是永久的。
进程数据结构PCB
一个进程只能对应一个程序,一个程序可以对应多个进程。
进程控制块(PCB)
每个进程在内核中都有一个进程控制块(Processing Control Block)简称PCB,Linux内核的进程控制块使用task_struct结构体来描述。
linux内核源码中定义的PCB结构体task_struct
进程标识符(pid)的定义位置
进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,所以说,PCB(进程控制块)是系统感知进程存在的唯一标志。
PCB包含的基本信息如下:
进程id、用户id和组id
进程的状态(就绪、运行、阻塞等)
进程切换时需要保存和恢复的CPU寄存器的值
描述虚拟地址空间的信息
描述控制终端的信息
当前工作目录
文件描述符表,包含很多指向file结构体的指针
当然还有很多其他信息,该结构体中还有结构体类型的变量。因此信息非常非常多。
进程管理相关的函数
进程管理相关的函数说明
fork:函数用于从已存在的进程中创建一个新进程(新进程称为子进程,而原进程称为父进程。这个函数有两个返回值,子进程返回0,父进程返回父进程的pid,pid 是一个标志进程的数字,可以用函数getpid() 获得)
exec:函数提供在进程中启动另一个程序执行的方法。
exit:函数用来进程结束。
wait:进程一旦调用了wait,就立即阻塞自己,当发现当前进程的子进程已经exit,便会收集这个子进程的信息,然后彻底销毁,如果没有找到这样的子进程,就会一直阻塞。
sleep:就是挂起进程指定的秒数。
getpid:返回当前进程(调用这一函数的进程)的ID。
getppid:返回当前进程的父进程的ID。
看下面的程序示例,展示了上面提到的函数
#include
从这个实验结果是否可以得到结论:在终端执行hello world程序时,他的父进程是?
僵尸进程
僵尸进程:子进程先于父进程退出后,子进程的PCB需要其父进程释放,但是父进程并没有释放子进程的PCB,这样的子进程就称为僵尸进程,僵尸进程实际上是一个已经死掉的进程。
下面的代码,模拟了一个僵尸进程(子进程)。当子进程退出后,父进程没有调用wait来释放子进程的资源,因此子进程就成了一个僵尸进程。
#include
可以在系统中查看到这个僵尸进程的信息。操作如下:
defunct和zombie这两个单词是什么意思?如果不知道,下面的图你一定知道是什么游戏了吧?
植物大战僵尸
在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。
这个僵尸进程需要它的父进程来为它收尸,如果他的父进程没有处理这个僵尸进程的措施,那么它就一直保持僵尸状态
如果这时父进程结束了,那么进程号为1的进程(例如init进程)自动会接手这个子进程,为它收尸,它还是能被清除的。
但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。
如果有大量的僵尸进程驻在系统之中,必然消耗大量的系统资源。但是系统资源是有限的,因此当僵尸进程达到一定数目时,系统因缺乏资源而导致奔溃。在实际编程中,避免和防范僵尸进程的产生显得尤为重要。
孤儿进程
孤儿进程:在操作系统领域中,孤儿进程指的是在其父进程执行完成或被终止后仍继续运行的一类进程。这些孤儿进程将被进程号为1的进程(例如init进程)所收养,并由进程号为1的进程(例如init进程)对它们完成状态收集工作。
#include
进程的一生
进程的一生
信号
操作系统中,信号是事件发生时对进程的通知机制,有时也称为软件中断。
信号与硬件中断的相似之处在于打断了程序执行的正常流程,大多数情况下,无法预测信号到达的准确时间。
一个进程进程可以向另一进程发送信号。
信号也是进程间通信的一种方式。
kill命令可以带信号号码选项,也可以不带。如果没有信号号码,kill命令就会发出终止信号(15),这个信号可以被进程捕获,使得进程在退出之前可以清理并释放资源。
例如: kill -2 1234 相当于kill -INT 1234
SIGINT 终止进程,通常Ctrl+C就发送的2号信号
杀死一个进程,可以使用kill 命令
守护进程
有了前面的知识,接下来,我们分析一下上篇文章改造守护进程的代码
void init_daemon(){
首次调用fork:父进程直接退出,貌似要“脱胎换骨搞独立”
setsid():调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。同时与控制终端脱离。
第二次调用fork:结束第一进程(组长),第二子进程继续(第二子进程不再是会话组长),目的是禁止进程重新打开控制终端。
close():关闭打开的文件描述符,进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸载以及可以引起无法预料的其他错误。
chdir():改变当前工作目录。脱离当前工作目录,改变到一个新的目录
umask():重设文件的权限掩码,为了防止继续沿用从父进程继承下来的掩码内容。
思考
当前守护进程的父进程是谁?
关联的终端是哪一个?
大家如果有问题,可以在下方留言区留言。
课后阅读材料清单:
《深入理解计算机系统》(第三版)教材
第1章:1.1,1.2,1.7
第8章:8.4,8.5.1,8.5.2,8.5.3,8.5.5
第10章:10.1,10.2,10.3,10.4,10.8
第12章:12.1,12.2