目录
一、进程是什么
二、task_struct
三、查看进程
四、创建进程
4.1 fork函数的认识
4.2 2. fork函数的返回值
五、进程终止
5.1. 进程退出的场景
5.2. 进程常见的退出方法
5.2.1 从main返回
5.2.1.1 错误码
5.2.2 exit函数
5.2.3 _exit函数
5.2.4 缓冲区问题补充(为什么_exit不刷新缓冲区)
六、进程等待
6.1. wait和waitpid等待回收子进程
6.2 阻塞与非阻塞
Linux专栏:传送门!
一、进程是什么
在操作系统中,进程是资源分配和独立运行的基本单位。它是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。单纯的只看一个定义很难理解什么是进程
我们磁盘中的可执行程序,CPU要想拿到并且执行,代码和数据要先放在内存中。操作系统是一个软件在内存中,当磁盘中的可执行程序被内存拿到,可执行程序的代码和数据会被内存拿到,内存中的操作系统会对代码和数据进行描述然后组织为数据结构(先组织在描述)形成内核数据结构对象,对进程的管理就变成了对数据结构对象的增删查改。 内核数据结构对象可以称为PCB,也叫做进程控制块。
内核数据结构对象通过指针指向本身的代码和数据也指向下一个结构体,进而形成真正的进程。进程=内核数据结构对象+自己的代码和数据。 这个数据结构就是进程列表。CPU对进程列表进行调度。
二、task_struct
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的⼀种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
三、查看进程
进程的信息可以通过/proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看/proc/1 这个文件夹。
大多数进程信息同样可以使用top和ps这些用户级工具来获取
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{while(1){sleep(1);}return 0;
}
通过系统调用获取进程标示符
• 进程id(PID)
• 父进程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{printf("pid: %d\n", getpid());printf("ppid: %d\n", getppid());return 0;
}
四、创建进程
4.1 fork函数的认识
在linux中fork函数非常重要, 它从已存在的进程中创建一个新的进程, 新进程为子进程,而原进程为父进程。
#include<unistd.h>
pid_t fork(void);
返回值:子进程中返回0, 父进程中返回子进程id, 出现错误则返回-1
进程调用fork,当控制转移到内核中的fork代码后, 内核会做下面几件事情:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表中
- fork返回, 开始调度器调度
当一个进程调用fork()之后,就有两个二进制代码相同的进程。而且他们都运行到相同的地方。但每个进程都将可以开始他们自己的旅程。
这里我们看到了三行输出, 一行before, 两行after,父进程先打印before消息,然后它又打印after,另一个after消息有子进程打印的,但是子进程没有打印before,为什么呢?
所以, fork()之前父进程独立执行,fork()之后,父子两个执行流分别执行,注意,fork之后,谁先执行由调度器决定。
4.2 2. fork函数的返回值
子进程返回0,父进程返回的是子进程的pid。
父进程与子进程是一对多的关系,父进程:子进程=1:N。对于子进程来说它只有一个父进程,对于父进程来说他有多个父进程。为了管理这些进程,所有父进程返回子进程的PID具有唯一性。子进程要想找到父进程肯定更方便。
为什么有两个返回值, 因为fork之后是两个不同的进程, 而返回值也是给不同的进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> int main() { pid_t pid = fork(); // 执行 fork() if (pid < 0) { // 处理 fork 失败的情况 perror("fork failed"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程执行 printf("This is the child process (PID: %d)\n", getpid()); } else { // 父进程执行 printf("This is the parent process (PID: %d, Child PID: %d)\n", getpid(), pid); } return 0; // 所有进程执行完毕
}
执行fork函数,会为子进程创建一个和父进程一样的PCB。但是他们所指向的代码都是一样的,所以他们的代码共享。也就是说父进程和子进程共享代码中return,都执行return。
五、进程终止
本质:释放系统资源, 就是释放进程申请的相关内核数据结构和对应的代码和数据。
5.1. 进程退出的场景
- 代码运行完毕, 结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
5.2. 进程常见的退出方法
5.2.1 从main返回
除了main函数的返回值表示进程结束,其它函数的return都表示函数结束。
5.2.1.1 错误码
main函数的返回值是返回给父进程或者系统的,命令行中获取最近一个进程的返回值我们可以使用echo $?来获取
对于返回值,0表示成功, 非0表示错误,为什么会失败呢?系统提供了不同的错误码信息记录了错误的原因, 也可以自己约定错误码。
那么什么是错误码呢?
举个栗子:
如果想要查看错误码, 我们可以使用errno函数, 使用man可以查看命令详情。
man 3 errno
如果想知道具体的错误内容, 可以使用strerror函数,参数传递错误码。
man 3 strerror
5.2.2 exit函数
在代码的任何地方, 让进程直接结束。参数就是返回的错误码。
如果使用exit, 如果缓冲区有数据, 则会被刷新出来。
5.2.3 _exit函数
是系统层的进程终止调用
如果使用_exit, 缓冲区的数据则不会被刷新出来。
5.2.4 缓冲区问题补充(为什么_exit不刷新缓冲区)
exit属于是语言级别的,在三号手册, 而_exit是系统级别的,在二号手册。
_exit本质上是系统调用
所以我们上面的_exit实际上是绕过了语言层, 直接进行了系统调用, 而刚刚的缓冲区是语言级别的, fflush也是语言级别的。
六、进程等待
首先我们可以查看一下fork的返回值, 如果fork失败, 则错误码会被设置。
6.1. wait和waitpid等待回收子进程
一般而言, 父进程创建的子进程, 父进程就要等待子进程进行回收, 如果子进程一直不退出, 则父进程就会阻塞在wait内部。
wait的作用,等待任意的子进程(参数可以传nullptr表示不获取status)
常用:
waitpid的作用:第一个参数 pid>0表示等待指定的一个进程,pid == -1表示等待任意一个子进程
看一下他们的返回值, 如果等待成功则返回对应的子进程,如果等待失败则返回-1.
举个栗子:
等待任何一个子进程
当然, 我也可以修改id的参数,比如更换为刚刚子进程id,这里就不展示了。
waitpid的第二个参数,它会帮助父进程获取子进程的退出信息,通过参数的方式给我们带出来。输出型参数
但是这里的退出信息却是256,为什么不是1呢?
其实,status这个参数包含的信息并不只是退出码,它的本质是一个位图, 它的结构中前八位是退出状态,有256种状态 ,低七位是终止信号, 还有一个标志位。
如果想要提取退出状态则需要进行位运算,如下
小问题: 那我们可不可以不使用status来获取状态码,而是用一个全局变量呢?
不可以, 因为进程具有独立性,子进程修改父进程看不到,会发生写时拷贝。
6.2 阻塞与非阻塞
waitpid的第三个参数就是关于阻塞等待与非阻塞等待
首先waitpid的返回值, 如果>0表示,返回目标进程pid, 如果 == 0, 等待成功,但是子进程没有退出, <0 等待失败.
参数如果传0表示阻塞等待, 如果传WNOHANG表示非阻塞等待
举个栗子:
总结
进程控制是操作系统中的一个重要主题,主要涉及如何管理和调度进程以确保计算机系统的高效运行!
本篇完,下篇见!