Linux学习——守护进程编程

一、守护进程含义及实现过程

1、含义

        守护进程(Daemon Process) 是操作系统中一种在后台长期运行的特殊进程,通常不与用户直接交互。它独立于控制终端,用于执行周期性任务或系统服务(如日志管理、网络服务等)。典型的守护进程包括 httpd(Web 服务)、mysqld(数据库服务)等。

2、实现过程

1、守护进程相关概念
        进程组:一个或多个进程的集合,进程组由进程组ID标识,进程组长的进程ID和进程组ID一致,并且进程组ID不会由于进程组长的退出而受到影响。
        会话周期:一个或多个进程组的集合,比如用户从登陆到退出,这个期间用户运行的所有进程都属于该会话周期。
        setsid函数:创建一个新会话,并担任该会话组的组长,调用setsid函数的目的:让进程摆脱原会话,原进程组,原终端的控制。如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期。
2、创建守护进程的过程
        (1)fork()创建子进程,父进程exit()退出;
        这是创建守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。

        (2)在子进程中调用 setsid() 函数创建新的会话;
        在调用了fork()函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开来,而 setsid() 函数能够使进程完全独立出来。

        (3)再次 fork() 一个孙进程并让子进程退出;
        为什么要再次fork呢,假定有这样一种情况,之前的父进程fork出子进程以后还有别的事情要做,在做事情的过程中因为某种原因阻塞了,而此时的子进程因为某些非正常原因要退出的话,就会形成僵尸进程,所以由子进程fork出一个孙进程以后立即退出,孙进程作为守护进程会被init接管,此时无论父进程想做什么都随它了。

        (4)在孙进程中调用 chdir() 函数,让根目录 ”/” 成为孙进程的工作目录;

         这一步也是必要的步骤,使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让"/"作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp,改变工作目录的常见函数是chdir。

        (5)在孙进程中调用 umask() 函数,设置进程的文件权限掩码为0;
        文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)。

        (6)在孙进程中关闭任何不需要的文件描述符,关闭输入输出和错误输出(stdin,stdout,stderr),并将其重定向到/dev/null;
        同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。

        ​ 在上面的第2)步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。

        (7)守护进程退出处理;
        当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的signal信号处理,达到进程的正常退出。

二、创建一个守护进程

        创建一个守护进程一般有 nohup命令、fork()函数和 daemon()函数三种方法,我们分别在阿里云服务器、树莓派上用这三种方式创建一个守护进程。

1、nohup命令


        nohup命令是“no hang up”的缩写,意为“不挂断”。它允许用户在系统后台运行命令,并确保命令在终端关闭或用户退出后仍然继续运行。默认情况下,nohup命令会将输出重定向到当前目录下的nohup.out文件中,除非另外指定了输出文件。

编写一个简单的脚本loop.sh:

#!/bin/bash
while true; do
    echo "Running daemon task..." >> /tmp/daemon.log
    sleep 10
done

赋予脚本执行权限:

chmod +c loop.sh

使用nohup启动守护进程:

nohup ./loop.sh > /dev/null 2>&1 &

验证守护进程是否运行:

ps aux | grep loop.sh

  • 阿里云服务器运行结果

2、fork()函数


        fork是复制进程的函数,程序一开始就会产生一个进程,当这个进程(代码)执行到fork()时,fork就会复制一份原来的进程即就是创建一个新进程,我们称子进程,而原来的进程我们称为父进程,此时父子进程是共存的,他们一起向下执行代码。
        注意的一点:就是调用fork函数之后,一定是两个进程同时执行fork函数之后的代码,而之前的代码以及由父进程执行完毕。

编写C程序fork_test.c:

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

void daemonize() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    }
    if (pid > 0) {
        // 父进程退出
        exit(EXIT_SUCCESS);
    }

    // 子进程继续运行
    setsid(); // 创建新会话
    chdir("/"); // 改变工作目录
    umask(0); // 重设文件权限掩码

    // 关闭标准输入输出流
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 执行守护任务
    while (1) {
        FILE *fp = fopen("/tmp/daemon_fork.log", "a");
        fprintf(fp, "Daemon running...\n");
        fclose(fp);
        sleep(10);
    }
}

int main() {
    daemonize();
    return 0;
}

