文章目录
进程相关概念
什么是程序,什么是进程
程序:
- 程序是静态的代码集合,用来完成特定的任务。程序本身是存储在磁盘上的文件,它包括源代码、编译后的二进制文件等。
- 程序是静态的,指在没有执行的状态下
进程:
-
进程是程序的一次执行实例,它是一个动态的实体。进程不仅包括程序代码,还包括当前活动中的程序计数器、寄存器内容、变量、堆栈和程序数据等。
-
进程是操作系统资源分配的基本单位,每个进程都有独立的内存空间,操作系统通过进程来管理程序的运行
进程是程序的一次运行活动,通俗意思是程序跑起来了,系统中就多了一个进程
区别:静态与动态:程序是静态的代码,进程是程序运行时的动态实体。
存储位置:程序通常存储在磁盘上,进程运行在内存中。
生命周期:程序没有生命周期概念,进程有从创建、执行到终止的生命周期
查看系统中的进程
- 使用 ps 命令,如 ps -aux 查看所有进程的详细信息。
- 使用 top 或 htop 命令实时监控系统的进程。
- 使用 pstree 命令以树形结构显示进程。
进程标识符(Process Identifier, PID):
-
进程标识符是操作系统分配给每个进程的唯一整数标识,用来区分不同的进程。
-
PID在操作系统中是唯一的,操作系统通过PID来进行进程管理和调度。
-
可以通过系统命令或编程接口获取进程的PID,例如在Linux中使用 ps 命令查看PID,在Windows中使用 tasklist 命令
Pid=0: 称为交换进程(swapper)
作用—进程调度
Pid=1:init进程
作用—系统初始化编程调用getpid函数获取自身的进程标识符
getppid获取父进程的进程标识符
什么叫父进程,什么叫子进程?
父进程(Parent Process):
- 父进程是创建其他进程的进程。每个进程都是由另一个进程创建的,这个创建它的进程称为它的父进程。
- 在Unix/Linux系统中,所有进程都是由系统的第一个进程(通常是 init 进程,PID为1)直接或间接创建的。
子进程(Child Process):
-
子进程是由父进程创建的进程。子进程继承父进程的一些属性(如环境变量、打开的文件描述符等),但它有自己独立的内存空间和资源。
-
子进程的创建通常通过系统调用(如 fork 或 spawn)完成。
关系:
- 每个子进程都有一个唯一的父进程,父进程可以创建多个子进程
- 父进程可以通过子进程的PID与其进行通信和管理。
- 当父进程终止时,操作系统通常会自动终止其所有子进程,或者将子进程的父进程重新分配给 init 进程
进程的创建
fork函数创建
fork函数用于创建一个新的进程,这个新进程称为子进程。子进程是父进程的副本,但有一些不同之处:
- 子进程有自己独立的地址空间。
- 子进程从父进程继承文件描述符、信号处理方式等
- fork函数调用成功,返回两次
返回值为0, 代表当前进程是子进程
返回值非负数,代表当前进程为父进程
代码示例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
printf("father: id=%d\n",getpid());
pid = fork();
if(pid > 0)
{
printf("this is father print, pid = %d\n",getpid());
}
else if(pid == 0){
printf("this is child print,child pid = %d\n",getpid());
}
return 0;
}
vfork创建
vfork函数也是用于创建一个新的进程,但是它与fork有一些显著的区别:
- 子进程和父进程共享相同的地址空间,不拷贝。
- vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行
由于子进程和父进程共享地址空间,因此子进程在vfork之后不能改变父进程的内存内容,否则会引起不可预测的行为。vfork主要用于提高性能,因为它避免了复制父进程的整个地址空间
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid > 0)
{
while(1){
printf("cnt=%d\n",cnt);
printf("this is father print, pid = %d\n",getpid());
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is chilid print, pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
exit(0);
break;
}
}
}
return 0;
}
错误打印函数
#include <stdio.h>
void perror(const char *s);
功能:打印错误信息(某些函数返回负值,表明发生错误,但是不知道具体类型,使用这个函数可以获得具体错误类型)
进程的退出
正常退出
- Main函数调用return。
注意:return不是结束,只是函数结束,当它刚好结束的是main函数,此时导致进程结束。造成return结束进程的错觉。
- 进程调用exit(),标准c库
- 进程调用_exit()或者_Exit(),属于系统调用
异常退出
- 调用abort
- 当进程收到某些信号时,如ctrl+C
- 最后一个线程对取消(cancellation)请求做出响应
父进程等待子进程退出
为啥要等待子进程退出?
- 资源释放:当一个子进程结束时,它的退出状态(exit
status)和其他资源不会立即被系统回收,而是保存在系统中,直到父进程读取它们。这种状态的子进程被称为**“僵尸进程**”。如果父进程不处理这些退出状态,系统资源将被浪费,导致僵尸进程的积累。 - 进程同步:父进程可能需要知道子进程的执行结果,以便根据结果执行进一步的操作。等待子进程退出可以确保父进程在子进程完成后进行相应的处理。
- 进程树的清理:等待子进程退出有助于保持进程树的整洁。如果父进程没有等待子进程退出,系统中将会出现越来越多的僵尸进程,从而使进程树混乱。
什么是僵尸进程
在 Linux 系统中,当子进程终止时,它的进程控制块(Process Control Block,PCB)仍然保留在内存中。这是为了保存子进程的退出状态和其他信息,直到父进程能够读取这些信息。理解这一点的关键在于以下几个方面:
僵尸进程(Zombie Process):
-
当一个子进程终止时,它的进程控制块不会立即被系统回收,而是转换为一个僵尸进程。
-
僵尸进程保留了一些信息,如进程 ID、退出状态、资源使用统计等。
等待子进程(Wait for Child Process):
-
父进程需要通过调用 wait() 或 waitpid() 等系统调用来读取子进程的退出状态。
-
这些调用会返回子进程的退出状态,并从系统中删除僵尸进程,释放其占用的资源。
释放资源:
- 如果父进程不读取子进程的退出状态,这些僵尸进程将一直存在,占用系统资源,最终可能导致资源耗尽。
- 通过读取子进程的退出状态,父进程可以确保这些资源被正确释放
wait函数
wait函数用于让父进程等待其任意一个子进程结束。当子进程结束时,wait函数会返回子进程的PID,并通过参数status来返回子进程的退出状态。
函数原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
wait函数的返回值:
-
成功时返回子进程的PID。
-
失败时返回-1,并设置errno来指示错误类型
-
status参数用于获取子进程的退出状态,可以使用宏来解析状态值
WIFEXITED(status):如果子进程正常退出,则为真。
WEXITSTATUS(status):如果WIFEXITED为真,则返回子进程的退出状态。
WIFSIGNALED(status):如果子进程因信号而终止,则为真。
WTERMSIG(status):如果WIFSIGNALED为真,则返回导致子进程终止的信号编号。status参数:
是一个整型数指针
非空:
子进程退出状态放在它所指向的地址中。
空:
不关心退出状态
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0)
{
wait(&status);
printf("child quit, child status = %d\n",WEXITSTATUS(status));
while(1){
printf("cnt=%d\n",cnt);
printf("this is father print, pid = %d\n",getpid());
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is chilid print, pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 5){
exit(3);
}
}
}
return 0;
}
waitpid函数
waitpid函数提供了更灵活的方式来等待子进程的结束。可以指定等待特定的子进程,并且可以选择是否以非阻塞方式等待。
函数原型:
复制代码
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
参数解释:
-
pid:指定等待的子进程的PID。
-1:等待任意子进程(等同于wait函数)。
正值:等待指定PID的子进程。
0:等待与调用进程在同一进程组中的任何子进程。
负值:等待与调用进程在同一进程组中的任何子进程,绝对值等于pid。 -
status:与wait函数中的status参数相同,用于返回子进程的退出状态。
-
options:可以是0或以下选项的组合:
WNOHANG:如果没有已结束的子进程,则立即返回0。
WUNTRACED:如果子进程已停止但未报告,则返回其状态。
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0)
{
// wait(&status);
waitpid(pid,&status,WNOHANG);
printf("child quit, child status = %d\n",WEXITSTATUS(status));
while(1){
printf("cnt=%d\n",cnt);
printf("this is father print, pid = %d\n",getpid());
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is chilid print, pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 5){
exit(3);
}
}
}
return 0;
}
孤儿进程
- 父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程
- Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程
exec族函数
我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
功能:
在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
函数族:
exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe
函数原型:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
返回值:
exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
参数说明:
-
path:可执行文件的路径名字
-
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
-
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。
exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l : 使用参数列表
p:使用文件名,并从PATH环境进行寻找可执行文件
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量
具体可以在这里学习 点击跳转链接
system函数
在Linux系统中,system函数用于创建一个子进程,该子进程执行由字符串命令指定的shell命令。这是一个方便的方法来执行shell命令,并获取其退出状态。
函数原型
复制代码
#include <stdlib.h>
int system(const char *command);
参数
- command:一个指向要执行的shell命令的字符串。如果command是NULL,system将检查是否存在默认的命令解释器(通常是/bin/sh),并返回一个非零值表示存在,否则返回0表示不存在。
返回值
- 成功时,返回由shell返回的状态代码(即命令的退出状态)。如果shell无法执行,system返回127。
- 失败时,返回-1,并设置errno以指示错误。
使用示例: 演示如何使用system函数来执行一个shell命令并获取其退出状态。
#include <stdio.h>
#include <stdlib.h>
int main() {
int status;
// 执行一个简单的shell命令
status = system("ls -l");
if (status == -1) {
perror("system");
exit(EXIT_FAILURE);
} else {
// 解析命令的退出状态
if (WIFEXITED(status)) {
printf("Command exited with status %d\n", WEXITSTATUS(status));
} else {
printf("Command did not exit normally\n");
}
}
return 0;
}
在这个示例中,system函数执行ls -l命令,并打印命令的退出状态。
system函数的工作原理
- system创建一个子进程。
- 子进程调用默认的shell(通常是/bin/sh)并传递命令字符串给shell。
- Shell解释并执行命令。
- 子进程等待命令执行完毕,并返回命令的退出状态给父进程。
- 父进程返回子进程的退出状态。
oppen函数
在Linux系统中,popen函数用于创建一个管道,连接到一个子进程的标准输入或标准输出。它可以让你通过C程序来执行一个命令,并且读取该命令的输出或者向该命令的输入写入数据。
函数原型
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
参数
-
command:一个指向要执行的shell命令的字符串。
-
type:表示管道方向的字符串。可以是"r"(读)或"w"(写)。
“r”:打开一个管道,以读取子进程的标准输出。
“w”:打开一个管道,以写入到子进程的标准输入。
返回值
- popen:返回一个指向
FILE的指针,用于I/O操作
。如果失败,返回NULL。 - pclose:返回命令的终止状态。如果调用失败,返回-1。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
int main(void)
{
char ret[1024] = {0};
FILE *fp;
fp = popen("ps","r");
int nread = fread(ret,1,1024,fp);
printf("read ret %d byte, ret=%s\n",nread,ret);
return 0;
}
区别system函数
比system在应用中的好处:可以获取运行的输出结果