UNIX环境编程-多进程(10)

一、进程标识符pid

在操作系统中,每个进程都有一个唯一的标识符,这个标识符就是进程ID,也被称为PID(Process ID)。进程ID是一个正整数,用于唯一标识一个正在运行的进程。操作系统内核使用这个标识符来跟踪各种进程状态信息

一些与进程ID相关的常见概念:

  • PID 0:在Unix/Linux系统中,PID为0的进程是调度进程,也被称为交换进程,它是系统的一部分,存在于内存中,但大部分时间处于休眠状态。
  • PID 1:在Unix/Linux系统中,PID为1的进程通常是init进程,是在系统启动后由内核自动启动的。所有的其他进程都是由这个进程直接或间接派生出来的(是所有进程的祖先(非父进程)进程)**。
  • Parent PID (PPID): 是创建新进程的父进程的PID。这是Unix/Linux系统中的一个重要概念,因为进程间的关系通常以父子继承的方式展现。

在编程中,有几个与PID相关的函数:

  • getpid(): 返回当前进程的PID。
  • getppid(): 返回父进程的PID。
  • fork(): 创建新的进程。新进程是调用进程的副本,被称为子进程。子进程从父进程继承数据,但是有自己的PID

进程标识符类型为pid_t类型,类型为有符号的整数,在每一个机器上面pid_t具体占多少位是未知的,可以将其转换为long long 类型的数据进行输出

在UNIX和Linux系统中,进程标识符(PID)的数据类型通常为 pid_t,这是POSIX标准定义的数据类型。在C和C++程序设计中,我们通常使用 pid_t 类型来存储进程ID。

pid_t 的具体实现因操作系统和硬件平台的不同而不同。但通常,pid_t 在大多数系统中都是一个有符号整数。然而,你不能假设 pid_t 就是某种特定的整数类型,比如 int,因为这种假设在某些系统上可能不成立。

所以,当你在编程时处理进程ID时,应始终使用 pid_t 数据类型,而不是直接使用 int 或其他整数类型。这是因为使用 pid_t 可以确保你的程序能在所有符合POSIX标准的系统上正确运行



1.ps命令

ps 是一个非常有用的命令,用于查看当前系统中正在运行的进程信息

一些基本的 ps 命令的使用例子:

  • ps: 如果不带任何参数地运行 ps,它将显示与当前终端关联的进程。
  • ps -Aps -e: 显示所有进程。
  • ps -u [用户名]: 显示指定用户的所有进程。
  • ps -f: 以全格式输出,显示更多信息,包括父进程的PID(PPID),进程的PID,进程启动时的命令行等。
  • ps -auxps aux: 这是最常用的选项之一。这里的参数可以分解为 -a(显示所有用户的所有进程)、-u(以用户为主的格式显示进程)和-x(显示没有控制终端的进程)。这将列出所有进程的详细快照


2.getpid与getppid

(1)getpid

getpid()函数用于获取当前进程的进程ID(Process ID)。进程ID是一个非负整数,它在操作系统中唯一标识一个正在运行的进程。每个进程都有一个唯一的进程ID,且进程ID在进程的整个生命周期内保持不变。

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);

返回值:

  • getpid()函数返回当前进程的进程ID(PID)

(2)getppid

getppid()函数用于获取当前进程的父进程的进程ID(Parent Process ID)。父进程ID是一个非负整数,用于唯一标识当前进程的父进程

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t getppid(void);

返回值:

  • getppid()函数返回当前进程的父进程的进程ID(PID)



二、进程的产生

1.fork()

fork()函数是一个在Unix-like操作系统中创建新进程的系统调用。它通过复制(一模一样执行的位置都一样)调用进程(父进程)来创建一个新的进程(子进程)父进程和子进程之间的区别在于返回值不同:在父进程中,fork()函数返回子进程的进程ID(PID),而在子进程中,它返回0。执行一次返回两次

1
2
3
#include <unistd.h>

pid_t fork(void);

返回值:

  • 在父进程中,fork()函数返回子进程的进程ID(PID)。该PID是一个正整数,代表新创建的子进程。
  • 在子进程中,fork()函数返回0,表示它是子进程
  • 返回值小于0,表示失败
重点:fork后父子进程的区别与联系
  • fork的返回值不一样
  • 父子进程的PID不一样,ppid(父进程的pid)也不相同
  • 未决信号和文件锁不继承,资源利用量清0
  • 创建方式:父进程通过系统调用(例如在Unix-like系统中的fork()函数)创建子进程。创建后,子进程是父进程的复制品(父进程干什么,子进程干什么),它继承了父进程的代码,数据,堆,栈,环境变量,打开的文件描述符等。但是,父进程和子进程有各自独立的地址空间,它们之间的任何变量和状态的改变并不会影响到对方
  • 运行状态:父进程和子进程可以独立运行,各自拥有自己的程序计数器,它们可以并行或者并发的执行。操作系统会对父进程和子进程进行调度,根据优先级和策略来决定哪个进程先运行,运行多久
  • 进程间通信:虽然父进程和子进程有各自独立的地址空间,但是它们可以通过特定的机制进行通信,例如管道(pipe)、消息队列、共享内存、信号(signal)等
  • 进程生命周期:通常情况下,当子进程结束后,父进程需要通过调用wait()或者waitpid()函数来获取子进程的退出状态(资源回收),这个过程被称为“收尸”。如果父进程没有做这个操作,那么子进程就会成为“僵尸进程”。如果父进程先于子进程结束,那么子进程的父进程会变成init进程,由init进程来负责“收尸”工作
  • 子进程的用途:子进程通常被用来执行其他的程序。这是通过exec()系列函数来实现的。exec()函数会替换当前进程的映像(包括代码和数据),然后运行新的程序。这是Unix/Linux系统中常见的“fork-exec”模式
  • 资源限制:子进程继承了父进程的资源限制,比如CPU时间限制,文件大小限制等

程序示例:

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
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
pid_t pid;

// 父进程打印
printf("[%d]:Begin!\n",getpid());

// 开始复制,一式两份
pid = fork();
// 失败小于0
if(pid < 0)
{
perror("getpid()");
exit(1);
}

// 子进程中==0
if (pid == 0)
{
printf("[%d]:Child is working!\n",getpid());
}
// 父进程中 返回子进程的PID号
else
{
printf("[%d]:Parent is working!\n",getpid());
}

printf("[%d]:End!\n",getpid());
exit(0);
}

make之后运行结果

1
2
3
4
5
6
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ ./1
[13423]:Begin!
[13423]:Parent is working!
[13423]:End!
[13424]:Child is working!
[13424]:End!

上述执行结果,是父进程先运行,但是实际上,父子进程的运行顺序是由调度器的调度策略决定此外可以发现fork之后,父进程打印了[13423]:Parent is working!与[13423]:End!子进程进行了同样的操作打印了[13424]:Child is working!与[13424]:End!

重点:在fork之前需要刷新所有的流

程序示例:1.c

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
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
pid_t pid;

// 父进程打印
printf("[%d]:Begin!\n",getpid());

// 开始复制,一式两份
pid = fork();
// 失败小于0
if(pid < 0)
{
perror("getpid()");
exit(1);
}

// 子进程中==0
if (pid == 0)
{
printf("[%d]:Child is working!\n",getpid());
}
// 父进程中 返回子进程的PID号
else
{
printf("[%d]:Parent is working!\n",getpid());
}

printf("[%d]:End!\n",getpid());
exit(0);
}

make之后将代码重定向到文件中,在当前目录下创建一个out文件

1
$ ./1 > ./out   // 将可执行文件的输出,重定向到./out文件中,而不打印到终端中

查看out下内容:

1
2
3
4
5
6
7
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ cat ./out 
[13911]:Begin!
[13911]:Parent is working!
[13911]:End!
[13911]:Begin!
[13912]:Child is working!
[13912]:End!

发现,程序执行结果相较于之前的直接在终端中输出,存在不同,父进程将begin打印了两次,执行出现了错误,理论上,begin只能打印一次,由fork之前的父进程打印。但是为什么存在这样的问题呢?

