欢迎光临
我们一直在努力

【Linux系统编程】进程 守护进程与实现/系统日志

一,守护进程

1,会话的概念


对话的结构包括进程组,进程,线程
调度器的调度策略是以线程为大内进行调度的

1,多进程并发和多线程并发

  • 多进程是操作系统为了多任务而自然演化出来的,先有了各种实现,后来才被标准化,所以是「先有事实,再有规范」;
  • 多线程是为了高效并发而主动设计的,先定好了模型和接口规范,再去各个系统实现,所以从一开始就更「规范」。

所以一般来说很多都要支持多线程并发的

2,前台进程组和后台进程组

前台进程组:一个终端有且仅有一个,独占标准输入(可以读取键盘),可以正常标准输出。

后台进程组:将前台运行的进程组切换到后台状态继续运行,禁止读取外部键盘输入,但可以正常输出。

如果后台进程组强行尝试读取标准输入(键盘),操作系统会发送信号将其暂停,而不是杀死。

比如前台正在压缩一个大文件,占用命令行无法操作,我们在压缩指令后面加 &,就能把它放到后台进程组运行,不影响前台使用命令行。

为什么后台进程组尝试读标准输入,会被暂停?

这是操作系统为了保护终端交互设计的强制规则,原理超简单:

  • 你的键盘输入只能给一个对象(前台进程组),这是终端的独占资源;
  • 后台进程如果能随便读键盘,就会和前台进程抢夺输入(比如你敲命令,被后台程序偷偷读走),导致命令行彻底混乱;
  • 内核检测到:后台进程试图读取终端标准输入 → 立刻给这个进程发送 SIGTTIN 信号;
  • 这个信号的默认行为是暂停进程(不是杀死!),进程会停止运行,直到你把它切回前台。
  • 2,函数的理解

    首先创建守护进程对应的函数是


    上面这个是对于这个函数的描述,

    如果调用进程不是进程组组长,setsid() 会创建一个新会话。
    调用进程将成为新会话的会话组长(即其会话 ID 会被设置为与自身进程 ID 一致)。
    调用进程同时也会成为该会话内一个新进程组的进程组组长(即其进程组 ID 会被设置为与自身进程 ID 一致)。
    调用进程将是这个新进程组和新会话中唯一的进程。
    新会话在初始状态下没有控制终端

    setsid() 创建新会话的那一瞬间✅
    这个新会话里 有且仅有 1 个进程组✅
    这个进程组里 有且仅有 1 个进程(就是调用 setsid() 的进程)
    这里很难理解,主播也是好不容易画出了这个示意图,有错误可以随时指出

    下面让我们逐步来理解这些话
    1,如果调用进程不是进程组组长,setsid() 会创建一个新会话。

    2,调用进程将成为新会话的会话组长(即其会话 ID 会被设置为与自身进程 ID 一致)。
    调用进程同时也会成为该会话内一个新进程组的进程组组长(即其进程组 ID 会被设置为与自身进程 ID 一致)。新会话在初始状态下没有控制终端

    一、会话ID=自身进程ID的作用

  • 彻底脱离原会话原来的会话 ID 是绑定终端的,现在把会话 ID 改成自己的 PID,意味着:
    • 这个进程不再属于原来的终端会话,和原终端、原会话彻底解绑。
    • 新会话没有控制终端,关终端、退 SSH 都不会影响这个进程。
  • 成为会话的「根」会话 ID = PID,说明这个进程是新会话的创建者和唯一组长,整个会话的生命周期由它主导
    • 只有它能为这个会话申请控制终端(一般守护进程不会这么做)。
    • 会话里后续所有进程组、子进程,都归这个会话管理。
  • 全局唯一标识PID 是系统里唯一的,所以新会话 ID 也天然唯一,不会和其他会话冲突。
  • 二、进程组ID=自身进程ID的作用

  • 彻底脱离原进程组原来的进程组 ID 是父进程或其他组长的,现在改成自己的 PID,意味着:
    • 这个进程不再属于原来的进程组,不受原进程组的信号影响(比如原进程组收到 Ctrl+C,不会传给它)。
  • 成为进程组的「根」进程组 ID = PID,说明这个进程是新进程组的唯一组长:
    • 它可以管理组内后续创建的子进程(比如给整个组发信号)。
    • 新进程组和原进程组完全隔离,信号、资源都互不干扰。
  • 保证进程组独立每个进程组有唯一 ID,用 PID 保证不会和其他进程组重复,避免管理混乱。
  • 三、组合起来的终极效果 ✨

    这两个设置同时生效后,进程就变成了 **「无父无母、自己当家」的独立个体 **:

    • ✅ 不受终端控制:关终端不会杀死它(守护进程的核心要求)
    • ✅ 不受原进程组 / 会话信号干扰:比如 Ctrl+C、终端挂断信号都传不到它这里。
    • ✅ 自己管理自己的后代:后续创建的子进程都会归到它的会话 / 进程组里,由它统一管理。
    • ✅ 系统层面唯一标识:会话 ID、进程组 ID 都等于它的 PID,内核能精准识别和管理。

    3,查看守护进程



    不难看到红色框住的TTY为?的,那么就是说明这个是脱离终端控制的,接下来就看PID,PGIDSID是不是一样的

    还有就是PPID一般都是为1的

    为什么守护进程的 PPID 几乎都是 1?这不是巧合,是人为故意做的!

    核心一句话

    守护进程要彻底断绝所有依赖:

  • 用 setsid() 断绝终端的依赖
  • 让父进程主动退出,断绝父进程的依赖 → 自动被系统 PID=1 进程收养所以守护进程的 PPID 天然就是 1!
  • 分步拆解(标准守护进程的强制步骤)

    这是 Linux 写守护进程固定不变的套路,一步都不能少:

    步骤 1:父进程 fork 创建子进程

    父进程(比如终端 shell、你的程序)生一个子进程。此时:

    • 子进程 PPID = 父进程 PID
    • 子进程是进程组的普通成员(不是组长)

    步骤 2:父进程直接退出、自杀!(最关键一步)

    父进程立刻死掉,子进程变成孤儿进程。系统规则:

    孤儿进程必须被系统的「始祖进程」PID=1(init/systemd)收养

    此时:

    • 子进程 PPID 直接变成 1
    • 子进程彻底没有了「原来的父进程」,无牵无挂

    步骤 3:子进程调用 setsid()

    因为父进程已经退出,子进程不是进程组组长,满足调用条件:

    • 创建新会话,脱离终端
    • 会话 ID = 进程组 ID=PID

    为什么非要让 PPID=1?(3 个核心理由)

    1. 父进程会 “拖累” 守护进程

    原来的父进程(比如终端、普通程序)随时可能关闭:

    • 父进程一死,子进程就会被系统骚扰
    • 让父进程主动退出,从根源上杜绝拖累

    2. PID=1 是系统唯一永远不死的进程

    • PID=1(init/systemd)是系统启动第一个进程,关机才会停
    • 它专门负责收养孤儿、回收进程资源,绝对靠谱
    • 守护进程交给它管,永远不用担心没人兜底

    3. 满足 setsid() 的硬性前提

    这是最现实的原因:只有父进程退出,子进程才不是进程组组长,才能调用 setsid()为了调用 setsid 脱离终端,必须先让父进程死 → PPID 自然变 1

        单实例的守护进程,同一时刻只运行运行一次: 锁文件 /var/run/name.pid
            每个守护进程启动时都会将pid写入锁文件中
        启动脚本文件:/etc/rc.d/rc.local 不同系统位置不同

    二,守护进程的实例

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

    #define FNAME "/tmp/out"

    static int daemonize(){

    pid_t pid;
    int fd;

    pid = fork();
    if(pid < 0){
    perror("fork()");
    return -1;
    }

    if(pid > 0){ // 父进程直接退出
    exit(0);
    }

    // 重定向输入输出到 /dev/null(丢弃所有终端输出)
    fd = open("/dev/null",O_RDWR);
    if(fd < 0){
    perror("open()");
    return -1;
    }

    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    if(fd > 2)
    close(fd);

    setsid(); // 创建新会话,脱离终端

    chdir("/"); // 切换工作目录到根目录,防止占用磁盘

    return 0;
    }

    int main(){
    FILE* fp;

    if(daemonize()){
    exit(1);
    }

    fp = fopen(FNAME, "w");
    if(fp == NULL){
    perror("fopen()");
    exit(1);
    }

    // 死循环,每秒写入数字
    for(int i = 0; ;i++){
    fprintf(fp,"%d\\n", i);
    fflush(NULL); // 刷新缓冲区,立即写入文件
    sleep(1);
    }

    exit(0);
    }



    可以观察到这这些都是一样的,说明创建成功了

    杀死进程就是 kill  进程号

    三,系统日志

    系统日志
        syslogd服务, 所有需要写系统日志的操作将其交给syslogd
        openlog(); 关联系统日志服务
        syslog();
        closelog();
        通过信号可以将异常终止转变为正常终止

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<fcntl.h>
    #include<unistd.h>
    #include<syslog.h>
    #include<errno.h>
    #include<string.h>

    #define FNAME "/tmp/out"

    static int daemonize(){

    pid_t pid;
    int fd;

    pid = fork();
    if(pid < 0){
    // perror("fork()");
    return -1;
    }

    if(pid > 0){ // 父进程直接退出
    exit(0);
    }

    // 重定向输入输出到 /dev/null(丢弃所有终端输出)
    fd = open("/dev/null",O_RDWR);
    if(fd < 0){
    // perror("open()");
    return -1;
    }

    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    if(fd > 2)
    close(fd);

    setsid(); // 创建新会话,脱离终端

    chdir("/"); // 切换工作目录到根目录,防止占用磁盘

    return 0;
    }

    int main(){
    FILE* fp;
    //我接下来要打日志了,我的名字叫
    // mydaemon,日志带上 PID,我是守护进程类型的程序!
    openlog("mydaemon", LOG_PID, LOG_DAEMON);

    if(daemonize()){
    //补药写\\n,因为格式是syslog服务决定的
    syslog(LOG_ERR, "daemonize() failed!");
    exit(1);
    }
    else{
    syslog(LOG_INFO, "daemonize() successded!");
    }

    fp = fopen(FNAME, "w");
    if(fp == NULL){
    // perror("fopen()");
    syslog(LOG_ERR, "fopen():%s", strerror(errno));
    exit(1);
    }

    syslog(LOG_INFO, "%s was opend.", FNAME);

    // 死循环,每秒写入数字
    for(int i = 0; ;i++){
    fprintf(fp,"%d\\n", i);
    fflush(NULL); // 刷新缓冲区,立即写入文件
    syslog(LOG_DEBUG, "%d is printed.", i);
    sleep(1);
    }

    fclose(fp);
    closelog();

    exit(0);
    }


    可以看到程序已经运行了

    可以看到这个日志已经输出进来了

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux系统编程】进程 守护进程与实现/系统日志
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址