目录
1.冯诺依曼体系结构(硬件)
2.操作系统(软件)
2.1概念
2.2设计os(操作系统)的目的
2.3如何理解管理
2.4系统调用和库函数概念
3.进程
3.1基本概念
3.2描述进程-PCB和组织进程
3.3ps axj指令
3.4查看进程
3.5通过系统调用获取进程表示符(PID)
getpid()系统调用函数,获取(子)进程pid
getppid()系统调用函数,获取子进程的父进程pid
3.6通过系统调用,创建子进程-fork()函数初识
4.进程状态
5.Z状态(zombie)-僵尸进程
6.孤儿进程
7.环境变量
8.进程优先级
9.程序地址空间
1.冯诺依曼体系结构(硬件)
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
冯·诺依曼体系结构是指一种将计算机硬件和软件分离的计算机结构,它由计算机科学家冯·诺依曼于1945年提出。该体系结构的核心思想是将数据和指令存储在同一存储器中,并通过提取和执行存储器中的指令来操作数据。
具体来说,完全冯·诺依曼体系结构包括以下几个主要组成部分:
- 中央处理器(Central Processing Unit,CPU):负责执行指令和处理数据。CPU包含算术逻辑单元(运算器(ALU))和控制单元(控制器(Control Unit,CU))。ALU执行算术运算和逻辑运算,而CU负责从存储器中提取指令并控制ALU和其他组件的操作(对计算硬件流程进行一定的控制)。
- 存储器(Memory):用于存储指令和数据。存储器可以分为随机访问存储器(Random Access Memory,RAM)和只读存储器(Read-Only Memory,ROM)。RAM用于存储运行时数据和程序,而ROM用于存储固化的程序和数据。(存储器指的是什么?指的是内存(主存储器(内存条),翻译为Memory))
- 控制器(Controller):负责将指令从存储器中提取到控制单元,并将执行结果返回到存储器。控制器通常包含指令寄存器(Instruction Register,IR)、程序计数器(Program Counter,PC)和指令译码器(Instruction Decoder)。
- 输入设备(Input Devices):用于与计算机进行交互,包括键盘、鼠标、摄像头、话筒、磁盘、网卡等。
- 输出设备(Output Devices):显示器、打印机、播放器硬件、磁盘、网卡等
注:有的设备是纯的输入或者输出,也有既是输入,又是输出设备。其中我们把输入、输出设备叫做外部设备,简称外设。
中央处理器(它独占两个分别是:运算器、控制器)、存储器、输入和输出设备,这五大单元它们都是独立的个体!这些硬件单元需要通过一组线互相连接,以传输数据和控制信号。这一组线统称为总线,是用于连接这些硬件单元的一组线,总线分为两类分别是:系统总线和IO总线。
- 系统总线(System Bus)是连接计算机系统中的主要硬件单元的总线,包括连接CPU、存储器和控制器的数据总线、地址总线和控制总线。它用于在这些硬件单元之间传输指令、数据和控制信号。
- IO总线(IO Bus)是连接输入和输出设备的总线,用于将数据和指令从输入设备发送到内存(存储器),或将数据和结果从内存(存储器)发送到输出设备。
总线的作用是提供数据和控制信号的传输路径,使各个硬件单元能够相互通信和协调工作。通过总线连接,这些独立的硬件单元可以协同工作,实现计算机系统的功能,所以就有了我们上面的那张图,这就叫做冯·诺依曼体系结构。
在冯诺依曼中不仅仅规定硬件方面的构成,还规定了一些软性方面的东西,比如数据:
1.储存方面:
首先我们要知道CPU、磁盘、内存、还有很多的设备本身都是具有数据储存能力的,像CPU内部所对应的寄存器储存的效率是非常高,而我们对应的内存储存的效率也还是不错的,而一般像外设他们的储存效率是非常底下的,在我们的储存领域有一个储存金字塔,储存金字塔他有一个规则,如图:
2.效率方面:
在冯诺依曼体系结构场景中,数据必须是从输入设备写到内存设备中,然后CPU(中央处理器)不能直接从外设(输入设备)里面拿取数据,只能从内存设备当中拿取数据,所以需要先从外设把代码和数据加载到内存,拿到数据之后进行处理做完计算之后,一样不能直接把数据输入到外设之中(输出设备),只能再把数据写回到内存设备中,最后通过内存设备把对应的数据刷新到输出设备,但是为什么CPU不能直接从外设拿取数据,然后再把数据直接刷新到输出设备呢?因为在于这两个外设和中央处理器的差别速度太大了,如果让两个外设直接和中央处理器直接进行交互,那么其中我们对应的整个计算机结构的效率就以输入、输出设备的效率为主了,当输入设备还在输入数据的时候,中央处理器就已经把数据处理完交给了输出设备,输入设备却还在的慢悠悠的输入数据,所以如果输入设备直接把数据交给我们的中央处理器,势必会拖慢中央处理器的速度,导致整个计算机的整机效率变得非常底下,所以在这个情况下引入了一个稍微比输入设备快,稍微比中央处理器慢的设备,这个设备叫内存,这个内存设备速度适中容量适中,所以我们可以让输入设备把数据输入到内存设备当中,中央处理器去内存设备拿取数据进行处理之后再写回到内存设备中,最后又内存设备刷新到输出设备中,这样就可以适配硬件层面上CPU和外设的速度差,虽然也有速度差,但是小了很多。有的人会问输入设备把数据读取到内存,CPU在到存储器拿取数据然后写回到存储器中,最后再刷新到输出设备,这个动作依然是串行的,快也没快到哪里去?依然是又输入设备决定整体的速度。虽然CPU的内存是有限的,但是从我们有了存储器开始,不要觉得我们把数据交给了存储器CPU才开始运算,那么可能是输入设备提前把数据预加载到了存储器,然后我们的CPU在存储器读取数据的时候早被全部加载到了存储器中。我们知道一个程序在运行时必须得先从磁盘把数据加载到内存中,这个过程CPU又可能正在做着其他任务的计算,当我们加载完的时候,CPU就可以开始读取执行我们的数据运行程序,所以CPU的计算和加载可以同时进行,所以就由 串行 变成 并行 那么经过这样数据加载调度,然后就可以保证我们各个硬件并行同时的跑起来,所以它的效率并没有我们想象中的那么差,它可以直接在一定的意义上提高我们的效率。虽然说了这么多但是上面这套工作由谁来完成?谁来把数据从外设读取内存,再通过CPU去计算完,然后再由内存把数据刷新到外设?这套工作由操作系统来完成的,后面在进行解释。
所以为什么一个程序要运行,必须要先加载到内存中运行。为什么?因为一个程序在编译好了之后,在磁盘中储存的是一个普通文件,那么我们编写的代码和数据要不要被CPU执行和计算?所以我们的数据最终必须是要去被CPU进行运行计算,代码和数据要被CPU进行运算就必须要先加载到内存中。因为CPU只从内存中拿数据,而我们数据是在外设当中的,就注定了必须从外设把数据加载到内存中,所以为什么我们的程序在进行运行时必须要把数据加载到内存中,是因为我们冯诺依曼体系规定的!
冯·诺依曼体系结构的特点包括:
- 存储程序:指令和数据以相同的格式存储在存储器中,可以按照程序的顺序执行。这使得程序可以根据需要修改和扩展,并允许计算机执行不同的任务。
- 存储器随机访问:存储器中的数据可以通过访问地址来获取,并且读写操作具有相同的时间开销。这使得计算机可以快速地访问和操作存储器中的数据。
- 指令流水线:指令的执行通过流水线的方式进行,可以同时执行多条指令的不同阶段,提高了计算机的执行效率。
- 计算机硬件和软件分离:冯·诺依曼体系结构将计算机的硬件和软件分离,使得计算机的设计更加灵活,可以通过改变软件来实现不同的功能。
总而言之,完全冯·诺依曼体系结构是一种经典的计算机体系结构,它的核心思想是通过将数据和指令存储在同一存储器中,并通过提取和执行存储器中的指令来操作数据。这种体系结构具有结构清晰、灵活性高和可扩展性强的特点,成为现代计算机体系结构的基础。
关于冯诺依曼,必须强调几点:
1.这里的存储器指的是内存,这里的存储器通常是指主存(Main Memory),也就是我们常说的内存。
在冯·诺依曼体系结构中,主存储器通常是位于计算机内部的一种半导体存储器,如随机存取存储器(RAM)。这是计算机中的一种高速读写存储器,用于存储正在执行的程序、数据和临时结果。主存储器具有较小的存储容量,但具有很快的读写速度。
2.不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
3.外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
4.一句话,所有设备都只能直接和内存打交道
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?
2.操作系统(软件)
操作系统是计算机系统中的核心组件,为用户和其他应用程序提供了一个可靠、高效和安全的运行环境。不同的操作系统有不同的特点和功能,例如Windows、MacOS、Linux等。
像我们上面说的那一套工作流程,我们对应的这个数据呢,可能要先读取到内存了,然后再到CPU里运算,然后运算完之后,就是写回到我的内存,再刷新到我们的网卡。那么所有的这些设备其实都很笨,他为我们提供最基本的这样的功能,但最终是谁用这些功能呢?我们刚说的整个的这一套过程啊,那么。在键盘看来,键盘它只具备获取用户数据,把用户数据写到内存的能力。但什么时候用这个能力啊,那么我们应该怎么用,所以这个东西其实是要有一个控制逻辑在上,然后呢,一定要有一个逻辑来宏观的控制。比如说我们对应的电脑当中,你的计算机里可能不仅仅只有QQ在跑,你可能登着游戏登着微信,你也才登的知道凭什么我就要跑你的软件?那我什么时候跑这个软件,什么时候跑其它的软件,那么这个是不是就必须得有人控制,而为了更好的能够进行使用,首先要知道冯诺依曼体系结构里面的这些硬件设备都很傻,都像是一个个典型的游戏里的NPC,相当于就是在一个游戏世界里面的一个个角色,每一个角色都有自己的任务,但是呢,最终得有人总体把他们统筹起来,把它考虑起来给它们分配执行任务。所以就在这样的时代的背景下,我们对应的操作系统就必须诞生了,操作系统那么他最核心的功能是什么?操作系统它其实本质上是一款进行管理的软件,那他管谁呢?首先要管理的一定是我们刚上面所看到的一堆的硬件,好,上面的这些我们冯诺一曼构成的这种硬件呢,那么这上面的这些硬件最终是不是也要被我们能够管理起来啊?为了操作系统能够管理的,然后同样的我们操作系统呢,他其实未来呢,也能学到对软件这块进行管理,而他自己本身也是一款软件。比如人可以管人,同理软件自然也可以管理软件,所以操作系统既能管理硬件又能管理软件。
结论:操作系统是一款进行管理的软件!
2.1概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)
操作系统是一种软件程序,它管理和控制计算机硬件资源,并为用户和其他软件提供一个统一的界面和环境。以下是操作系统的几个详细概念:
- 资源管理:操作系统管理计算机的各种硬件资源,包括处理器、内存、磁盘、网络等。它分配资源给不同的程序或用户,并确保它们公平共享和高效利用。
- 进程管理:操作系统创建、调度和终止进程(程序的执行实例)。它负责分配处理器时间片、管理进程的状态转换、处理进程通信和同步等。
- 内存管理:操作系统分配和回收计算机内存,以便程序可以执行。它管理物理内存和虚拟内存,提供内存保护和地址转换功能。
- 文件系统:操作系统提供文件和目录的组织和管理。它负责文件的创建、读取、写入、删除等操作,并提供文件访问权限控制。
- 设备管理:操作系统管理计算机的输入和输出设备,如键盘、鼠标、打印机、磁盘驱动器等。它负责设备的分配、调度和控制。
- 用户界面:操作系统提供图形用户界面(GUI)或命令行界面,以便用户与计算机交互。它接收用户输入,并将其传递给相应的程序或操作。
- 安全性:操作系统确保计算机系统的安全性和保密性。它提供用户认证、访问控制、数据加密和防火墙等安全功能。
- 错误处理:操作系统监测和处理硬件或软件错误。它提供错误检测、恢复机制和故障排除功能,以确保系统的稳定性和可靠性。
2.2设计os(操作系统)的目的
设计操作系统的主要目的是提供一个有效、可靠和方便的计算机系统环境,以支持用户和应用程序的运行。以下是设计操作系统的一些主要目的:
- 资源管理:操作系统的一个主要目的是管理计算机的硬件资源,如处理器、内存、磁盘和网络等。它负责分配和调度这些资源,以确保它们能够高效地被各个程序或用户共享和利用。
- 简化和抽象化:操作系统提供一个抽象的接口,隐藏底层硬件的复杂性,使用户和应用程序能够以简单且一致的方式与计算机系统交互。它将底层硬件细节抽象化为高级的操作和功能,使得编程和使用计算机变得更加方便和易于理解。
- 进程管理:操作系统负责管理和调度各个进程(程序的执行实例)。它分配处理器时间片,控制程序的执行顺序和并发性,以实现程序的并行执行和资源共享。
- 内存管理:操作系统管理计算机的内存资源,包括分配、回收和保护。它确保进程能够正常运行所需的内存空间,并提供虚拟内存机制来扩展可用的内存空间。
- 文件系统:操作系统提供文件和目录的组织和管理,以便用户和应用程序可以方便地存储、读取和管理文件。它处理文件的物理存储和逻辑访问,并提供文件的权限控制和数据保护。
- 驱动管理:驱动程序是为了与硬件设备进行通信而设计的软件模块。操作系统通过驱动管理来支持和控制各种硬件设备的使用。驱动管理的主要任务包括以下几个方面:
驱动程序的加载和卸载:当硬件设备插入计算机或启动计算机时,操作系统需要加载相应的驱动程序,以便与设备进行通信。类似地,当设备被拔出或计算机关闭时,操作系统需要卸载对应的驱动程序。
驱动程序的匹配和配置:操作系统需要检测和识别计算机系统中的硬件设备,并根据设备的类型和特征选择合适的驱动程序进行匹配和配置。这确保了设备能够正常工作并与操作系统进行交互。
设备驱动程序的接口和抽象化:操作系统提供一组标准的设备驱动程序接口(如API),使应用程序能够以统一的方式与硬件设备进行通信,而不需要了解设备的底层细节。这种抽象化简化了应用程序的开发和维护过程,并提供了对不同设备的兼容性。
驱动程序的更新和升级:随着硬件技术的进步和改进,驱动程序可能需要进行更新和升级,以提供更好的性能、兼容性和稳定性。操作系统需要提供机制来管理驱动程序的更新,例如通过自动更新、手动下载或集成在操作系统更新中。
通过驱动管理,操作系统能够有效地管理和控制硬件设备,使其能够与应用程序和操作系统本身进行良好的协作。这确保了硬件设备的正常工作,并提供了良好的用户体验和系统性能。
- 设备管理:操作系统管理计算机的输入和输出设备,如键盘、鼠标、显示器、打印机等。它控制设备的分配、调度和操作,以支持用户和程序的设备访问需求。
- 安全性:操作系统提供安全功能,以保护计算机系统和用户的数据安全。它实施用户认证控制、访问权限管理、数据加密和防火墙等机制,以防止未经授权的访问和保护数据的机密性。
总之,设计操作系统的目的是为了提供一个高效、可靠、方便和安全的计算机系统环境,以满足用户和应用程序的需求,并提升计算机系统的性能和可用性。
结论:操作系统为什么对软硬件资源进行管理?
1.操作系统是为了帮助用户,管理好下面的软硬件资源。(手段)
2.为了对上给用户提供一个良好(稳定、高效、安全)的运行环境。(目的)
在计算机层次结构中,用户通常不能直接访问操作系统。操作系统位于计算机系统的内核层(Kernel),是计算机系统的最底层软件,负责管理和控制硬件资源。用户通过操作系统提供的用户界面与操作系统进行交互,但在大多数情况下,用户无法直接访问操作系统的内部功能和代码。
操作系统通常提供了两种主要类型的用户界面:
- 命令行界面(Command Line Interface,CLI):在命令行界面中,用户通过输入命令行指令来与操作系统进行交互。用户需要记住特定的命令和参数,以执行特定的操作系统功能和任务。
- 图形用户界面(Graphical User Interface,GUI):在图形用户界面中,操作系统提供了图形化的用户界面,使用鼠标、键盘和可视化元素(如窗口、按钮、菜单等)来进行操作。用户可以通过点击、拖放等方式与操作系统进行交互。
这些用户界面作为操作系统和用户之间的中介,将用户的请求和操作转化为对操作系统的指令和调用。用户可以通过这些界面来执行文件操作、启动程序、管理文件和目录、配置系统设置等操作。
然而,在特定的情况下,高级用户或系统管理员可能会通过特权访问操作系统的内部功能和设置,以进行系统配置、故障排除、性能调优等高级操作。这通常需要特定的权限和知识,并且需要小心操作,以防止对计算机系统造成损害。
系统调用接口是操作系统与应用程序之间的桥梁,它提供了一种机制,允许应用程序通过操作系统的功能来执行特权操作和访问底层资源。
以下是一个例子来帮助理解为什么要提供系统调用接口:
假设你正在编写一个应用程序,需要打开、读取和写入文件。你可以使用系统调用接口来完成这些操作,这样可以借助操作系统的底层功能来实现文件的访问。
在这个例子中,你的应用程序调用操作系统提供的系统调用接口来请求打开文件,读取文件内容以及写入文件内容。操作系统接收到这些请求后,会在内核态执行相应的操作,访问文件系统并完成对文件的操作。
系统调用接口的好处在于它提供了一种受控的、受权限控制的方式,让应用程序能够利用操作系统的功能,而不需要直接操作底层资源。这样,操作系统可以保护底层资源的安全性和完整性,并提供统一的接口供应用程序使用。
此外,系统调用接口还提供了一种标准化的方式来跨不同的硬件和操作系统平台进行开发。通过使用操作系统提供的系统调用接口,应用程序可以在不同的操作系统上运行,无需对底层操作系统的细节和差异进行过多的关注。
综上所述,系统调用接口提供了一种高层次、安全和标准化的方式,使得应用程序能够利用操作系统的功能和资源,从而实现特权操作和访问底层资源的需求。
2.3如何理解管理
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。那它是如何进行管理的?
举个例子:
总结
计算机管理硬件
1.描述起来,用struct结构体
2.组织起来,用链表或其他高效的数据结构
2.4系统调用和库函数概念
系统调用是操作系统提供给应用程序的接口,用于访问操作系统的底层服务和资源。通过系统调用,应用程序可以请求操作系统执行特定的任务,如文件操作、进程管理、网络通信等。系统调用是用户程序与操作系统之间的接口,用户程序通过系统调用发出请求,操作系统将执行请求并返回结果。
系统调用的特点:
- 安全性:系统调用通过操作系统来管理资源和权限,确保应用程序在访问资源时受到保护,不会越权访问。
- 可移植性:系统调用提供了一致的接口,使得应用程序可以在不同的操作系统上运行,而不用关心底层的实现差异。
- 开销较大:系统调用需要从用户态切换到内核态,这涉及到用户态和内核态之间的上下文切换,开销相对较大,比直接在用户程序中执行的开销高。
库函数是由编程语言或者第三方库提供的一组函数,用于完成某个特定的任务。库函数封装了一些常用的操作,为应用程序提供了简化编程的接口,可以提高开发效率。库函数可以提供各种各样的功能,如字符串处理、日期时间计算、数学运算、图形绘制等。
库函数的特点:
- 高层抽象:库函数提供了更高层次的抽象,隐藏了底层的实现细节,使得编程更加简单明了。
- 可重用性:库函数可以被多个应用程序重复调用,提高代码的重用性。
- 性能较高:相比于系统调用,库函数执行的开销较小,因为它们通常在用户态中执行,避免了用户态和内核态之间的切换开销。
综上所述,系统调用和库函数在功能和使用方式上有所区别,系统调用提供了底层的操作和资源访问,而库函数提供了更高层次的封装和功能抽象,用于简化编程。
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分
由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统
调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
3.进程
3.1基本概念
首先我们要找到的是程序运行起来就变成了进程,程序加载到了内存就叫做进程,被操作系统调度起来的程序就叫做进程。
进程是计算机中正在运行的程序的实例。它是操作系统进行任务调度和资源分配的基本单位。每个进程都有自己的独立内存空间、程序计数器(记录下一条要执行的指令地址)、寄存器集合、堆栈和文件描述符等。进程间相互独立,彼此之间不能直接访问和操作。
进程的基本特点:
1. 动态性:进程是动态生成和销毁的,当一个程序被执行时,操作系统会为其创建一个新的进程,当程序执行完毕或被终止时,进程会被销毁。
2. 并发性:多个进程可以同时运行,相互之间独立进行,操作系统通过时间片轮转等方式实现进程的并发执行。
3. 独立性:每个进程拥有独立的地址空间和资源,它们之间的内存空间是隔离的,一个进程的崩溃不会影响其他进程的运行。
4. 随机性:多个进程在竞争相同的资源时,可能会出现竞争条件和死锁等问题,需要通过同步机制来解决。
进程的状态:
1. 运行态:进程正在执行。
2. 就绪态:进程已经准备好,但还未分配到CPU资源,等待系统调度执行。
3. 阻塞态:进程由于等待某些事件的发生(如IO操作)而暂停执行,直到事件发生后才能继续执行。
进程控制块(PCB)是操作系统中用于管理进程的数据结构。它保存了进程的各种状态信息,包括进程的标识符、状态、程序计数器、寄存器值、内存分配情况、打开文件表等。操作系统通过PCB来管理进程的创建、调度、终止等操作。
进程是操作系统中非常重要的概念,它使得多个程序可以并发执行,共享系统资源,提高了系统的利用率和效率。
3.2描述进程-PCB和组织进程
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct 是 PCB 的一种
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
pcb/task_ struct内容分类:
1.标示符: 描述本进程的唯一标示符,用来区别其他进程。
2.状态: 任务状态,退出代码,退出信号等。
3.优先级: 相对于其他进程的优先级。
4.程序计数器: 程序中即将被执行的下一条指令的地址。
5.内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
6.上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
7. I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
8.记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
9.其他信息
Linux是如何提取出进程的属性从而形成task_ struct结构的?
在Linux内核中,进程的属性被提取出来并填充到task_struct结构中的过程主要由以下步骤完成:
- 进程的创建: 当一个新的进程被创建时,内核会调用fork()或clone()系统调用来创建新的进程。在这个过程中,内核会为新进程分配一个task_struct结构。
- 初始化task_struct: 内核会先在进程所在的内核堆栈空间中分配一块内存区域,用于存储task_struct结构。然后,内核会将一些默认值(如进程ID、进程状态等)填充到task_struct中。
- 设置进程的上下文: 在task_struct中,有一些重要的字段,如进程的命令行参数、文件描述符表、虚拟内存管理信息等,这些信息需要在进程创建过程中被捕获并设置到task_struct中。这是通过执行fork或clone系统调用的内核代码来完成的。
- 调度器相关的设置: 进程的调度信息(如调度类别、优先级等)也需要被设置到task_struct中,以便内核能够对进程进行调度。这些信息通常在调度器代码中进行设置。
总而言之,当一个进程被创建时,内核会分配一个task_struct结构来表示该进程,并在进程创建和初始化的过程中,将进程的各种属性和上下文信息填充到这个task_struct结构中。这样,内核就能够通过访问task_struct结构来获取和操作进程的各种属性和信息。
总结:进程 = 内核数据结构 + 代码和数据。
3.3ps axj指令
在Linux系统中,ps axj 是一个常用的命令行指令,用于显示当前运行的进程信息。下面是对ps axj指令的解释:
ps: 进程状态(Process Status)的缩写,用于显示进程信息。
a: 显示所有用户的所有进程,包括其他用户的进程。
x: 显示没有控制终端的进程。
j: 以进程树的形式显示进程信息,包括进程的父进程和子进程关系。
执行`ps axj`指令后,会列出当前系统所有的进程信息,包括进程的PID(进程ID)、PPID(父进程ID)、PGID(进程组ID)、SID(会话ID)、TTY(终端设备)、STAT(状态码)、TIME(运行时间)、COMMAND(命令名称)等。通过进程的父进程ID和子进程ID可以构建出进程之间的父子关系。
`ps axj`指令在系统调试、进程监控、性能分析等场景中非常有用,可以帮助用户了解系统中运行的进程情况,并对进程之间的关系进行分析。
注:可以使用Ctrl + c杀掉或终止我们的进程。
3.4查看进程
总结:我们查看进程的方式有两种:
1.使用ps axj查看所有的进程,可以加上 管道 和 grep + 文件名 筛选我们需要查看的进程。
2./proc 查看根目录底下的proc目录,proc目录底下保存着以进程的PID创建的目录,该PID目录包含的进程的所有相关属性和信息。
3.5通过系统调用获取进程表示符(PID)
getpid()系统调用函数,获取(子)进程pid
在Linux中,getpid() 是一个系统调用,用于获取当前进程的进程ID(PID)。
系统调用是操作系统提供给用户程序的接口,用于访问操作系统的底层功能。getpid() 系统调用允许用户程序获取自己的进程ID,以便在程序中进行进程管理或其他操作。
getpid() 系统调用没有参数,直接返回当前进程的PID作为结果。
在C语言中,可以通过包含 <unistd.h>和<sys/types.h> 这两个头文件来使用 getpid() 系统调用。下面是一个示例代码:
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main()
{
pid_t pid = getpid();//getpid()的返回类型是pid_t,它其实就是一个有符号整数
printf("当前进程的ID是:%d\n", pid);
return 0;
}
在上述示例中,首先包含了 <unistd.h>和<sys/types.h> 头文件,然后调用 getpid() 系统调用获取当前进程的PID,并用 printf() 函数输出该PID。
编译并运行这个程序,你将会看到输出当前进程的PID。
除此之外,进程之间除了一个一个的独立的进程,那么进程还有一些叫做兄弟关系和父子关系的概念,我们先了解父子关系的概念。
getppid()系统调用函数,获取子进程的父进程pid
在Linux中,getppid() 是一个系统调用,用于获取当前进程的父进程的进程ID(Parent Process ID)。
getppid() 系统调用没有参数,直接返回当前进程的父进程的PID作为结果。
在C语言中,可以通过包含 <unistd.h>和<sys/types.h> 这两个头文件来使用 getppid() 系统调用。下面是一个示例代码:
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main()
{
pid_t ppid = getppid();
printf("当前进程的父进程ID是:%d\n", ppid);
return 0;
}
在上述示例中,首先包含了 <unistd.h>和<sys/types.h> 头文件,然后调用 getppid() 系统调用获取当前进程的父进程的PID,并用 printf() 函数输出该PID。
编译并运行这个程序,你将会看到输出当前进程的父进程的PID。
3.6通过系统调用,创建子进程-fork()函数初识
在Linux中,fork() 是一个重要的系统调用,也是以通过包含 <unistd.h>和<sys/types.h>两个头文件来使用。它用于在代码中创建一个新的子进程,新进程是原始进程的副本。fork()系统调用会在当前进程的地址空间中创建一个新的子进程,并且复制父进程的代码资源(包括内存、文件描述符等)给子进程。
fork()调用成功后,在父进程中会返回子进程的进程ID(PID),而在子进程中则返回0,如果fork()调用失败,返回值为负数(-1)。这样父进程和子进程就可以通过返回值来区分彼此并执行不同的代码。子进程会继续执行fork()调用之后的代码,而父进程在fork()调用处继续执行。
注:在fork()之后,父进程和子进程哪一个先运行,是完全随机的取决于操作系统调度器先调度哪一个进程。
通过fork()系统调用的使用,可以创建多进程的程序,实现并发执行,充分利用多核处理器资源。同时,fork()也为进程间通信提供了基础,通过共享内存等方式,父子进程可以进行数据传递和共享。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid;
// 创建子进程
pid = fork();
if (pid < 0) {
// fork()调用失败
fprintf(stderr, "Fork failed.\n");
return 1;
} else if (pid == 0) {
// 在子进程中
printf("Hello, I am the child process! My PID is %d.\n", getpid());
} else {
// 在父进程中
printf("Hello, I am the parent process! My PID is %d. Child's PID is %d.\n", getpid(), pid);
}
return 0;
}
什么叫做在父进程中?在父进程中指的是在调用fork()函数的进程中的代码执行路径。当一个进程调用fork()函数时,会创建一个新的子进程。在父进程中,fork()函数返回子进程的PID(大于0),父进程可以通过判断返回值来确定自己是父进程。父进程可以继续执行fork()调用之后的代码,执行自己特定的逻辑。
换句话说,"在父进程中"意味着当前执行的代码是在fork()函数调用之前的进程中执行的,即原始的父进程。父进程可以通过判断返回值来区分自己和子进程,并执行相应的逻辑。通常情况下,父进程可以用来执行一些特定的任务,例如监控子进程、处理子进程产生的结果等。
结论:如何创建子进程?
1.fork之后,执行流会变成2个执行流。
2.fork之后,先运行由调度器决定。
3.fork之后的代码共享,通常使用if和else if来进行执行流分流。
4.进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
首先要理解两个概念阻塞和挂起,在计算机科学中,"阻塞"(Blocking)和"挂起"(Suspending)是与进程或线程状态相关的两个概念。它们用于描述程序在执行中遇到某些条件而需要等待的状态。
阻塞(Blocking):
阻塞是指一个进程或线程在执行过程中由于某些条件(例如等待用户输入、等待磁盘I/O、等待网络数据等)而被暂时停止执行的状态。在阻塞状态下,进程或线程不会占用CPU时间,它会暂时让出CPU,直到等待的条件满足,之后才会被唤醒继续执行。
阻塞通常发生在需要等待外部事件完成的情况下,例如等待文件读取完成、等待网络响应等。在阻塞状态下,程序会暂时停止执行,不占用系统资源,直到等待的事件发生。
结论:进程因为等待某种条件就绪,而导致的一种不推进的状态--进程卡住了--阻塞一定是在等待某种资源--为什么阻塞?进程要通过等待的方式,等具体的资源被别人是用完成或者等待某种资源完成之后,在被自己使用,阻塞:就是进程等待某种资源就绪的过程。(等待说明有大量的进程,资源:比如显卡,网卡,磁盘等各种设备)
所以阻塞就是不被调度 --- 一定是因为当前进程需要等待某种资源的就绪 --- 一定是进程task_struct结构体需要在某种被OS管理的资源下排队(注:PCB可以被维护在不同的队列中,比如说,进程的task_struct结构体在网卡的队列里排队)
挂起(Suspending):
挂起是指将一个进程或线程从活跃状态转移到非活跃状态的过程。一个被挂起的进程或线程被暂时停止,并且不参与到系统的调度和执行中。挂起通常是由操作系统或者其他上层管理程序调用的,用于控制系统中的活跃进程数量。
被挂起的进程或线程不会被调度执行,它会暂时停止,并且不会占用系统资源。挂起状态通常发生在系统资源紧张或者需要暂时关闭某个任务时。
需要注意的是,挂起和阻塞是不同的概念。挂起是一个系统级别的操作,而阻塞是一个进程或线程的行为状态。在某些情况下,挂起可以导致一个进程或线程被阻塞,但它们是两个不同层次的概念。
在实际应用中,阻塞和挂起可以有不同的应用方式和目的。它们可以用于等待外部输入、等待资源的可用性、控制进程或线程的执行顺序,或者在系统资源有限时进行进程或线程的调度和管理。
需要注意的是,阻塞和挂起是操作系统中的概念,在不同的操作系统或编程环境中,具体的实现和使用方式可能有所不同。
下面的状态在kernel源代码里定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
//task_struct是一个结构体(PCB),内部包含了各种属性,如果要描述一个进程的状态,
//那么里面就一定包含了以下状态属性。
static const char* const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
R:运行状态(running):并不意味着进程一定在CPU里运行中,实际上CPU里还维护着一个运行队列(也称为就绪队列),这是一组等待运行的进程,它表明进程要么是在CPU里运行中,要么在运行队列中等待CPU的分配。进程在运行队列中排队时,状态仍然是运行状态,只是在等待CPU资源。(进程的具体状态还取决于它在哪个队列中、是否正在执行等因素。)
注:实际上运行队列是由操作系统维护的,而不是由 CPU 维护。操作系统负责管理和调度进程,包括决定将 CPU 时间分配给哪个进程。为了实现这个功能,操作系统维护着一个运行队列(也称为就绪队列)来存储等待执行的进程。
当一个进程处于就绪状态时,它被添加到运行队列中等待操作系统将其调度到 CPU 上执行。操作系统利用调度算法,根据进程的优先级、等待时间、资源需求等因素,从运行队列中选择下一个要执行的进程。
CPU 并不直接维护运行队列。CPU 主要负责执行当前被操作系统调度选择的进程,并按照指令顺序逐步执行指令。一旦进程的时间片用完或者发生 I/O 等待,操作系统会将其调度出 CPU,然后根据需要,将另一个进程从运行队列中选出来并调度到 CPU 上执行。
所以可以说,运行队列是操作系统为了有效地管理和调度进程而维护的。
S:睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
D:磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的
进程通常会等待IO的结束。
T:停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可
以通过发送 SIGCONT 信号让进程继续运行。
指令:kill -19(SIGSTOP) PID,使用这个指令可以暂停进程。
X:死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
5.Z状态(zombie)-僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(没有使用wait()系统调用,后面介绍)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程,僵死进程会以终止状态保持在进程表中,并且僵尸进程已经停止了运行,但仍然在进程表中保留着一些关键信息(如进程号、退出状态等),直到父进程调用wait()或waitpid()来获取这些信息。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
注:僵尸进程无法被杀死,因为进程已经终止了,就算使用kill命令也是一样的。
僵尸进程危害:
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎
么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!内存泄漏?是的!
6.孤儿进程
孤儿进程(Orphan Process)是指其父进程在该进程终止之前就已经终止或者不存在的进程。当父进程先于子进程退出或终止时,子进程就成为孤儿进程。
孤儿进程可能带来以下几个问题:
- 没有正常的终止处理:孤儿进程的父进程已经退出,因此孤儿进程的终止状态无法被父进程接收和处理。这可能使得孤儿进程的资源无法正常释放,例如打开的文件描述符等。
- 进程表资源浪费:孤儿进程会占用系统的进程表资源,即使它们没有其他实质性的活动。如果有大量的孤儿进程积累,将导致进程表资源的浪费,从而限制系统能够创建新的进程。
为了解决孤儿进程的问题,操作系统(OS)通常会将孤儿进程的父进程ID(PPID)设置为init进程(进程ID为1)的进程ID。init进程会周期性地检查是否有孤儿进程,并接管这些孤儿进程的处理。当init进程接管孤儿进程后,它会负责回收孤儿进程的资源并将其终止状态传递给操作系统。
这样,通过init进程的处理,系统可以避免孤儿进程资源的浪费,并确保孤儿进程的正常终止处理。
7.环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
常见环境变量:
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash
查看环境变量方法:
列如:echo $NAME //NAME:你的环境变量名称。(示范:echo $PATH)
那么如何查看系统的全部环境变量呢?
输入env指令就会在屏幕上显示当前系统定义的所有的环境变量
在Linux系统中,有一些常用的环境变量可以用来配置系统行为和程序运行的参数。以下是一些常见的Linux环境变量:
- PATH: 这是一个包含多个目录路径的字符串,用冒号分隔。它定义了系统在执行命令时会搜索可执行文件的路径的顺序。当我们在终端输入一个命令时,系统会按照PATH环境变量的定义,在其中的目录中逐个自动搜索可执行文件。
- HOME: 这个环境变量指定了当前用户的主目录路径。当我们打开终端时,系统会自动定位到这个目录。
- USER: 这个环境变量存储了当前登录用户的用户名。
- SHELL: 这个环境变量指定了当前使用的Shell解释器的路径。
- LANG: 这个环境变量定义了当前系统的语言环境。它影响了系统的语言设置和本地化显示。
- LD_LIBRARY_PATH: 这个环境变量用于指定程序运行时搜索共享库的路径。当程序需要依赖特定的库文件时,系统会在LD_LIBRARY_PATH指定的路径中查找。
- PS1: 这个环境变量定义了终端提示符的样式。可以修改它来自定义终端的外观。
- PWD: 这个环境变量保存了当前工作目录的路径。
除了这些常见的环境变量,还有很多其他的环境变量可以配置系统和应用程序的行为。用户可以通过export命令来设置或修改环境变量的值。
测试PATH环境变量:
问题:
为什么我们写的代码,编译之后生成的可执行程序,运行的时候为什么要需要带上 ' ./ ' ? 而像 ls 这些指令却不用,这些指令本质上不也是一个可执行程序吗?
在环境变量中,=左边的字符串表示变量的名称,而=右边的字符串表示变量的值。
环境变量是操作系统中用于存储与进程相关的配置信息的一种机制。它们是以键值对的形式存在的,其中键是变量的名称,值是变量的内容。在环境变量中,=左边的字符串是键或者变量的名称,=右边的字符串是键对应的值或者变量的内容。
例如,PATH=/usr/local/bin,这个环境变量表示了一个叫做PATH的变量,它的值为"/usr/local/bin"。在这个例子中,"PATH"是变量的名称,"/usr/local/bin"是变量的值。
需要注意的是,=左右两边的字符串之间通常没有空格,并且=左边的字符串是不可修改的,而=右边的字符串可以根据需要进行修改。这意味着通过修改=右边的字符串,可以改变环境变量的值。
不同写法的区别:
比如,export PATH=/your/new/path和export PATH=$PATH:/your/new/path两种写法的区别在于如何处理原有的PATH值。
- export PATH=/your/new/path表示将PATH变量的值直接替换为/your/new/path。这将覆盖原来的PATH值,新的PATH值只包含添加的路径/your/new/path。
- export PATH=$PATH:/your/new/path是一种追加路径的方式。它将新的路径/your/new/path添加到当前的PATH值的末尾,而不影响原来的路径。这样做可以确保新的路径被添加到已有的PATH值的后面。
因此,如果希望完全替换掉原有的PATH值,可以使用export PATH=/your/new/path。如果希望在原有的PATH值后面追加新的路径,应使用export PATH=$PATH:/your/new/path。
测试HOME
用root和普通用户,分别执行 echo $HOME ,对比差异,执行 cd ~; pwd ,对应 ~ 和 HOME 的关系:
通过显示结果可以知道对于不同登录的人,同一个环境变量它里面可能放的就是不同的内容,所以环境变量是针对特定的人在特定的场景当中被使用的变量就叫做环境变量。
和环境变量相关的命令
以下是对几个命令的简要解释:
- echo: 用于显示某个环境变量的值。可以使用echo $变量名的格式来显示该变量的值。
- export: 用于设置一个新的环境变量或修改已有环境变量的值。可以使用export 变量名=值 的格式来设置或修改环境变量。
- env: 用于显示当前系统中所有环境变量的值。运行env命令将显示所有环境变量及其对应的值。
- unset: 用于清除一个或多个环境变量。可以使用unset 变量名的格式来清除指定的环境变量。
- set: 用于显示所有本地定义的shell变量和环境变量。运行set命令将显示当前shell会话中定义的所有变量和环境变量。
这些命令在Linux系统中很常用,可以帮助用户获取、设置和管理环境变量。
通过代码如何获取环境变量
在C语言中,main函数是程序的入口点,它是程序执行的起始位置。main函数可以带有两个参数(实际上可以携带三个),分别是argc和argv,这些参数统称为命令行参数。
- argc参数(参数计数):argc是一个整数类型的参数,用于表示命令行参数的数量。它表示了在运行程序时,通过命令行传递给程序的参数的个数,包括程序名称本身。因此,argc的值至少为1。(比如在命令行输入的:ls -a 指令,就表示两个参数,则argc的值为2)
- argv参数(参数向量):argv是一个字符指针数组(char *argv[]),用于存储命令行参数的值,其中每个元素是一个指向参数字符串的指针。argv[0]是程序名称本身,argv[1]以及之后的元素存储了按顺序传递给程序的其他命令行参数。(比如指令:ls -a,argv[0]的值是ls,argv[1]的值是-a,意思就是以空格为分隔符把字符串拆成一个个的子串,并把每一个子串保存到argv里,以NULL作为结尾)。
int main(int argc, char* argv[])
{
for(int i = 0; i < argc; i++)
{
printf("argv[%d]->%s\n", i, argv[i]);//输出argv里面的内容
}
return 0;
}
- main函数是可以携带三个参数的,这取决于编译器和操作系统的实现。除了常见的两个参数argc和argv之外,一些编译器和操作系统允许在main函数中添加一个可选参数envp。
- envp参数(环境变量):envp是一个字符指针数组(char *envp[]),用于存储程序的环境变量。环境变量是在操作系统中设置的一些全局变量,用于配置和控制程序的运行环境。envp参数通常用于访问环境变量,例如系统路径、用户配置等。每个元素都是一个指向环境变量字符串的指针。(和上面的argv类似,envp数组最后是以NULL结尾。)
全局变量environ是一个指向环境变量表的指针,它是C语言标准库中定义的。通过访问environ变量,可以获取每个环境变量的名称和值,类似于通过envp参数遍历环境变量表。environ指针数组的最后一个元素为NULL,表示环境变量表的结束。
环境变量通常是具有全局属性的
环境变量通常具有全局属性,可以被子进程继承下去
#include <stdio.h>
#include <stdlib.h>
int main()
{
char * env = getenv("MYENV");
if(env)
{
printf("%s\n", env);
}
return 0;
}
1.调用这个代码生成的可执行直接查看,发现没有输出任何结果,说明该环境变量根本不存在,那么在命令行中输入 export MYENV="hello world" 之后,再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的!想想为什么?
环境变量是可以被子进程继承下去的原因是因为在操作系统中,当一个新的进程被创建时,它会继承父进程的环境变量。当你在命令行中使用export命令导出一个环境变量时,操作系统会将该环境变量添加到当前 shell 进程的环境中。然后,当你运行一个新的程序时(比如通过./a.out命令执行可执行文件),操作系统会创建一个新的进程来运行该程序,并且这个新的进程会继承当前 shell 进程的环境变量。
所以,在你的代码中,当你通过命令行输入export MYENV="hello world"之后,运行程序时,新创建的进程会继承当前 shell 进程的环境变量,包括添加的MYENV变量。因此,程序中就能够获取到该环境变量的值,并打印出来。
总结一下,环境变量是通过操作系统来传递给子进程的,父进程定义和添加的环境变量会被子进程继承。这样子进程就可以直接访问和使用这些环境变量的值了。
2.那么如果只进行 MYENV="helloworld" ,不调用export添加,在用我们该代码生成的可执行程序查看,会有什么结果?为什么?
如果只进行MYENV="helloworld"而不调用export添加环境变量,在用该代码生成的可执行程序中查看,结果会为空。这是因为当你在命令行中执行MYENV="helloworld"时,这是在当前 shell 进程中定义了一个普通的变量,而不是添加一个环境变量(这种我们叫做shell的本地变量,只在shell的内部有效)。本地的普通变量只在当前 shell 进程的上下文中有效,子进程无法继承这个普通变量。因此,当你调用生成的可执行程序时,它的子进程无法获取到该普通变量的值,因而程序中无法打印出来。
只有通过export命令把你的本地变量添加到环境变量表里面才能被子进程继承,才能在程序中通过getenv函数获取到其值。所以,在这种情况下,程序中无法获取到这个普通变量的值,结果为空。
原因是因为环境变量具有全局属性。环境变量是在操作系统级别定义的,可以被整个系统中的进程访问和使用。当一个进程创建时,它会从父进程继承一个拷贝的环境变量表,并可以对其进行修改和添加。这样,任何在该进程中运行的程序都可以访问和使用这些环境变量。
环境变量是全局的,意味着它们在整个操作系统中都是可见的。当我们使用export命令导出一个环境变量时,它会将该变量添加到当前 shell 进程的环境变量表中,并且这个变量会在当前 shell 进程以及它的子进程中可见和可用。子进程可以继承父进程的环境变量,并且可以读取和修改这些变量的值。
因此,环境变量的全局性使得它们可以在不同的进程间共享和传递信息,包括在不同的程序和脚本中使用这些变量。这也是为什么我们可以通过设置环境变量来配置和影响整个系统和其运行的程序的行为。
什么是命令行参数以及main函数前面两个形参的作用
命令行参数是指在运行程序时,通过命令行传递给程序的参数。它们用于向程序提供额外的信息或指令,可以影响程序的执行行为或操作。
在C语言中,main函数是程序的入口函数,并且可以接受两个形参。它的原型一般为:
int main(int argc, char *argv[])
这两个形参的作用如下:
- argc(argument count):表示命令行参数的数量,包括程序本身。即argc表示了有多少个命令行参数被传递给了程序。
- argv(argument vector):是一个指向指针的指针,其中每个指针指向一个字符串,代表一个命令行参数。argv是一个字符串数组,每个元素存储一个命令行参数的字符串。
通过argc和argv,程序可以获取和使用命令行传递的参数。一般来说,argv[0]存储的是程序本身的名称(例如可执行文件名),而argv[1]、argv[2]等依次存储着后续的命令行参数。
例如,如果在命令行中运行程序 ./myprogram arg1 arg2,那么argc的值为3(命令行参数的个数),argv[0]存储的是"myprogram",argv[1]存储的是"arg1",argv[2]存储的是"arg2"。(和我们在执行指令 ls -a 是一样的,那么argc的值为2,argv[0]存储的是ls,那么argv[1]存储的是"-a"。)
通过这两个参数,程序可以根据命令行参数的不同,实现不同的功能逻辑、配置选项或参数解析等操作。
8.进程优先级
优先级和权限:
- 优先级(Priority):优先级是指在进程调度过程中,进程获取处理器资源的相对优先程度。操作系统根据进程的优先级来确定下一个要执行的进程,较高优先级的进程会在较低优先级的进程之前得到执行。优先级通常是一个数值,具体的范围和含义取决于操作系统。较高优先级的进程在资源分配和调度中会获得更多的资源和更多的执行时间片,从而具有更高的执行优先级。
- 权限(Permission):权限是指进程或用户对系统资源进行访问和操作的权限级别。权限决定了进程或用户可以执行的操作范围和访问的资源。权限可以分为不同的级别,如读、写、执行等。操作系统通过访问控制机制来管理和限制进程或用户对系统资源的访问权限,以提高系统的安全性和保护资源的完整性。
尽管优先级和权限都涉及进程或用户对资源的访问和操作,但它们是不同的概念。优先级是在进程调度中用于决定进程获取处理器资源的顺序,而权限是用于控制进程或用户对系统资源的访问和操作的级别。两者在操作系统中扮演不同的角色,并且由不同的机制进行管理和控制。
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI and NI:
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值,PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new:新)=PRI(old:旧,指的是初始默认的值80)+nice,这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行,所以,调整进程优先级,在Linux下,就是调整进程nice值nice其取值范围是-20至19,一共40个级别
PRI vs NI:
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进
程的优先级变化。
可以理解nice值是进程优先级的修正修正数据
查看进程优先级的命令
用top命令更改已存在进程的nice:
1.top
2.进入top后按“r”–>输入进程PID–>输入nice值
其他概念:
1.竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
2.独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
3.并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
4.并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
9.程序地址空间
我们在学习C语言的时候,应该画过或者见过这样的空间布局图:
通过代码来感受一下
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量,但地址值是一样的,说明,该地址绝对不是物理地址!在Linux地址下,这种地址叫做 虚拟地址,我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理,OS必须负责将 虚拟地址 转化成 物理地址 。
进程地址空间
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:
分页(页表)&虚拟地址空间(本质就是一个内核数据结构)
//虚拟地址空间本质就是一个内核数据结构
struct mm_struct//数据结构里一定充满了大量起始地址和结束地址,用来划分每一个区域
{
long code_start;//代码区
long code_end;
long init_start;//已初始化数据区
long init_end;
........
long brk_start;//堆区
long brk_end;
long stack_start;//栈区
long stack_end;
}
虚拟地址空间的大小取决于操作系统的架构和配置。不同的操作系统和硬件平台可能有不同的限制和默认设置。
在32位操作系统中,虚拟地址空间的大小通常是4 GB(2^32字节)。这是因为32位系统使用32位的寻址方式,每个地址由32位二进制表示,因此最多可以表示2^32个地址。然而,实际可用的虚拟地址空间可能会更小,因为一部分地址空间留给操作系统或保留给特定的内存映射(如显存、设备内存等)。
在64位操作系统中,虚拟地址空间的大小可以是更大的值,通常是2^48字节或2^64字节。这是因为64位系统使用64位的寻址方式,每个地址由64位二进制表示,因此可以表示更大的地址范围。
需要注意的是,虚拟地址空间的大小并不直接决定了进程可以使用的实际内存大小。操作系统通过虚拟内存管理机制来将虚拟地址映射到物理内存,根据需要分配和释放内存。因此,一个进程可以拥有一个非常大的虚拟地址空间,但实际使用的物理内存可以远远小于虚拟地址空间的大小。
另外,还有一些特殊的情况和配置,例如使用大页(Huge Pages)技术来增加虚拟地址空间的大小,或者使用特定的内存管理技术来扩展虚拟地址空间的范围。这些情况下,虚拟地址空间的大小可以进一步增加。
地址空间:
地址空间指的是在计算机系统中能够寻址的内存范围。它可以是物理地址空间或虚拟地址空间的概念。
物理地址空间是指计算机实际上存在的内存地址范围,也就是硬件直接访问的内存空间。物理地址空间的大小取决于计算机体系结构和硬件实现,通常是固定且有限的。
虚拟地址空间是在计算机系统中为每个进程分配的抽象内存地址范围。每个进程都有自己独立的虚拟地址空间,进程可以认为它独占地使用整个内存空间。虚拟地址空间的大小可以根据操作系统的设计和配置进行调整,通常可以远远超过物理内存的大小。
通过地址映射机制,操作系统可以将虚拟地址空间映射到物理地址空间,使得进程可以使用虚拟地址进行内存访问,而不必直接考虑物理内存的真实情况。地址空间的划分和映射使得每个进程可以独立地访问自己的内存空间,提供了内存隔离和安全保护,同时也为操作系统提供了更大的灵活性和可管理性。
页表:
页表是操作系统中用于将虚拟地址转换为物理地址的数据结构,它的作用主要有以下几个方面:
- 地址转换:页表通过映射虚拟页面和物理页面之间的对应关系,将虚拟地址转换为物理地址。当程序访问一个虚拟地址时,操作系统通过页表查找对应的物理页面,然后将物理页面的地址与偏移量组合成物理地址,从而实现内存的访问。
- 内存隔离和保护:通过页表,操作系统可以为每个进程分配独立的虚拟地址空间,并将其映射到不同的物理页面上。这样可以实现进程之间的内存隔离,使得每个进程只能访问自己所拥有的内存,提高系统的安全性和稳定性。
- 虚拟内存:页表是实现虚拟内存的关键之一。虚拟内存允许进程可以使用比物理内存更大的地址空间,通过在页表中记录虚拟页面与物理页面的映射关系,并通过页面置换算法和页面调度策略实现在物理内存和磁盘之间的数据交换。这样可以提高内存利用率,并允许运行更大的程序和处理更大的数据。
- 内存管理:页表是操作系统管理内存的重要数据结构之一。通过页表,操作系统可以跟踪和管理虚拟页面和物理页面之间的映射关系,更好地管理内存的分配和回收。它可以实现页面的分配、回收和页面置换等策略,以优化内存的使用和性能。
- 内存保护和权限管理:页表可以设置访问权限位,控制对内存的读写权限。通过页表,操作系统可以实现内存保护,防止进程越界访问内存或者修改其他进程的内存数据。
总之,页表是一种关键的数据结构,用于将虚拟地址转换为物理地址,实现虚拟地址空间和物理内存之间的映射关系。它提供了虚拟地址空间的管理、内存隔离、内存优化以及内存保护等重要功能。
如果没有虚拟空间地址和页表,OS是如何工作的?会发生什么情况?
如果没有虚拟地址空间和页表,操作系统的工作将受到极大的限制,并且可能会面临严重的问题。以下是可能发生的情况:
- 内存冲突和覆盖:没有虚拟地址空间和页表的情况下,所有进程将共享同一块物理内存空间。这会导致内存冲突和覆盖问题,不同进程之间的内存访问会相互干扰,可能导致数据损坏或程序崩溃。
- 内存保护性差:没有虚拟地址空间和页表,操作系统无法实现内存隔离和保护,进程无法在内存中独立运行。这意味着一个进程可能会意外地访问、修改或破坏其他进程的内存数据,从而引发安全性问题和系统崩溃。
- 内存管理困难:没有虚拟地址空间和页表,操作系统无法进行内存管理,无法进行内存的动态分配、回收和页面置换。这将导致内存利用率低下,无法满足多进程或多任务的需求,可能导致内存耗尽或无法加载更多进程。
- 地址空间限制:没有虚拟地址空间,每个进程只能使用物理内存的固定大小。这将限制进程的可用内存空间,无法运行需要大量内存的程序或处理大型数据。
- 进程间通信困难:没有虚拟地址空间和页表,进程间的通信变得更加复杂。无法通过内存共享的方式进行高效的进程间通信,进程需要使用其他机制,如管道、消息传递等,来实现相互之间的交互。
总结起来,没有虚拟地址空间和页表将导致内存冲突和覆盖、内存保护性差、内存管理困难、地址空间限制和进程间通信困难等严重问题。虚拟地址空间和页表的引入解决了这些问题,提供了更好的内存管理、安全性和灵活性,并提高了系统的性能和可靠性。
重新理解地址空间
我们的程序再被编译的时候,没有被加载到内存中时,请问我们的程序内部有没有地址呢?答案是有的,源代码被编译的时候,就是按照虚拟地址空间的方式进行对代码和进行编译,早就已经编号了对应的编制!不要认为虚拟地址这样的策略只会影响OS还要让我们的编译器遵守这样的规则。
CPU读到数据的地址是物理地址还是虚拟地址?
CPU读取的地址是虚拟地址。
当程序在运行时,CPU通过虚拟地址来访问内存。虚拟地址是进程所见的地址空间,由程序使用的地址。这个地址空间被视作连续的一维地址空间,从0开始计数。
虚拟地址由操作系统和硬件联合管理。当程序试图访问虚拟地址时,CPU使用地址转换机制将虚拟地址转换为物理地址。这个地址转换过程由硬件中的内存管理单元(MMU)完成,其中包括页表等数据结构。
物理地址是实际的硬件内存地址,它是在内存芯片上的物理位置。虚拟地址通过地址转换机制映射到物理地址,以实现内存访问。
通过地址转换机制,操作系统可以为每个进程提供独立的虚拟地址空间,使不同进程的地址不会相互干扰。这样,每个进程都可以在自己的虚拟地址空间中进行内存访问,而不需要考虑实际的物理内存布局。这为进程的隔离和保护提供了一定的安全性和灵活性。
总结起来,CPU通过虚拟地址访问内存,而虚拟地址经过地址转换机制映射到对应的物理地址。
malloc本质开辟的是物理地址还是虚拟地址?
malloc函数实际上是用来在虚拟地址空间中动态分配内存的。它返回的是虚拟地址的起始位置,而不是物理地址。
当调用malloc函数时,操作系统会根据请求的内存大小,在虚拟地址空间中分配一块连续的内存空间。这块内存空间是虚拟地址空间的一部分,属于进程的地址空间。malloc函数返回的是这块内存空间的虚拟地址,程序可以通过该虚拟地址进行内存访问。
实际的物理内存分配和映射工作是在操作系统的内存管理模块中完成的。操作系统将虚拟地址映射到物理地址,并将分配到的物理内存与虚拟地址关联起来。但对于调用malloc函数的程序来说,它只需要使用返回的虚拟地址进行内存访问,而不需要关心具体的物理地址。
需要注意的是,操作系统可能使用了虚拟内存技术,将部分虚拟地址空间映射到物理内存,而将其他部分映射到磁盘上的交换空间。这样,即使物理内存有限,程序仍然可以访问一个比实际物理内存更大的虚拟地址空间。这种虚拟内存管理机制可以提供更大的地址空间和更高的内存利用率。