原因

  • 在终端中输出为行缓冲,而在文件中为全缓冲。在行缓冲的遇到begin后的\n换行符,缓冲区就会刷新,但是全缓冲则直到缓冲区满才会被动刷新

解决

  • 需要在fork之前,主动进行所有流的刷新fflush(NULL)
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
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
pid_t pid;

// 父进程打印
printf("[%d]:Begin!\n",getpid());

fflush(NULL); /*尤为重要!!!!*/


// 开始复制,一式两份
pid = fork();
// 失败小于0
if(pid < 0)
{
perror("getpid()");
exit(1);
}

// 子进程中==0
if (pid == 0)
{
printf("[%d]:Child is working!\n",getpid());
}
// 父进程中 返回子进程的PID号 pid 为其他正整数时
else
{
printf("[%d]:Parent is working!\n",getpid());
}

printf("[%d]:End!\n",getpid());
exit(0);
}

运行结果:

1
2
3
4
5
6
7
8
9
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ make 1
cc 1.c -o 1
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ ./1 > ./out
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ cat ./out
[14218]:Begin!
[14218]:Parent is working!
[14218]:End!
[14219]:Child is working!
[14219]:End!
fork写时拷贝机制

fork目前已经融合了vfork的优势,fork() 函数在创建新的子进程时采用的是 “Copy-on-Write” 也就是写时复制机制。这意味着当 fork() 函数被调用时,子进程并不会立即复制父进程的所有内存空间(指针均指向父进程所使用的内存空间),而是等到有需要写入内存时才进行复制

在写时复制机制中,当子进程被创建时,父进程的所有内存页都会被标记为只读,并且父子进程都会共享这些内存页。当父进程或子进程尝试写入这些共享页时,操作系统就会拦截这个操作,复制这个内存页,然后把复制出的页标记为可写,之后再进行写操作

优势: 使用写时复制机制的好处是,如果子进程并未修改父进程的数据,那么就没有必要复制内存,这样就大大节省了内存使用和提高了程序执行效率。同时,由于内存页在需要修改时才复制,所以 fork() 调用的时间成本也大大降低

然而,这种机制也有一些局限性。例如,如果父进程和子进程都需要修改同一内存页中的数据,那么这个页还是需要被复制一份,这样就减小了写时复制的优势。所以,如果预期子进程会大量修改内存,那么 fork() 可能并不是最好的选择,因为它可能会产生许多的内存复制操作

这就是 fork() 中的写时复制机制。它使得 fork() 能更有效率地在父子进程之间共享数据,同时也提高了创建新进程的效率

行缓冲与全缓冲的区别

行缓冲(Line Buffering)和全缓冲(Fully Buffering)是两种标准I/O(stdio)库中的缓冲模式,用于控制数据在输入输出过程中的缓冲行为。

行缓冲:

  • 在行缓冲模式下,标准I/O库会将数据缓冲到一个特定大小的缓冲区(通常是一行数据的大小),然后在遇到换行符 \n 时,才会将缓冲区的数据一次性输出(对于输出)或读取(对于输入)。
  • 当程序输出换行符时,或者缓冲区已满时,数据会被立即输出或读取。否则,数据会一直留在缓冲区中,直到满足刷新缓冲的条件。
  • 典型的行缓冲的例子是终端设备(比如控制台),因为用户通常希望在输入或输出一行完整数据后才进行实际的显示或处理。

全缓冲:

  • 在全缓冲模式下,标准I/O库会将数据缓冲到一个较大的缓冲区(通常几 KB 到几十 KB),直到缓冲区被填满或者显式地要求刷新缓冲区时,才会将数据一次性输出(对于输出)或读取(对于输入)。
  • 当程序显示调用 fflush() 函数刷新缓冲区时,或者缓冲区已满时,数据会被输出或读取。否则,数据会一直留在缓冲区中。
  • 全缓冲适用于文件设备等,因为在这种情况下,通常需要一次性读取或输出大量数据,而不需要频繁地进行 I/O 操作。

缓冲模式的选择取决于使用的设备或文件,以及对于实时性和性能的需求。对于交互式输入输出,通常会使用行缓冲以便及时显示数据。而对于大文件的读写,使用全缓冲可以提高性能,因为较少的系统调用可以减少开销。

C语言中,使用标准I/O库提供的函数(如 printfscanffopen 等)进行输入输出时,默认情况下,通常是行缓冲模式,但是也可以通过调用 setvbuf 函数来手动设置缓冲模式



2.进程实例

(1)使用单个进程的策略

编写一个程序2.c,计算一个范围内的所有质数

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
#include <stdio.h>
#include <stdlib.h>

#define LEFT 30000
#define RIGHT 30200

int main()
{
int i,j,mark;
for(i = LEFT;i<=RIGHT;i++)
{
mark = 1;
for(j=2;j<i/2;j++)
{
if(i%j == 0)
{
// 则i不为质数,mark设置为0
mark = 0;
break;
}
}
if(mark)
printf("%d is a primer\n",i);
}

exit(0);
}

运行结果:

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
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ time ./2
30011 is a primer
30013 is a primer
30029 is a primer
30047 is a primer
30059 is a primer
30071 is a primer
30089 is a primer
30091 is a primer
30097 is a primer
30103 is a primer
30109 is a primer
30113 is a primer
30119 is a primer
30133 is a primer
30137 is a primer
30139 is a primer
30161 is a primer
30169 is a primer
30181 is a primer
30187 is a primer
30197 is a primer

real 0m0.004s
user 0m0.004s
sys 0m0.000s

最后打印的

1
2
3
real	0m0.004s
user 0m0.004s
sys 0m0.000s

是使用了$ time [可执行文件] 命令可以为测量执行指定可执行文件的时间,输出三个时间数据为:实际时间(real)、用户态CPU时间(user)和内核态CPU时间(sys)

  • 实际时间(real):表示从命令开始执行到命令结束的实际流逝时间,包括所有的I/O等待时间和其他进程执行时间。这个时间是最直观的执行时间,也是我们最常关注的时间
  • 用户态CPU时间(user):表示 CPU 在用户态运行可执行文件的时间。当进程执行自己的代码时,它处于用户态
  • 内核态CPU时间(sys):表示 CPU 在内核态运行可执行文件的时间。当进程执行系统调用或者受到中断时,它处于内核态

一般情况下,用户态CPU时间(user) + 内核态CPU时间(sys) 的总和应该接近实际时间(real),但可能会稍微大于实际时间,因为在执行过程中可能还有其他进程的干扰


