Linux操作系统基础知识

〇、操作系统基础

1、什么是操作系统

操作系统(Operating System, OS),介于硬件资源和应⽤程序之间的⼀个系统软件。


2、操作系统的功能

操作系统位于硬件资源之上,管理硬件资源;应⽤程序之下,为应⽤程序提供服务,同时管理应⽤程序

1.进程管理:
①进程控制:创建和撤销进程,分配资源、资源回收,控制进程运行过程中的状态转换。
②进程同步:多进程运行进行协调–进程互斥(临界资源上锁)、进程同步。
③进程通信:实现相互合作之间的进程的信息交换。
④调度:作业调度,进程调度。

2.内存管理:
为多道程序的运行提供良好的环境,提高存储器的利用率,方便用户使用,并能从逻辑上扩充内存。
①内存分配:静态分配、动态分配。
②内存保护:各在其内存空间内运行(设两界限寄存器),互不干扰。
③地址映射:地址空间中的逻辑地址转换为内存空间中与之对应的物理地址。
④内存扩充:借助于虚拟存储技术,逻辑上扩充内存容量。

3.设备IO管理:
完成用户进程提出的 I/O 请求,为其分配所需I/O设备,完成指定I/O操作;提高CPU和I/O设备的利用率,提高I/O速度,方便用户使用I/O设备。
①缓冲管理
②设备分配
③设备处理:设备驱动程序,用于实现CPU和设备控制器之间的通信。

4.文件管理:
对用户文件和系统文件进行管理以方便用户使用,并保证文件的安全性。
①文件存储空间的管理:为文件分配合理外存空间,文件存储空间的使用情况。
②目录管理:为每个文件建立一个目录项。
③文件的读/写管理和保护


1. 资源分配,资源回收

计算机必要重要的硬件资源⽆⾮就是 CPU、内存、硬盘、I/O设备
⽽这些资源总是有限的,因此需要有效管理,资源管理最终只有两个问题:资源分配、资源回收。

  • 资源分配: 体现在CPU上,⽐如进程调度,多个进程同时请求CPU下,应该给哪⼀个进程呢?再⽐如内存分配,内存不够了怎么办?A进程⾮法访问了B进程的内存地址怎么办?内存内、外碎⽚问题等。
  • 资源回收: 考虑内存回收后的合并等等。

2. 为应用程序提供服务

操作系统将硬件资源的操作封装起来,提供相对统⼀的接⼝(系统调⽤)供开发者调⽤

如果没有操作系统,应⽤程序将直接面对硬件,除去给开发者带来的编程困难不说,直接访问硬件,使⽤不当极有可能直接损坏硬件资源。


3. 管理应用程序

即控制进程的⽣命周期:进程开始时的环境配置和资源分配,进程结束后的资源回收、进程调度等。


4. 操作系统内核的功能

(1)进程调度能力: 管理进程、线程,决定哪个进程、线程使⽤CPU。
(2)内存管理能力: 决定内存的分配和回收。
(3)硬件通信能力: 管理硬件,为进程和硬件之间提供通信。
(4)系统调用能力: 应⽤程序进行更⾼限权运行的服务,需要系统调用,⽤户程序和操作系统之间的接口


3、用户程序与操作系统的关系(相互调用)

  • 操作系统的角度
    计算机启动后启动的第⼀个软件就是操作系统,随后启动的所有进程都运行在操作系统之上,使⽤操作系统提供的服务,同时被操作系统监控,进程结束后也由操作系统回收

  • 进程角度
    调用操作系统提供的服务,实现自己的功能。

4、Linux 常见命令大全

5、Linux 内核态 和 用户态

  • 内核空间和用户空间

操作系统的核心是内核(kernel),它独立于普通的应用程序,负责管理系统的进程、内存、设备驱动程序、文件和网络系统,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。

为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。

在 32 位的操作系统的虚拟地址空间中,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间

因此,最高 1G 的内核空间是被所有进程共享的,只有剩余的 3G 才归每个进程自己使用。

  • 区分内核空间和用户空间的原因

CPU 的指令分为特权指令和非特权指令,有些指令使用不当会非常危险,比如清内存、设置时钟、修改用户访问权限、分配系统资源等等,可能导致系统崩溃。

当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。只有处于内核态的进程才能使用特权指令和非特权指令,即内核态进程可以调用系统的一切资源;而用户态进程只能使用非特权指令,也就是说用户态进程只能执行简单运算,不能直接调用系统资源。

CPU中有一个程序状态字PSW(Program Status Word),标志着线程的运行状态。用户态和内核态对应着不同的值,用户态为3,内核态为0。

对于 Linux 来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码与应用程序代码(操作系统的代码要比应用程序的代码健壮很多)。即便是单个应用程序出现错误也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行。

「所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性及安全性。」

  • 如何从用户空间进入内核空间

其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。

我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。

比如应用程序要读取磁盘上的一个文件,通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。

从用户态切换到内核态的方式有三种:

  1. 系统调用

这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,而其核心是使用了操作系统为用户特别开放的一个**(软)中断**来实现。

进程调用:exit、fork
文件系统访问:chmod、chown
设备调用:read、write
信息读取:读取设备信息
通信:mmap、pipe等

  1. 异常

当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常

  1. 外设中断

当外设完成用户的请求时,会向CPU发送中断信号。



一、进程 和 线程

