Linux应用进程控制四(特殊的进程 僵尸进程、孤儿进程、守护进程)
一、僵尸进程
1.1、僵尸进程的概念
如果一个进程已经终止,但是它的父进程尚未调用 wait() 或 waitpid() 对它进行清理,这时的进程状态称为僵死状态,处于僵死状态的进程称为僵尸进程(zombie process)。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源。
在 Linux 系统中,僵尸进程(Zombie Process)是指已经完成执行(终止)但其父进程尚未收集其退出状态(exit status)的进程。僵尸进程本身已经不再执行任何任务,所有资源(如 CPU、内存等)都已释放,但它仍然在进程表中占据一个条目,直到父进程调用 wait()
或 waitpid()
等系统调用来获取子进程的退出状态。
1、僵尸进程的形成
当一个进程终止时,它的父进程需要调用 wait()
或 waitpid()
来读取其退出状态,这样操作系统才能完全清理该进程。如果父进程没有及时收集子进程的退出状态,那么子进程就会进入“僵尸”状态,直到父进程主动去清理。
2、僵尸进程的生命周期
- 子进程终止:当一个子进程结束时,它的状态变为“已终止”,但它依然占用进程表条目,直到父进程调用
wait()
。 - 父进程未调用
wait()
:如果父进程没有调用wait()
或waitpid()
来获取子进程的退出状态,子进程的进程表条目不会被操作系统清除,进程变为僵尸状态。 - 僵尸进程被清理:一旦父进程调用了
wait()
,操作系统会清除僵尸进程的进程表条目,彻底释放与该进程相关的资源。
3、僵尸进程的特点
- 已完成执行:僵尸进程实际上已经结束,不会再占用 CPU 时间。
- 占用进程表条目:尽管进程已经结束,但仍然占用进程表中的条目。
- 需要父进程回收:僵尸进程的清理需要父进程通过调用
wait()
或waitpid()
来完成。如果父进程没有处理,则该进程会一直保持僵尸状态。
4、如何查看僵尸进程
可以使用 ps
或 top
命令查看僵尸进程:
使用 ps
命令
ps aux | grep 'Z'
在输出中,状态(STAT)列显示为 Z
表示该进程是僵尸进程。
使用 top
命令 在 top
命令的输出中,僵尸进程的状态也会显示为 Z
。你可以通过按 z
键来高亮显示僵尸进程。
5、僵尸进程的影响
- 占用进程表条目:每个进程都占用一个进程表条目。如果系统中有大量的僵尸进程,可能会导致进程表的条目被耗尽,无法创建新的进程。
- 系统资源消耗:尽管僵尸进程不会消耗 CPU 和内存等资源,但它依然占用进程表条目,可能影响系统的管理和资源分配。
1.2、僵尸进程的危害:
在进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号 PID,退出状态 the termination status of the process,运行时间 the amount of CPU time taken by the process 等)。直到父进程通过 wait / waitpid 来取时才释放。
如果进程不调用 wait / waitpid 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
1.3、如何避免僵尸进程:
1.在fork子进程后,父进程应该调用wait()和waitpid()函数等待子进程结束。(但是如果直接采用wait()函数会导致阻塞)
2.当父进程没有调用wait()或waitpid()函数的时候,可以直接kill掉父进程,让子进程成为孤儿进程,init进程会去接管孤儿进程。(通常不采取这种方法)
3.当子进程终止时,内核就会向它的父进程发送一个SIGCHLD信号,父进程可以选择忽略该信号,也可以提供一个接收到信号以后的处理函数。对于这种信号的系统默认动作是忽略它。
父进程主动调用 wait()
或 waitpid()
: 父进程应当调用 wait()
或 waitpid()
来主动收集子进程的退出状态,避免僵尸进程的产生。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程,执行任务
printf("Child process\n");
exit(0); // 子进程结束
} else if (pid > 0) {
// 父进程,等待子进程退出
wait(NULL); // 收集子进程的退出状态,避免僵尸进程
printf("Parent process\n");
}
return 0;
}
-
使用
signal
机制: 父进程可以设置一个信号处理程序,捕获SIGCHLD
信号,自动清理已终止的子进程。这样一来,当子进程结束时,父进程会收到SIGCHLD
信号,然后调用wait()
来清理子进程,避免僵尸进程。示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
void sigchld_handler(int sig) {
// 处理子进程退出信号
wait(NULL);
}
int main() {
// 设置 SIGCHLD 信号处理程序
signal(SIGCHLD, sigchld_handler);
pid_t pid = fork();
if (pid == 0) {
// 子进程执行任务
printf("Child process\n");
exit(0); // 子进程结束
} else if (pid > 0) {
// 父进程继续运行,子进程结束时会自动回收
printf("Parent process\n");
sleep(10); // 让父进程等待一段时间
}
return 0;
}
如何手动清理僵尸进程
-
等待父进程处理:如果僵尸进程的父进程没有清理它,可以尝试终止父进程。操作系统会自动将僵尸进程的父进程改为
init
进程(PID 为 1),init
进程会自动收集所有未处理的子进程的退出状态,清理僵尸进程。 -
强制杀死父进程: 如果僵尸进程的父进程确实存在问题,可以使用
kill
命令终止父进程。终止父进程后,僵尸进程会被init
进程接管并清理。示例:
kill -9 <父进程PID>
这将终止父进程,然后僵尸进程将会被 init
清理。
1.4、wait和waitpid函数:
调用wait或waitpid的进程会发生什么:
- 如果其所有子进程都还在运行,则阻塞。
- 如果一个子进程已终止,正等待父进程会获取其终止状态,然后立即返回。
- 如果没有任何子进程,则立即出错返回。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
两个函数返回值:成功,返回进程ID;出错:返回0或-1
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:pid=wait(NULL);
这两个函数的区别:
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。
- waitpid并不等待在其调用之后额第一个终止子进程,它有若干个选项,可以控制它所等待的进程。
二、孤儿进程
如果父进程退出而它的一个或多个子进程还在运行,那么这些子进程就被称为孤儿进程孤儿进程最终将被 init 进程 (进程号为 1 的 init进程) 所收养并由 init 进程完成对它们的状态收集工作。
孤儿进程是没有危害的,孤儿进程是没有父进程的子进程,当孤儿进程没有父进程时,内核就会init设置为孤儿进程的父进程,init进程就会调用wait去释放那些已经退出的子进程,当孤儿进程完成其声明周期之后,init会释放掉其状态信息。孤儿进程实际上是不占用资源的,不会像僵尸进程那样占用ID,损害运行系统。
注意:一个子进程结束,必然先变成僵尸进程。如果父进程有调用wait()或waitpid()函数,则会将资源释放。如果父进程先于子进程结束,那么子进程是在运行期间直接变成孤儿进程,进而被init进程接管,在子进程结束运行后,init进程会自动调用wait函数去释放资源。也就是一个子进程结束运行后必然都会变成僵尸进程知道父进程调用wait()或waitpid()。
1、孤儿进程的形成
孤儿进程的产生是因为进程的父进程在子进程仍在运行时就退出或终止了。当子进程无法访问父进程的资源时,它就变成了孤儿进程。操作系统会将这些孤儿进程的父进程设置为 init
进程,这样 init
就成为了孤儿进程的新父进程。
2、孤儿进程与僵尸进程的区别
- 孤儿进程:是指父进程已经终止,但子进程仍在运行,子进程会被
init
进程收养。 - 僵尸进程:是指子进程已经终止,但父进程尚未回收其退出状态,子进程已经不再执行任何操作,但仍占用进程表中的条目。
孤儿进程通常不会对系统造成严重影响,因为它们会由 init
进程处理和清理。而僵尸进程则占用进程表资源,可能导致资源浪费。
3、孤儿进程的生命周期
- 父进程终止:孤儿进程的父进程在其子进程仍在运行时意外终止,子进程没有父进程。
- 孤儿进程被收养:操作系统将孤儿进程的父进程设置为
init
进程(PID 1),init
进程会接管这个孤儿进程,并继续管理它。 - 孤儿进程的退出:孤儿进程在完成工作后会退出,
init
进程会处理其退出状态,释放资源。
4、孤儿进程的影响
- 资源释放:孤儿进程在
init
进程的管理下,资源会被适当释放,因此孤儿进程对系统的资源占用影响较小。 - 不会导致进程表问题:由于孤儿进程被
init
进程收养,进程表条目也会被适当回收,因此不会像僵尸进程一样导致进程表条目浪费。
5、如何查看孤儿进程
可以通过 ps
或 top
命令查看孤儿进程。孤儿进程的父进程 PID 会显示为 1
,因为它们的父进程已经被操作系统设置为 init
进程。
使用 ps
命令:
ps -ef | grep ' 1 '
这条命令将显示父进程 PID 为 1 的进程,这些进程就是孤儿进程。
使用 top
命令: 在 top
命令的输出中,你可以查找父进程 PID 为 1
的进程,显示出来的就是孤儿进程。
6、孤儿进程的举例应用
示例 1:孤儿进程的自然形成
假设有两个进程:父进程和子进程。父进程在子进程运行时崩溃或终止。此时,子进程变成孤儿进程,被 init
进程收养。
父进程:
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid > 0) {
// 父进程
printf("Parent process exiting...\n");
_exit(0); // 父进程退出
} else if (pid == 0) {
// 子进程
printf("Child process running...\n");
sleep(10); // 子进程运行 10 秒
printf("Child process exiting...\n");
_exit(0); // 子进程退出
}
return 0;
}
在这个例子中,父进程在子进程运行时退出,子进程变成孤儿进程,被 init
进程收养。此时,父进程退出后,init
进程会成为子进程的新父进程,子进程继续执行。
示例 2:进程间关系
考虑以下情形,父进程是一个服务器进程,它会派生子进程来处理客户端请求。如果父进程异常终止,而客户端请求处理的子进程仍然在运行,那么这些子进程就会成为孤儿进程,init
进程会接管它们,确保它们的状态得以清理。
三、守护进程
3.1、守护进程的概念
Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。
一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
守护进程的名称通常以d结尾,比如sshd、xinetd、crond等。
3.2、创建守护进程的步骤
首先我们要了解一些基本概念:
进程组 :
- 每个进程也属于一个进程组
- 每个进程组都有一个进程组号,该号等于该进程组组长的PID号 .
- 一个进程只能为它自己或子进程设置进程组ID号
会话期:
会话期(session)是一个或多个进程组的集合。
setsid()函数可以建立一个对话期:
如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期。
(1)此进程变成该对话期的首进程。
(2)此进程变成一个新进程组的组长进程。
(3)此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。 如果该进程是一个进程组的组长,此函数返回错误。
(4)为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行。
编写守护进程的一般步骤步骤:
(1)在父进程中执行fork并exit推出;
(2)在子进程中调用setsid函数创建新的会话;
(3)在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
(4)在子进程中调用umask函数,设置进程的umask为0;
(5)在子进程中关闭任何不需要的文件描述符
说明:
(1)在后台运行
1 2 3 |
|
(2)脱离控制终端,登录会话和进程组
1 2 3 4 |
|
(3)禁止进程重新打开控制终端
1 2 3 |
|
(4)关闭打开的文件描述符
1 2 |
|
(5)改变当前工作目录
1 |
|
(6)重设文件创建掩模
1 |
|
(7)处理SIGCHLD信号
1 2 3 |
|
3.3、创建守护进程
1 2 3 |
|
以下程序是创建一个守护进程,然后利用这个守护进程每个一分钟向daemon.log文件中写入当前时间。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#define LOG_FILE "/path/to/daemon.log"
void create_daemon() {
pid_t pid, sid;
// 创建子进程,脱离父进程
pid = fork();
if (pid < 0) {
perror("Fork failed");
exit(1);
}
// 父进程退出,子进程继续执行,成为守护进程
if (pid > 0) {
exit(0);
}
// 创建新的会话,脱离控制终端
sid = setsid();
if (sid < 0) {
perror("setsid failed");
exit(1);
}
// 改变工作目录为根目录,避免占用文件系统
if (chdir("/") < 0) {
perror("chdir failed");
exit(1);
}
// 设置文件权限掩码为默认值
umask(0);
// 关闭标准输入输出和错误输出
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
}
void log_time() {
FILE *logfile;
time_t rawtime;
struct tm *timeinfo;
char time_str[80];
// 打开日志文件,若不存在则创建
logfile = fopen(LOG_FILE, "a");
if (logfile == NULL) {
perror("Failed to open log file");
exit(1);
}
// 获取当前时间
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", timeinfo);
// 将当前时间写入日志文件
fprintf(logfile, "Current time: %s\n", time_str);
fclose(logfile);
}
int main() {
// 创建守护进程
create_daemon();
// 每分钟写入当前时间
while (1) {
log_time();
sleep(60); // 每隔60秒写入一次
}
return 0;
}
代码说明
-
创建守护进程:
- 调用
fork()
创建一个子进程。如果fork()
返回值大于 0,表示当前进程是父进程,父进程退出,这样子进程就变成了孤儿进程,由init
进程收养。 - 在子进程中,调用
setsid()
创建新的会话,脱离终端。 - 使用
chdir("/")
将工作目录更改为根目录,以避免守护进程锁住文件系统。 - 调用
umask(0)
将文件权限掩码设置为默认值。 - 关闭标准输入、输出和错误输出,以防止守护进程继续使用终端。
- 调用
-
日志记录:
- 定义
log_time()
函数,每次调用时获取当前时间并将其格式化,然后追加到daemon.log
文件中。 - 日志文件路径为
/path/to/daemon.log
,你可以根据实际需求修改路径。
- 定义
-
主循环:
- 主函数中调用
create_daemon()
来创建守护进程。 - 然后进入无限循环,每隔 60 秒调用
log_time()
函数,将当前时间写入日志文件。
- 主函数中调用
编译和运行
-
编译: 你可以使用 GCC 编译此程序:
-
gcc -o daemon_example daemon_example.c
-
运行: 运行守护进程:
-
./daemon_example &
守护进程将会在后台运行,并且每分钟将当前时间写入日志文件
daemon.log
。
3.4、利用库函数daemon()创建守护进程
其实完全可以利用daemon()函数创建守护进程,其函数原型:
#include <unistd.h>
int
daemon(
int
nochdir,
int
noclose);
功能:创建一个守护进程
参数:
nochdir:=0将当前目录更改至“/”
noclose:=0将标准输入、标准输出、标准错误重定向至“/dev/null”
返回值:
成功:0; 失败:-1
利用daemon()改写刚才那个程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#define LOG_FILE "/path/to/daemon.log"
void log_time() {
FILE *logfile;
time_t rawtime;
struct tm *timeinfo;
char time_str[80];
// 打开日志文件,若不存在则创建
logfile = fopen(LOG_FILE, "a");
if (logfile == NULL) {
perror("Failed to open log file");
exit(1);
}
// 获取当前时间
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", timeinfo);
// 将当前时间写入日志文件
fprintf(logfile, "Current time: %s\n", time_str);
fclose(logfile);
}
int main() {
// 创建守护进程
if (daemon(0, 0) == -1) {
perror("daemon() failed");
exit(1);
}
// 每分钟写入当前时间
while (1) {
log_time();
sleep(60); // 每隔60秒写入一次
}
return 0;
}
-
daemon()
函数:daemon(0, 0)
:第一个参数0
表示不改变当前工作目录,第二个参数0
表示不重定向标准输入输出。daemon()
函数成功调用后,会使程序成为一个守护进程,子进程会脱离控制终端,且标准输入、输出和错误输出会被关闭。
-
日志记录:
log_time()
函数会获取当前时间,并将其格式化后写入daemon.log
文件。
-
主循环:
- 守护进程进入一个无限循环,每隔 60 秒调用一次
log_time()
函数,写入当前时间。
- 守护进程进入一个无限循环,每隔 60 秒调用一次
编译和运行
-
编译: 使用 GCC 编译程序:
gcc -o daemon_example daemon_example.c
-
运行: 以后台进程方式运行守护进程:
./daemon_example &
3.5、使用 daemon()
函数创建守护进程
daemon()
函数会将当前进程变成一个守护进程,并自动执行以下操作:
- 调用
fork()
使父进程退出(子进程继续运行)。 - 调用
setsid()
创建新的会话并脱离控制终端。 - 修改工作目录为根目录(
chdir("/")
)。 - 关闭标准输入输出和错误输出(
close(STDIN_FILENO)
,close(STDOUT_FILENO)
,close(STDERR_FILENO)
)。