(2)使用多个进程的策略(使用exit使得子进程正常结束

产生201个子进程,进行质数的筛查

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define LEFT 30000
#define RIGHT 30200

int main()
{
int i,j,mark;
pid_t pid;
for(i = LEFT;i<=RIGHT;i++)
{
fflush(NULL); /* 刷新所有的流 */
// 产生一个进程
pid = fork();
//失败
if (pid < 0)
{
perror("fork()");
exit(1);
}
// 子进程操作,子进程进程质数筛查的工作
if (pid == 0)
{
mark = 1;
for(j=2;j<i/2;j++)
{
if(i%j == 0)
{
// 则i不为质数,mark设置为0
mark = 0;
break;
}
}
if(mark)
printf("%d is a primer\n",i);
}
// 父进程 不进行操作 pid 为其他正整数时
else
{
// 不操作
}
}

exit(0);
}

执行上述代码,系统直接卡死,因为系统实际产生的子进程并非201个,而是2的201次方-1个(2^201-1)

代码debug,给子进程一个退出的标记,使用exit()等待子进程结束,使得子进程正常结束

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define LEFT 30000
#define RIGHT 30200

int main()
{
int i,j,mark;
pid_t pid;
for(i = LEFT;i<=RIGHT;i++)
{
fflush(NULL); /* 刷新所有的流 */
// 产生一个进程
pid = fork();
//失败
if (pid < 0)
{
perror("fork()");
exit(1);
}
// 子进程操作,子进程进程质数筛查的工作
if (pid == 0)
{
mark = 1;
for(j=2;j<i/2;j++)
{
if(i%j == 0)
{
// 则i不为质数,mark设置为0
mark = 0;
break;
}
}
if(mark)
printf("%d is a primer\n",i);
// 等待子进程结束
exit(0);
}
// 父进程 不进行操作 pid 为其他正整数时
else
{
// 不操作
}
}

exit(0);
}

make之后执行,$ time [可执行文件]

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
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ time ./3
30011 is a primer
30013 is a primer
30029 is a primer
30047 is a primer
30059 is a primer
30071 is a primer
30089 is a primer
30091 is a primer
30097 is a primer
30103 is a primer
30109 is a primer
30113 is a primer
30119 is a primer
30133 is a primer
30137 is a primer
30139 is a primer
30161 is a primer
30169 is a primer
30181 is a primer
30187 is a primer
30197 is a primer

real 0m0.018s
user 0m0.000s
sys 0m0.018s

对于执行的结果,实际上每个子进程的执行顺序,是由于调度器的调度策略来决定。但是对于时间,使用多进程并发,real时间好像并没有减少,这与调度器的调度策略以及系统处理器的内核数量(处理器的内核数量(CPU核心数)决定了计算机在同一时刻可以并行处理多少个线程或进程)有关。


(3)若不使用exit退出子进程,计算总共产生子进程的数量

编写一个程序,在for循环中fork一定数量的子进程,但是不使用exit等待其结束,计算总产生的子进程数量

#define process_NUM 1
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define process_NUM 1

int main()
{
pid_t pid;
int i;

// 父进程打印
printf("[%d]:Begin!\n",getpid());

for(i = 0;i<process_NUM;i++)
{
fflush(NULL);
// 开始复制,一式两份
pid = fork();
// 失败小于0
if(pid < 0)
{
perror("getpid()");
exit(1);
}

// 子进程中==0
if (pid == 0)
{
printf("[%d]:Child is working!\n",getpid());
}
// 父进程中 返回子进程的PID号
else
{
printf("[%d]:Parent is working!\n",getpid());
}

printf("[%d]:End!\n",getpid());
}

// 使得进程一直运行,不结束,终端中等待字符输入
getchar();
exit(0);
}

make 之后运行:

1
2
3
4
5
6
7
8
9
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ make 4
cc 4.c -o 4
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ ./4
[9235]:Begin!
[9235]:Parent is working!
[9235]:End!
[9236]:Child is working!
[9236]:End!

产生了一个子进程,PID为9236,使用ps -auf命令查看进程之间的关系

1
2
3
zxz         5895  0.0  0.0  14372  5764 pts/0    Ss   08:55   0:00 bash
zxz 9235 0.0 0.0 2500 580 pts/0 S+ 10:14 0:00 \_ ./4
zxz 9236 0.0 0.0 2500 88 pts/0 S+ 10:14 0:00 \_ ./4

5895为shell的进程,而9235为程序的主进程,9236为9235的子进程,此时产生了一个子进程

#define process_NUM 2
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define process_NUM 2

int main()
{
pid_t pid;
int i;

// 父进程打印
printf("[%d]:Begin!\n",getpid());

for(i = 0;i<process_NUM;i++)
{
fflush(NULL);
// 开始复制,一式两份
pid = fork();
// 失败小于0
if(pid < 0)
{
perror("getpid()");
exit(1);
}

// 子进程中==0
if (pid == 0)
{
printf("[%d]:Child is working!\n",getpid());
}
// 父进程中 返回子进程的PID号
else
{
printf("[%d]:Parent is working!\n",getpid());
}

printf("[%d]:End!\n",getpid());
}


getchar();
exit(0);
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ make 4
cc 4.c -o 4
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ ./4
[9376]:Begin!
[9376]:Parent is working!
[9376]:End!
[9376]:Parent is working!
[9376]:End!
[9377]:Child is working!
[9377]:End!
[9378]:Child is working!
[9378]:End!
[9377]:Parent is working!
[9377]:End!
[9379]:Child is working!
[9379]:End!

使用ps -auf命令查看进程之间的关系

1
2
3
4
5
zxz         5895  0.0  0.0  14372  5764 pts/0    Ss   08:55   0:00 bash
zxz 9376 0.0 0.0 2500 584 pts/0 S+ 10:18 0:00 \_ ./4
zxz 9377 0.0 0.0 2500 88 pts/0 S+ 10:18 0:00 \_ ./4
zxz 9379 0.0 0.0 2500 88 pts/0 S+ 10:18 0:00 | \_ ./4
zxz 9378 0.0 0.0 2500 88 pts/0 S+ 10:18 0:00 \_ ./4

9376为程序的主进程,9377与9378为主进程的子进程,9379为9377的子进程,程序总共产生了3个子进程

#define process_NUM 3
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define process_NUM 3

int main()
{
pid_t pid;
int i;

// 父进程打印
printf("[%d]:Begin!\n",getpid());

for(i = 0;i<process_NUM;i++)
{
fflush(NULL);
// 开始复制,一式两份
pid = fork();
// 失败小于0
if(pid < 0)
{
perror("getpid()");
exit(1);
}

// 子进程中==0
if (pid == 0)
{
printf("[%d]:Child is working!\n",getpid());
}
// 父进程中 返回子进程的PID号
else
{
printf("[%d]:Parent is working!\n",getpid());
}

printf("[%d]:End!\n",getpid());
}


getchar();
exit(0);
}

运行结果

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
28
29
30
31
32
33
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ make 4
cc 4.c -o 4
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ ./4
[9528]:Begin!
[9528]:Parent is working!
[9528]:End!
[9528]:Parent is working!
[9528]:End!
[9529]:Child is working!
[9529]:End!
[9528]:Parent is working!
[9528]:End!
[9530]:Child is working!
[9530]:End!
[9529]:Parent is working!
[9529]:End!
[9531]:Child is working!
[9531]:End!
[9530]:Parent is working!
[9530]:End!
[9529]:Parent is working!
[9529]:End!
[9532]:Child is working!
[9532]:End!
[9533]:Child is working!
[9533]:End!
[9534]:Child is working!
[9534]:End!
[9532]:Parent is working!
[9532]:End!
[9535]:Child is working!
[9535]:End!

使用ps -auf命令查看进程之间的关系

1
2
3
4
5
6
7
8
9
zxz         5895  0.0  0.0  14372  5764 pts/0    Ss   08:55   0:00 bash
zxz 9528 0.0 0.0 2500 512 pts/0 S+ 10:23 0:00 \_ ./4
zxz 9529 0.0 0.0 2500 84 pts/0 S+ 10:23 0:00 \_ ./4
zxz 9532 0.0 0.0 2500 84 pts/0 S+ 10:23 0:00 | \_ ./4
zxz 9535 0.0 0.0 2500 84 pts/0 S+ 10:23 0:00 | | \_ ./4
zxz 9534 0.0 0.0 2500 84 pts/0 S+ 10:23 0:00 | \_ ./4
zxz 9530 0.0 0.0 2500 84 pts/0 S+ 10:23 0:00 \_ ./4
zxz 9533 0.0 0.0 2500 84 pts/0 S+ 10:23 0:00 | \_ ./4
zxz 9531 0.0 0.0 2500 84 pts/0 S+ 10:23 0:00 \_ ./4

9528为程序的主进程,总共产生了7个子进程

#define process_NUM 4
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define process_NUM 4

int main()
{
pid_t pid;
int i;

// 父进程打印
printf("[%d]:Begin!\n",getpid());

for(i = 0;i<process_NUM;i++)
{
fflush(NULL);
// 开始复制,一式两份
pid = fork();
// 失败小于0
if(pid < 0)
{
perror("getpid()");
exit(1);
}

// 子进程中==0
if (pid == 0)
{
printf("[%d]:Child is working!\n",getpid());
}
// 父进程中 返回子进程的PID号
else
{
printf("[%d]:Parent is working!\n",getpid());
}

printf("[%d]:End!\n",getpid());
}


getchar();
exit(0);
}

运行结果

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ make 4
cc 4.c -o 4
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ ./4
[9697]:Begin!
[9697]:Parent is working!
[9697]:End!
[9697]:Parent is working!
[9697]:End!
[9698]:Child is working!
[9698]:End!
[9699]:Child is working!
[9699]:End!
[9697]:Parent is working!
[9697]:End!
[9697]:Parent is working!
[9698]:Parent is working!
[9697]:End!
[9698]:End!
[9699]:Parent is working!
[9699]:End!
[9700]:Child is working!
[9700]:End!
[9701]:Child is working!
[9701]:End!
[9698]:Parent is working!
[9698]:End!
[9699]:Parent is working!
[9699]:End!
[9702]:Child is working!
[9702]:End!
[9703]:Child is working!
[9700]:Parent is working!
[9703]:End!
[9700]:End!
[9705]:Child is working!
[9705]:End!
[9701]:Parent is working!
[9701]:End!
[9706]:Child is working!
[9706]:End!
[9704]:Child is working!
[9704]:End!
[9698]:Parent is working!
[9698]:End!
[9703]:Parent is working!
[9703]:End!
[9701]:Parent is working!
[9701]:End!
[9709]:Child is working!
[9709]:End!
[9708]:Child is working!
[9707]:Child is working!
[9708]:End!
[9707]:End!
[9710]:Child is working!
[9704]:Parent is working!
[9710]:End!
[9704]:End!
[9707]:Parent is working!
[9707]:End!
[9711]:Child is working!
[9711]:End!
[9712]:Child is working!
[9712]:End!

使用ps -auf命令查看进程之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
zxz         5895  0.0  0.0  14372  5764 pts/0    Ss   08:55   0:00 bash
zxz 9697 0.0 0.0 2500 580 pts/0 S+ 10:27 0:00 \_ ./4
zxz 9698 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 \_ ./4
zxz 9701 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | \_ ./4
zxz 9707 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | | \_ ./4
zxz 9712 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | | | \_ ./4
zxz 9710 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | | \_ ./4
zxz 9704 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | \_ ./4
zxz 9711 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | | \_ ./4
zxz 9708 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | \_ ./4
zxz 9699 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 \_ ./4
zxz 9703 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | \_ ./4
zxz 9709 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | | \_ ./4
zxz 9705 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | \_ ./4
zxz 9700 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 \_ ./4
zxz 9706 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 | \_ ./4
zxz 9702 0.0 0.0 2500 88 pts/0 S+ 10:27 0:00 \_ ./4

9697为程序的主进程,总共产生了15个子进程

#define process_NUM n

若将预期产生n个子进程,若不使用exit退出子进程,那么最后总共产生的进程数量为2^n-1,存在数量之庞大的子进程,会吃紧系统的资源,造成系统的性能下降,由此可见,使用exit退出子进程,是多么的重要!!!


(4)孤儿进程与僵尸进程
孤儿进程

在计算机操作系统中,孤儿进程是指其父进程结束,而它自己还在运行的进程。当一个父进程结束,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程会被init进程(进程号为1的进程)所接管。init进程会定期进行清理,等待这些孤儿进程结束。

孤儿进程并不会对系统造成直接的危害,因为他们是被init进程接管的,而不是“无人看管”。但是,他们占用系统资源,比如内存,如果数量过多可能会影响系统性能

创建孤儿进程的一种常见情况是父进程在启动子进程后立即退出,或者是因为某种原因(比如程序错误或意外情况)父进程提前退出。为了避免产生孤儿进程,父进程通常会通过调用wait()或相关的系统调用来等待一个或多个子进程结束。这样,父进程就可以获得子进程的退出状态,并且子进程在结束后就不再存在,因此不会成为孤儿进程

代码产生孤儿进程

在子进程结束之前使用sleep(1000)使其休眠,使得主进程先结束,这时候的子进程变成了孤儿进程

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define process_NUM 3

int main()
{
pid_t pid;
int i;

// 父进程打印
printf("[%d]:Begin!\n",getpid());

for(i = 0;i<process_NUM;i++)
{
fflush(NULL);
// 开始复制,一式两份
pid = fork();
// 失败小于0
if(pid < 0)
{
perror("getpid()");
exit(1);
}

/************************重点代码段*********************/
// 子进程中==0
if (pid == 0)
{
printf("[%d]:Child is working!\n",getpid());
// 子进程休眠
sleep(1000);
// 子进程退出
exit(0);
}
/************************重点代码段*********************/

// 父进程中 返回子进程的PID号
else
{
printf("[%d]:Parent is working!\n",getpid());
}

printf("[%d]:End!\n",getpid());

}

exit(0);
}

执行之后使用ps -auf查看进程关系

1
2
3
4
zxz         5895  0.0  0.0  14372  5764 pts/0    Ss+  08:55   0:00 bash
zxz 10789 0.0 0.0 2500 84 pts/0 S 10:50 0:00 ./4
zxz 10788 0.0 0.0 2500 84 pts/0 S 10:50 0:00 ./4
zxz 10787 0.0 0.0 2500 84 pts/0 S 10:50 0:00 ./4

PID为10789、10788、10787的子进程状态均为 S(可中断的睡眠态)当前进程的./4顶格写表示当前子进程的父进程为init,表示这些子进程变成了孤儿进程,并且由init开始接管,这个时候,init进程需要等待这一批孤儿进程sleep(1000)结束之后给其收尸

僵尸进程

在Unix和类Unix操作系统中,当一个子进程比它的父进程先结束,在父进程还没有来得及获取(回收)子进程的退出状态信息时,子进程就会进入“僵尸”状态。僵尸进程已经结束,不再占用CPU资源,但它的进程描述符仍保留在系统中,包括它的进程ID、退出状态和资源使用信息等

僵尸进程的存在主要是为了让父进程在稍后的某个时间点能够获取子进程的退出状态和相关信息。父进程通过调用wait()waitpid()系统调用来获取这些信息,此时系统将清理僵尸进程的进程描述符,释放其占用的资源。如果父进程没有调用wait()waitpid(),子进程将会一直保持僵尸状态。

如果一个进程结束时,它的父进程已经退出,那么这个进程会成为孤儿进程,被init进程(PID为1的进程)接管。init进程会定期调用wait()来获取孤儿进程的退出状态,因此不会有僵尸进程长时间存在。

虽然单个僵尸进程并不占用多少资源,但如果有大量的僵尸进程存在,它们会消耗有限的进程ID,可能会导致无法创建新的进程。因此,程序应该适当地处理子进程的退出,避免产生大量的僵尸进程

代码产生僵尸进程

在主进程结束之前使用sleep(1000)使其休眠,使得子进程先结束,这时候的子进程没有主进程进行收尸变成了僵尸进程

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define process_NUM 3

int main()
{
pid_t pid;
int i;

// 父进程打印
printf("[%d]:Begin!\n",getpid());

for(i = 0;i<process_NUM;i++)
{
fflush(NULL);
// 开始复制,一式两份
pid = fork();
// 失败小于0
if(pid < 0)
{
perror("getpid()");
exit(1);
}

// 子进程中==0
if (pid == 0)
{
printf("[%d]:Child is working!\n",getpid());

// 等待子进程结束
exit(0);
}
// 父进程中 返回子进程的PID号
else
{
printf("[%d]:Parent is working!\n",getpid());
}

printf("[%d]:End!\n",getpid());

}


// 主进程休眠
sleep(1000);
exit(0);
}

运行之后,使用ps -auf查看进程关系

1
2
3
4
5
zxz         5895  0.0  0.0  14372  5764 pts/0    Ss   08:55   0:00 bash
zxz 11176 0.0 0.0 2500 576 pts/0 S+ 10:59 0:00 \_ ./4
zxz 11177 0.0 0.0 0 0 pts/0 Z+ 10:59 0:00 \_ [4] <defunct>
zxz 11178 0.0 0.0 0 0 pts/0 Z+ 10:59 0:00 \_ [4] <defunct>
zxz 11179 0.0 0.0 0 0 pts/0 Z+ 10:59 0:00 \_ [4] <defunct>

PID为11176、11177、11179的子进程状态均为 Z+(僵尸态)

3.vfork()

fork() 一样,vfork() 也用于创建新的进程,但与 fork() 不同的是,vfork() 创建的子进程会共享父进程的地址空间,而不是像 fork() 那样创建一个完全独立的地址空间。这使得 vfork() 在创建进程时更加高效,因为不需要复制父进程的页表条目

1
pid_t vfork(void);

返回类型是 pid_t,适用于存储进程 ID 的整数类型。

vfork() 的返回值和 fork() 相同:

  • 如果 vfork() 的返回值为负数,表示创建新进程失败。
  • 如果 vfork() 的返回值为 0,表示这是新创建的子进程。在子进程中,vfork() 函数将返回 0。
  • 如果 vfork() 的返回值为正数,这个值就是新创建子进程的 PID(进程ID),这意味着当前代码在父进程中执行

由于 vfork() 会导致子进程和父进程共享同一地址空间,因此需要特别小心处理父进程和子进程之间的交互,以避免数据竞争和其他并发问题。在子进程中,必须避免修改父进程的内存,且在执行任何可能影响这个内存的操作之前,子进程必须先通过调用 _exit() 或者 exec() 来退出。因此,使用 vfork() 需要特别小心,只有在特定的性能要求下才会选择使用

重点:使用 fork() 通常是更安全、更可靠的选择,vfork基本已经废弃




三、进程的消亡与释放资源

1.wait与waitpid与waitid

(1)wait()

wait() 是一个系统调用,它用于使父进程暂停执行(阻塞),直到它的一个子进程结束为止。这对于防止子进程变成僵尸进程(已经终止但尚未被父进程收集的进程)非常有用

存在误解

1
2
3
确切地说,wait() 函数的确使父进程暂停执行,直到其任意一个子进程结束。使用 wait() 函数的主要目的是回收子进程的资源,防止其成为僵尸进程。当一个子进程结束,但父进程还没有调用 wait() 或 waitpid() 来读取它的退出状态时,这个子进程就会成为僵尸进程。

你所说的孤儿进程是指父进程结束,而子进程仍然在运行的情况。在这种情况下,子进程会被 init 进程(在 Unix 和 Linux 系统中 PID 为 1 的进程)接管,子进程不会变成僵尸进程,因为 init 进程会自动调用 wait() 来等待它们结束。

函数原型

1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

这个函数的参数是一个指向整数的指针,它用于存储子进程的退出状态信息。如果你对退出状态不感兴趣,你可以传递一个空指针

函数的返回值是已经终止的子进程的进程 ID。如果调用失败,函数将返回 -1

示例

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
28
29
30
31
32
33
34
35
36
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
pid_t pid;
int status;
pid_t tpid;

pid = fork();

// 失败
if (pid < 0) {
perror("fork() failure\n");
return 1;
}

// 子进程操作
if (pid == 0) {
printf("This is the child process. pid = %d\n", getpid());
sleep(10); // Sleep for 10 seconds
return EXIT_SUCCESS;
}
// 父进程操作
else {
do {
// 等待子进程结束,并且回收子进程资源
tpid = wait(&status);
printf("Exited child pid = %d\n", tpid);
} while (tpid != pid);

return EXIT_SUCCESS;
}
}

在这个例子中,我们首先创建一个新的子进程。子进程会睡眠 10 秒,然后正常退出。父进程会通过 wait() 调用来等待子进程退出。当 wait() 返回时,我们知道子进程已经退出,我们可以获取和打印子进程的退出状态


(2)waitpid()

waitpid() 是一个系统调用,它用于让父进程等待其子进程的结束。不过,与 wait() 不同,waitpid() 允许你指定你想要等待的特定子进程,或者等待满足特定条件的子进程

函数原型

1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

pid: 是你想要等待的子进程的进程 ID。有几种不同的值:

  • 如果 pid > 0,waitpid() 只会等待特定 PID 的子进程。
  • 如果 pid == -1,waitpid() 会等待任何子进程,和 wait() 相同。
  • 如果 pid == 0,waitpid() 会等待与当前进程在同一进程组的任何子进程。
  • 如果 pid < -1,waitpid() 会等待其进程组 ID 等于 pid 的绝对值的任何子进程。

status: 是一个指向 int 的指针,这个 int 用来存储子进程的结束状态信息

options: 是一个控制 waitpid() 行为的标志位,如下:

  • WNOHANG:如果没有子进程已经结束,那么 waitpid() 不会阻塞,而是立即返回一个 0。
  • WUNTRACED:如果子进程已经停止(不是结束),那么也视为结束。

waitpid() 函数的返回值是子进程的 PID(如果子进程已经结束),或者是 0(如果设置了 WNOHANG 选项并且没有子进程已经结束),或者是 -1(如果函数调用失败)

示例:

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
28
29
30
31
32
33
34
35
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
pid_t pid,tpid;
int status;

pid = fork();

if (pid < 0) {
perror("fork() failure\n");
return 1;
}

if (pid == 0) {
printf("This is the child process. pid = %d\n", getpid());
sleep(10); // Sleep for 10 seconds
return EXIT_SUCCESS;
} else {
do {
tpid = waitpid(pid, &status, WNOHANG);
if(tpid == 0) {
printf("Child is still running.\n");
sleep(1); // Sleep for 1 second
} else {
printf("Child exited with status %d\n", status);
}
} while (tpid == 0);

return EXIT_SUCCESS;
}
}

