导言
在Linux操作系统中,任务管理和守护进程是系统运维和开发中非常重要的方面。本篇博客将深入探讨Linux下任务管理和守护进程的概念、使用方法以及一些实际的例子。
在Linux中,每个运行的程序都是一个进程。每个进程都有一个唯一的进程标识符(PID),并且可以属于一个父进程。通过ps命令可以查看当前系统中运行的进程。
ps 命令用于查看系统中的进程信息。以下是常见的字段及其含义:
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 12345 6789 ? Ss Dec01 0:05 /sbin/init
john 22 1.0 0.5 123456 54321 pts/0 R+ 10:30 0:10 ./my_program
字段名称 | 含义 |
---|---|
USER (UID): | 进程属于哪个用户。 |
PID: | 进程ID,唯一标识一个进程。 |
USER (UID): | 进程属于哪个用户。 |
%CPU: | 进程占用的CPU使用率。 |
%MEM: | 进程占用的内存使用率。 |
RSS (resident set size): | 进程占用的物理内存大小(以KB为单位)。 |
TTY: | 进程关联的终端。 |
STAT (process status): | 进程状态,如R(运行)、S(睡眠)、Z(僵尸)等。 |
TIME: | 进程累计占用CPU的时间。 |
COMMAND: | 启动进程的命令。 |
$ ps axj
PID PPID PGID SID TTY STAT TIME COMMAND
1 0 1 1 ? Ss 0:03 /usr/lib/systemd/systemd
2 0 0 0 ? S 0:00 [kthreadd]
3 2 0 0 ? I< 0:00 [rcu_gp]
...
对比ps aux不同字段含义
字段名称 | 含义 |
---|---|
PPID (Parent Process ID): | 父进程ID,标识创建当前进程的父进程。 |
PGID (Process Group ID): | 进程组ID,标识进程所属的进程组。 |
SID (Session ID): | 会话ID,标识进程所属的会话。 |
TTY (Controlling Terminal): | 控制终端,表示与进程关联的终端设备。 |
我们以一个示例演示
首先创建四个进程,process_1,porcess_2,process_3,porcess_4
概念:
进程组是一个或多个相关进程的集合,它们共享同一个进程组ID(PGID)。进程组的一个典型应用是将一组相关的进程组织在一起,以便能够方便地对它们进行管理。
将process_1,porcess_2放在后台
运行
将process_3,porcess_4放在前台
运行
开启另一终端(shell),用命令行ps axj | head -1 && ps axj | grep process | grep -v grep
查看前台进程和后台进程
此时杀掉进程也是按照进程组进行的
当需要杀掉一个或多个进程时,通常会按照进程组进行操作。可以使用kill命令发送信号给进程和进程组。通过kill -n -pgid
可以将信号n发送到进程组pgid中的所有进程。也可以使用kill -n pid
将信号n发送到指定pid进程。
需要注意的是
,如果一个进程启动了子进程,只杀死父进程,子进程仍在运行,因此仍消耗资源。为了防止这些“僵尸进程”,应确保在杀死父进程之前,先杀死其所有的子进程。
概念:
作业是一个或多个相关联的进程组的集合。通常,作业表示由一个用户发起的命令或脚本。
jobs
命令用于显示当前 shell 中正在运行的作业(jobs)列表。在一个 shell 会话中,你可能会同时运行多个作业,其中一些可能在后台运行。
$ jobs
[1]+ Running command1 &
[2]- Stopped command2
[3]+ Running command3
在上面的例子中:
command1 正在后台运行,并且是最近放入后台的作业。
command2 已经停止。
command3 正在后台运行。
如果你想操作这些作业,你可以使用 fg(将作业放到前台运行)或 bg(将作业放到后台运行)命令,以及 kill 命令来终止作业。例如:
$ fg %1 # 将作业1放到前台运行
$ bg %2 # 将作业2放到后台运行
$ kill %3 # 终止作业3
使用Ctrl+Z可以将当前前台任务挂起,然后可以使用bg将其放入后台运行,或使用fg将其恢复到前台。
# 挂起当前任务
Ctrl+Z
# 将任务恢复到前台
fg %1
概念:
会话是一个或多个相关联的作业的集合。会话是由一个终端发起的一组作业的集合,可以包括多个进程组。
例子: 当用户登录到系统时,通常会创建一个新的会话。用户在终端中执行的所有作业都属于同一个会话。
# 创建新会话
ssh user@hostname
在这个例子中,用户通过SSH连接到远程主机,创建了一个新的会话。在这个会话中,用户可以启动不同的作业,每个作业可能包含一个或多个进程组。
守护进程(Daemon)是在后台运行的系统进程,它们独立于用户会话并且没有直接的控制终端。它们通常在系统启动时启动,并在系统关闭时终止。守护进程通常用于执行系统级任务,如日志记录、网络服务等。
ps aux | grep [守护进程名称]
过滤出数据库守护进程
字段名称 | 含义 |
---|---|
USER (UID): | 进程属于哪个用户。 |
PID: | 进程ID,唯一标识一个进程。 |
USER (UID): | 进程属于哪个用户。 |
%CPU: | 进程占用的CPU使用率。 |
%MEM: | 进程占用的内存使用率。 |
RSS (resident set size): | 进程占用的物理内存大小(以KB为单位)。 |
TTY: | 进程关联的终端。 |
STAT (process status): | 进程状态,如R(运行)、S(睡眠)、Z(僵尸)等。 |
TIME: | 进程累计占用CPU的时间。 |
COMMAND: | 启动进程的命令。 |
一个守护进程通常通过编写一个程序来实现。以下是创建一个简单的守护进程的一般步骤:
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
// 1、设置文件掩码为0
umask(0);
// 2、fork后终止父进程,子进程创建新会话
if (fork() > 0) {
// father
exit(0);
}
setsid();
// 3、忽略SIGCHLD信号
signal(SIGCHLD, SIG_IGN);
// 4、再次fork,终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联
// (不是必须的,防御性编程)
if (fork() > 0) {
// father
exit(0);
}
// 5、更改工作目录为根目录(可选的选项)
chdir("/");
// 6、将标准输入、标准输出、标准错误重定向到/dev/null(可选的选项)
close(0);
int fd = open("/dev/null", O_RDWR);
dup2(fd, 1);
dup2(fd, 2);
while (1)
;
return 0;
}
状态 | 说明 |
---|---|
Ss: | 会话领导。 |
Ss+: | 会话领导,且在前台运行。 |
S: | 睡眠状态 |
运行后发现该进程的TPGID为-1,TTY显示的是?
,也就意味着该进程已经与终端去关联了。
该进程的PID与其PGID和SID
是不同的,也就是说该进程既不是组长进程也不是会话首进程。
该进程的SID与bash进程的SID是不同的
,即它们不属于同一个会话。
该进程的工作目录已经成功改为了根目录。
该进程的标准输入、标准输出以及标准错误也成功重定向到了/dev/null,
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#define MAX_ERROR_LENGTH 256
int main() {
pid_t pid;
int log_fd;
char error_buffer[MAX_ERROR_LENGTH];
char log_file[MAX_ERROR_LENGTH];
// 创建子进程
pid = fork();
// 子进程退出
if (pid == 0) {
exit(EXIT_SUCCESS);
}
// 父进程创建新会话
if (setsid() < 0) {
perror("setsid failed");
exit(EXIT_FAILURE);
}
// 打开日志文件
sprintf(log_file, "error_%d.log", getpid()); // 根据进程ID创建日志文件名
log_fd = open(log_file, O_CREAT | O_WRONLY | O_APPEND, S_IRUSR | S_IWUSR);
if (log_fd < 0) {
perror("open failed");
exit(EXIT_FAILURE);
}
// 重定向标准错误输出到日志文件
if (dup2(log_fd, STDERR_FILENO) < 0) {
perror("dup2 failed");
exit(EXIT_FAILURE);
}
// 关闭日志文件描述符,因为dup2已经复制了一个副本,所以不需要再使用它了。
close(log_fd);
// 捕捉其他进程产生的错误信息,并打印到日志文件中。
signal(SIGPIPE, SIG_IGN); // 忽略SIGPIPE信号,防止子进程在写日志文件时退出。
while (1) { // 循环等待错误信息打印。
// 这里通过wait()函数等待子进程结束,并获取其退出状态和错误信息。
int status;
wait(&status); // 等待子进程结束。
if (WIFEXITED(status)) { // 如果子进程正常退出,获取其退出状态。
printf("Child process exited with status %d.\n", WEXITSTATUS(status)); // 打印退出状态。
} else if (WIFSIGNALED(status)) { // 如果子进程被信号杀死,获取其被杀死的信号。
printf("Child process killed by signal %d.\n", WTERMSIG(status)); // 打印被杀死的信号。
} else { // 其他情况。
printf("Unknown exit status for child process.\n"); // 打印未知的退出状态。
}
// 清空错误信息缓冲区。
memset(error_buffer, 0, MAX_ERROR_LENGTH);
// 从子进程的stdout和stderr中读取错误信息。
read(STDIN_FILENO, error_buffer, MAX_ERROR_LENGTH - 1); // 从stdin中读取错误信息。
read(STDERR_FILENO, error_buffer + strlen(error_buffer), MAX_ERROR_LENGTH - strlen(error_buffer) - 1); // 从stderr中读取错误信息。
// 将错误信息写入日志文件中。
write(log_fd, error_buffer, strlen(error_buffer)); // 将错误信息写入日志文件。
fflush(stdout); // 将输出刷新到标准输出缓冲区,防止在程序退出后丢失输出信息。
}
return 0; // 程序正常退出,返回0表示成功结束进程。
}
#include <stdio.h>
#include <stdlib.h>
void cause_wild_pointer() {
int *ptr = NULL;
*ptr = 42; // 这里会引发野指针问题
}
int main() {
cause_wild_pointer();
return 0;
}
这里可以看到守护进程完成了错误的捕捉(TODO
)