一、进程与程序
程序链接加载和传参
c语言从main函数执行。事实上操作系统下的应用程序在运行main函数前需要执行一段引导代码,由引导代码调用main函数
编译链接时,由链接器将引导代码链接到我们的应用程序当中,一起构成最终的可执行文件。
程序运行需要操作系统中的加载器(一段程序),执行程序时,加载器负责将这段程序加载到内存中执行。
终端执行程序,命令行参数由shell进程逐一解析,shell会将这些参数传递给加载器,加载器加载应用程序时会将其传递给应用程序引导代码,当引导程序调用main函数时将参数一并传递。
程序结束
就是进程终止,包括正常终止和异常终止
正常终止:
-
return返回
-
exit(),exit(),Exit()调用
异常终止:
-
abort()
-
接收到特殊信号,比如SIGKILL
注册进程终止处理函数
atexit()
注册一个进程在正常终止时要调用的函数
int atexit(void (*function)(void));
返回值:成功返回0,失败返回非0
程序使用_exit()和 _Exit()退出不会调用终止处理函数
进程:进程就是一个可执行程序的实例,可执行程序被运行
进程时一个动态过程,不是静态文件,是程序的一个运行过程,当应用程序被加载到内存中运行之后它就是一个进程,程序运行结束意味着进程终止,这就是进程的一个生命周期。
进程号
Process ID(PID),一个正数,用于唯一标识系统中的某一个进程。可以用ps命令查看进程号
1、getpid函数
获取本进程的进程号
pid_t getpid(void);
2、getppid函数
获取父进程的进程号
pid_t getppid(void);
二、进程的环境变量
每个进程都有一组相关的环境变量,环境变量以字符串的形式存储在一个字符串数组列表中,该数组也叫环境列表。
每个字符串都以"name = value"形式定义,环境变量是名称-值的成对集合。
终端可以用env命令查看shell进程的所有环境变量
export添加环境变量 eg:export LINUX_APP=123456
删除环境变量 eg: export -n LINUX_APP
应用程序中获取环境变量
进程的环境变量是从父进程中继承过来的
环境变量存放在一个字符串数组中,应用程序中,通过全局变量environ指向它,申明即可使用
extern char **environ; // 申明外部全局变量 environ
1、getenv函数
获取指定环境变量
char *getenv(const char *name);
返回值:存在返回指针,不存在返回NULL
不要修改返回的字符串,这回导致环境变量被修改
2、putenv函数(劣)
添加环境变量
int putenv(char *string);
string: 指向name = value的字符串,不应为自动变量(在栈中分配的字符数组)
返回值:成功返回0,失败返回非0
3、setenv函数(优)
添加或者修改环境变量
int setenv(const char *name, const char *value, int overwrite);
overwrite:为0,不改变环境变量的值,非0,如果那么存在则覆盖,不存在则创建新的环境变量
setenv和putenv的区别:
-
putenv不会为name=value字符串分配内存
-
setenv可以通过overwrite控制添加和修改环境变量(setenv使用自动变量也可以)
终端向进程传参:
NAME=value ./testAPP
如果有多个环境变量要传入进程,则使用空格隔开
4、unsetenv函数
从环境变量表中移除name环境变量
int unsetenv(const char *name);
清空环境变量
1、将全局变量赋值为NULL
environ = NULL;
2、clearenv函数
int clearenv(void);
某些情况使用setenv和clearenv会导致内存泄露,setenv会为环境变量分配一块内存缓冲区,随之成为进程的一部分,调用clearenv并不知道有这块缓冲区,故而无法释放,反复调用这两个函数会造成内存泄漏。
环境变量的作用
HOME:用户家目录
USER: 当前用户名
SHELL:shell解析器名称
PWD:当前所在目录
三、进程的内存布局
堆(heap):由用户申请释放,若用户不释放,程序结束可能由OS回收
栈(stack):编译器自动分配释放,存放函数参数,局部变量的值
静态区/全局区(static):存放静态变量和全局变量,初始化的在(.rwdata or .data),未初始化的在(.bss),C++不区分data和bss
文字常量区(.rodata):常量字符串,程序结束由系统释放
代码段(.txt):存放函数体的二进制代码
Linux下的size命令可以查看文本段、数据段、bss段的段大小
四、进程的虚拟地址空间
大多数系统采用虚拟内存管理技术
每一个进程都有自己独立的地址空间。
32位系统,每个进程的逻辑地址均为4GB,用户3GB(0x00000000~0xc0000000),内核1GB(0xc0000000~0xffffffff)
Linux系统下,应用程序运行在虚拟地址空间中。
程序中读写的内存地址是虚拟地址,不是真正的地址。如应用程序读写0x80000000这个地址,并不是对应硬件的0x800000000物理地址
为什么引入虚拟地址?
如果应用程序使用物理地址:
-
多个程序需要运行时,要保证内存总量小于实际物理内存大小
-
内存使用效率低。内存不足,需要将将其他程序暂时拷贝到硬盘,将新程序装入内存,效率低下。
-
进程地址空间不隔离。直接访问物理内存 ,每个进程都可以修改其他进程的数据,甚至修改内核空间的数据。
-
无法确定程序的链接地址。链接地址和运行地址必须一致,否则无法运行。
应用程序实际上是使用虚拟地址
-
进程和进程,进程和内核相互隔离。每一个程序的虚拟地址空间映射到不同的物理地址空间。
-
在某些场合,两个及以上进程可以共享内存。共享内存可用于进程间通信。每个进程有自己的映射表,可以让不同的虚拟地址空间映射到相同的物理地址空间。
-
便于实现内存保护机制。多进程共享内存时,允许每个进程对内存有不同的保护机制。进程1对物理内存段1可读,进程2对这段可读可写)
-
编译应用程序时,无需关心链接地址。
五、fork创建子进程
现有进程可调用fork创建子进程
pid_t fork(void);
在一个大型的应用程序任务中,创建子进程通常会简化应用程序的设计,同时提高了系统的并发性(即同时能够处理更多的任务或请求,多个进程在宏观上实现同时运行)。
如何理解fork?
完成fork调用存在两个进程,一个父进程,一个子进程,两个进程都从fork函数的返回处继续执行,导致fork返回两次值。可通过返回值来区分父进程还是子进程。
返回值:fork调用成功,父进程会返回子进程的PID,子进程返回0。
调用失败,父进程返回-1,子进程创建失败。
子进程是父进程的副本,两个进程执行相同的代码段(父子进程共享代码段,内存中只存在一份代码段数据)
在调用了 fork()之后,父、子进程中一般只有一个会通过调用 exit()退出进程,而另一个则应使用_exit()退出
父子进程共享代码段,不共享数据段,堆,栈等。子进程拥有父进程的数据段,堆,栈的副本。
子进程是一个独立的过程,有自己独立的进程空间,系统内唯一的进程号,自己独立的pcb,被内核同等调度。
六、父子进程间的文件共享
调用fork创建子进程后,子进程得到父进程的文件描述符副本,意味着父子进程对应文件描述符指向相同的文件表(磁盘中相同的文件)。
这些文件在父子进程间实现了共享。(如:父进程偏移量多少,子进程的偏移量也是多少)
注意:在创建子进程之前打开的文件共享文件偏移量,接续写,创建之后打开的文件偏移量是独立的,会覆盖写
fork函数使用场景:
⚫ 父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。如网络服务进程中,父进程等待客户端的服务请求,当接收到客户端发送的请求事件后,调用 fork()创建一个子进程,让子进程去处理此请求、而父进程可以继续等待下一个服务请求。
⚫ 一个进程要执行不同的程序。 譬如在程序 app1 中调用 fork()函数创建了子进程,此时子进程是要去执行另一个程序 app2,也就是子进程需要执行的代码是 app2 程序对应的代码,子进程将从 app2程序的 main 函数开始运行。这种情况,通常在子进程从 fork()函数返回之后立即调用 exec 族函数来实现。
七、系统调用vfork
vfork()系统调用用于创建子进程 ,vfork()与 fork()函数在功能上是相同的,并且返回值也相同,在一些细节上存在区别
pid_t vfork(void);
fork()认作对父进程的数据段、堆段、栈段以及其它一些数据结构创建拷贝,代价是很大的,它复制了父进程中的数据段和堆栈段中的绝大部分内容,这将会消耗比较多的时间, 效率会有所降低,而且太浪费,原因有很多,其中之一在于, fork()函数之后子进程通常会调用 exec 函数,使得子进程不再执行父程序中的代码段,而是执行新程序的代码段, 从新程序的 main 函数开始执行、 并为新程序重新初始化其数据段、堆段、栈段等; 那么在这种情况下,子进程并不需要用到父进程的数据段、堆段、栈段(譬如父程序中定义的局部变量、全局变量等)中的数据, 此时就会导致浪费时间、 效率降低。
出于这一原因,引入了 vfork()系统调用,虽然在一些细节上有所不同,但其效率要高于 fork()函数。类似于 fork(), vfork()可以为调用该函数的进程创建一个新的子进程,然而, vfork()是为子进程立即执行 exec()新的程序而专门设计的。
vfork()与 fork()函数主要有以下两个区别:
-
fork ():子进程拷贝父进程的数据段,代码段 vfork ( ):子进程与父进程共享数据段
-
fork ()父子进程的执行次序不确定 vfork 保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec或exit 之后父进程才可能被调度运行。
-
如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
牛人理解:
为什么会有vfork,因为以前的fork 很傻, 它创建一个子进程时,将会创建一个新的地址 空间,并且拷贝父进程的资源,而往往在子进程中会执行exec 调用,这样,前面的拷贝工 作就是白费力气了,这种情况下,聪明的人就想出了vfork,它产生的子进程刚开始暂时与 父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中 运行,所以子进程不能进行写操作,并且在儿子 霸占”着老子的房子时候,要委屈老子一 下了,让他在外面歇着(阻塞),一旦儿子执行了exec 或者exit 后,相 于儿子买了自己的 房子了,这时候就相 于分家了。
虽然vfork效率更高,但可能会导致一些难以察觉的bug,尽量避免使用它。
fork效率虽然没有那么高,但现在的Linux采取了如写时复制等技术,速度得到了一定提升。
除非是都速度有非常高要求的场景,否则应避免使用vfork。
八、fork之后的竞争条件
fork调用之后,无法确定父子进程谁先执行(率先访问CPU)
大多数情况下,父进程先于子进程执行,但是也不排除子进程先执行的情况
为了明确先后执行关系,可以采用同步技术比如信号来解决这个问题。
比如让进程先执行,父进程阻塞,子进程执行完代码段后使用信号唤醒父进程
进程的诞生和终止
进程的诞生
比如,shell终端下执行./app 那么shell就是父进程,app就是子进程。
所有的进程都有父进程创建。
所有进程的父进程是init进程,PID为1,是linux启动之后运行的第一个进程,它管理着系统上的所有其他进程
进程的终止
进程有两种终止方式:正常终止和异常终止
正常终止: return,exit,exit,Exit等
异常终止:abort,接收到某些信号
exit和_exit函数的status参数定义了进程的终止状态,父进程可以调用wait()函数获取该状态。
status为int类型,但只有低8位表示它的终止状态,0表示成功终止,非0表示异常终止。
一般使用exit()库函数,而不是_exit()系统调用,原因是exit()最终也会调用 _exit(),在这之前还会完成一些其他工作。
exit()执行的动作:
-
如果程序注册了进程终止处理函数,会调用终止处理函数
-
刷新stdio流缓冲区
-
执行_exit()系统调用
父、子进程不应都使用 exit()终止,只能有一个进程使用 exit()、而另一个则使用exit()退出。
一般推荐的是子进程使用_exit()退出、而父进程则使用 exit()退出。
其原因就在于调用 exit()函数终止进程时会刷新进程的 stdio 缓冲区。
避免打印出重复的输出结果:
-
行缓冲设备,加上换行符。如printf加\n, puts本身会添加换行符
-
调用fork之前,使用fflush()来刷新stdio缓冲区,也可以使用setbuf或setvbuf关闭stdio流的缓冲功能。
-
子进程调用_exit()退出
九、监视子进程
父进程需要知道子进程何时被终止,以及终止状态信息:正常终止、异常终止还是被信号终止
1、wait函数
等待进程的任一子进程终止,同时获取子进程的终止状态信息
pid_t wait(int *status);
status:存放子进程终止时的状态信息,设置为NULL表示不接受子进程程终止时的状态信息。
返回值:成功返回终止时子进程对应的进程号,失败返回-1
wait执行的动作:
-
调用wait()函数,wait()会一直阻塞等待,直到某一子进程终止
-
进程调用wait(),该进程并没有子进程,wait将返回错误(-1),并设置errno为ECHILD
-
进程调用wait之前,有一个或多个子进程终止,wait不会阻塞。直接回收子进程的一些资源。
用以下宏检查status参数:
2、waitpid函数
wait函数存在如下限制:
⚫ 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
⚫ 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
⚫ 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。
pid_t waitpid(pid_t pid, int *status, int options);
pid:要等待的子进程
-
pid大于0,要等待的子进程的进程号
-
pid等于0,要等待父进程同一进程组的所有子进程
-
pid小于-1,等待进程组标识符与 pid 绝对值相等的所有子进程
-
pid等于-1,等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价
options:位掩码,包含一个或多个标志。
-
WNOHANG: 如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
-
WUNTRACED: 除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息;
-
WCONTINUED: 返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。
返回值: 返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现0 。
3、waitid函数(功能更加强大)
僵尸进程和孤儿进程
当一个进程创建子进程,就会存在父进程和子进程,父进程和子进程的生命周期是不一样的。
孤儿进程
父进程先于子进程结束,此时子进程变成孤儿进程。
子进程调用getppid将返回-1,此时它被init进程收养,init为新的父进程。
(ubuntu 16.04图像化界面下,孤儿进程会被upstart进程收养,CTRL + ALT + F1进入字符界面,孤儿进程的父进程为init)
CTRL + ALT + F7返回图像化界面
僵尸进程
子进程先于父进程结束,父进程还来不及回收子进程,释放子进程占用的资源。
如何移除僵尸进程:
-
父进程调用wait或其它变体,僵尸进程会被内核彻底删除
-
父进程未调用wait就退出,此时由init进程会接管它的子进程并自动调用wait,移除僵尸进程
系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。
僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号 SIGKILL 也无法将其杀死,那么这种情况下,只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉!
程序设计中,一定要监视子进程的状态变化,如果子进程终止了,要调用 wait()将其回收,避免僵尸进程。
SIGCHLD信号
父进程的某个子进程暂停、终止或恢复时,父进程会收到SIGCHLD信号
父进程不能一直wait阻塞等待子进程终止(或轮询),这样父进程啥事也做不了,需要通过SIGCHLD信号解决这个问题
SIGCHLD信号的默认处理方式是忽略,我们需要捕获它,绑定信号处理函数。
在信号处理函数中调用wait回收子进程,回收完毕后再回到父进程的工作流程中。
当调用信号处理函数时,会暂时将引发调用的信号添加到进程的信号掩码中(除非 sigaction()指定了 SA_NODEFER 标志), 这样一来,当 SIGCHLD 信号处理函数正在为一个终止的子进程“收尸”时,如果相继有两个子进程终止,即使产生了两次 SIGCHLD 信号,父进程也只能捕获到一次 SIGCHLD 信号,结果是,父进程的 SIGCHLD 信号处理函数每次只调用一次 wait(),那么就会导致有些僵尸进程成为“漏网之鱼”。
解决方案就是:在 SIGCHLD 信号处理函数中循环以非阻塞方式来调用 waitpid(),直至再无其它终止的子进程需要处理为止,所以,通常 SIGCHLD 信号处理函数内部代码如下所示:
while (waitpid(-1, NULL, WNOHANG) > 0) continue;
上述代码一直循环下去,直至 waitpid()返回 0,表明再无僵尸进程存在;或者返回-1,表明有错误发生。应在创建任何子进程之前,为 SIGCHLD 信号绑定处理函数
十、执行新程序
子进程的工作不再是运行父进程的代码段,而是运行另一个程序的代码,子进程可以通过exec函数函数来实现另一个程序。
1、execve函数(系统调用)
将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main()函数开始执行
int execve(const char *filename, char *const argv[], char *const envp[]);
filename:新程序位置
argv:传递给新函数的命令行参数,是一个字符串指针数组,以NULL结束
envp:新程序的环境变量列表,对应于新程序的environ数组,以NULL结束(格式:name=value)
返回值:成功不返回,失败返回-1
基于系统调用 execve(),还提供了一系列以 exec 为前缀命名的库函数 ,基本功能类似。,这些函数(包括系统调用 execve())称为 exec 族函数 ,将调用这些 exec 函数加载一个外部新程序的过程称为 exec 操作
通常由 fork()生成的子进程对 execve()的调用最为频繁,也就是子进程执行 exec 操作;
直接在子进程中编写操作代码不够灵活,可以单独将子进程编写成一个任务,直接exec调用。
2、exec库函数
基于系统调用 execve()而实现的,虽然参数各异、但功能相同, 包括: execl()、 execlp()、 execle()、 execv()、execvp()、 execvpe()等
extern char **environ; int execl(const char *path, const char *arg, ... /* (char *) NULL */); int execlp(const char *file, const char *arg, ... /* (char *) NULL */); int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);
execl与execve不同在于第二个参数的形式不同:
execlp和execvp,它们只需要提供新程序的名字即可,不需要提供具体路径(提供也行,不提供实际路径更加方便)。
execle()和 execvpe() 这两个函数可以指定自定义的环境变量列表给新程序, 参数envp与系统调用execve()的envp参数相同。
3、system函数
在程序中执行任意的shell命令
system()函数其内部的是通过调用 fork()、execl()以及 waitpid()这三个函数来实现它的功能, 首先 system()会调用 fork()创建一个子进程来运行 shell(可以把这个子进程成为 shell 进程) ,并通过 shell 执行参数command 所指定的命令。
int system(const char *command);
返回值:
-
参数为NULL,shell可用返回非0,不可用返回0
-
无法创建子进程或无法获取子进程的终止状态,返回-1
-
子进程不能执行shell,子进程调用_exit(127)终止
-
调用成功,返回执行command的shell进程的终止状态
system使用简单,但是是以牺牲效率为代价的,
使用 system()运行 shell命令需要至少创建两个进程,一个进程用于运行 shell、另外一个或多个进程则用于运行参数 command 中解析出来的命令,每一个命令都会调用一次 exec 函数来执行;
十一、进程状态和进程关系
Linux 系统下进程通常存在 6 种不同的状态:
就绪态、运行态、僵尸态、 可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态
就绪态:满足被CPU调度的所有条件,此时并未调度执行,当进程的时间片到达,操作系统及就会从就绪态链表中调度一个进程
运行态:进程正在被CPU调度
僵尸态:进程已结束,父进程还未替它释放资源。
可中断睡眠状态:可被信号唤醒
不可中断睡眠状态:无法被信号唤醒,需等到满足特殊条件。睡眠状态就是阻塞态,等待某种条件成立就会进入就绪态
暂停态:进程暂停运行而不是结束(比如收到SIGSTIOP)。处于暂停态的进程收到SIGCONT信号会进入就绪态。
新创建的进程会进入就绪态。各状态之间的转化关系如下,
进程关系
在 Linux 系统下, 每个进程都有自己唯一的标识:进程号(进程 ID、 PID),也有自己的生命周期,进程都有自己的父进程、而父进程也有父进程,这就形成了一个以 init 进程为根的进程家族树; 当子进程终止时,父进程会得到通知并能取得子进程的退出状态。
进程间的关系主要分为:无关系,父子进程关系,进程组和会话
1、无关系
两个进程间没有任何关系,相互独立。
2、父子进程关系
一个进程fork出另一个子进程,它们之间就是父子进程关系。当父进程先于子进程结束,则子进程成为孤儿进程,会被init进程收养,它们之间也是父子进程关系。
3、进程组
每个进程有进程ID,父进程ID还有一个进程组ID。进程组是一个或者多个进程的集合,它们之间不是孤立的,它们彼此之间存在父子,兄弟关系,或者功能上有联系。
进程组主要是为了方便进程管理,如完成某项任务需要并发运行100个进程,终止是需要一个个终止,有了进程组之后,直接终止该进程组即可。
-
每个进程属于且只属于一个进程组
-
每个进程组有一个组长,组长ID等于进程组ID
-
操作进程组只需要在组长ID前加负号
-
组长进程不能创建新的进程组
-
进程组中只要有一个进程在,进程组就存在,与组长进程是否退出无关
-
进程组的生命周期从被创建开始,知道所有的进程组成员退出
-
新创建的进程默认会继承父进程的进程组ID
-
getpgrp()或 getpgid()
获取进程对应的进程组 ID
pid_t getpgid(pid_t pid);
pid_t getpgrp(void);
getpgid可以获得对应进程的进程组id,设置为0,表示获取调用者进程的进程组ID
返回值:失败返回-1.成功返回进程组ID
-
setpgid()或 setpgrp()
加入一个现有的进程组或创建一个新的进程组
int setpgid(pid_t pid, pid_t pgid);
int setpgrp(void);
如果pid为0,表示使用调用进程的PID,如果pgid为0,表示创建一个新的进程组,有pid指定的进程作为进程组组长进程
setpgrp == setpgid(0,0)
一个进程只能为它自己或者它的子进程设置进程组ID,在子进程调用exec函数后,就不能更改该子进程的进程组ID
4、会话
会话是一个或者多个进程组的集合
一个会话可以有一个或多个进程组,但只有一个前台进程组,其它都是后台进程组,每个会话有一个会话首领(创建会话的进程)
一个会话可以有或没有控制终端,有(只有一个),通常是登陆到其上的终端设备(终端登录)或伪终端设备(SSH协议网络登录)
会话ID就是首领进程的进程组ID
-
getsid
获取进程的会话ID
pid_t getsid(pid_t pid);
pid为0则返回调用者进程的会话ID,不为0则返回对应pid进程的会话ID。
-
setsid
创建一个会话id
pid_t setsid(void);
调用者进程不是进程组的组长进程,调用 setsid()将创建一个新的会话,
调用者进程是新会话的首领进程,同样也是一个新的进程组的组长进程,调用 setsid()创建的会话将没有控制终端。
十一、守护进程
守护进程也称为精灵进程,是运行在后台的特殊进程,独立于控制终端并且周期性的执行某种任务或者等待处理某些事情的发生。
长期运行。一般系统启动开始运行,除非强行终止,否则直到系统关机都会运行。普通进程在用户登录或运行程序时创建,在运行结束或用户注销时终止。守护进程不受用户登陆注销的影响,将会一直运行,直到系统关机。
与控制终端脱离。Linux中,系统与用户交互的界面称为终端,每一个从终端开始运行的程序都会依附于这个终端。(会话控制终端)
控制终端关闭时,会话就会退出,控制终端运行的所有程序都会被终止,守护进程能独立于终端之外运行。
-
Linux中大多数服务器就是用守护进程实现的,如Internet服务器inetd,Web服务器httpd等。
-
守护进程还能完成系统任务,如作业规划进程crond
守护进程 Daemon,通常简称为 d,一般进程名后面带有 d 就表示它是一个守护进程。守护进程与终端无任何关联,用户的登录与注销与守护进程无关、不受其影响,守护进程自成进程组、自成会话,即pid=gid=sid
编写守护进程
1、创建子进程,终止父进程
2、子进程调用setsid创建会话
3、将工作目录改为根目录
4、重设文件掩码umask
5、关闭不再需要的文件描述符
6、将文件描述符0,1,2定位到/dev/null
7、忽略SIGCHLD信号
守护进程可以通过终端命令行启动,通常是由系统初始化脚本启动,如:/etc/rc*, /etc/init.d/ *
SIGHUP信号
当用户准备退出会话时, 系统向该会话发出 SIGHUP 信号,会话将 SIGHUP 信号发送给所有子进程,子进程接收到 SIGHUP 信号后,便会自动终止,当所有会话中的所有进程都退出时,会话也就终止了;SIGHUP 信号对应的处理方式为系统默认方式终止进程 。
忽略SIGHUP信号会导致进程在终端关闭后继续运行,而不是默认退出。
十二、单例模式运行
有些应用可以被打开多次,如qq(应用多开),但是有些应用不允许多次打开,只要程序没有停止运行就不允许再次打开。
比如:守护进程就是单例模式运行。
如何实现单例模式运行?
1、判断文件是否存在 (不靠谱)
程序运行正式代码之前, 先判断一个特定的文件是否存在,如果存在则表明进程已经运行,此时应该立马退出;
如果不存在则表明进程没有运行, 然后创建该文件,当程序结束时再删除该文件即可
2、使用文件锁(常用)
当程序启动之后,
首先打开该文件,调用 open 时一般使用O_WRONLY | O_CREAT 标志,当文件不存在则创建该文件,然后尝试去获取文件锁,若是成功,则将程序的进程号(PID)写入到该文件中,写入后不要关闭文件或解锁(释放文件锁) ,保证进程一直持有该文件锁;若是程序获取锁失败,代表程序已经被运行、则退出本次启动。
Tips:当程序退出或文件关闭之后,文件锁会自动解锁!
系统调用flock()、fcntl()或库函数 lockf()均可实现对文件进行上锁
守护进程(单例模式运行),应该将这个特定文件放置于 Linux 系统/var/run/目录下,并且文件的命名方式为 name.pid