在这个例子中,我们首先创建一个新的子进程。子进程会睡眠 10 秒,然后正常退出。父进程通过 waitpid() 调用来检查子进程是否已经退出,但如果子进程还在运行,父进程不会被阻塞,而是打印一条消息然后继续检查。当子进程退出时,父进程获取并打印子进程的退出状态



2.进程的交叉分配筛选质数

在第二节的第2小节的进程实例中,使用多个进程的策略,创建了201个子进程实现质数的筛选,这样的做法不太好,可以使用交叉分配法来实现质数的筛选,总共fork三个子进程,轮流将待检查的数字交给创建的子进程

代码实现

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>


#define LEFT 30000000
#define RIGHT 30000200
#define N 3

int main()
{
int i,j,mark,n,f_n;
pid_t pid;

// fork三个子进程
for(n = 0;n<N;n++)
{
fflush(NULL); /* 刷新所有的流 */
// 产生一个进程
pid = fork();

//失败
if (pid < 0)
{
perror("fork()");
// 还需要进行资源的回收
for(f_n = 0;f_n<n;f_n++)
{
wait(NULL);
}
// 异常退出
exit(1);
}

// 子进程操作,子进程进程质数筛查的工作
if(pid == 0)
{
// 将待检查的数字交叉分配给创建的子进程进行判断
for(i = LEFT+n;i<=RIGHT;i+=N)
{
mark = 1;
for(j=2;j<i/2;j++)
{
if(i%j == 0)
{
// 则i不为质数,mark设置为0
mark = 0;
break;
}
}
if(mark)
// [%d] 表示的是那一个进程执行的此次任务,并非pid号
printf("[%d]:%d is a primer\n",n,i);
}
// 每个子进程将各自的任务完成后,子进程结束
exit(0);
}
}

// 子进程的资源回收
for(n=0;n<N;n++)
wait(NULL);


exit(0);
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ make 7
cc 7.c -o 7
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ ./7
[2]:30000023 is a primer
[1]:30000001 is a primer
[2]:30000041 is a primer
[1]:30000037 is a primer
[2]:30000059 is a primer
[1]:30000049 is a primer
[2]:30000071 is a primer
[1]:30000079 is a primer
[1]:30000109 is a primer
[2]:30000083 is a primer
[1]:30000133 is a primer
[2]:30000137 is a primer
[1]:30000163 is a primer
[2]:30000149 is a primer
[2]:30000167 is a primer
[1]:30000169 is a primer
[1]:30000193 is a primer
[1]:30000199 is a primer



四、exec函数族

exec() 函数系列用于从当前进程空间加载并运行一个新的程序exec() 函数实际上是一组函数,也被称为 exec 函数族。这些函数包括:execl()execle()execlp()execv()execvp()execvpe()

这些函数的主要区别在于如何指定新程序的参数,以及如何查找新程序的文件。

所有的 exec() 函数都有以下几个共同的特性:

  • 执行 exec() 会替换当前进程的映像,新的程序从其 main() 函数开始执行。如果 exec() 执行成功,它就不会返回。如果返回了,那就表示有错误发生
  • exec() 函数使用的文件描述符默认都是继承自原来的进程。这就意味着在 exec() 之后,新的程序将能够使用已经打开的文件描述符

1.execl与execp与exece

(1)execl()

execl() 函数它用于替换当前进程的映像为一个新的程序

在使用exec函数族的函数时,都需要刷新所有的流,同fork函数

函数原型

1
2
3
#include <unistd.h>

int execl(const char *path, const char *arg0, ..., NULL);

函数的参数如下:

  • path:新程序的完全路径名。
  • arg0arg1,…:传递给新程序的参数列表。按照惯例,arg0 应该是程序的名称(它不必是程序的完整路径)。这个参数列表必须以一个 NULL 指针作为结束

如果 execl() 函数调用成功,那么这个函数不会返回,因为当前进程已经被新程序完全替换。如果函数返回,那只能说明出现了错误。在这种情况下,函数返回 -1,并且设置 errno 为具体的错误代码

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
puts("Begin!");
// 在使用exec函数族的函数时,都需要刷新所有的流,同fork函数
fflush(NULL);

// 如果这句代码执行成功,则会执行一个新的程序 "/bin/ls"
execl("/bin/ls", "ls", "-l", NULL);

// 若执行成功后面的语句均不会执行
// 若执行失败,则会继续执行下面的语句
perror("execl()");
puts("End!");
exit(1);
}

