【Linux进程篇】进程概念(1)
目录
- 【Linux进程篇】进程概念(1)
- 进程
- 基本概念
- 描述进程-PCB
- task_struct-PCB的一种
- task_ struct内容分类
- 组织进程
- 查看进程
- 通过系统调用获取进程标示符
- 通过系统调用创建进程——fork初识
作者:爱写代码的刚子
时间:2023.7.30
前言:操作系统和软硬件的关系就相当于管理者和被管理者的关系,管理者并不需要见到被管理者,通过执行者拿到对应数据,通过对数据的管理来达到管理的目的。==对软硬件的管理最后一定会变成对某种数据结构的增删查改。==本篇博客将介绍操作系统如何对进程进行管理,如何创建进程。
进程
基本概念
-
课本概念:程序的一个执行实例,正在执行的程序等
-
内核观点:担当分配系统资源(CPU时间,内存)的实体。
-
- 通俗一点:一个已经加载到内存中(运行)的程序叫做进程(任务),计算机在开机时也需要将操作系统从外设加载到内存中。
-
- 一个操作系统,不仅仅只能运行一个进程,可以同时运行多个进程,所以操作系统必须将进程管理起来。
-
- 进程需要先描述再进行管理
-
- 任何一个进程,在加载到内存的时候,形成真正的进程时,操作系统要先创建描述进程的结构体对象(进程属性的集合)——PCB,process ctrl block——进程控制块
-
- 进程属性: 进程编号、进程状态、优先级…
-
- 操作系统根据进程的PCB,为该进程创建对应的PCB对象
-
- 进程 = 内核PCB数据结构对象(描述你这个进程的所有属性值)[系统维护] + 你自己的代码和数据
-
- 操作系统管理进程只需要管理PCB结构体对象
-
- PCB中包含对应的指针信息找到代码和数据
-
- 操作系统中中含有多个进程,对进程的管理变成了对双向链表进行增删查改
-
- 所有操作系统都是按照以上方案来对进程进行管理(思路是一致),但是具体管理方法有差别。
-
- 不要以为将程序加载到内存就叫进程,而是操作系统为了管理这个进程所创建的PCB结构体。(只写一份可执行程序,把同样的进程跑了两次,但是在系统层面上是两进程)
-
ps ajx查看所有进程
用vim编写一段代码,并执行:
-
ps ajx | grep myprocess查看myprocess的进程状态
- ps ajx | head -1 && ps ajx | grep myprocess将进程的属性也显示出来
描述进程-PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct(PCB具体的一种)
task_struct-PCB的一种
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。(CPU中有一个寄存器(程序计数器),当运行一个进程时,永远要把当时运行指令的下一条指令的地址记录下来,因为进程可能因为某些原因不被运行,唤醒时才运行)
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针(找到代码数据)
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
Linux内核中,最基本的组织进程task_struct的方式是采用双向链表组织的,PCB还有其他的链式属性(队列、二叉树等,数据结构之间的关系非常错综复杂)
查看进程
进程的信息可以通过/proc系统文件夹查看
-
如:要获取PID为1的进程信息,你需要查看/proc/1这个文件夹。
-
ls /proc查看当前系统的所有进程(关机之后目录里的数据没了,上面的所有信息是Linux操作系统用文件系统的方式把内存当中的文件包括进程信息给我们可视化出来,上面的数据都是内存级的)
运行指令时,所有的指令都变成了进程,这些指令都要加载到内存,以进程的方式去运行(很快)
- ls /proc -l显示进程(蓝色数字是当前进程的PID)
当进程重新运行时PID很可能会变化
cwd:current work directory当前进程的工作目录(目录的重新理解)
- 大多数进程信息同样也可以使用top和ps这些用户级工具来获取
通过系统调用获取进程标示符
-
- 进程id(PID)
-
- 父进程id(PPID)
- kill -9 +PID杀死对应进程
- while :;do ps ajx | head -1 ; ps ajx | grep proc | grep -v grep;echo “--------------------------------------------”; sleep 1;done 打印进程相关的信息
-
getppid() 利用这个函数可以查看父进程id(头文件:#include <sys/types.h>)
-
多次运行时父进程不会变,但是子进程会变
-
查看该程序的父进程(bash进程)
运行一个进程时,命令行解释器会将这个指令变成bash的子进程,由子进程执行对应的命令。 -
每次登入iTerm时,系统会为我们单独创建一个bash进程(命令行解释的进程)(所有指令的父进程)
通过系统调用创建进程——fork初识
- 运行man fork认识fork
- fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
进程创建成功,给父进程返回子进程的PID,给子进程返回0;创建失败返回-1
- 那是不是表示fork函数会返回两次?
说明这个id具有两个值,说明fork()在程序运行期间创建了一个子进程。
【问题】:
-
- 为什么能跑两份的代码?
因为存在了两个进程。
- 为什么能跑两份的代码?
-
- 为什么fork要给子进程返回0,给父进程返回子进程PID?
返回不同的返回值,是为了区分让不同的执行流,执行不同的代码块,一般而言fork之后的代码父子共享。给父进程返回子进程PID是为了标识子进程的唯一性,父进程来控制子进程,子进程只需调用getpid()就可以知道子进程的PID。
- 为什么fork要给子进程返回0,给父进程返回子进程PID?
-
- 一个函数是如何做到返回两次的?如何解释?
fork()之后父子进程的代码是共享的,代码是不可以被修改的。虽然代码是共享的,但是我们是希望他们执行不同的代码块,这样才可能让父子进程协同起来。
fork()也是一个函数,一个函数return了说明该函数的核心工作已经完成。
fork()函数的功能可以总结为以下:- 创建子进程PCB
- 填充PCB对应的内容
- 让子进程和父进程指向同样的代码
- 父子进程都是有独立的task_struct,可以被CPU调度运行
- … …
- return ret;
- 一个函数是如何做到返回两次的?如何解释?
既然return ret;也是一份代码,说明父子进程共享这份代码,父进程在函数调度时返回一次,子进程在函数调度时返回一次。所以fork()函数返回了两次fork()return之前,子进程早就创建了
-
- 一个变量怎么会有不同的内容?
在任何平台,进程在运行的时候,是具有独立性的,直接决定了父子进程不能访问同一份数据,因为数据可能会被修改。(共享代码并不影响独立性),理论上子进程需要将父进程数据拷贝一份(操作系统实现)。但是子进程并不一定使用父进程的全部数据,如果全部拷贝父进程的数据可能造成资源的浪费,造成不必要的内存负担,所以子进程拷贝数据时会进行写时拷贝。所以在进程刚创建的时候父子进程的代码和数据全都是共享的,但是一旦当子进程尝试去修改父进程的数据时,操作系统会重新开辟一块空间,去新开辟的这块空间进行写入。(用多少开多少空间)
(深浅拷贝也用到了写实拷贝)
由于fork()的返回值写入了id,子进程写入时发生了写实拷贝,操作系统对同一个id变量拷贝了两份,所以父子进程在使用id时,父子进程看到的id的内容就会不一样。(访问了不同的内存区)
- 一个变量怎么会有不同的内容?
【存疑】:同一个变量是如何让父子进程看到不同的内容?(地址空间章节里会介绍)
- 如果父子进程被创建好,fork()往后谁先运行呢?
谁先运行是不确定的,由调度器决定,调度的本质是对CPU资源的竞争,调度器的存在保证了竞争的公平。
【问题】但是作者发现了一个问题:
按照上面的理解该程序的输出结果应该为:111111122222221111111
实际运行时:
【解释现象】:因为代码中的第一个printf打印的字符串后面没有加上\n,导致父进程打印完1111111,该字符串还存在缓冲区中,导致创建子进程时也将父进程的缓冲区也拷贝了,所以也将"1111111"打印了出来。
进程概念篇(1)结束