目录
前言
Linux进程是Linux操作系统中执行程序的一个实例,它是系统进行资源分配和调度的一个独立单元。每个Linux进程都拥有独立的虚拟内存空间、文件描述符表、系统栈等,以实现对程序执行环境的隔离。进程是操作系统中最基本、最重要的概念之一,它是系统并发执行的基本单位。
一、进程的基本概念
1、进程与程序
程序是存储在磁盘上的可执行文件,程序被加载到内存中开始运行时叫做进程
一个程序可以被多次加载生成多个进程,进程就是处于活动状态的计算机程序
2、进程的分类
进程一般分为三个种类:交互进程、批处理进程、守护进程
3、查看进程
简单模式:ps 显示当前用户有终端控制进程简单信息
列表模式:ps -auxw 显示所有进程的详细信息
a 所有用户的有终端控制的进程
x 无终端控制的进程
u 显示进程的详细信息
w 以更大的列宽显示
USER 进程的属主用户名
PID 进程号
%CPU CPU的使用率
%MEM 内存的使用率
VSZ 虚拟内存使用的字节数
RSS 物理内存使用的字节数
TTY 终端设备号 ? 表示无终端控制
STAT 进程的状态
O 就绪态 等待被调用
R 运行态,Linux系统没有O,就绪也用R表示
S 可被唤醒的睡眠态,如系统中断、获取资源、收到信号等都可以唤醒进入运行态
D 不可被唤醒的睡眠态,只能被系统唤醒
T 暂停态 收到SIGTSTP信号进入暂停态,收到SIGCONT信号转回运行态
X 死亡态
Z 僵尸态
N 低优先级
< 高优先级
l 多线程进程
s 进程的领导者
START 进程的启动时间
TIME 进程运行时间
COMMAND 启动进程的命令
4、父进程、子进程、孤儿进程、僵尸进程
一个进程可以被另一个进程创建,创建者叫做父进程,被创建者叫子进程,子进程被父进程创建后会在操作系统的调度下同时运行
当子进程先于父进程结束,死前子进程会向父进程发送信号SIGCHLD,此时父进程应该去回收子进程的相关资源。
孤儿进程:父进程先于子进程结束,子进程就变成了孤儿进程,孤儿进程会被孤儿院(init守护进程)领养,init就是孤儿进程的父进程
僵尸进程:该进程已死亡,但是它的父进程没有立即回收它的相关资源,该进程就进入僵尸态 。
5、进程标识符
每个进程都有一个用非负整数表示唯一标识,即进程ID\PID
进程ID在任意时刻都是唯一的,但是可以重用,进程一旦结束它的进程ID就会被系统回收,过一段时间后再重新分配给其他新创建的进程使用(延时重用)
pid_t getpid(void);
功能:返回调用者的进程ID
pid_t getppid(void);
功能:返回父进程的ID
二、进程的创建
使用fork()函数
pid_t fork(void);
功能:创建子进程
返回值:一次调用两次返回,子进程返回0,父进程返回子进程的ID,当进程数量超过系统的限制时会创建失败,返回-1
- 通过fork创建的子进程会拷贝父进程(数据段、bss段、堆、栈、I/O缓冲区),与父进程共享代码段、子进程会继承父进程的信号处理方式
- fork函数调用后父子进程各自独立运行,谁先返回不确定,但是可以通过睡眠确定让哪个进程先执行
- 通过fork创建的子进程可以共享父进程的文件描述符
- 可以根据返回值的不同让父子进程进入不同的分支,执行不同的代码
注意:
1.Fork操作的影响:当一个进程调用fork()创建子进程时,操作系统会复制当前进程的所有资源(包括代码段、数据段、堆栈 等),生成一个新的子进程。这个子进程会从fork()调用后的位置开始执行代码。
2.并发执行问题:如果在父进程或其他子进程调用fork()创建新的子进程时,此时正在执行fork()操作的进程可能会被影响,因为fork()会复制当前进程的状态,包括指令执行位置等。这可能导致竞争条件或意外的行为。
下面是一个使用fork()
创建子进程的简单示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
// 使用fork()创建一个新进程
pid = fork();
if (pid == -1) {
// fork失败
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程代码
printf("This is the child process. PID = %d\n", getpid());
// 子进程可以执行一些任务...
// 然后退出
exit(EXIT_SUCCESS);
} else {
// 父进程代码
int status;
printf("This is the parent process. Child PID = %d\n", pid);
// 父进程可以继续执行其他任务...
// 等待子进程结束
if (waitpid(pid, &status, 0) == -1) {
// 等待子进程失败
perror("waitpid failed");
exit(EXIT_FAILURE);
}
// 检查子进程的退出状态
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child killed by signal %d\n", WTERMSIG(status));
}
// 父进程继续执行其他任务...
}
return 0;
}
在这个示例中,父进程通过fork()
创建了一个子进程。在fork()
调用之后,父进程和子进程都会从fork()
调用之后的那条指令开始执行,但是它们会分别拥有各自的执行路径。通过fork()
的返回值,可以区分当前是在父进程中还是在子进程中执行代码。在父进程中,fork()
返回子进程的PID;在子进程中,fork()
返回0。
父进程通过waitpid()
等待子进程结束,并获取子进程的退出状态。这是一个好习惯,因为它可以防止产生僵尸进程(即已经结束但父进程尚未通过wait()
或waitpid()
获取其退出状态的进程)。
使用vfork()函数
pid_t vfork(void);
功能:以加载可执行文件的方式来创建子进程
返回值:子进程返回0,父进程返回子进程的ID
注意:vfork创建的子进程一定先返回,此时子进程并没有创建成功,需要加载一个可执行文件替换当前子进程当前的所有资源,当替换完成后子进程才算创建成功,此刻父进程才返回
使用 exec 系列函数让子进程加载可执行文件。
extern char **environ;
int execl(const char *path, const char *arg, ...
/* (char *) NULL */);
path:可执行文件的路径
arg:命令行参数,个数不定,由实际的可执行文件所需命令行参数决定
一般第一个是可执行文件的名字,至少有一个,一定要以NULL结尾
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
file:可执行文件名字
arg:命令行参数,同上
注意:会去系统默认路径 PATH 指定的路径下加载file
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[]*/);
path:可执行文件的路径
arg:命令行参数,同上
envp:环境变量表,父进程可以在加载子进程时把环境变量表传递给子进程
int execv(const char *path, char *const argv[]);
path:可执行文件的路径(指定路径中可执行文件)
argv:命令行参数数组,最后以NULL结尾
int execvp(const char *file, char *const argv[]);
file:可执行文件名字
argv:命令行参数数组,同上
注意:也是根据PATH的路径加载file
int execvpe(const char *file, char *const argv[],
char *const envp[]);
file:可执行文件名字(环境变量PATH中可执行文件)
argv:命令行参数数组,同上
envp:环境变量表
注意:也是根据PATH的路径加载file
注意:exec系列函数正常情况下是不会返回的,当子进程加载失败时才会返回-1。虽然通过vfork、exec系列函数创建加载的子进程不会继承父进程的信号处理函数,但是能继承父进程的信号屏蔽集
下面是一个使用vfork()创建子进程的简单示例:
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid;
// 使用vfork()创建一个新进程
pid = vfork();
if (pid == -1) {
// vfork失败
perror("vfork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程代码
// 注意:在调用exec()之前,子进程应避免任何可能影响父进程的操作
execlp("ls", "ls", "-l", (char *)NULL);
// 如果execlp失败,则子进程应退出
// 注意:这里使用_exit()而不是exit(),因为exit()会调用清理函数,这些函数可能会与父进程共享的地址空间交互
_exit(EXIT_FAILURE);
} else {
// 父进程代码
int status;
// 父进程等待子进程结束
if (waitpid(pid, &status, 0) == -1) {
// 等待子进程失败
perror("waitpid failed");
exit(EXIT_FAILURE);
}
// 检查子进程的退出状态
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child killed by signal %d\n", WTERMSIG(status));
}
// 父进程继续执行其他任务...
}
return 0;
}
比较fork()和vfork()
- 用途:fork() 用于创建一个新的进程,该进程可以独立于父进程运行,或者执行与父进程不同的任务。而 vfork() 主要用于创建一个新进程来执行一个新的程序(即exec()系列函数)。
- 地址空间:fork() 会为新进程创建独立的地址空间,而 vfork() 的子进程与父进程共享地址空间(直到exec()或exit()被调用)。
- 性能:vfork()通常比 fork() 更快,因为它避免了为新进程创建新的地址空间的开销。但是,这种性能优势是以牺牲安全性和灵活性为代价的。
- 限制:vfork()的使用更加受限,因为它要求子进程在调用exec()或exit()之前不能修改任何共享的数据。
在现代的Linux系统和编程实践中,vfork() 的使用已经相当罕见,因为 fork() 加exec()的组合已经足够高效,并且更安全、更灵活。
三、进程的退出
进程的正常退出
1、在main函数中执行 return n,该返回值可以被父进程获取,几乎与exit等价
2、进程调用了exit函数,该函数是C标准库中的函数
void exit(int status);
功能:在任何时间、地点调用该函数都可以立即结束进程
status:结束状态码 (EXIT_SUCCESS\EXIT_FAILURE),与main函数中return的返回值效果是一样的
返回值:该函数不会返回
进程退出前要完成:
1、先调用事先通过atexit\on_exit函数注册的函数,如果都注册了,则执行顺序与注册顺序相反
int atexit(void (*function)(void));
功能:向内核注册一个进程结束前必须调用的函数
int on_exit(void (*function)(int,void *), void *arg);
功能:向内核注册一个进程结束前必须调用的函数
arg:会在调用function时传给它
2、冲刷并关闭所有打开状态的标准IO流
3、底层继续调用_Exit/_exit函数
3、调用_Exit/_exit函数
void _exit(int status);
功能:结束进程,由系统提供的
void _Exit(int status);
功能:结束进程,由标准库提供的
1、它们的参数会被父进程获取到
2、进程结束前会关闭所有处于打开状态的文件描述符
3、向父进程发送信号SIGCHLD
4、该函数也不会返回
4、进程的最后一个线程执行了return返回语句
5、进程的最后一个线程执行了pthread_exit函数
进程的异常终止
1、进程调用了abort函数,产生 SIGABRT(6) 信号
2、进程接收到某些信息,可以是其它进程发送的,也可能自己的错误导致的
3、进程的最后一个线程接收到 "取消" 请求操作,并响应
这三种方式结束进程,它的父进程都无法获取结束状态码,因此叫做异常终止
注意:无论进程是如何结束的,它们最后都会执行同一段代码,会关闭所有打开的文件,并释放所有的内存
四、监控子进程
对于子进程的结束而言,都希望父进程能够知道并作出一定的反应,通过 wait、waitpid 函数可以知道子进程是如何结束的以及它的结束状态码。
pid_t waitpid(pid_t pid, int *status, int options);
功能:等待回收指定的某个或某些进程
返回值:
成功时:返回清理掉的子进程ID。
失败时:返回-1,并设置errno以指示错误。
特殊返回值: 如果options中包含了WNOHANG,且没有子进程结束,则返回0。
pid:
>0 等待该进程结束
0 等待同组的任意进程结束
-1 等待任意进程结束,功能与wait等价
<-1 等待abs(pid) 进程组中的任意进程结束
status:输出型参数,接收结束状态码
options:
WNOHANG 非阻塞模式,如果当前没有子进程结束,则立即返回0
WUNTRACED 如果有子进程处于暂停态,返回该进程的状态
WCONTINUED 如果有子进程从暂停态转为继续运行,返回该子进程的状态
WIFSTOPPED(status) // 判断子进程是否转为暂停态,是返回真
WSTOPSIG(status) // 获取导致子进程进入暂停态的信号
WIFCONTINUED(status) // 判断子进程是否由暂停转为继续,是返回真
int system(const char *command);
功能:通过创建子进程去执行一个可执行文件
返回值:子进程结束后才返回
注意:该函数底层调用了fork、vfork、exec、waitpid函数;在调用函数时,先使用fork创建子进程,再在创建的进程中使用vfork创建孙子进程运行可执行文件。
pid_t wait(int *status);
功能:等待子进程结束,并获取结束状态码
status:输出型参数,接收结束状态码
返回值:结束的子进程的ID
1、如果所有子进程都还在运行,则阻塞
2、如果有一个子进程结束,立即返回该进程的结束状态码和ID
3、如果没有子进程返回-1
WIFEXITED(status) // 判断进程是否是正常结束,如果是返回真
WEXITSTATUS(status) // 如果进程是正常结束的,可以获取到正确的结束状态码
WIFSIGNALED(status) // 判断进程是否异常结束,如果是返回真
WTERMSIG(status) // 如果进程是异常结束的,可以获取到杀死进程的信号