Linux学习_进程(7)——工作在幕后

守护进程

        守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点:

        长期运行。守护进程是一种生存期很长的一种进程,它们一般在系统启动时开始运行,除非强行终止,否则直到系统关机都会保持运行。与守护进程相比,普通进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但守护进程不受用户登录注销的影响,它们将会一直运行着、 直到系统关机。

        与控制终端脱离。在 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都会依附于这个终端,这是上一小节给大家介绍的控制终端,也就是会话的控制终端。当控制终端被关闭的时候,该会话就会退出,由控制终端运行的所有进程都会被终止,这使得普通进程都是和运行该进程的终端相绑定的;但守护进程能突破这种限制,它脱离终端并且在后台运行,脱离终端的目的是为了避免进程在运行的过程中的信息在终端显示并且进程也不会被任何终端所产生的信息所打断。

        守护进程是一种很有用的进程。Linux 中大多数服务器就是用守护进程实现的,譬如,Internet 服务器 inetd、Web 服务器 httpd 等。同时,守护进程完成许多系统任务,譬如作业规划进程 crond 等。

        守护进程 Daemon,通常简称为 d,一般进程名后面带有 d 就表示它是一个守护进程。守护进程与终端无任何关联,用户的登录与注销与守护进程无关、不受其影响,守护进程自成进程组、自成会话,即 pid=gid=sid。通过命令"ps -ajx"查看系统所有的进程,如下所示:

         TTY 一栏是问号?表示该进程没有控制终端,也就是守护进程,其中 COMMAND 一栏使用中括号[]括 起来的表示内核线程,这些线程是在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用 k 开头的名字,表示 Kernel。

        守护进程的学习暂时到这,我们先来看单例模式运行。

单例模式运行

        通常情况下,一个程序可以被多次执行,即程序在还没有结束的情况下,又再次执行该程序,也就是系统中同时存在多个该程序的实例化对象(进程),譬如大家所熟悉的聊天软件 QQ,我们可以在电脑上同时登陆多个 QQ 账号,譬如还有一些游戏也是如此,在一台电脑上同时登陆多个游戏账号,只要你电脑不卡机、随便你开几个号。

         但对于有些程序设计来说,不允许出现这种情况,程序只能被执行一次,只要该程序没有结束,就无法再次运行,我们把这种情况称为单例模式运行。譬如系统中守护进程,这些守护进程一般都是服务器进程, 服务器程序只需要运行一次即可,能够在系统整个的运行过程中提供相应的服务支持,多次同时运行并没有意义、甚至还会带来错误!

        如果希望我们的程序具有单例模式运行的功能,应该如何去实现呢?

通过文件存在与否进行判断

        首先这是一个非常简单且容易想到的方法:用一个文件的存在与否来做标志,在程序运行正式代码之前,先判断一个特定的文件是否存在,如果存在则表明进程已经运行,此时应该立马退出;如果不存在则表明进程没有运行,然后创建该文件,当程序结束时再删除该文件即可!

         这种方法是大家比较容易想到的,通过一个特定文件的存在与否来做判断,当然这个特定的文件的命名要弄的特殊一点,避免在文件系统中不会真的存在该文件,接下来我们编写一个程序进行测试。

#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#define LOCK_FILE "./testApp.lock"

static void delete_file(void){
    remove(LOCK_FILE);
}