编译程序

gcc -o fork_test fork_test.c

运行守护进程

./fork_test

验证守护进程是否运行

ps aux | grep fork_test

  • 阿里云服务器运行结果

3、daemon()函数

编写c程序daemon_test.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    // 调用 daemon() 函数
    if (daemon(1, 0) == -1) {
        perror("daemon failed");
        exit(EXIT_FAILURE);
    }

    // 执行守护任务
    while (1) {
        FILE *fp = fopen("/tmp/daemon_builtin.log", "a");
        fprintf(fp, "Daemon running...\n");
        fclose(fp);
        sleep(10);
    }

    return 0;
}

编译程序

gcc -o daemon_test daemon_test.c

运行守护进程

./daemon_test

验证守护进程是否运行

ps aux | grep daemon_test

阿里云服务器结果:

三、GDB调试原理

        GDB(GNU Debugger)是一个强大的调试工具,用于调试使用C、C++等语言编写的程序。它允许用户在程序执行期间检查变量值、设置断点、单步执行代码以及分析崩溃转储等。

1、基本原理


        符号表:当编译一个程序时,如果启用了调试信息(例如通过-g选项),编译器会生成包含函数名、变量名及其类型等信息的符号表。GDB利用这些信息来帮助开发者理解正在运行的程序。
        控制流管理:GDB可以暂停(breakpoints)、继续(continue)、单步执行(step into/over)程序的执行流程,以便于观察特定时刻的状态。
        数据操作:GDB能够读取和修改程序中变量的值,这有助于测试不同的条件分支或修正错误状态。
        信号处理:GDB能够捕获并响应来自操作系统或其他进程的信号(如SIGSEGV、SIGINT等),从而允许对异常情况下的程序进行调试。
        远程调试支持:GDB还支持通过网络连接到另一个运行中的程序实例,实现远程调试功能。


2、调试C程序


编写并编译你的C程序:
创建一个名为 test.c 的文件,并编写如下代码:

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 5;
    int y = 7;
    printf("Sum is %d\n", add(x, y));
    return 0;
}

然后,使用 -g 选项编译这个程序以包含调试信息:

gcc -g -o test test.c

  • 启动GDB调试会话

gdb ./test

  • 设置断点

在你感兴趣的函数或行号处设置断点。例如,在 add 函数入口处设置断点:

(gdb) break add

或者按行号设置断点:

(gdb) break test.c:6

  • 运行程序

输入 run 来开始执行程序:

(gdb) run

程序将在遇到第一个断点时暂停。

  • 检查状态

使用命令查看当前局部变量的值:

(gdb) print a
(gdb) print b

也可以查看调用栈信息:

(gdb) backtrace

  • 单步执行

使用 next 或 step 命令逐行执行代码。区别在于 step 会进入函数内部,而 next 则跳过函数调用直接到下一行。

(gdb) next
(gdb) step

  • 结束调试

完成调试后,你可以使用 quit 退出GDB:

(gdb) quit

四、SSH反向代理树莓派

1、 确保树莓派和阿里云服务器的 SSH 服务正常运行

检查树莓派的ssh服务

sudo systemctl status ssh

如果未启用,请启动并设置开机自启:

sudo systemctl enable ssh
sudo systemctl start ssh

检查阿里云服务器的SSH服务

sudo systemctl status ssh

2、在阿里云服务器上检查端口是否被占用

sudo netstat -tuln | grep 9624

如果有输出,说明该端口已被占用,否则,该端口可以使用。

3、在树莓派上建立 SSH反向代理

使用 ssh 命令建立反向隧道,将树莓派的 SSH 服务映射到阿里云服务器的指定端口(例如:9699):

ssh -R 9699:localhost:22 huhs@114.55.126.125

// huhs:阿里云服务器的登录用户

// 9699:指定远程端口

// 114.55.126.125 阿里云服务器公网IP

在阿里云服务器上运行下面命令,测试是否可以通过localhost:9699访问树莓派

ssh -p 9699 hhs@localhost

// xlq:树莓派登录用户

// localhost: 指的是阿里云服务器的本地回环接口(即 127.0.0.1)

直接从外网访问树莓派

ssh -p 9699 hhs@114.55.126.125

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值