我们编译的代码可执行文件只是储存在硬盘的静态文件,运行时被加载到内存,CPU执行内存中指令,这个运行的程序被称为进程。

进程是对运行时程序的封装,是操作系统进行资源(CPU、内存等)调度和分配的基本单位。

线程CPU调度和分配的基本单位(程序执行的最小单位)。


1、进程与线程的区别与联系

  • 拥有资源:进程是资源分配的基本单位,而线程是CPU分配和调度的基本单位。
    进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
  • 调度:线程是实现独立调度的基本单位。在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
  • 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
  • 通信线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC (Inter-Process Communication)。

2、 Linux理论上最多可以创建多少个进程?一个进程可以创建多少线程,和什么有关

因为进程的 pid 是用 pid_t 来表示的,pid_t 的最大值是 32768,所以理论上最多有32768个进程。

至于线程,进程最多可以创建的线程数是根据分配给调用栈的大小,以及操作系统位数(32位和64位不同)共同决定的。Linux32位下是300多个。


3、并发与并行

  • 单个核心在很短时间内分别执行(轮流执行多个进程,称为并发(concurrency)。
    对于并发来说,CPU需要从⼀个进程切换到另⼀个进程,这个过程需要保存进程的状态信息。
  • 多个核心同时执行多个进程称为并行(parallel)。

4、进程的执行过程

进程的执行过程需要经过三大步骤:编译,链接和装入。

  • 编译:将源代码编译成若干模块

  • 链接:将编译后的模块和所需的库函数进行链接;
    链接包括三种形式:静态链接装入时动态链接(将编译后的模块在链接时一边链接一边装入),运行时动态链接(在执行时才把需要的模块进行链接)

  • 装入:将模块装入内存运行
    将进程装入内存时,通常使用分页技术,将内存分成固定大小的进程分为固定大小的,加载时将进程的块装入页中,并使用页表记录。减少外部碎片
    通常操作系统还会使用虚拟内存的技术将磁盘作为内存0-的扩充。

  • main 函数的入口地址写入到下一行指令寄存器中


5、进程的状态和转换图

(1)执行:进程分到CPU时间片,正在执行

(2)就绪:进程已经就绪,只要分配到CPU时间片,随时可以执行

(3)阻塞(等待):有IO事件或者等待其他资源(请求I/O,申请缓冲空间等);阻塞态的进程占⽤着物理内存,但无法参与系统进程调度。

(3.1)可中断睡眠状态:也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒;
(3.2)不可中断睡眠状态:也称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。

(4)新建:进程刚被创建时的状态,尚未进入就绪队列。创建步骤包括:申请空白的 PCB,向 PCB 中填写一些控制和管理信息,系统向进程分配运行时所需的资源。

(5)终止:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所被终止时所处的状态。

(6-7)挂起:把阻塞的进程置换到磁盘中,此时进程未占用物理内存,我们称之为挂起;挂起不仅仅可能是物理内存不足,比如sleep系统调用,或用户执行Ctrl+Z也可能导致挂起。

–(6)就绪挂起:进程在外存(硬盘),但只要进入内存,马上运⾏。

–(7)阻塞挂起:进程在外存(硬盘)并等待某个事件的出现(进入就绪挂起态)。

  • 三态模型
    Linux操作系统基础知识_第1张图片

  • 五态模型:新建态、就绪态、运行态、阻塞态、终止态
    Linux操作系统基础知识_第2张图片

  • 七态模型:新建态、就绪挂起态、就绪态、运行态、阻塞态、阻塞挂起态、终止态
    Linux操作系统基础知识_第3张图片

  • 只有就绪态和运⾏态可以互相转换,其他都是单向转换。就绪态的进程通过调度算法从⽽获得CPU 时间,转为运行状态;

  • 进程因为等待资源⽽阻塞,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运⾏态转换为就绪态。


6、进程控制块(PCB)

进程PCB控制块是标志进程存在的数据结构,其中包含系统对进程进行管理所需要的的全部信息。系统通过PCB控制块控制和管理进程

进程ID、进程堆栈空间、进程优先级只是PCB控制块中的一项基本信息。

操作系统对进程的感知,是通过进程控制块PCB数据结构来描述的。内核为每个进程分配一个 PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体。

它是进程存在的唯⼀标识,其包括以下信息:

  1. 进程描述信息: 进程标识符、⽤户标识符等;
  2. 进程控制和管理信息: 进程状态,进程优先级等;
  3. 进程资源分配清单: 虚拟内存地址空间信息,打开⽂件列表,IO设备信息等;
  4. CPU相关信息: 当进程切换时,CPU寄存器的值都被保存在相应PCB中,以便CPU重新执行该进程时能从断点处继续执行;

PCB通过链表形式组织起来,比如有就绪队列、阻塞队列等,方便增删,方便进程管理。


7、父进程、子进程

父进程调用 fork() 以后,克隆出一个子进程,子进程的代码从 fork() 之后开始执行,初始用户区数据和父进程一样,但所使用的内存空间不同;内核区也会拷贝过来,但是 pid 进程号不同。

子进程和父进程拥有相同内容的代码段、数据段和用户堆栈(读时共享,写时拷贝)。

父进程和子进程谁先执行不一定,看CPU。所以我们一般我们会设置父进程等待子进程执行完毕。

父子进程操作同一个文件的情况:

  1. 父进程打开一个文件后 fork(),已打开的文件描述符 fd 被子进程继承,父子进程之间共享 fork 之前打开的文件描述符和文件读写偏移量,引用计数增加,可实现对文件的接续写操作;(在执行逻辑上, fork 之前打开的文件, 要 close 两次!)
  2. 在 fork() 后父进程和子进程分别打开同一个文件,父子进程的这两个文件描述符分别指向的是不同的文件表,意味着它们有各自的文件偏移量,一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以实现的是独立写,写入的数据会出现覆盖的情况;
  3. 父进程和子进程分别打开同一个文件后进行写操作,打开文件时使用 O_APPEND,实现的是接续写

8、孤儿进程、僵尸进程

孤儿进程与僵尸进程[总结]
在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态

  • 孤儿进程父进程退出后它的子进程还在执行,这时候这些子进程就成为孤儿进程。孤儿进程会被 init 进程(进程号为1) 收养并完成状态收集。
  • 僵尸进程是指子进程完成并退出后父进程没有使用wait()或者waitpid()对它们进行状态收集,这些子进程的进程描述符 (PCB)仍然会留在系统 中。这些子进程就成为僵尸进程。

9、进程之间的通信方式

进程通信的方式主要有六种:管道,信号量,信号,共享内存,消息队列,套接字

  • 管道

管道是半双工的,双方需要通信的时候,需要建立两个管道。

管道的实质是一个内核缓冲区,进程以先进先出 FIFO的方式从缓冲区存取数据(一端发一端读):管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后内存中数据将会清空,不能使用 lseek() 改变读写位置。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。管道是最容易实现的。

匿名管道 pipe 和命名管道除了建立、打开、删除的方式不同外,其余都是一样的。
匿名管道没有文件实体,有名管道有文件实体,但不存储数据。
匿名管道只允许有亲缘关系的进程之间通信,也就是父子进程之间的通信,命名管道允许具有非亲缘关系的进程间通信。

当以阻塞的方式写操作时,当没有任何进程访问读端时,写操作会收到 SIGPIPE 的信号,write函数会返回 -1

当以阻塞的方式读管道,如果没有任何进程访问写端,那么读操作会立即返回,并按如下操作:
(1)如果管道现有数据无数据,立即返回 0;
(2)如果管道现有数据大于读出数据,立即读取期望大小的数据;
(3)如果管道现有数据小于读出数据,立即读取现有所有数据。

  • 信号量

信号量是一个计数器,可以用来控制多个进程对共享资源的访问。信号量只有等待和发送两种操作。
等待(P(sv))就是将其值减一或者挂起进程,发送(V(sv))就是将其值加一或者将进程恢复运行。

  • 信号

信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。 信号是开销最小的。

  • 共享内存

共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,就像由malloc()分配的内存一样使用。一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。共享内存的效率最高,缺点是没有提供同步机制,需要使用锁等其他机制进行同步。

  • 消息队列

消息队列就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。

  • socket 套接字

套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信


10、进程调度方法

操作系统的常见进程调度算法

1、批处理系统中的调度

  • 先来先服务(FCFS):按照作业到达任务队列的顺序调度。FCFS是非抢占式的,易于实现,效率不高,性能不好,比较有利于长进程,而不利于短进程,有利于CPU 繁忙的进程,而不利于I/O 繁忙的进程。

  • 最短作业优先(SJF):每次从队列里选择预计时间最短的作业运行。SJF是非抢占式的,优先照顾短作业,具有很好的性能,降低平均等待时间,提高吞吐量。但是不利于长作业,长作业可能一直处于等待状态,出现饥饿现象;完全未考虑作业的优先紧迫程度,不能用于实时系统。

  • 最短剩余时间优先(SRTF):该算法首先按照作业的服务时间挑选最短的作业运行,在该作业运行期间,一旦有新作业到达系统,并且该新作业的服务时间比当前运行作业的剩余服务时间短,则发生抢占;否则,当前作业继续运行。该算法确保一旦新的短作业或短进程进入系统,能够很快得到处理。

2、交互式系统中的调度

  • 时间片轮转: 用于分时系统的进程调度。基本思想:系统将CPU处理时间划分为若干个时间片(q),进程按照到达先后顺序排列。每次调度选择队首的进程,执行完1个时间片q后,计时器发出时钟中断请求,该进程移至队尾。以后每次调度都是如此。该算法能在给定的时间内响应所有用户的而请求,达到分时系统的目的。

  • 优先级调度:为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

  • 多级反馈队列:时间片轮转算法对于需要运行较长时间的进程很不友好,假设有一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。因此发展出了多级反馈队列的调度方式。
    多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个就绪队列,每个队列时间片大小都不同,例如 :1, 2, 4, 8, … 这样呈指数增长。如果进程在第一个队列没执行完,就会被移到下一个队列。
    在这种情况下,一个需要 100 个时间片才能执行完的进程只需要交换 7 次就能执行完 (1 + 2 + 4 + 8 + 16 + 32 + 64 = 127 > 100)。


11、互斥和同步

互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的

同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问

在大多数情况下,同步已经实现了互斥,特别是所有写入共享资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时读访问资源。


12、多进程和多线程的选择

对比维度 多进程 多线程 占优选项
共享数据、同步问题 共享数据复杂,需要IPC;数据独立,同步简单 共享进程数据,简单;导致同步问题 各有优点
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程
创建、销毁、切换 复杂,速度慢 简单,速度快 线程
编程、调试 简单 复杂 进程
可靠性 进程间不会相互影响 一个线程挂掉将导致整个进程挂掉 进程
分布式 适用于多核、多机分布式,多机扩展更为简单 适用于多核分布式 进程
  • 频繁修改:需要频繁创建和销毁的优先使用多线程;
  • 计算量:需要大量计算的优先使用多线程,因为需要消耗大量CPU资源且切换频繁,所以多线程好一点;
  • 相关性:任务间相关性比较强的用多线程,相关性比较弱的用多进程。因为线程之间的数据共享和同步比较简单。
  • 多分布:可能要扩展到多机分布的用多进程,多核分布的用多线程。

13、线程间的同步方式

  • 互斥锁 / 量(mutex):加锁机制,确保任意时刻仅有一个线程可以访问某项共享资源(临界区);当获取锁操作失败时,线程会进入睡眠(阻塞),等待锁释放时被唤醒。
  • 自旋锁(spinlock):与互斥锁类似,但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。
  • 读写锁(rwlock):与互斥锁类似,但允许多个读出,只允许一个写入的需求(读时不阻塞读,但阻塞写;写时阻塞读写);
  • 条件变量(cond):通过条件变量通知操作的方式来保持多线程同步。
  • 信号量(semaphore)计数器,允许多个线程同时访问同一个资源;不一定实现了互斥,肯定实现了同步。

14、进程死锁、饥饿和饿死

死锁(deadlock) 是指在多道程序系统中,一组进程中的每一个进程都无限期等待被该组进程中的另一个进程所占有且永远不会释放的资源

死锁的发生必须同时满足四个条件:互斥,持有并等待,非抢占, 形成等待环

饥饿(starvation) 是指系统不能保证某个进程的等待时间上界,从而使该进程长时间等待,当等待时间给进程推进和响应带来明显影响时,称发生了进程饥饿。

饿死(starve to death) 即是当饥饿到一定程度的进程所赋予的任务即使完成也不再具有实际意义

  • 相同点:死锁和饥饿都是由于竞争资源而引起的;
  • 不同点:
    (1)从进程状态考虑,死锁进程都处于等待状态,饥饿的进程并非处于等待状态(处于运行或就绪状态,忙等待),但却可能被饿死;
    (2)死锁进程等待永远不会被释放的资源,饥饿进程等待会被释放但却不会分配给自己的资源(CPU),表现为等待时限没有上界(排队等待或忙式等待);
    (3)死锁一定发生了循环等待,而饿死则不然。这也表明通过资源分配图可以检测死锁存在与否,但却不能检测是否有进程饿死;
    (4)死锁一定涉及多个进程,而饥饿或被饿死的进程可能只有一个。
    (5)在饥饿的情形下,系统中有至少一个进程能正常运行,只是饥饿进程得不到执行机会。而死锁则可能会最终使整个系统陷入死锁并崩溃。

15、死锁的恢复

  • 重新启动:是最简单、最常用的死锁消除方法,但代价很大,因为在此之前所有进程已经完成的计算工作都将付之东流,不仅包括死锁的全部进程,也包括未参与死锁的全部进程。
  • 终止进程(process termination):终止参与死锁的进程并回收它们所占资源。
    (1) 一次性全部终止
    (2) 逐步终止(优先级,代价函数)
  • 剥夺资源(resource preemption):剥夺死锁进程所占有的全部或者部分资源。
    (1) 一次剥夺:一次性地剥夺死锁进程所占有的全部资源。
    (2) 逐步剥夺:一次剥夺死锁进程所占有的一个或一组资源,如果死锁尚未解除再继续剥夺,直至死锁解除为止。
  • 进程回退(rollback):让参与死锁的进程回退到以前没有发生死锁的某个点处,并由此点开始继续执行,希望进程交叉执行时不再发生死锁。但是系统开销很大:
    (1) 要实现“回退”,必须“记住”以前某一点处的现场,而现场随着进程推进而动态变化,需要花费大量时间和空间。
    (2) 一个回退的进程应当“挽回”它在回退点之间所造成的影响,如修改某一文件,给其它进程发送消息等,这些在实现时是难以做到的。

16、死锁的检测和预防

死锁的检测

  • 资源获取环可以使用有向图来存储,线程 A 获取线程 B 已占用的锁,则为线程 A 指向线程 B。
    检测的原理采用另一个线程定时对图进程检测是否有环的存在。

死锁的预防即打破死锁的条件之一:

  • 打破互斥条件: 允许进程同时访问某些资源。 但是, 有些资源是不能被多个进程所共享的, 这是由资源本身属性所决定的, 因此, 这种办法通常并无实用价值。
  • 打破占有并等待条件: 可以实行资源预先分配策略(进程在运行前一次性向系统申请它所需要的全部资源, 若所需全部资源得不到满足, 则不分配任何资源, 此进程暂不运行; 只有当系统满足当前进程所需的全部资源时, 才一次性将所申请资源全部分配给该进程)或者只允许进程在没有占用资源时才可以申请资源(一个进程可申请一些资源并使用它们, 但是在当前进程申请更多资源之前, 它必须全部释放当前所占有资源)。但是这种策略也存在一些缺点:在很多情况下, 无法预知一个进程执行前所需的全部资源, 因为进程是动态执行的, 不可预知的; 同时, 会降低资源利用率, 导致降低了进程的并发性。
  • 打破非抢占条件:允许进程强行从占有者那里夺取某些资源。 也就是说当一个进程占了一部分资源, 在其中请求新的资源且得不到满足, 它必须释放所有占有的资源以使其它线程使用, 这种预防死锁的方式实现起来困难, 会降低系统性能。
  • 打破循环等待条件:实行资源有序分配策略。 对所有资源排序编号, 所有进程对资源的请求必须严格按资源序号递增的顺序提出, 即只有占用了小号资源才能申请大号资源, 这样就不会产生环路, 预防死锁的发生。

https://blog.csdn.net/cui240558023/article/details/103948907/

17、进程内存结构

一个程序被加载到内存中,这块内存首先就存在两种属性:
静态分配内存:是在程序编译和链接时(运行前)就确定好的内存。
动态分配内存:是在程序加载、调入、执行的时候分配/回收的内存。

Linux操作系统基础知识_第4张图片

  • .text 代码段:
    用来存放程序执行的二进制代码,也有可能包含一些只读的常数变量(字符串常量等)。
    该段内存为静态分配,只读(某些架构可能允许修改)。
    这块内存是共享的,当有多个相同进程(Process)存在时,共用同一个text段。

  • .data 数据段:
    也叫GVAR(global value),用来存放程序中已初始化的全局/静态变量静态分配

  • .bss 数据段:
    存放程序中未初始化的全局/静态变量静态分配,在程序开始时通常会被清零。

代码段和初始化数据段存放在程序可执行文件中,在编译时已经分配了空间;
.bss 段并不占用可执行文件的大小,它是由链接器来获取内存的。

  • heap 堆区:
    用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
    堆是从低地址位向高地址位增长,采用链式存储结构。
    进程调用malloc / free等函数进行动态分配和手动释放;频繁操作会造成内存空间的不连续,产生碎片
    堆空间的大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大

  • stack 栈区:
    存放程序临时创建的局部变量(static声明的变量存放在数据段);除此以外,在函数被调用时的参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中(动态分配)。
    堆是从高地址位向低地址位增长
    内存由编译器自动分配和释放,是一块连续的内存区域。
    栈空间的最大大小在编译时确定,速度快,但自由性差,最大空间不大(2M)。

每个线程都会从内存区域中划分出自己的栈和代码段,但是其他空间是共用的。

int a = 0; 	// 全局初始化区 
char *p1; 	// 全局未初始化区 
int main(){ 
    int b; //栈 
    char s[] = "abc"; 	//栈 
    char *p2; 			//栈 
    char *p3 = "123456"; 	//123456\0在常量区,p3在栈上。 
    static int c =0//全局(静态)初始化区 
    p1 = (char *)malloc(10); 	//堆 
    p2 = (char *)malloc(20);  	//堆 
}

Linux操作系统基础知识_第5张图片

18、线程有哪几种状态

  • 就绪(Runnable): 线程准备运行, 不一定立马就能开始执行
  • 运行中(Running):进程正在执行线程的代码
  • 等待中(Waiting):线程处于阻塞的状态, 等待外部的处理结束
  • 睡眠中(Sleeping):线程被强制睡眠
  • I/O阻塞(Blocked on I/O):等待I/O操作完成
  • 同步阻塞(Blocked on Synchronization):等待获取锁
  • 死亡(Dead):线程完成了执行

19、内存映射的内存空间在进程结束后会自动释放吗?

映射的内存在用户区堆和栈之间的共享库中,进程结束后会自动释放。

20、创建共享内存后,进程结束,共享内存是否会消失?

不会。进程间通信使用的管道、socket、共享内存、消息队列、信号量等,是属于内核级的,一旦创建后就由内核管理,若进程不对其主动释放,那么这些变量会一直存在,除非重启系统。

21、进程结束后打开的文件描述符会自动关闭吗?

会,每个进程的PCB也都不一样,维护的文件描述符表也不一样。

22、wait() 函数

wait 函数只有两种返回值,即成功则返回终止的子进程对应的进程号;失败则返回 -1

如果其所有子进程都还在运行,则wait()会一直阻塞等待,直到某一个子进程终止,然后返回该子进程的进程号;

如果该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那么 wait() 将返回错误,也就是返回 -1,并且会将 errno 设置为ECHILD

23、atexit() 函数

atexit 函数是 linux标准 I/O 自带的库函数,使用该函数需要包含头文件
用于注册一个进程在正常终止时要调用的函数,我们可以通过该函数打印或者执行一些其他功能代码。

该函数原型是:int atexit(void (*function)(void));
function:函数指针,指向注册的函数,此函数无需传入参数、无返回值。
返回值:成功返回0;失败返回非0。

24、Linux 的特殊进程(PID = 0, 1, 2 …)

PID 为 0 的是调度进程,该进程是内核的一部分,也称为系统进程;

PID 为 1 的是 init 进程,它是由内核启动的第一个用户进程,以超级用户特权运行,管理着系统上所有其它进程,因此理论上说它没有父进程。它是所有子进程的父进程,一切从1开始、一切从init进程开始!

PID 为 2 的是页守护进程,负责支持虚拟存储系统的分页操作。

25、单例模式运行

在linux中,对于有些程序设计来说,程序只能被执行一次,只要该程序没有结束,就无法再次运行,我们把这种情况称为单例模式运行

譬如系统中守护进程,这些守护进程一般都是服务器进程,服务器程序只需要运行一次即可,能够在系统整个的运行过程中提供相应的服务支持,多次同时运行并没有意义、甚至还会带来错误!
多核和单核CPU对进程运行几次没有关系。

26、实时信号和非实时信号、可靠信号和不可靠信号

在Linux系统中,

从可靠性方面将信号分为可靠信号和不可靠信号;
从时间关系上将信号分为实时信号和非实时信号。

前31个信号编号(1~31)为非实时信号,等同于不可靠信号,不支持排队;
其他(32~64)为实时信号,等同于可靠信号,都支持排队。

Linux信号机制基本上是从Unix系统中继承过来的,早期不可靠信号的主要问题是:

进程每次处理信号后,就将对信号的响应重置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用 signal(),重新安装该信号。

因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。

Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。

因此,Linux下的不可靠信号问题主要指的是信号可能丢失,因为这些不可靠信号阻塞的时候是不支持排队的,即未决信号集只有一个 0 或 1 的标记位,不能记录相关信号的触发次数。

信号值位于SIGRTMIN和SIGRTMAX(32~64)之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。
目前Linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。

27、进程的特性和构成

  • 动态性:进程的实质是程序的一次执行过程,进程是动态产生、动态消亡的。
  • 并发性:是指多个进程实体同存于内存中,任何进程都可以同其他进程一起并发执行。
  • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
  • 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。
  • 结构性:进程由程序段、数据段和PCB组成。

程序段、相关数据段和PCB三部分构成进程的实体,一般简称为进程。
所谓创建进程就是创建进程实体中的PCB,而撤销进程也就是撤销进程的PCB。

28、线程池

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些 "池化资源"技术 产生的原因,线程池为线程生命周期开销问题和资源不足问题提供了解决方案。

线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

创建线程池时可以设置线程池的最大线程数和最小线程数
当任务队列当中没有任务时,线程池阻塞在条件变量上,等待任务;
当有任务进来时,条件变量发信号或者广播,唤醒线程,此时对任务队列而言属于共享资源,需要使用互斥量,避免资源冲突。

线程池的伸缩性对性能有较大的影响。
1、创建太多线程,将会浪费一定的资源,有些线程未被充分使用。
2、销毁太多线程,将导致之后浪费时间再次创建它们。
3、创建线程太慢,将会导致长时间的等待,性能变差。
4、销毁线程太慢,导致其它线程资源饥饿。

线程池的主要组成部分
1、线程池管理器(ThreadPoolManager):用于创建并管理线程池;
2、工作线程(WorkThread):线程池中线程;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行;
4、任务队列:用于存放没有处理的任务。提供一种缓冲机制。

线程池的应用场景
1、需要大量的线程来完成任务,且完成任务的时间比较短;
2、对性能要求苛刻的应用;
3、接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

29、产生信号的情况

在linux中,信号是事件发生时对进程的通知机制,也可以把它称为软件中断。一个具有合适权限的进程能够向另一个进程发送信号,产生信号的情况包括:

  • 硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。
    硬件检测到异常的例子包括执行一条异常的机器语言指令,诸如,除数为0、数组访问越界导致引用了无法访问的内存区域等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。
  • 用于终端下输入了能够产生信号的特殊字符
    譬如在终端上按下CTRL + C组合按键可以产生中断信号(SIGINT),通过这个方法可以终止在前台运行的进程;按下CTRL + Z组合按键可以产生暂停信号(SIGCONT),通过这个方法可以暂停当前前台运行的进程。
  • 进程调用 kill() 系统调用可将任意信号发送给另一个进程或进程组。当然对此是有所限制的,接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是root超级用户。
  • 发生了软件事件,即当检测到某种软件条件已经发生。这里指的不是硬件产生的条件(如除数为0、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件。

30、进程的环境变量

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。

环境变量是进程中一组变量信息,环境变量分为系统环境变量、用户环境变量和进程环境变量

系统有全局的环境变量,在进程创建时,进程继承了系统的全局环境变量、当前登录用户的用户环境变量和父进程的环境变量。进程也可以有自己的环境变量。

在linux系统中,每一个进程都有一组与其相关的环境变量,这些环境变量以字符串形式存储在一个字符串数组列表中,把这个数组称为环境列表。

其中每个字符串都是以“名称=值(name=value)”形式定义,所以环境变量是“名称-值”的成对集合,譬如在shell终端下可以使用env命令查看到shell进程的所有环境变量。

31、在linux中,进程间的相互联系与相互作用包括:

(1)相关进程:在逻辑上具有某种联系的进程称为相关进程。例如:属于同一进程家族内的所有进程(父子进程、兄弟进程);
(2)无关进程:在逻辑上没有任何联系的进程称为无关进程。
(3)直接相互作用:进程之间不需要通过某种媒介而发生的相互作用,这种相互作用通常是有意识的
(4)间接相互作用:进程之间需要通过某种媒介而发生的相互作用,这种相互作用通常是无意识的

32、在linux中,调用pthread_create()创建线程时,线程的默认属性值是:

inux线程默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级

33、在linux中,线程的特点包括:

在多线程应用程序中,线程不单独存在、而是包含在进程中,

线程是参与系统调度的基本单位,每个线程都可以参与系统调度、被CPU执行,同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果;

同一进程中的各个线程,可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等。

34、线程同步机制的属性

互斥锁:进程共享属性 和 类型属性;
读写锁:进程共享属性;
条件变量:进程共享属性。

35、linux中创建一个会话

在linux中,可以使用系统调用setsid()可以创建一个会话,这里需要注意的是:如果调用者进程不是进程组的组长进程,调用setsid()将创建一个新的会话,调用者进程是新会话的首领进程,同样也是一个新的进程组的组长进程调用setsid()创建的会话将没有控制终端

系统调用 setpgid() 或 setpgrp() 可以加入一个现有的进程组或创建一个新的进程组。

36、常见的信号处理措施

在linux中,信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,常见的信号处理措施包括:

(1)忽略信号。也就是说,当信号到达进程后,该进程并不会去理会它、直接忽略,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理。

(2)捕获信号。当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,Linux系统提供了 signal()sigaction() 系统调用可用于注册信号的处理函数。

(3)执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。

进程并不会主动删除信号,对于不需要的信号,忽略就可以了。

37、守护进程的实现步骤

  1. 创建子进程、终止父进程;
    (子进程作为守护进程可以确保守护进程开启的新会话 id 与原 id 不冲突。)
  2. 子进程调用 setsid() 创建会话,新会话默认没有控制终端;
  3. 将工作目录更改为根目录;
  4. 重设文件权限掩码 umask;
  5. 关闭不再需要的文件描述符。

38、fork() 和 vfork() 创建进程

  • fork()

子进程拷贝父进程的地址空间,子进程是父进程的一个复制品。

父子进程同时并发运行,但执行次序不确定

在子进程中,成功的 fork( ) 调用会返回 0。在父进程中 fork( ) 返回子进程的 pid。如果出现错误,fork( )返回一个负值。

  • vfork()

新进程的目的通常是 exec 一个新程序,如shell。

在调用 exec 或 exit 之前,子进程共享父进程的地址空间;

vfork 并不将父进程的地址空间完全复制给子进程,因为子进程会立即调用exec 或 exit,也就不会访问该地址空间,只在子进程调用 exec 之前,它在父进程空间中运行,共享父进程数据;这种优化工作方式在 fork() 写时拷贝出现之前可以提高效率。

vfork 保证子进程先运行,在它调用 exec(进程替换) 或 exit(进程退出) 之后父进程才能调度运行。

如果在调用这两个函数之前子进程依赖于父进程的进一步操作,或 子进程没有调用 exec / exit,程序则会导致死锁。

如果子进程修改了父进程的数据(除了vfork返回值的变量)、进行了函数调用、或者没有调用exec或_exit就返回将可能带来未知的结果。

Linux操作系统基础知识_第6张图片

39、system()

system 函数和 exec 函数一样,都是执行进程外的命令,而 system() 则是把 exec 函数封装起来,指定执行任意shell命令

#include
int system(const char * command);
参数 command 就是需要读取的命令,函数的返回值表示执行结果。

基本原理说明:
(1)fork一个子进程;
(2)在子进程中调用 execl 函数来调用 /bin/sh-c command 拉起 sh 执行 command 命令;
(3)使父进程等待子线程执行完毕;
(4)返回出错信息或者子进程执行后的返回值。

system() 会调用 fork() 产生子进程,由子进程来调用 /bin/sh-c command 来执行参数 command 字符串所代表的命令,此命令执行完后随即返回原调用的进程。在父进程中调用 wait 去等待子进程结束。

在调用 system() 期间 SIGCHLD 信号会被暂时阻塞,SIGINT 和 SIGQUIT 信号则会被忽略。

返回值:
若参数 command 为空指针(NULL),则立即返回非零值,一般为 1;
如果 fork() 失败,即无法创建子进程,返回 -1;
如果 exec() 失败,表示不能执行 shell,相当于 shell 执行了exit,返回 127;
如果执行成功则返回执行 command 的 shell 进程的终止状态。(该 shell 进程不一定执行成功,因此返回值情况众多,若无法获取子进程的终止状态,返回 -1)

那么什么时候system()函数返回0呢?只在 command 命令返回 0 时。

system() 与 exec() 的区别:
1、system 是在原进程上开辟了一个新的进程再执行 exec,但是 exec 是用新进程(命令)覆盖了原有的进程;
2、system() 和 exec() 都有能产生返回值,system 的返回值并不影响原有进程,但是 exec 的返回值影响了原进程(exec调用成功时无返回值,原进程被替代)

40、线程的同步和异步



三、内存管理

1、分段和分页

两者都采用离散分配方式,且都地址映射机构来实现地址的转换

分页 分段
目 的 页是信息的物理单位,分页是为实现离散分配方式,以消减内存的外零头,提髙内存的利用率。或者说,分页仅权是由于系统管理的需要而不是用户的需要 是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了能更好地满足用户的需要
长 度 页的大小固定且由系统决定,由系统把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的,因而在系统中只能有一种大小的页面 段的长度不固定,决定于用户所编写的程序, 通常由编译程序在对流程序进行编译时,根据信息的性质来划分
地址空间 作业地址空间是一维的,即单一的线性地址空间,程序员只需利用一个记忆符,即可表示 一个地址 作业地址空间是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址
碎 片 有内部碎片,无外部碎片 有外部碎片,无内部碎片
”共享“和“动态链接” 不容易实现 容易实现

2、内存泄漏 和 内存溢出

内存泄漏(memory leak):是指程序在申请内存后,无法释放已申请的内存空间,导致系统无法及时回收内存并且分配给其他进程使用。一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后就会导致内存不够用,最终导致内存溢出。

内存溢出 (out of memory):如果过度占用资源而不及时释放,在程序申请内存时,没有足够的内存供申请者使用,导致数据无法正常存储到内存中。也就是说给你个 int 类型的存储数据大小的空间,但是却存储一个 long 类型的数据,这样就会导致内存溢出。

内存泄露是由于GC无法及时或者无法识别可以回收的数据进行及时的回收,导致内存的浪费;内存溢出是由于数据所需要的内存无法得到满足,导致数据无法正常存储到内存中。内存泄露的多次表现就是会导致内存溢出。

3、缺页中断

在请求分页系统中,每当所要访问的页面不在内存时,便产生一个缺页中断,请求操作系统将所缺的页调入内存

此时应将缺页的进程阻塞(调页完成唤醒),如果内存中有空闲块,则分配一个块,将要调入的页装入该块,并修改页表中相应页表项;若此时内存中没有空闲块,则要淘汰某页(若被淘汰页在内存期间被修改过,则要将其写回外存)。

中断次数 = 进程的物理块数 + 页面置换次数
缺页中断率 =(缺页中断次数 / 总访问页数 )

缺页中断作为中断同样要经历,诸如保护CPU环境、分析中断原因、转入缺页中断处理程序、恢复CPU环境等几个步骤。但与一般的中断相比,它有以下两个明显的区别:

  • 在指令执行期间产生和处理中断信号,而非一条指令执行完后,属于内部中断。
  • 一条指令在执行期间,可能产生多次缺页中断。

4、页面置换算法

  1. 最佳(Optimal, OPT) 置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。
    【无法实现】

  2. 先进先出(FIFO) 页面置换算法:优先淘汰最早进入内存的页面,亦即在内存中驻留时间最久的页面。该算法只需把调入内存的页面根据先后次序链接成队列,设置一个指针总指向最早的页面。
    【FIFO算法实现简单,但性能差,会出现物理块数增大而页故障数不减反增的Belady异常现象】
    Linux操作系统基础知识_第7张图片

  3. 最近最久未使用(LRU,Least Recently Used) 置换算法选择最近最长时间未访问过的页面予以淘汰,它认为过去一段时间内未访问过的页面,在最近的将来可能也不会被访问。
    该算法为每个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰。
    【LRU性能较好,但需要寄存器和栈的硬件支持,开销更大。】
    Linux操作系统基础知识_第8张图片

  4. 最不经常访问淘汰算法(LFU,Least Frequently Used):根据数据的历史访问频率来淘汰数据。最近使用频率高的数据很大概率将会再次被使用,而最近使用频率低的数据,很大概率不会再使用。
    每个数据块一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序,每次淘汰队尾数据块
    Linux操作系统基础知识_第9张图片

  5. 时钟(CLOCK)置换算法:给每一帧关联一个附加位,称为使用位。当某一页首次装入主存,以及后续被访问时,使用位被置为1。( 最近未用(Not Recently Used, NRU)算法 )

5、在执行malloc申请内存的时候,操作系统是怎么做的?/ 内存分配的原理说一下 / malloc函数底层是怎么实现的?/ 进程是怎么分配内存的?

malloc() 函数其实就在内存中找一片指定大小的空间,然后将这个空间的首地址范围给一个指针变量;这里 malloc 分配的内存空间在逻辑上连续的,而在物理上可以连续也可以不连续。

从操作系统层面上看,malloc 是通过两个系统调用来实现的: brkmmap(不考虑共享内存)。

  • brk 是将进程数据段(.data)的最高地址指针 _edata 向高处移动,这一步可以扩大进程在运行时的堆大小;
  • mmap 是在进程的虚拟地址空间中(堆与栈之间的内存映射区域)寻找一块空闲的虚拟内存,这一步可以获得一块可以操作的堆内存。

这两种方式分配的都是虚拟内存没有分配物理内存
进程先通过这两个系统调用获取或者扩大进程的虚拟内存,获得相应的虚拟地址,在第一次访问这些虚拟地址的时候,通过缺页中断,让内核分配相应的物理内存,建立虚拟内存和物理内存之间的映射关系,这样内存分配才算完成。

通常,分配的内存小于 128k 时,使用 brk 调用来获得虚拟内存,大于 128k 时就使用 mmap 来获得虚拟内存。

Linux操作系统基础知识_第10张图片
Linux操作系统基础知识_第11张图片

brk 分配的虚拟内存和物理内存需要等到高地址内存释放以后才能释放,因此有内存碎片的产生;但是这块被 free 的内存地址可以重用,分配给其他大小合适的变量。
当最高地址空间的连续空闲内存超过 128k(可由M_TRIM_THRESHOLD选项调节)时,通过 brk 分配的内存将被执行内存紧缩操作(trim),即释放掉分配的虚拟内存和物理内存。

mmap 分配的内存可以可以通过 free 直接单独释放

6、内核空间和用户空间是怎样区分的

  • 在Linux中虚拟地址空间范围为 0到4G,最高的1G地址(0xC0000000到0xFFFFFFFF)供内核使用,称为内核空间,低的 3G 空间(0x00000000到0xBFFFFFFF)供各个进程使用,就是用户空间
  • 内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据


持续学习更新中…

你可能感兴趣的:(嵌入式系统,Linux高并发服务器开发,linux,硬件架构,服务器)