在这个例子中,我们用 /bin/ls 程序替换了当前进程,传递了两个参数给 ls 程序:ls-l。请注意,参数列表必须以 NULL 指针作为结束。如果 execl() 函数调用成功,那么这个函数不会返回。如果函数返回,那就表示有错误发生

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ ./8
Begin!
总用量 196
-rwxrwxr-x 1 zxz zxz 16912 729 15:58 1
-rw-rw-r-- 1 zxz zxz 517 730 09:06 1.c
-rwxrwxr-x 1 zxz zxz 16736 729 16:09 2
-rw-rw-r-- 1 zxz zxz 331 729 16:09 2.c
-rwxrwxr-x 1 zxz zxz 16912 730 10:39 3
-rw-rw-r-- 1 zxz zxz 734 730 10:43 3.c
-rwxrwxr-x 1 zxz zxz 16952 730 10:59 4
-rw-rw-r-- 1 zxz zxz 689 730 10:59 4.c
-rwxrwxr-x 1 zxz zxz 16960 731 10:11 5
-rw-rw-r-- 1 zxz zxz 751 731 10:11 5.c
-rwxrwxr-x 1 zxz zxz 17048 731 10:26 6
-rw-rw-r-- 1 zxz zxz 801 731 10:26 6.c
-rwxrwxr-x 1 zxz zxz 16912 731 11:07 7
-rw-rw-r-- 1 zxz zxz 1164 731 11:08 7.c
-rwxrwxr-x 1 zxz zxz 16824 731 11:29 8
-rw-rw-r-- 1 zxz zxz 391 731 11:29 8.c
-rw-rw-r-- 1 zxz zxz 94 729 15:58 out
在子进程中使用execl函数
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>


