一、程序与进程
1、进程结构一般由3部分组成:代码段、数据段和堆栈段;
代码段是用于存放程序代码的数据;数据段则存放程序的全局变量、常量和静态变量;堆栈段中的栈用于函数调用,它存放着函数的参数、函数内部定义的局部变量,堆栈段还包括了进程控制块(Process Control Block,PCB);PCB处于进程核心堆栈的底部,不需要额外分配空间;PCB是进程存在的唯一标识,系统通过PCB的存在而感知进程的存在;系统通过PCB对进程进行管理和调度;PCB包括创建进程、执行程序、退出进程以及改变进程的优先级等;
2、程序转换为进程:
1)内核将程序读入内存,为程序分配内存空间;
2)内核为该进程分配进程标识符PID和其他所需资源;
3)内核为进程保存PID及相应的状态信息,把进程放到运行队列中等待执行,程序转化为进程后就可以被操作系统的调度程序调度执行了。
二、进程的创建与结束
1、进程的创建有两种方式:一种是由操作系统创建,一种是由父进程创建;
2、系统允许一个进程创建新进程(即为子进程),子进程还可以创建新的子进程,形成进程树结构;
3、进程的创建——fork()函数
Linux系统允许任何一个用户进程创建一个子进程,创建成功后,子进程将存在于系统之中,并且独立于父进程;该子进程可以接受系统调度,可以得到分配的系统资源;系统也可以检测到子进程的存在,并且赋予它与父进程同样的权利;
创建新进程的进程,即调用fork()函数的进程就是父进程,而新创建的进程就是子进程;
fork()函数不需要参数,返回值是一个进程标识符(PID);对于返回值,有以下3中:
1)对于父进程,fork()函数返回新创建的子进程的ID;
2)对于子进程,fork()函数返回0;
3)如果创建出错,则fork()函数返回-1;
fork()函数会创建一个新的进程,并从内核中为此进程分配一个新的可用的进程标识符PID,之后为这个新进程分配进程空间,并将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段;因此,fork()函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回;
getpid()是获得当前进程的pid;而getppid()则是获得父进程的id;
4、进程的结束——exit()函数
在Linux中进程退出分为正常退出和异常退出两种
1)正常退出
在main()函数中执行return;
调用exit()函数;
调用_exit()函数;
2)异常退出
调用abort函数;
进程收到某个信号,而该信号使程序终止;
不管是哪种退出方式,系统最终都会执行内核中的同一代码,这段代码用来关闭进程所用已打开的文件描述符,释放它所占用的内存和其他资源;
5、exit和return的区别:
1)exit是一个函数,带有参数,参数表示进程的退出状态,exit执行完后把控制权交给系统;
2)return是函数执行完后的返回,return执行完后把控制权交给调用函数;
6、exit和abort的区别:
1)exit是正常终止进程;
2)abort是异常终止;
7、exit()和 _exit()函数的异同:
1)exit和 _exit函数都是用来终止进程的;
当程序执行到exit或 _exit时,系统无条件地停止剩下所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行;
2)exit()是在头文件stdlib.h中声明,而_exit()是在头文件unistd.h中声明;exit中的参数exit_code为0时,代表进程正常终止,若为其他值,表示程序执行过程中有错误发生;并且_exit()执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核;
在调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin、stdout、stderr)的数据。exit函数是在_exit函数之上的一个封装,其会自动调用_exit,并在调用之前先刷新流数据。
3)exit()函数和_exit()函数最大区别就在于exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件;由于Linux 的标准函数库中,有一种被称作“缓冲I/O”的操作, 其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时, 会连续的读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等)后,再将缓冲区中的内容一次性写人文件。这种技术大大增加了文件读写的速度,但也给编程带来了一点儿麻烦。比如有一些数据,理论上应该已经写入了文件,但实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时如果用_exit()函数直接将进程关闭, 缓冲区的数据就会丢失。因此,要想保证数据的完整性,就一定要使用exit()函数。
三、僵尸进程
在UNIX/Linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程;子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束,于是就产生了孤儿进程和僵尸进程。
1、孤儿进程:是指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程,孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作;
2、僵尸进程:是指一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称为僵尸进程。当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
孤儿进程是父进程已退出,而子进程未退出;
僵尸进程是父进程未退出,而子进程已退出;
3、进程一旦调用了wait函数,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止;
4、wait()会暂停目前进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一块返回。如果不需要结束状态值,则参数status可以设成NULL。
如果执行成功则返回子进程识别码PID,如果有错误发生则返回-1,并将失败原因存于errno中;
5、waitpid头文件及函数原型:
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
wait()会暂停目前进程的执行,直到有信号来到或子进程结束。如果在调用waitpid()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一块返回。如果不需要结束状态值,则参数status可以设成NULL。
1)参数pid为欲等待的子进程识别码,
pid<-1:等待进程组识别码为pid绝对值的任何子进程;
pid=-1:等待任何子进程,相当于wait;
pid=0:等待进程组识别码与目前进程相同的任何子进程;
pid>0:等待任何子进程识别码为pid的子进程;
2)参数options的值
options=WNOHANG,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
options=WUNTRACED,则子进程进入暂停则马上返回,但结束状态不予以理会。
3)waitpid的返回值
当正常返回时waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0.
如果调用中出错,则返回-1,这是errno会被设置成相应的值以指示错误所在。
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD。
四、守护进程
1、在Linux或者UNIX操作系统中在系统的引导的时候会开启很多服务,这些服务就叫作守护进程。为了增加灵活性,root可以选择系统开启的模式,这些模式叫作运行级别,每一种运行级别以一定的方式配置系统;
2、守护进程是脱离于终端并且在后台运行的进程。守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断。
3、守护进程是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,守护进程常常在系统引导装入时启动,在系统关闭时终止。
4、如果想让某个进程不因为用户或终端或其他变化而受到影响,就必须把这个进程变成守护进程。
5、创建一个简单的守护进程的步骤:
1)创建子进程,父进程退出;
2)在子进程中创建新会话
进程组:是一个或多个进程的集合。进程组由进程组ID来唯一标识,除了进程号PID之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID,且该进程组ID不会因组长进程的退出而受到影响。
会话周期:会话期是一个或多个进程组的集合。通常,一个会话开始与用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。
setsid函数用于创建一个新的会话,并担任该会话组的组长。调用setsid有3个作用:1让进程摆脱原会话的控制;2让进程摆脱原进程组的控制;3让进程摆脱原控制终端的控制。
3)改变当前目录为根目录;
4)重设文件权限掩码;
文件权限掩码是指屏蔽掉文件权限中的对应位。因此把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask,通常的使用方法为umask(0)。
5)关闭文件描述符。