【三】进程
1. 进程与程序的区别
-
程序:存放在磁盘上的指令和数据的有序集合(文件),是静态的。
-
进程:执行一个程序所分配的资源的总称,是动态的。
2. 进程的组成部分
-
BSS段(bss):存放程序中未初始化的全局变量。
-
数据段(data):存放已初始化的全局变量。
-
代码段(text):存放程序执行代码。
-
堆(heap) :通过
malloc
等函数分配内存,动态扩张或缩减。 -
栈(stack):存放局部变量、函数参数和函数返回值,先进后出。
-
进程控制块(PCB):包含进程ID(PID)、进程优先级、文件描述符表等。
3. 进程类型
-
交互进程:用户通过终端与系统交互的进程。
-
批处理进程:在后台运行,不需要用户交互。
-
守护进程:在后台运行的特殊进程,通常用于系统服务。
4. 进程状态
-
运行态:进程正在运行。
-
等待态:进程等待某些资源(如I/O)。
-
停止态:进程被暂停。
-
死亡态:进程已结束,但父进程尚未读取其状态信息。
5. 堆与栈
-
堆(heap):用于存放进程运行中被动态分配的内存段,当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
-
栈(stack):又称堆栈,是用户存放程序临时创建的局部变量,(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
一、查看进程信息
1. ps
命令
用于查看系统进程的快照。
1.1 常用参数
-
-e
:显示所有进程。 -
-l
:长格式显示更详细信息。 -
-f
:全格式显示,通常与其他选项联用。
1.2 输出字段说明
字段 | 含义 |
---|---|
F | 进程标志,说明进程的权限。常见标志:1(不可执行)、4(超级用户权限)等。 |
S | 进程状态。常见状态:-D(不可唤醒的睡眠状态)、-R(运行)、-S(睡眠)、-T(停止)、-Z(僵尸进程)等。 |
UID | 运行此进程的用户ID。 |
PID | 进程的ID。 |
PPID | 父进程的ID。 |
C | 进程的CPU使用率(百分比)。 |
PRI | 进程的优先级,数值越小优先级越高。 |
NI | 进程的nice值,数值越小优先级越高。 |
ADDR | 进程在内存中的位置。 |
SZ | 进程占用的内存大小。 |
WCHAN | 进程是否运行,"-"表示正在运行。 |
TTY | 进程由哪个终端产生。 |
TIME | 进程占用CPU的时间。 |
CMD | 产生此进程的命令名。 |
1.3 示例
ps -elf
2. top
命令
用于查看进程的动态信息。
2.1 常用操作
-
shift + >
:后翻页。 -
shift + <
:前翻页。 -
top -p PID
:查看指定进程。
3. /proc
目录
用于查看进程的详细信息。
每个进程在/proc
目录下都有一个以PID命名的子目录,包含进程的各种信息文件,如status
、maps
等。
二、改变进程优先级
1. nice
命令
按用户指定的优先级运行进程。
1.1 命令格式
nice [-n NI值] 命令
-
NI值范围:-20~19。
-
数值越大,优先级越低。
-
普通用户可调整范围为0~19,只能调高优先级。
-
只有
root
用户可以设置负值。
-
2. renice
命令
改变正在运行进程的优先级。
2.1 命令格式
renice [优先级] PID
三、进程管理
1. 查看后台进程
1.1 命令
jobs
2. 将挂起的进程在后台运行
2.1 命令
bg
3. 将后台进程放到前台运行
3.1 命令
fg
4. 将前台进程转为后台并停止
4.1 操作
Ctrl + Z
5. 后台运行程序
5.1 示例
./test &
四、创建子进程
1. fork()
函数
创建新的进程。
1.1 函数原型
#include <unistd.h>
pid_t fork(void);
1.2 返回值
-
成功:
-
父进程返回子进程的PID。
-
子进程返回0。
-
-
失败:返回-1。
1.3 特点
-
子进程只执行
fork()
之后的代码。 -
父子进程的执行顺序由操作系统决定。
-
子进程继承父进程的内容,但父子进程有独立的地址空间。
1.4 注意事项
-
若父进程先结束:
-
子进程成为孤儿进程,被
init
进程收养。 -
子进程可能成为后台进程。
-
-
若子进程先结束:
- 父进程如果没有及时回收,子进程变成僵尸进程。
1.5 示例
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
} else if (pid == 0) {
printf("I am the child process, PID: %d\n", getpid());
} else {
printf("I am the parent process, PID: %d, Child PID: %d\n", getpid(), pid);
}
return 0;
}
五、进程的退出
1. exit()
函数
结束当前进程。
1.1 函数原型
#include <stdlib.h>
void exit(int status);
1.2 特点
-
会刷新流缓冲区。
-
通常用于正常退出。
2. _exit()
函数
结束当前进程。
2.1 函数原型
#include <unistd.h>
void _exit(int status);
2.2 特点
-
不刷新流缓冲区。
-
通常用于异常退出。
3. return
与exit
的区别
-
main
函数结束时会隐式调用exit
。 -
普通函数的
return
是返回上一级。
六、进程的回收
1. wait()
函数
回收子进程。
1.1 函数原型
#include <unistd.h>
pid_t wait(int *status);
1.2 返回值
-
成功时返回回收的子进程的 PID。
-
失败时返回 -1。
1.3 特点
-
若子进程未结束,父进程会阻塞。
-
status
用于保存子进程的返回值和结束方式。 -
若
status
为NULL
,表示直接释放子进程的 PCB。
2. waitpid()
函数
更灵活的子进程回收。
2.1 函数原型
#include <unistd.h>
pid_t waitpid(pid_t pid, int *status, int options);
2.2 参数说明
-
pid
:-
>0
:只等待指定 PID 的子进程。 -
= -1
:等待任意子进程。 -
= 0
:等待同一进程组中的子进程。 -
< -1
:等待指定进程组中的子进程。
-
-
options
:-
WNOHANG
:非阻塞,若子进程未结束,立即返回 0。 -
WUNTRACED
:返回终止子进程信息和因信号停止的子进程信息。
-
2.3 示例代码
示例 1:wait()
示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
}
if (pid == 0) {
// 子进程
printf("Child process, PID: %d\n", getpid());
sleep(2); // 模拟子进程运行
printf("Child process finished\n");
return 0; // 子进程退出
} else {
// 父进程
printf("Parent process, PID: %d, waiting for child...\n", getpid());
int status;
wait(&status); // 等待子进程结束
printf("Child process exited with status: %d\n", status);
return 0;
}
}
示例 2:waitpid()
示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
}
if (pid == 0) {
// 子进程
printf("Child process, PID: %d\n", getpid());
sleep(3); // 模拟子进程运行
printf("Child process finished\n");
return 0; // 子进程退出
} else {
// 父进程
printf("Parent process, PID: %d, waiting for child...\n", getpid());
int status;
pid_t ret = waitpid(pid, &status, WNOHANG); // 非阻塞等待子进程
if (ret == 0) {
printf("Child process is still running\n");
} else if (ret == -1) {
perror("waitpid failed");
} else {
printf("Child process exited with status: %d\n", status);
}
return 0;
}
}
七、exec
函数族
1. 背景
fork
创建进程后,子进程会继承父进程的代码和数据,父子进程执行相同的代码。然而,在实际开发中,我们通常希望子进程执行与父进程不同的程序。exec
函数族的作用就是用来替换当前进程的代码和数据,从而执行指定的程序。
2. 作用
exec
函数族用于替换当前进程的代码和数据,执行指定的程序。这些函数不会创建新的进程,而是直接替换当前进程的内容。
3. 函数原型
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int system(const char *command);
4. 参数说明
4.1 execl
int execl(const char *path, const char *arg, ...);
-
path
:指定要执行的程序的路径。 -
arg
:传递给程序的参数列表,必须以NULL
结尾。 -
特点:参数列表是可变参数,每个参数都是字符串。
4.2 execlp
int execlp(const char *file, const char *arg, ...);
-
file
:指定要执行的程序的文件名。execlp
会在环境变量PATH
中查找该文件。 -
arg
:传递给程序的参数列表,必须以NULL
结尾。 -
特点 :不需要指定文件的全路径,
execlp
会自动在PATH
中查找。
4.3 execv
int execv(const char *path, char *const argv[]);
-
path
:指定要执行的程序的路径。 -
argv
:传递给程序的参数数组,必须以NULL
结尾。 -
特点:参数以数组形式传递,适合参数较多的情况。
4.4 execvp
int execvp(const char *file, char *const argv[]);
-
file
:指定要执行的程序的文件名。execvp
会在环境变量PATH
中查找该文件。 -
argv
:传递给程序的参数数组,必须以NULL
结尾。 -
特点 :不需要指定文件的全路径,
execvp
会自动在PATH
中查找。
4.5 system
int system(const char *command);
-
command
:要执行的命令字符串。 -
特点 :
system
是一个高级函数,会调用/bin/sh
来执行命令。它会创建一个子进程来运行命令,并等待命令执行完成。
5. 返回值
-
成功 :执行指定的程序,不会返回到调用
exec
的代码。 -
失败 :返回 -1,并设置
errno
。
6. 特点
-
进程替换 :
exec
函数会替换当前进程的代码和数据,但进程号(PID)保持不变。 -
参数传递:
-
第一个参数(如
arg
或argv[0]
)通常是程序的名称,虽然它在程序中没有实际用途。 -
参数列表或数组必须以
NULL
结尾。
-
-
路径查找:
-
execl
和execv
需要指定程序的完整路径。 -
execlp
和execvp
会在环境变量PATH
中查找程序。
-
-
环境变量 :
exec
函数不会改变当前进程的环境变量。
7. 示例代码
示例 1:execl
示例
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Before exec\n");
execl("/bin/ls", "ls", "-l", NULL); // 替换当前进程为 ls 程序
printf("After exec\n"); // 不会执行到这里
return 0;
}
示例 2:execlp
示例
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Before exec\n");
execlp("ls", "ls", "-l", NULL); // 替换当前进程为 ls 程序
printf("After exec\n"); // 不会执行到这里
return 0;
}
示例 3:execv
示例
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Before exec\n");
char *args[] = {"/bin/ls", "-l", NULL};
execv("/bin/ls", args); // 替换当前进程为 ls 程序
printf("After exec\n"); // 不会执行到这里
return 0;
}
示例 4:execvp
示例
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Before exec\n");
char *args[] = {"ls", "-l", NULL};
execvp("ls", args); // 替换当前进程为 ls 程序
printf("After exec\n"); // 不会执行到这里
return 0;
}
示例 5:system
示例
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Before system\n");
system("ls -l"); // 使用 shell 执行命令
printf("After system\n"); // 会执行到这里
return 0;
}
exec
函数族的使用注意事项
1. 参数传递
-
参数列表或数组必须以
NULL
结尾,否则会导致未定义行为。 -
第一个参数(如
arg
或argv[0]
)通常是程序的名称,虽然它在程序中没有实际用途,但建议正确填写。
2. 环境变量
exec
函数不会改变当前进程的环境变量。如果需要修改环境变量,可以使用putenv
或setenv
函数。
3. 文件描述符
-
在调用
exec
函数之前,应确保关闭不需要的文件描述符,以避免资源泄漏。 -
如果需要保留某些文件描述符(如日志文件),应确保它们在调用
exec
之前已正确打开。
4. 错误处理
-
如果
exec
函数失败,会返回 -1,并设置errno
。可以通过perror
或strerror
获取错误信息。 -
常见错误包括:
-
文件路径错误或文件不存在。
-
文件没有执行权限。
-
环境变量
PATH
中未找到程序。
-
- 与
fork
结合使用
-
exec
函数通常与fork
结合使用,创建子进程并执行不同的程序。 -
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
}
if (pid == 0) {
// 子进程
printf("Child process, PID: %d\n", getpid());
execlp("ls", "ls", "-l", NULL); // 子进程执行 ls 程序
perror("execlp failed"); // 如果 execlp 失败,会执行到这里
return 1;
} else {
// 父进程
printf("Parent process, PID: %d\n", getpid());
wait(NULL); // 等待子进程结束
printf("Child process finished\n");
}
return 0;
}
6. 使用 system
的注意事项
-
system
函数会调用/bin/sh
来执行命令,因此可能会引入安全风险(如注入攻击)。 -
如果需要执行简单的命令,可以使用
system
,但建议优先使用exec
函数族。 -
示例:避免注入攻击
#include <stdio.h>
#include <stdlib.h>
int main() {
char *filename = "test.txt";
char command[256];
snprintf(command, sizeof(command), "ls -l %s", filename); // 安全地构造命令
system(command);
return 0;
}
八、守护进程(Daemon Process)
1. 概念
守护进程(Daemon Process)是一种生存期较长的进程,通常独立于控制终端,并周期性地执行某种任务或等待处理某些事件。
2. 特点
-
始终在后台运行,独立于任何终端。
-
周期性地执行任务或等待处理特定事件。
-
是一种特殊的孤儿进程,脱离终端,避免被终端信息打断。
3. 创建守护进程
3.1 简便方法
使用 nohup
命令:
nohup xxxx &
3.2 使用 setsid
函数
#include <unistd.h>
pid_t setsid(void);
-
成功时返回调用进程的会话 ID。
-
失败时返回 -1,并设置
errno
。 -
调用
setsid
的进程成为新的会话组长和组长进程。
3.3 使用 getsid
函数
#include <unistd.h>
pid_t getsid(pid_t pid);
-
成功时返回调用进程的会话 ID。
-
失败时返回 -1,并设置
errno
。 -
参数
pid
为 0 时,查看当前进程的会话 ID。
3.4 其他辅助函数
-
getpid
:获取进程 ID。pid_t getpid(void);
-
getpgid
:获取进程组 ID。pid_t getpgid(pid_t pid);
4. 创建守护进程的步骤
-
创建子进程,父进程退出:
if (fork() > 0) { exit(0); // 父进程退出 }
-
子进程变成孤儿进程,被
init
进程收养:- 子进程在后台运行。
-
子进程创建新会话:
if (setsid() < 0) { exit(-1); // 创建会话失败 }
-
子进程成为新的会话组长。
-
子进程脱离原先的终端。
-
-
更改当前工作目录:
chdir("/"); // 或 chdir("/tmp");
- 防止守护进程的工作目录被卸载。
-
重设文件权限掩码:
umask(0); // 文件权限掩码设置为 0
-
关闭打开的文件描述符:
for (int i = 0; i < 3; i++) { close(i); // 关闭标准输入、输出和错误 }
4.1 示例代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
// 创建子进程,父进程退出
if (fork() > 0) {
exit(0); // 父进程退出
}
// 创建新会话
if (setsid() < 0) {
perror("setsid failed");
exit(-1);
}
// 更改当前工作目录
if (chdir("/") < 0) {
perror("chdir failed");
exit(-1);
}
// 重设文件权限掩码
umask(0);
// 关闭打开的文件描述符
for (int i = 0; i < 3; i++) {
close(i); // 关闭标准输入、输出和错误
}
// 执行守护进程的任务
while (1) {
printf("Daemon process running...\n");
sleep(10); // 模拟周期性任务
}
return 0;
}
九、守护进程的注意事项
1. 避免守护进程占用终端
守护进程不应与终端关联,否则可能会因终端关闭而退出。使用 setsid()
可以确保进程脱离终端。
2. 防止工作目录被卸载
守护进程的工作目录应设置为根目录(/
)或 /tmp
,避免因工作目录被卸载而导致守护进程无法运行。
3. 关闭不必要的文件描述符
守护进程不应继承父进程的文件描述符。关闭标准输入、输出和错误(stdin
、stdout
、stderr
),并根据需要重新打开日志文件。
4. 日志记录
守护进程通常需要记录日志以便调试和监控。可以将日志写入 /var/log
或其他日志文件中,而不是直接输出到终端。
5. 示例:守护进程与日志记录
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
// 创建子进程,父进程退出
if (fork() > 0) {
exit(0); // 父进程退出
}
// 创建新会话
if (setsid() < 0) {
perror("setsid failed");
exit(-1);
}
// 更改当前工作目录
if (chdir("/") < 0) {
perror("chdir failed");
exit(-1);
}
// 重设文件权限掩码
umask(0);
// 关闭标准输入、输出和错误
for (int i = 0; i < 3; i++) {
close(i);
}
// 打开日志文件
int log_fd = open("/var/log/mydaemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (log_fd < 0) {
perror("open log file failed");
exit(-1);
}
// 重定向标准输出和错误到日志文件
dup2(log_fd, STDOUT_FILENO);
dup2(log_fd, STDERR_FILENO);
// 执行守护进程的任务
while (1) {
printf("Daemon process running...\n");
sleep(10); // 模拟周期性任务
}
return 0;
}