int main(void) {
puts("Begin!");

pid_t pid;

// fork至其需要刷新所有的流
fflush(NULL);

pid = fork();

if(pid < 0)
{
perror("fork()");
exit(1);
}

// 子进程操作
if(pid ==0)
{
fflush(NULL);
// 如果这句代码执行成功,则会执行一个新的程序 "/bin/ls"
execl("/bin/ls", "ls", "-l", NULL);
// 执行失败才会执行下面的语句
perror("execl()");
// 子进程结束
exit(1);
}

// 父进程,等待回收子进程资源
wait(NULL);

puts("End!");
exit(0);
}

运行结果

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
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ make 9
cc 9.c -o 9
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/process$ ./9
Begin!
总用量 220
-rwxrwxr-x 1 zxz zxz 16912 729 15:58 1
-rw-rw-r-- 1 zxz zxz 517 730 09:06 1.c
-rwxrwxr-x 1 zxz zxz 16736 729 16:09 2
-rw-rw-r-- 1 zxz zxz 331 729 16:09 2.c
-rwxrwxr-x 1 zxz zxz 16912 730 10:39 3
-rw-rw-r-- 1 zxz zxz 734 730 10:43 3.c
-rwxrwxr-x 1 zxz zxz 16952 730 10:59 4
-rw-rw-r-- 1 zxz zxz 689 730 10:59 4.c
-rwxrwxr-x 1 zxz zxz 16960 731 10:11 5
-rw-rw-r-- 1 zxz zxz 751 731 10:11 5.c
-rwxrwxr-x 1 zxz zxz 17048 731 10:26 6
-rw-rw-r-- 1 zxz zxz 801 731 10:26 6.c
-rwxrwxr-x 1 zxz zxz 16912 731 11:07 7
-rw-rw-r-- 1 zxz zxz 1164 731 11:08 7.c
-rwxrwxr-x 1 zxz zxz 16864 731 11:32 8
-rw-rw-r-- 1 zxz zxz 502 731 11:32 8.c
-rwxrwxr-x 1 zxz zxz 16952 731 11:41 9
-rw-rw-r-- 1 zxz zxz 735 731 11:40 9.c
-rw-rw-r-- 1 zxz zxz 94 729 15:58 out
End!



五、用户权限及组权限

视频教程




六、解释器文件

视频教程




七、system()

理解fork + execl + wait的封装

函数原型

1
int system(const char *command);

函数的参数如下:

  • command:一个 C 字符串,包含了要执行的 shell 命令。

system() 函数的返回值取决于 shell 命令的执行情况:如果 shell 命令执行成功,那么返回值通常是命令的退出状态。如果 shell 命令执行失败,那么返回值通常是 -1

1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>

int main(void) {
int return_value;

return_value = system("ls -l");
printf("Return value is %d\n", return_value);

return 0;
}

使用 system() 函数执行 ls -l 命令。然后我们打印 system() 函数的返回值。请注意,system() 函数的行为可能受到信号、阻塞操作等因素的影响,所以在具有高安全要求的环境中应谨慎使用




八、进程会计、进程时间

视频教程




九、守护进程

守护进程是 Unix 和类 Unix 系统(比如 Linux)中的一种特殊的后台进程。它们独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程通常在系统引导装载的时候启动,然后一直运行直到系统关闭。有时,它们也会在需要时启动,然后在不再需要时结束。

守护进程一般满足以下条件

  • 脱离控制终端,控制终端上的输入输出对守护进程存在影响(同时会将其标准的输入输出IO进行重定向),可以在终端中发送信号直接关联上守护进程,会影响守护进程的使用
  • 守护进程一般是一个会话的leader以及一个进程组的leader

守护进程的名字通常以 “d” 结尾,以表明它们是后台运行的守护进程,比如 sshdhttpdmysqld

为了创建一个守护进程,一个进程通常会包含以下步骤

  • fork() 创建子进程,然后使父进程退出。这样做的目的是让守护进程在后台运行,并且让 shell 认为该命令已经执行完毕。
  • 在子进程中创建一个新的会话。这样做的目的是让守护进程摆脱原来的控制终端。
  • 改变当前目录到根目录(/)。这样做的目的是防止守护进程占用一个可能已经被卸载的文件系统。
  • 重设文件权限掩码。这样做的目的是确保守护进程有可能创建的任何文件都有合适的权限。
  • 关闭所有继承的文件描述符。这样做的目的是防止守护进程保持打开不必要的文件描述符


