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

一,守护进程

1,会话的概念

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

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

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

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

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

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

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

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

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

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

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

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

2,函数的理解

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

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

如果调用进程不是进程组组长setsid()创建一个新会话
调用进程将成为新会话的会话组长 (即其会话 ID 会被设置为与自身进程 ID 一致)。

调用进程同时也会成为该会话内一个新进程组的进程组组长 (即其进程组 ID 会被设置为与自身进程 ID 一致)。

调用进程将是这个新进程组和新会话中唯一 的进程。
新会话在初始状态下没有控制终端

setsid() 创建新会话的那一瞬间

这个新会话里 有且仅有 1 个进程组

这个进程组里 有且仅有 1 个进程 (就是调用 setsid() 的进程)

这里很难理解,主播也是好不容易画出了这个示意图,有错误可以随时指出

下面让我们逐步来理解这些话

1,如果调用进程不是进程组组长setsid()创建一个新会话

2,调用进程将成为新会话的会话组长 (即其会话 ID 会被设置为与自身进程 ID 一致)。

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

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

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

二、进程组ID=自身进程ID的作用

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

三、组合起来的终极效果 ✨

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

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

3,查看守护进程


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

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

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

核心一句话

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

  1. setsid() 断绝终端的依赖
  2. 父进程主动退出 ,断绝父进程 的依赖 → 自动被系统 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. 父进程会 "拖累" 守护进程

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

  • 父进程一死,子进程就会被系统骚扰
  • 让父进程主动退出,从根源上杜绝拖累
  1. PID=1 是系统唯一永远不死的进程
  • PID=1(init/systemd)是系统启动第一个进程,关机才会停
  • 它专门负责收养孤儿、回收进程资源,绝对靠谱
  • 守护进程交给它管,永远不用担心没人兜底
  1. 满足 setsid() 的硬性前提

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

单实例的守护进程,同一时刻只运行运行一次: 锁文件 /var/run/name.pid

每个守护进程启动时都会将pid写入锁文件中

启动脚本文件:/etc/rc.d/rc.local 不同系统位置不同

二,守护进程的实例

cpp 复制代码
#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();

通过信号可以将异常终止转变为正常终止

cpp 复制代码
#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);
}

可以看到程序已经运行了

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

相关推荐
tobias.b1 小时前
ubuntu 系统维护
linux·运维·ubuntu
门豪杰2 小时前
使用WSL2安装Ubuntu子系统
linux·运维·ubuntu·wsl
2501_946490382 小时前
城市地标光影呈现技术实践——Hirender集群与多边形融合带在《生命之树》的实操解析
服务器·xr·媒体·hirender·hecoos
面对疾风叭!哈撒给2 小时前
Linux之Docker使用JDK21安装包制作JDK21镜像包
java·linux·运维·docker
九皇叔叔2 小时前
【保姆级教程】CentOS 7.5/RHEL 7.x 编译安装 Redis 6.0.1
linux·redis·centos
绘梨衣的sakura路2 小时前
[特殊字符] 2026 年 AI 自动化新范式:OpenClaw 核心 Skill 精选与实战指南
运维·人工智能·自动化
RisunJan2 小时前
Linux命令-mail (发送和接收电子邮件)
linux·服务器
万象.2 小时前
Linux套接字socket编程(含TCP,UDP)
linux·tcp/ip·udp
历程里程碑2 小时前
39. 从零实现UDP服务器实战(带源码) V1版本 - Echo server
服务器·开发语言·网络·c++·网络协议·udp·php