|
文章目录
- 自学网站
- 上文回顾
- 进程控制块—PCB
- 查看进程
- 初识系统调用
- 初始fork函数
- 练习题
自学网站
推荐给老铁们两款学习网站:
面试利器&算法学习:牛客网
风趣幽默的学人工智能:人工智能学习
首个付费专栏:《C++入门核心技术》
上文回顾
下面我们对于上篇文章重点部分进行回顾:
进程就是可执行程序+该进程所对应的内核数据结构。
进程控制块—PCB
前面我们说到:
进程 = 可执行程序 + 该进程所对应的内核数据结构。
那“进程所对应的内核数据结构”是啥呢?
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
很多时候我们称其为PCB,而我们现在所学习的Linux操作系统,它的PCB叫做 task_struct
(可以这样想:PCB是汽车,而Linux中具体的PCB——task_struct是宝马)
程序是文件吗?是的,而文件在磁盘中。
操作系统中可能存在大量进程,操作系统要不要将所有的进程管理起来呢?当然要,对进程的管理,本质是对进程数据的管理,所以本质也就是对进程的内核数据结构的管理。
这就好像我们前面说的那个学生的例子,所以为什么管理进程要有PCB呢?
先描述,再组织。(其实这也就解释了为什么说我们之前在实现数据结构的时候,总是先定义一个结构体)
比如定义顺序表:
typedef int SLTDataType;
struct SeqList
{
SLTDataType* a;
size_t size;
size_t capacity;
}
task_struct 的内容分类:
- 标识符:描述本进程的唯一标识符,用来区分其他进程;
- 状态:任务状态,退出代码,退出信号等;
- 优先级:相对于其他进程的优先级;
- 程序计数器:程序中即将被执行的下一条指令的地址;
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针;
- 上下文数据:进程执行时处理器的寄存器中的数据;
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表;
- 记账信息:可能包括处理器的时间总和,使用的时钟数总和,时间限制,记账号等;
- 其他信息。
写到这,可能有老铁会问,进程这块我们学习什么呢?我们学的就是进程控制块里的属性,就是上面所列出来的内容,大家在这里简单了解知道即可,后面会一一详细说明。
查看进程
注意哦, 我们经常使用的指令, 像ls, touch…这些指令在启动之后本质上也是进程.
好的, 那么我们怎么查看进程呢?
看下面一段程序:
#include<stdio.h>
int main()
{
while(1)
printf("Hello Linux.\n");
return 0;
}
方式一: ps axj(最常用)
方式二: 通过 proc 查看
注意: proc 是内存文件系统, 存放着当前系统的实时进程信息. 每一个进程在系统中, 都会存在一个唯一的标识符(pid -> process id), 就如同学生在学校里有一个专门的学号一样.
我们经常会听到当前路径, 何为当前路径呢?
当前路径就是当前进程所在的路径, 进程自己会维护.
那么 pid 和当前路径这些都在哪里呢? 它们属于进程的内部属性, 在进程的进程控制块当中, 也就是PCB(task_struct)结构中.
初识系统调用
下面我们来使用第一个系统接口, 通过系统调用获取进程标识符.
看下面一段程序:
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("pid : %d\n", getpid());//系统接口
printf("ppid: %d\n", getppid());
return 0;
}
执行程序:
再次执行程序:
请老铁对比这两次这行结果, 有什么不同吗? 我们发现 pid 是变化的, 但是 ppid 没有发生变化, 诶呀, 这是什么意思? 所以,这也就引入了下面的一个问题: 创建进程有很多方法, 如 ./你的程序(./myProcess), 每次执行./myProcess 时, 进程 pid 发生变化, 但是父进程 ppid 不变, 那为什么 ppid 不变, 父进程是谁呢? 几乎我们在命令行上执行的所有指令(包括你的 cmd), 都是bash进程的子进程.
初始fork函数
认识 fork 函数:
返回值:
看下面一段程序:
#include<stdio.h>
#include<unistd.h>
int main()
{
int ret = fork();
printf("hello proc\n");
sleep(1);
return 0;
}
运行程序:
什么情况, 为什么会打印两次呢?
fork 函数很特殊, 它的作用就是创建子进程, 它有两个返回值, 给父进程返回子进程的 pid, 给子进程返回0.
父子进程代码共享, 数据按照写时拷贝的方式来共享或独立.
肯定有老铁会疑惑, 为什么 fork 会给父进程返回子进程的 pid, 给子进程返回0呢?
因为父进程必须要有标识子进程的方案, fork 之后, 给父进程返回子进程的 pid; 而我们的子进程最重要的是要知道自己被创建成功了, 因为子进程找父进程的成本非常低, 直接调用 getppid() 即可.
看下面一段程序:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id = fork();//返回值id==0,子进程; >0,父进程
if(id == 0)
{
//子进程
while(1)
{
printf("我是子进程, 我的pid : %d, 我的父进程 : %d\n", getpid(), getppid());
sleep(1);
}
}
else
{
//父进程
while(1)
{
printf("我是父进程, 我的pid : %d, 我的父进程 : %d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
我们之前在学习C语言的时候, if 和 else 可以同时执行吗? 我们知道是不可以的, 那在我们的C语言中有没有可能两个以上的死循环同时运行, 这也是不可能的.
但是在上面的代码中却可以!
fork 之后, 父进程和子进程会共享代码, 一般都会执行后续的代码, 所以这也就解释了为什么会打印两次, fork 之后, 父子进程的返回值不同, 可以通过不同的返回值进行判断, 让父子进程执行不同的代码块.
为什么 fork 会返回两次, 有两个返回值?
fork 之后, 操作系统做了什么?
OS多了一个进程(子进程).
task_struct + 进程的代码和数据
task_struct + 子进程的代码和数据
子进程的 task_struct 对象内部的数据从哪里来呢? 基本是从父进程那里继承(拷贝)下来的.
子进程执行代码, 计算数据的, 子进程的代码从哪里来呢? 和父进程执行同样的代码, fork 之后, 父子进程代码共享(不同的返回值, 让不同的进程执行不同的代码块), 而数据会按照写时拷贝的方式来共享或独立.
练习题
1.下面关于系统调用的描述中,错误的是()
A.系统调用把应用程序的请求传输给系统内核执行
B.系统调用函数的执行过程应该是在用户态
C.利用系统调用能够得到操作系统提供的多种服务
D.是操作系统提供给编程人员的接口
E.系统调用给用户屏蔽了设备访问的细节
F.系统调用保护了一些只能在内核模式执行的操作指令
解析:
系统调用是操作系统向上层提供的用于访问内核特定功能的接口。应用程序通过系统调用将自己需要完成的功能传递给内核,进行执行完成;
系统调用的运行过程是在内核态完成的,操作系统并不允许用户直接访问内核,也就是说用户运行态并不满足访问内核的权限;
用户只需要将自己的请求以及数据通过系统调用接口传递给内核,内核中完成对应的设备访问过程,最终返回结果;
系统向上层提供系统调用接口用于访问内核服务或功能的很大原因也是因为这样可以最大限度的保护内核的稳定运行。
2.下列有关进程的说法中,错误的是? [多选]
A.进程与程序是一一对应的
B.进程与作业是一一对应的
C.进程是静态的
D.进程是动态的过程
解析:
程序是静态的指令集合,保存在程序文件中, 进程是程序的一次运行过程中的描述。
作业是用户需要计算机完成的某项任务,是要求计算机所做工作的集合。
一个程序可以同时运行多次,也就有了多个进程;
一个作业任务的完成可由多个进程组成,且必须至少由一个进程组成;
程序是静态的,而进程是动态的。
3.系统感知进程的唯一实体是()
A.进程id
B.进程控制块
C.进程管理器
D.进程名
解析:
进程是操作系统对于程序运行过程的描述,而这个描述学名叫做进程控制块-PCB,它是操作系统操作系统管理以及调度控制程序运行的唯一实体。