1.会话

会话(Session)是一种组织进程的方式。会话中可以包含一个或多个进程组(Process Group),每个进程组又可以包含一个或多个进程。会话和进程组都是以进程 ID(PID)来标识的,即一个会话的 ID 就是该会话的领头进程的 PID,一个进程组的 ID 就是该进程组的领头进程的 PID

会话通常由 shell(登陆shell可以理解为一次成功的终端登陆) 或者登录进程创建。当用户登录系统时,登录进程就会创建一个新的会话,然后在这个会话中启动 shell。然后,用户通过 shell 创建的所有进程(包括后台进程)都会成为这个会话的一部分

一个会话内的所有进程组共享一个控制终端(Controlling Terminal)。这个控制终端通常就是用户登录时的终端(可以是物理终端,也可以是伪终端,比如一个终端窗口)。会话的领头进程就是拥有这个控制终端的进程,也是可以接收来自控制终端的各种信号的进程。

一个会话中的所有进程共享同样的登录凭据,包括相同的用户 ID、组 ID、项目 ID 等。

在创建守护进程时,通常需要调用 setsid() 来创建一个新的会话,以便摆脱原有的控制终端和登录会话,使守护进程在后台独立运行

(1)前台进程组与后台进程组

进程组是一个或多个进程的集合,这些进程可以接收来自同一终端的信号。一个会话中可以有多个进程组,其中一个被标记为前台进程组,其他的被标记为后台进程组

前台进程组 是当前与用户交互的进程组。在命令行终端中,用户输入的命令通常在前台进程组中运行,并且只有前台进程组可以从终端接收输入(前台进程组只能有一个)。如果你在命令行中运行一个命令,这个命令就会在前台执行,直到它完成

后台进程组 是那些当前不与用户交互的进程组。后台进程组不会接收到来自终端的输入。你可以在命令行中使用 & 符号来启动一个后台进程。例如,command & 会在后台启动 command 命令,而终端会立即返回命令提示符,让你可以输入其他命令。注意,若我们尝试将一个标准输入的内容给后台进程,那么这个时候会将后台进程杀死

前台和后台的概念允许用户在一个终端中同时运行多个命令。你可以在前台运行一个命令,同时在后台运行一个或多个其他命令。然后,你可以使用 fgbg 命令来在前台和后台之间移动进程



2.setsid

setsid() 是一个用来创建新会话(session)的系统调用。当一个进程调用 setsid() 时,该进程将成为新会话的领头进程和新进程组的领头进程

函数原型

1
pid_t setsid(void);

该函数没有参数。如果成功,它将返回新会话的会话 ID。如果失败,它将返回 -1 并设置 errno

调用 setsid() 的进程不能是进程组的领头进程(父进程与子进程在同一个进程组里面,并且父进程为组里面的领头进程,父进程不能调用setsid函数)。这就是为什么在创建守护进程时,我们通常先调用 fork() 然后使父进程退出,这样子进程就不是进程组的领头进程了,然后在子进程中调用 setsid()

创建新会话后,该进程将:

  • 成为新会话的领头进程和新进程组的领头进程
  • 没有控制终端。如果调用 setsid() 前该进程有控制终端,那么这个连接将被断开
  • 成为其创建的任何新文件的文件主

在创建守护进程时,通常会调用 setsid() 来创建新会话,使守护进程不受控制终端的影响



3.getpgrp

getpgrp()函数用于获取当前进程的进程组ID。进程组是一种可以将相关进程组织在一起的机制,允许操作系统对一组进程进行统一管理

1
2
3
#include <unistd.h>

pid_t getpgrp(void);

getpgrp()函数没有参数。如果成功,它将返回当前进程的进程组ID。如果失败,它将返回-1并设置errno

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
pid_t pgid;

pgid = getpgrp();
if (pgid == -1) {
perror("getpgrp()");
exit(1);
}

printf("The process group ID is %d\n", pgid);
exit(0);
}

调用getpgrp()获取当前进程的进程组ID,然后打印它。如果调用失败,我们打印错误信息并退出程序



4.getpgid

getpgid() 函数用于获取指定进程的进程组 ID。

函数的原型:

1
2
3
#include <unistd.h>

pid_t getpgid(pid_t pid);

函数的参数如下:

  • pid:要查询的进程的 ID。如果 pid 是 0,getpgid() 将返回调用进程的进程组 ID。

如果成功,getpgid() 函数将返回指定进程的进程组 ID。如果失败,它将返回 -1,并且将 errno 设置为具体的错误码

实例

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
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
// 当前进程的进程组ID(即当前进程组领头进程的PID)
pid_t pgid;
// 当前进程的PID
pid_t cP_pid;
// 当前进程的进程组ID
pid_t cP_G_pid;


pgid = getpgrp();
if (pgid == -1) {
perror("getpgrp()");
exit(1);
}

printf("The process group ID is %d\n", pgid);

// 获取当前进程的pid
cP_pid = getpid();
// 获取当前进程的进程组ID
cP_G_pid = getpgid(cP_pid);
if (cP_G_pid == -1) {
perror("getpgid()");
exit(1);
}

printf("The process group ID is %d\n", cP_G_pid);


exit(0);
}

使用 getpid() 函数获取当前进程的 ID,然后使用 getpgid() 函数获取当前进程的进程组 ID,然后打印它。如果调用失败,我们打印错误信息并退出程序



5.创建一个守护进程

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/*
* 创建一个守护进程,向一个指定的文件每一秒输入一个数字
**/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>


#define FNAME "/tmp/out"

// 守护进程函数
static int daemonize(void)
{
int fd;
pid_t pid;
// 创建一个子进程
pid = fork();
// 失败
if(pid < 0)
{
perror("fork()");
return -1;
}

// 使父进程退出,这样做的目的是让守护进程在后台运行
if(pid > 0)
{
exit(0);
}

// 其他是子进程的操作

// 守护进程脱离控制终端,但是子进程会复制父进程的相关数据,文件描述符有关标准输入输出的0,1,2也会被与预定义,需要进行重定向
// 打开空设备

fd = open("/dev/null",O_RDWR);
if(fd < 0)
{
perror("open()");
return -1;
}
// 对文件描述符0,1,2进行重定向
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
if(fd > 2)
{
close(fd);
}

// 在子进程中创建一个新的会话,这样做的目的是让守护进程摆脱原来的控制终端
setsid();

umask(0);

// 改变工作路径---切换到跟目录
chdir("/");
// umask(0);

return 0;
}

int main()
{
FILE *fp;
int i;

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


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

// 向文件中一秒输入一个字符
for(i=0;;i++)
{
fprintf(fp,"%d\n",i);
// 向文件写入是全缓冲模式,刷新流的操作
fflush(fp);
sleep(1);
}

exit(0);
}

运行之后,使用ps axj查看进程树

1
1874   15262   15262   15262 ?             -1 Ss    1000   0:00 ./guard

上述的结果TTY为?表示脱离了控制终端,但是PPID,父进程的PID为1874不知道为什么

但是上面的代码中使用perror()进行输出是不正确的




十、系统日志

视频教程

1./var/log目录

ubuntu下的系统日志目录

1
/var/log

Ubuntu系统的主要日志文件存储在 /var/log/ 目录中,其中包括了大量的关于系统和应用行为的信息。这些日志文件有助于诊断和解决系统和应用程序的问题。以下是一些最重要的日志文件:

  • /var/log/syslog:这是系统的主要日志文件,几乎所有的系统活动都会在这里被记录。这包括系统启动过程中的信息,以及运行中各种服务和应用程序的日志信息。
  • /var/log/auth.log:这个日志文件记录了所有的系统授权信息,包括用户登录和身份验证失败等。
  • /var/log/kern.log:这个日志文件包含了内核的日志信息,这在解决硬件和操作系统问题时可能会有用。
  • /var/log/dmesg:这个日志文件包含了系统在启动过程中,内核打印的信息。
  • /var/log/boot.log:这个日志文件包含了系统启动过程中的信息,这有助于诊断系统启动问题。
  • /var/log/apache2//var/log/mysql/:如果你在服务器上运行Apache或MySQL,那么这两个目录将包含这些应用程序的日志文件。

