一、守护进程含义及实现过程
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