int main(void){
    /* 打开文件 */
    int fd = open(LOCK_FILE, O_RDONLY | O_CREAT | O_EXCL, 0666);
    if (-1 == fd) {
        fputs("不能重复执行该程序!\n", stderr);
        exit(-1);
    }

    /* 注册进程终止处理函数 */
    if (atexit(delete_file))
        exit(-1);
    
    puts("程序运行中...");
    sleep(10);
    puts("程序结束");
 
    close(fd); //关闭文件
    exit(0);    
}

        在上述示例代码中,通过当前目录下的 testApp.lock 文件作为特定文件进行判断该文件是否存在,当然这里只是举个例子,如果在实际应用编程中使用了这种方法,这个特定文件需要存放在一个特定的路径下。

        代码中以 O_RDONLY | O_CREAT | O_EXCL 的方式打开文件,如果文件不存在则创建文件,如果文件存在则 open 会报错返回-1;使用 atexit 注册进程终止处理函数,当程序退出时,使用 remove()删除该文件。        

        

         在上面测试中,首先第一次以后台方式运行了 testApp 程序,之后再运行 testApp 程序,由于文件已经存在,所以 open()调用会失败,所以意味着进程正在运行中,所以会打印相应的字符串然后退出。直到第一 次运行的程序结束时,才能执行 testApp 程序,这样就实现了一个简单地具有单例模式运行功能的程序。 虽然上面实现了一个简单地单例模式运行的程序,但是仔细一想其实有很大的问题,主要包括如下三个 方面:

  1. 程序中使用_exit()退出。那么将无法执行 delete_file()函数,意味着无法删除这个特定的文件;
  2. 程序异常退出。程序异常同样无法执行到进程终止处理函数 delete_file(),同样将导致无法删除这个特定的文件;
  3. 计算机掉电关机。这种情况就更加直接了,计算机可能在程序运行到任意位置时发生掉电关机的情况,这是无法预料的;如果文件没有删除就发生了这种情况,计算机重启之后文件依然存在,导致程序无法执行

        针对第一种情况,我们使用 exit()代替_exit()可以很好的解决这种问题;但是对于第二种情况来说,异常退出,譬如进程接收到信号导致异常终止,有一种解决办法便是设置信号处理方式为忽略信号,这样当进程接收到信号时就会被忽略,或者是针对某些信号注册信号处理函数,譬如 SIGTERM、SIGINT 等,在信号处理函数中删除文件然后再退出进程;但依然有个问题,并不是所有信号都可被忽略或捕获的,譬如 SIGKILL 和 SIGSTOP,这两个信号是无法被忽略和捕获的,故而这种也不靠谱。

        针对第三种情况的解决办法便是,使得该特定文件会随着系统的重启而销毁,这个怎么做呢?其实这个非常简单,将文件放置到系统/tmp 目录下,/tmp 是一个临时文件系统,当系统重启之后/tmp 目录下的文件就会被销毁,所以该目录下的文件的生命周期便是系统运行周期。

        由此可知,虽然针对第一种情况和第三种情况都有相应的解决办法,但对于第二种情况来说,其解决办法并不靠谱,所以使用这种方法实现单例模式运行并不靠谱。

使用文件锁

        介绍完上面第一种比较容易想到的方法外,接下来介绍一种靠谱的方法,使用文件锁来实现,事实上这种方式才是实现单例模式运行靠谱的方法。

        同样也需要通过一个特定的文件来实现,当程序启动之后,首先打开该文件,调用 open 时一般使用 O_WRONLY | O_CREAT 标志,当文件不存在则创建该文件,然后尝试去获取文件锁,若是成功,则将程序的进程号(PID)写入到该文件中,写入后不要关闭文件或解锁(释放文件锁),保证进程一直持有该文件锁;若是程序获取锁失败,代表程序已经被运行则退出本次启动。

        Tips:当程序退出或文件关闭之后,文件锁会自动解锁,通过系统调用 flock()、fcntl()或库函数 lockf()均可实现对文件进行上锁,本小节我们以系统调用flock()为例,系统调用 flock() 产生的是咨询锁(建议性锁)、并不能产生强制性锁。

        接下来编写一个示例代码,使用 flock()函数对文件上锁,程序以单例模式运行如下所示:

        

#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

#define LOCK_FILE "./testApp.pid"

int main(void){
    char str[20] = {0};
    int fd;
 
    /* 打开 lock 文件,如果文件不存在则创建 */
    fd = open(LOCK_FILE, O_WRONLY | O_CREAT, 0666);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }

    /* 以非阻塞方式获取文件锁 */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {
        fputs("不能重复执行该程序!\n", stderr);
        close(fd);
        exit(-1);
    }
 
    puts("程序运行中...");
 
    ftruncate(fd, 0); //将文件长度截断为 0
    sprintf(str, "%d\n", getpid());
    write(fd, str, strlen(str));//写入 pid
    
    for ( ; ; )
        sleep(1);
 
    exit(0);
}

        程序启动首先打开一个特定的文件,这里只是举例,以当前目录下的 testApp.pid 文件作为特定文件, 以 O_WRONLY | O_CREAT 方式打开,如果文件不存在则创建该文件;打开文件之后使用 flock 尝试获取文件锁,调用 flock()时指定了互斥锁标志 LOCK_NB,意味着同时只能有一个进程拥有该锁,如果获取锁失败, 表示该程序已经启动了,无需再次执行,然后退出;如果获取锁成功,将进程的 PID 写入到该文件中,当程序退出时,会自动解锁、关闭文件。

        这种机制在一些程序尤其是服务器程序中很常见,服务器程序使用这种方法来保证程序的单例模式运 行;在 Linux 系统中/var/run/目录下有很多以.pid 为后缀结尾的文件,这个实际上是为了保证程序以单例模 式运行而设计的,作为程序实现单例模式运行所需的特定文件。

        关于实现单例模式运行相关内容就给大家介绍这么多,最常用的还是使用文件锁,第一种方法通过文件存在否与进行判断事实上并不靠谱;除此之外,还有其它一些方法也可用于实现单例模式运行,譬如在程序启动时通过 ps 判断进程是否存在等。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值