注意,访问这些日志文件通常需要管理员权限。你可以使用 sudo 命令来查看这些文件,例如 sudo less /var/log/syslog



2.syslogd服务

syslogd 是一个在 Unix 和 Unix-like 操作系统中运行的守护进程,它用于处理系统日志。其全名是 System Logging Daemon

syslogd 会接收来自系统的各种消息,包括内核、系统库以及各种运行中的服务和应用程序。它将这些消息进行分类并写入到不同的日志文件中,通常这些文件都位于 /var/log/ 目录

在 C 语言中,你可以使用 syslog 函数来向 syslogd 发送消息。这个函数在 syslog.h 头文件中声明

实例程序

1
2
3
4
5
6
7
8
#include <syslog.h>

int main(void) {
openlog("myprogram", LOG_PID|LOG_CONS, LOG_USER);
syslog(LOG_INFO, "This is a test message.");
closelog();
return 0;
}

在这个示例中:

  • openlog 函数初始化了 syslog 系统。它接受三个参数:日志名,日志选项以及默认的日志设施(通常是 LOG_USER)。在这个例子中,日志选项包括了 LOG_PID(在每条日志信息中包含进程 ID)和 LOG_CONS(如果消息不能发送到 syslogd,将其写入到控制台)。
  • syslog 函数发送了一条日志消息。它接受两个参数:消息的优先级(在这个例子中是 LOG_INFO)以及消息本身。
  • closelog 函数关闭了syslog连接。在程序结束时调用这个函数是一个好的实践。

请注意,syslogopenlogcloselog 这些函数都是线程安全的,可以在多线程程序中使用


(1)openlog

openlog()函数是一个系统调用,它用于设置syslogd的参数,以便向syslog守护进程发送消息。这个函数通常在调用syslog()函数之前被调用,(与syslogd服务实现关联)

函数原型

1
2
3
#include <syslog.h>

void openlog(const char *ident, int option, int facility);

参数说明如下:

  • ident:这是一个字符串,它将添加到每个生成的日志条目之前。通常,它被设置为程序的名称(这个参数随便给就可以)
  • option:这是一个位掩码,用于设置日志记录的各种选项。可能的选项包括LOG_PID(在每条日志消息中记录进程ID)、LOG_CONS(如果消息不能发送给syslogd,那么将消息直接写入到控制台)、LOG_NDELAY(立即打开连接,而不是等到第一条消息被发送)、LOG_ODELAY(延迟打开连接,直到第一条消息被发送),等等。选项可以通过或运算(|)来组合。
  • facility:这是一个标识符,用于指示消息的类型**(信息来源,从那里来的)**。可能的值包括LOG_USER(用户级别的消息)、LOG_MAIL(邮件系统的消息)、LOG_DAEMON(系统守护进程的消息)、LOG_AUTH(安全/授权消息),等等

返回值:

  • openlog()函数并不会返回任何值

(2)syslog

syslog() 函数用于向系统日志发送一条消息。此函数通过syslogd守护进程将消息写入日志。在使用 syslog() 之前,通常会使用 openlog() 函数来设置 syslogd 的一些参数

函数原型

1
2
3
#include <syslog.h>

void syslog(int priority, const char *format, ...);

参数说明如下:

  • priority:这是一个整数,用于指定消息的优先级。这是由两部分组成的,一部分是设施值(facility value),一部分是严重性级别(severity level)。设施值用于指定消息的类型,比如 LOG_USER(用户级消息)、LOG_MAIL(邮件系统)、LOG_DAEMON(系统守护进程)等。严重性级别包括 LOG_EMERG(紧急情况,需要立即采取行动)、LOG_ALERT(应立即更正的问题)、LOG_CRIT(关键条件,如硬设备错误)、LOG_ERR(错误条件)、LOG_WARNING(警告条件)、LOG_NOTICE(正常,但重要的条件)、LOG_INFO(信息性消息)以及 LOG_DEBUG(调试级别的信息)。设施值和严重性级别可以用 | 运算符组合起来。
  • format:这是一个格式化字符串,和 printf() 函数中的格式化字符串相同。你可以在这个字符串中使用 % 转义序列来插入变量值,这些值通过可变参数列表提供

实例程序:

1
2
3
4
5
6
7
8
#include <syslog.h>

int main(void) {
openlog("myProgram", LOG_PID | LOG_CONS, LOG_USER);
syslog(LOG_INFO, "Informational message: %d", 123);
closelog();
return 0;
}

syslog() 发送了一条用户级别的信息性消息,消息内容是 “Informational message: 123”


(2)closelog

closelog() 函数是一个系统调用,它用于关闭当前进程与系统日志服务(syslogd)的连接。在 openlog()syslog() 调用之后,可以使用 closelog() 来释放资源

函数原型

1
2
3
#include <syslog.h>

void closelog(void);

closelog() 函数没有参数,并且也不返回任何值。

通常,程序会在所有日志消息都已经被发送到 syslog 服务之后调用 closelog() 函数。这是一个良好的实践,尽管大多数现代的 Unix 和 Unix-like 系统会在进程退出时自动关闭 syslog 连接



3.使用系统日志实例

在之前的守护进程的基础上,将出错信息,发送给系统 日志

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/*
* 创建一个守护进程,向一个指定的文件每一秒输入一个数字
**/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <syslog.h>
#include <string.h>


#define FNAME "/tmp/out"

// 守护进程函数
static int daemonize(void)
{
int fd;
pid_t pid;
// 创建一个子进程
pid = fork();
// 失败
if(pid < 0)
{
return -1;
}

// 使父进程退出,这样做的目的是让守护进程在后台运行
else if(pid > 0)
{
exit(0);
}

// 其他是子进程的操作
else
{
// 守护进程脱离控制终端,但是子进程会复制父进程的相关数据,文件描述符有关标准输入输出的0,1,2也会被与预定义,需要进行重定向
// 打开空设备

fd = open("/dev/null",O_RDWR);
if(fd < 0)
{
return -1;
}
// 对文件描述符0,1,2进行重定向
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
if(fd > 2)
{
close(fd);
}

// 在子进程中创建一个新的会话,这样做的目的是让守护进程摆脱原来的控制终端
setsid();

umask(0);

// 改变工作路径---切换到根目录
chdir("/");
}


return 0;
}

int main()
{
FILE *fp;
int i;

// 与系统日志建立链接
// 参数1:随便给
// 参数2:每条日志中需要包含的信息
// 参数3:信息来源,该程序中是守护进程将日志信息发送给syslog服务
openlog("mydaemon",LOG_PID,LOG_DAEMON);

if(daemonize())
{
// 若守护进程创建失败,将错误信息向系统日志进行提交
// 参数1: 信息的级别
// 参数2: 信息格式(换行符这些类型的字符,也会被syslog函数直接识别的原本的字符进行输入,不会将其理解为换行)
syslog(LOG_ERR,"daemonize() failed!");
// 守护进程结束
exit(1);
}else
{
syslog(LOG_INFO,"daemonize() successded!");
}


fp = fopen(FNAME,"w");
if(fp==NULL)
{
// 向系统日志发送错误信息
syslog(LOG_ERR,"fopen():%s",strerror(errno));
exit(1);
}

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

// 向文件中一秒输入一个字符
for(i=0;;i++)
{
fprintf(fp,"%d\n",i);
// 向文件写入是全缓冲模式,刷新流的操作
fflush(fp);
// 每向文件中输入一个数字,就向日志文件中输入一个调试信息
syslog(LOG_DEBUG,"%d is printed.",i);
sleep(1);
}

fclose(fp);

// 关闭与系统日志syslogd服务的链接
closelog();

exit(0);
}

运行之后,前往目录/var/log目录下的syslog文件查看日志信息tail -f /var/log/syslog

image-20230802111750637