操作系统
操作系统是什么
- 操作系统(Operating System,简称 OS)是计算机系统中的核心软件之一,它是一个管理计算机硬件和提供用户与应用程序之间接口的系统软件。
- 进程管理:Linux 也支持多任务和多线程,并使用进程标识符(PID)来管理进程。
- 内存管理:Linux 使用分页机制和交换空间来管理内存,允许多个程序在同一时间运行。
- 文件系统管理:Linux 支持多种文件系统,包括Ext4、XFS、和Btrfs等,以管理文件和存储设备。
- 设备管理:Linux 的设备管理通过文件系统提供,一切皆文件的理念使得硬件管理变得更加统一。
- 用户管理:Linux 使用用户账户和权限管理,允许多用户在系统上进行安全访问和控制。
进程和线程有什么区别?
- 进程(Process)是系统进行资源分配和调度的基本单位,线程(Thread)是CPU调度和分派的基本单位;
- 线程依赖于进程而存在,一个进程至少有一个线程;
- 进程有自己的独立地址空间,线程共享所属进程的地址空间;
- 进程切换的开销远大于线程切换的开销; 在进程切换时,涉及到整个当前进程CPU环境的保存环境的设置以及新被调度运行的CPU环境的设置,而线程切换只需保存和设置少量的寄存器的内容.
- 线程之间的通信更方便,同一进程下的线程共享全局变量等数据,而进程之间的通信需要以进程间通信(IPC)的方式进行;
- 多进程更加健壮:多线程程序只要有一个线程崩溃,整个程序就崩溃了,但多进程程序中一个进程崩溃并不会对其它进程造成影响,因为进程有自己的独立地址空间
- 进程的进程ID,进程的状态,分配的内存,打开的文件描述符,CPU使用时间等都记录在进程控制块
PID中
同一进程中的线程可以共享哪些数据?
- 进程代码段
- 进程的公有数据(全局变量、静态变量…)
- 进程打开的文件描述符
- 进程的当前目录
- 信号处理器/信号处理函数:对收到的信号的处理方式
- 进程ID与进程组ID
线程独占哪些资源?
- 线程ID
- 一组寄存器的值
- 通用寄存器(General-Purpose Registers):这些寄存器用于存储计算过程中的临时数据、中间结果以及指针。它们在执行指令时被频繁使用,可以加速指令的执行。
- 程序计数器(Program Counter):程序计数器是一个特殊的寄存器,它存储了当前线程执行的指令的地址。当线程切换到另一个函数或线程时,程序计数器的值会被保存和恢复,以确保程序从正确的位置继续执行。
- 栈指针寄存器(Stack Pointer Register):栈指针寄存器存储了线程栈的顶部地址,用于管理栈上的数据的入栈和出栈操作。
- 线程自身的栈(堆是共享的)
- 栈帧(Stack Frame):线程栈由多个栈帧组成,每个栈帧表示函数的调用或执行。当线程调用一个函数时,会创建一个新的栈帧,包含了函数的局部变量、参数、返回地址等信息。
- 错误返回码:线程可能会产生不同的错误返回码,一个线程的错误返回码不应该被其它线程修改;
- 信号掩码/信号屏蔽字(Signal mask):表示是否屏蔽/阻塞相应的信号(SIGKILL,SIGSTOP除外)
进程间通信有哪些方式?
- 无名管道(Pipe)
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
- 一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据;
- 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)
- 命名管道
- 可以用于非亲缘关系进程通信
- 消息队列
允许进程之间交换消息,可以不是父子进程,是按照消息报文交换的
可以区分消息的优先级 - 信号(Signal)
- 一个进程通过操作系统向另一个进程发送信号
- 共享内存
- 效率高
- 套接字(Socket)
- 可以TCP或者UDP
更详细的可以参考(待整理):
生产者-消费者问题
- 问题描述:使用n个缓冲区来存放数据,只有缓冲区没有满,生产者才可以写入数据;只有缓冲区不为空,消费者才可以读出数据
- 一共需要三个信号量,一个代表此时的空余空闲位置的数量,初始值是n,另一个代表此时已经存在的产品的数量,初始为0,另外还有一个是mutex,初始是1
- 生产者需要先P空闲位置,然后P操作mutex存放产品,生产完之后先V操作mutex释放锁,然后V已经存在的产品加1
- 消费者则是先P已经存在的产品,然后P操作mutex获取锁,然后取走产品,然后V操作mutex释放锁,然后V操作空闲位置加一
- 对mutex加锁的操作一定要在对空间和产品的操作之后,防止死锁
代码实现:
// 伪代码描述 |
- 注意信号量实际上具有排队的功能,条件满足的时候先唤醒先来的进程
吸烟者问题
- 只有一张桌子,卷烟需要三种原料,每个卷烟者具有一种但是需要另外两种
- 有一个供应者,随机往桌上放某二种原料的组合
- 只有桌子有空的时候才能放置
- 此时需要将三种原料中的任意两种看成一个整体的组合(一共三种),分别对组合和桌子设置条件变量控制
读者-写者问题
同时可以有多个读者
但是只能有一个写者
写的时候不能读
读的时候不能写
需要设置一个读计数变量记录当前有几个读者在读,一个互斥锁用于保护计数变量,一个读写锁,一个写锁
写者写之前分别P操作写锁和读写锁,写之后则分别V操作读写锁和写锁
读者读之前先P操作写锁,然后P操作计数变量的mutex,如果自己当前是第一个读的人(也就是计数变量为0)那么P操作读写锁,然后无论是不是都将读者计数+1,释放保护计数变量的mutex,释放写锁,然后读文件,读完之后同样的获取保护计数变量的mutex,将其减一,如果此时自己是最后一个读者(计数变量等于0),则释放读写锁,然后释放mutex
设置读者计数器的操作是防止读者之间互斥,实现多个同时读操作
写锁单独存在的意义是保证每个读者对于读者计数变量的操作不被打断,防止改到一半切换到写者导致问题
哲学家就餐问题
问题描述:有五位哲学家围绕着餐桌坐,每一位哲学家要么思考,要么吃饭。为了吃饭,哲学家必须拿起两双筷子(分别放于左右两端)不幸的是,筷子的数量和哲学家相等,所以每只筷子必须由两位哲学家共享。
解决方法:一次最多只允许四位哲学家同时进餐,也就是设置一个初始值为4的信号量进行控制
或者要求奇数号的哲学家先拿左边的筷子,偶数号的先拿右边的之类的
或者使用一个公用的mutex保护哲学家拿左边和右边的筷子的操作,防止被打断,避免循环等待
临界区的概念?
各个进程中对临界资源(互斥资源/共享变量,一次只能给一个进程使用)进行操作的程序片段
同步与互斥的概念?
- 同步:多个进程因为合作而使得进程的执行有一定的先后顺序。比如某个进程需要另一个进程提供的消息,获得消息之前进入阻塞态;
- 进程同步:多个进程因为合作而使得进程的执行有一定的先后顺序,进程同步就是要保证进程执行顺序正确,并且有序访问共享资源。
- 互斥:多个进程在同一时刻只有一个进程能进入临界区
并发、并行、异步的区别?
并发:在一个时间段中同时有多个程序在运行,但其实任一时刻,只有一个程序在CPU上运行,宏观上的并发是通过不断的切换实现的;
多线程:并发运行的一段代码。是实现异步的手段
并行(和串行相比):在多CPU系统中,多个程序无论宏观还是微观上都是同时执行的
异步(和同步相比):同步是顺序执行,异步是在等待某个资源的时候继续做自己的事
Linux下的线程同步
互斥锁(Mutex)
- 互斥锁是最基本的线程同步机制之一。它确保同一时间只有一个线程可以访问共享资源或执行临界区代码,从而避免了竞争条件和数据不一致的问题。Linux下使用POSIX线程库中的pthread_mutex系列函数来操作互斥锁。
条件变量(Condition Variable)
- 条件变量提供了线程间通信和同步的机制。一个线程可以等待某个条件变为真,而另一个线程可以通知条件已经满足,从而唤醒等待线程。条件变量通常与互斥锁一起使用,Linux下使用pthread_cond系列函数操作条件变量。条件变量(pthread_cond)是通过等待(pthread_cond_wait)和通知(pthread_cond_signal或pthread_cond_broadcast)的方式实现线程间的协作
信号量(Semaphore)
- 信号量是一种计数器,用于控制对有限资源的访问。线程可以通过原子操作来获取或释放资源。信号量常用于限制可以同时访问某个资源的线程数量。Linux下使用POSIX信号量API(sem_系列函数)操作信号量。
自旋锁(Spin Lock)
- 自旋锁是一种在等待获取锁时,线程不会进入睡眠状态,而是持续循环检测锁的状态,直到获取到锁。自旋锁适用于锁定时间较短的情况,否则会浪费大量CPU资源。C11使用
atomic_flag_test_and_set和atomic_flag_clear或者gcc提供的原子操作__sync_lock_test_and_set和__sync_lock_release
- 自旋锁是一种在等待获取锁时,线程不会进入睡眠状态,而是持续循环检测锁的状态,直到获取到锁。自旋锁适用于锁定时间较短的情况,否则会浪费大量CPU资源。C11使用
读写锁(Read-Write Lock)
- 读写锁允许多个线程同时获取读锁(共享访问),但同一时间最多只能有一个线程获取写锁(独占访问)。这种机制在读操作远多于写操作的情况下可以提高并发性能。Linux下使用pthread_rwlock系列函数操作读写锁。
屏障(Barrier)
- 屏障用于阻塞一组线程,直到所有线程都到达屏障时,才允许所有线程继续执行。它常用于并行计算中,确保所有线程完成当前阶段的工作后再进入下一阶段。Linux下使用pthread_barrier系列函数操作屏障。
信号(Signal)
- 信号是一种软件中断,可用于在线程之间传递异步事件。线程可以发送信号给另一个线程,或者捕获由其他线程发送的信号。大多数时候并不使用,而是使用锁和信号量、条件变量等
linux下mutex是如何实现的
- 获取互斥锁 (pthread_mutex_lock)
尝试锁定:线程首先在用户空间使用原子操作(如compare-and-swap,CAS)尝试锁定互斥锁。如果锁未被占用,获取成功,进入临界区。
锁定失败:如果锁已被其他线程持有,线程会进入内核,通过futex系统调用将自己挂起,进入等待队列。 - 释放互斥锁 (pthread_mutex_unlock)
解锁:持锁线程使用原子操作将互斥锁状态从“锁定”改为“未锁定”。
唤醒线程:如果有其他线程在等待,通过futex唤醒其中一个线程,使其可以尝试获取锁。 - 快速路径:大多数锁操作在用户空间完成,只有竞争时才进入内核。
- 等待和唤醒:锁被占用时,线程通过 futex 进入等待,锁释放时通过 futex 唤醒。
多线程同时使用同一个socket描述符可行吗
- 在 Linux 下,对 TCP socket 的写操作是原子的,但这是有限制的。对于较小的数据(通常是小于或等于 64 KB 的数据),系统能够保证多个线程同时向同一个 TCP socket 写入时,数据不会交错。如果写入的数据量较大,数据可能会被分片,导致数据出现交错等问题
- 读操作可能发生竞争:多个线程从同一个 TCP socket 读取数据时,文件描述符中的数据是共享的。
进程有哪几种状态?
- 创建状态:进程正在被创建的状态,创建完之后进入就绪态
- 就绪状态:进程已获得除处理机以外的所需资源,等待分配处理机资源
- 运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数
- 阻塞状态:进程等待某个事件的发生,在条件满足之前无法执行
- 终止状态:进程请求操作系统结束自己,进入终止态,系统回收资源
进程调度的时机一般有什么
- 时间片到期
- 进程阻塞
- 进程终止
- 有更高优先级的进程就绪抢占
切换进程做了什么
保存当前进程的上下文
当操作系统决定暂停当前进程时,它需要保存这个进程的所有状态信息,以便在将来重新调度这个进程时能够恢复其执行状态。这些信息包括:
CPU寄存器:所有通用寄存器(如eax、ebx等)、程序计数器(PC)、栈指针(SP)、基址指针(BP)等。
程序状态字 (PSW):包括条件码和控制信息。
内存管理信息:如页表基址寄存器等,涉及进程的地址空间。
其他硬件状态:如浮点运算寄存器、控制寄存器等。
这些信息通常存储在进程控制块(PCB)中。
更新进程状态
- 操作系统将当前进程的状态更新为“就绪”或“等待”,以反映其当前状况。
选择下一个进程
- 调度器根据调度算法选择下一个要运行的进程。选择过程涉及对进程队列的管理。
恢复新进程的上下文
操作系统需要恢复即将运行的进程的上下文,这包括:
CPU寄存器:从新进程的PCB中恢复寄存器值。
程序计数器 (PC):设置为新进程的下一条指令地址。
栈指针 (SP) 和 基址指针 (BP):指向新进程的栈。
内存管理信息:加载新进程的页表或段表,配置地址转换硬件。
切换内存地址空间
- 操作系统需要更改内存管理单元(MMU)中的页表或段表基址寄存器,以便CPU能够正确访问新进程的地址空间。
恢复硬件状态
- 如果有专用的硬件状态(如浮点寄存器、SIMD寄存器),也需要恢复。
转移控制权
- 最后,操作系统将控制权交给新进程的下一条指令,使其继续执行。这通常通过恢复程序计数器(PC)和执行返回指令来完成。
用户模式和内核模式切换
- 在用户模式运行的进程通常通过系统调用或中断进入内核模式。上下文切换涉及在这两种模式之间切换,并确保正确的模式和权限设置。
进程调度策略有哪些?
- 批处理系统:
按照请求的顺序进行调度。非抢占式,开销小,无饥饿问题,响应时间不确定(可能很慢);
对短进程不利,对IO密集型进程不利。
按估计运行时间最短的顺序进行调度。非抢占式,吞吐量高,开销可能较大,可能导致饥饿问题;
对短进程提供好的响应时间,对长进程不利。
按剩余运行时间的顺序进行调度。(最短作业优先的抢占式版本)。吞吐量高,开销可能较大,提供好的响应时间;
可能导致饥饿问题,对长进程不利。
- 交互式系统
交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。
将所有就绪进程按 FCFS 的原则排成一个队列,用完时间片的进程排到队列最后。抢占式(时间片用完时),开销小,无饥饿问题,为短进程提供好的响应时间;
若时间片小,进程切换频繁,吞吐量低;若时间片太长,实时性得不到保证。
设置多个就绪队列1、2、3…,优先级递减,时间片递增。只有等到优先级更高的队列为空时才会调度当前队列中的进程。如果进程用完了当前队列的时间片还未执行完,则会被移到下一队列。
抢占式(时间片用完时),开销可能较大,对IO型进程有利,可能会出现饥饿问题。
什么叫优先级反转?如何解决?
高优先级的进程等待被一个低优先级进程占用的资源时,就会出现优先级反转,即优先级较低的进程比优先级较高的进程先执行。此处详细解释优先级反转带来的问题:如果有一个中等优先级的进程将低优先级的进程抢占,那么此时低优先级的进程无法正常进行并在后续释放被占用的资源,导致高优先级的任务一直被挂起,直到中等优先级的进程完成后,低优先级的进程才可以继续并在后续释放占用的资源,最后高优先级的进程才可以执行。导致的问题就是高优先级的进程在中等优先级的进程调度之后。
解决方法:
- 优先级天花板(priority ceiling):当任务申请某资源时,把该任务的优先级提升到可访问这个资源的所有任务中的最高优先级,这个优先级称为该资源的优先级天花板。简单易行。
- 优先级继承(priority inheritance):当任务A申请共享资源S时,如果S正在被任务C使用,通过比较任务C与自身的优先级,如发现任务C的优先级小于自身的优先级,则将任务C的优先级提升到自身的优先级,任务C释放资源S后,再恢复任务C的原优先级。
什么是僵尸进程?
- 一个子进程结束后,它的父进程并没有等待它(调用wait或者waitpid),那么这个子进程将成为一个僵尸进程。僵尸进程是一个已经死亡的进程,但是并没有真正被销毁。它已经放弃了几乎所有内存空间,也不能被调度,仅仅在进程表中保留一个位置,记载该进程的进程ID、终止状态以及资源利用信息(CPU时间,内存使用量等等)供父进程收集,除此之外,僵尸进程不再占有任何内存空间。这个僵尸进程可能会一直留在系统中直到系统重启
- 父进程可以通过wait得到子进程的返回码,可以对其调用一些系统提供的宏得知子进程的死因
危害:占用进程号,而系统所能使用的进程号是有限的;占用内存。
以下情况不会产生僵尸进程:
- 该进程的父进程先结束了。每个进程结束的时候,系统都会扫描是否存在子进程,如果有则用Init进程接管,成为该进程的父进程,并且会调用wait等待其结束。
- 父进程调用wait或者waitpid等待子进程结束(需要每隔一段时间查询子进程是否结束)。wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。waitpid则可以加入
WNOHANG(wait-no-hang)选项,如果没有发现结束的子进程,就会立即返回,不会将调用waitpid的进程阻塞。同时,waitpid还可以选择是等待任一子进程(同wait),还是等待指定pid的子进程,还是等待同一进程组下的任一子进程,还是等待组ID等于pid的任一子进程; - 子进程结束时,系统会产生
SIGCHLD(signal-child)信号,可以注册一个信号处理函数,在该函数中调用waitpid,等待所有结束的子进程(注意:一般都需要循环调用waitpid,因为在信号处理函数开始执行之前,可能已经有多个子进程结束了,而信号处理函数只执行一次,所以要循环调用将所有结束的子进程回收); - 也可以用
signal(SIGCLD, SIG_IGN)(signal-ignore)通知内核,表示忽略SIGCHLD信号,那么子进程结束后,内核会进行回收。
什么是孤儿进程?
一个父进程已经结束了,但是它的子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程会被Init(进程ID为1)接管,当这些孤儿进程结束时由Init完成状态收集工作。
线程同步有哪些方式?
为什么需要线程同步:线程有时候会和其他线程共享一些资源,比如内存、数据库等。当多个线程同时读写同一份共享资源的时候,可能会发生冲突。因此需要线程的同步,多个线程按顺序访问资源。
- 互斥量 Mutex:互斥量是内核对象,只有拥有互斥对象的线程才有访问互斥资源的权限。因为互斥对象只有一个,所以可以保证互斥资源不会被多个线程同时访问;当前拥有互斥对象的线程处理完任务后必须将互斥对象交出,以便其他线程访问该资源;
- 信号量 Semaphore:信号量是内核对象,它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。信号量对象保存了最大资源计数和当前可用资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就减1,只要当前可用资源计数大于0,就可以发出信号量信号,如果为0,则将线程放入一个队列中等待。线程处理完共享资源后,应在离开的同时通过
ReleaseSemaphore函数将当前可用资源数加1。如果信号量的取值只能为0或1,那么信号量就成为了互斥量; - 事件 Event:允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。事件分为手动重置事件和自动重置事件。手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而且一直保持为激发状态,直到程序重新把它设置为未激发状态。自动重置事件被设置为激发状态后,会唤醒一个等待中的线程,然后自动恢复为未激发状态。
- 临界区 Critical Section:拥有临界区对象的线程可以访问该临界资源,任意时刻只允许一个线程对临界资源进行访问。,其它试图访问该资源的线程将被挂起,直到临界区对象被释放。
线程安全是什么,怎么实现
- 线程安全(Thread Safety)是指在多线程环境中,多个线程访问和操作同一资源(如数据结构、文件、变量等)时,不会引发竞争条件(race condition)、数据不一致或程序崩溃等问题
- 怎么实现
- 各种锁
- 原子操作
- 线程本地存储
互斥量和临界区有什么区别?
互斥量是可以命名的,可以用于不同进程之间的同步;而临界区只能用于同一进程中线程的同步。创建互斥量需要的资源更多,因此临界区的优势是速度快,节省资源。
什么是协程?
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
协程与线程进行比较?
- C++20引入了协程(coroutines),这是一种强大的语言特性,可以简化异步编程和生成器模式
- 协程的关键组件
- co_await:暂停协程执行,等待一个值或者任务完成。
- co_yield:生成一个值并暂停协程执行,稍后可以恢复执行。
- co_return:结束协程并返回一个值。
进程的异常控制流:陷入、中断、异常和信号
中断是系统从用户态到内核态的唯一方式
陷入(trap)是有意造成的“异常”,是执行一条指令的结果。陷入是同步的。陷入的主要作用是实现系统调用。比如,进程可以执行 syscall n 指令向内核请求服务。当进程执行这条指令后,会中断当前的控制流,陷入到内核态,执行相应的系统调用。内核的处理程序在执行结束后,会将结果返回给进程,同时退回到用户态。进程此时继续执行下一条指令。
故障是由错误条件引起的,可能可以修复(比如缺页),修复之后会将CPU使用权交还给用户进程继续执行
外中断由处理器外部的硬件产生,CPU在每条指令执行的末尾会自动查询是否有待处理的外中断,外中断不是执行某条指令的结果,也无法预测发生时机。由于中断独立于当前执行的程序,因此中断是异步事件。中断包括 I/O 设备发出的 I/O 中断、各种定时器引起的时钟中断、调试程序中设置的断点等引起的调试中断等。
异常是一种错误情况,是执行当前指令的结果,可能被错误处理程序修正,也可能直接终止应用程序。异常是同步的。这里特指因为执行当前指令而产生的错误情况,比如除法异常无法恢复、缺页异常就可能可以恢复等。有些书上为了区分,也将这类“异常”称为“故障”。
信号是一种更高层的软件形式的异常,同样会中断进程的控制流,可以由进程进行处理。一个信号代表了一个消息。信号的作用是用来通知进程发生了某种系统事件。
更详细的可以参考
用户空间程序和内核空间程序通信的方式
- 系统调用,使用系统调用传递特定的支持的信息
- 信号,内核在特定情况下可以向用户程序发送信号
- 读写
/proc目录,可以读取和修改内核部分信息和设置 - 读写指定的文件实现通信
- 使用
netlink类似于socket,可以与内核互传大量数据 - 使用
ioctl,在数据量较少的时候实现通信
处理中断的流程
中断发生
- 当CPU正在执行程序时,如果发生了中断事件(如硬件中断、软件中断等),会触发中断信号,CPU暂停当前程序的执行。
中断向量表查询
- CPU根据中断类型查询中断向量表(Interrupt Vector Table),获取相应中断处理程序的入口地址。
保护现场
- CPU将当前程序的执行现场(如程序计数器、寄存器等)主要是寄存器,压入内核堆栈,以便中断处理完成后能恢复执行。
关中断
- 保证整个中断处理程序不被打断,在中断服务结束之后才再次开中断
也存在多重中断
- 只在保护现场和恢复现场的时候关闭中断防止被打断,其他步骤均开启中断允许打断
切换到内核态
- CPU切换到内核态(特权模式),准备执行中断处理程序。
执行中断处理程序
- CPU跳转到中断处理程序的入口地址,开始执行中断处理程序。中断处理程序会根据中断类型进行相应的处理,如读取数据、发送数据等。
中断处理完成
- 中断处理程序完成相应的工作后,会执行”中断返回”指令。
恢复现场
- CPU从内核堆栈中恢复被中断程序的执行现场。
切换到用户态
- 如果被中断的是用户程序,CPU切换回到用户态(非特权模式)。
继续执行
- CPU继续执行被中断的程序,就像中断从未发生过一样。
以上是中断处理函数的上半部分,一般执行中断处理中比较快,用时较短的部分
如果中断处理函数中有一些执行时间较长但是本身对实时性不是很敏感的任务,可以在中断处理函数以外的部分执行,以保证中断处理函数快进快出。可以在上半部中将数据拷贝到内存,在下半部分处理。不希望被打断、对时间敏感和硬件相关的一般都在中断处理函数中执行
什么是IO多路复用?怎么实现?
IO多路复用(IO Multiplexing)是指单个进程/线程就可以同时处理多个IO请求。
实现原理:用户将想要监视的文件描述符(File Descriptor)添加到select/poll/epoll函数中,由内核监视,函数阻塞。一旦有文件描述符就绪(读就绪或写就绪),或者超时(设置timeout),函数就会返回,然后该进程可以进行相应的读/写操作。
select:将文件描述符放入一个集合中,调用select时,将这个集合从用户空间拷贝到内核空间(缺点1:每次都要复制,开销大),由内核根据就绪状态修改该集合的内容。(缺点2)集合大小有限制,32位机默认是1024(64位:2048);采用水平触发机制。select函数返回后,需要通过遍历这个集合,找到就绪的文件描述符(缺点3:轮询的方式效率较低),当文件描述符的数量增加时,效率会线性下降;poll:和select几乎没有区别,区别在于文件描述符的存储方式不同,poll采用链表的方式存储,没有最大存储数量的限制;epoll:通过内核和用户空间共享内存,避免了不断复制的问题;支持的同时连接数上限很高(1G左右的内存支持10W左右的连接数);文件描述符就绪时,采用回调机制,避免了轮询(回调函数将就绪的描述符添加到一个链表中,执行epoll_wait时,返回这个链表);支持水平触发和边缘触发,采用边缘触发机制时,只有活跃的描述符才会触发回调函数。
总结,区别主要在于:
- 一个线程/进程所能打开的最大连接数
- 文件描述符传递方式(是否复制)
- 水平触发 or 边缘触发
- 查询就绪的描述符时的效率(是否轮询)
当连接数较多并且有很多的不活跃连接时,epoll的效率比其它两者高很多;但是当连接数较少并且都十分活跃的情况下,由于epoll需要很多回调,因此性能可能低于其它两者。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
内核通过文件描述符来访问文件。文件描述符指向一个文件。
什么是水平触发?什么是边缘触发?
- 水平触发(LT,Level Trigger)模式下,只要一个文件描述符就绪,就会触发通知,如果用户程序没有一次性把数据读写完,下次还会通知;
- 边缘触发(ET,Edge Trigger)模式下,当描述符从未就绪变为就绪时通知一次,之后不会再通知,直到再次从未就绪变为就绪(缓冲区从不可读/写变为可读/写)。
- 区别:边缘触发效率更高,减少了被重复触发的次数,函数不会返回大量用户程序可能不需要的文件描述符。
- 为什么边缘触发一定要用非阻塞(non-block)IO:避免由于一个描述符的阻塞读/阻塞写操作让处理其它描述符的任务出现饥饿状态。
有哪些常见的IO模型?
- 同步阻塞IO(Blocking IO):用户线程发起IO读/写操作之后,线程阻塞,直到可以开始处理数据;对CPU资源的利用率不够;
- 同步非阻塞IO(Non-blocking IO):发起IO请求之后可以立即返回,如果没有就绪的数据,需要不断地发起IO请求直到数据就绪;不断重复请求消耗了大量的CPU资源;
- IO多路复用
- 异步IO(Asynchronous IO):用户线程发出IO请求之后,继续执行,由内核进行数据的读取并放在用户指定的缓冲区内,在IO完成之后通知用户线程直接使用。
什么是用户态和内核态?
为了限制不同程序的访问能力,防止一些程序访问其它程序的内存数据,CPU划分了用户态和内核态两个权限等级。
任何共享资源都需要通过系统调用进行处理,比如外接设备,文件,内存,进程等
用户态只能受限地访问内存,且不允许访问外围设备,没有占用CPU的能力,CPU资源可以被其它程序获取;
内核态可以访问内存所有数据以及外围设备,也可以进行程序的切换。
所有用户程序都运行在用户态,但有时需要进行一些内核态的操作,比如从硬盘或者键盘读数据,这时就需要进行系统调用(程序向操作系统请求服务的方式),使用陷入指令,CPU切换到内核态,执行相应的服务,再切换为用户态并返回系统调用的结果。
注意CPU在用户态和内核态之间转换也是有一定时间成本的
为什么要分用户态和内核态?
(我自己的见解:)
- 安全性:防止用户程序恶意或者不小心破坏系统/内存/硬件资源;
- 封装性:用户程序不需要实现更加底层的代码;
- 利于调度:如果多个用户程序都在等待键盘输入,这时就需要进行调度;统一交给操作系统调度更加方便。
如何从用户态切换到内核态?
- 系统调用:本质上还是通过中断实现
- 用户程序发生异常时:比如缺页异常
- 外围设备的中断:外围设备完成用户请求的操作之后,会向CPU发出中断信号,这时CPU会转去处理对应的中断处理程序
什么是死锁?
在两个或者多个并发进程中,每个进程持有某种资源而又等待其它进程释放它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁(deadlock)。
死锁产生的必要条件?
- 互斥:一个资源一次只能被一个进程使用;
- 占有并等待:一个进程至少占有一个资源,并在等待另一个被其它进程占用的资源;
- 非抢占:已经分配给一个进程的资源不能被强制性抢占,只能由进程完成任务之后自愿释放;
- 循环等待:若干进程之间形成一种头尾相接的环形等待资源关系,该环路中的每个进程都在等待下一个进程所占有的资源。
死锁有哪些处理方法?
基本思想是破坏形成死锁的四个必要条件:
- 破坏互斥条件:允许某些资源同时被多个进程访问。但是有些资源本身并不具有这种属性,因此这种方案实用性有限;
- 破坏占有并等待条件:
- 实行资源预先分配策略(当一个进程开始运行之前,必须一次性向系统申请它所需要的全部资源,否则不运行);
- 或者进程申请资源前先释放占有的资源
- 缺点:很多时候无法预知一个进程所需的全部资源;同时,会降低资源利用率,降低系统的并发性;
- 破坏非抢占条件:允许进程强行抢占被其它进程占有的资源。会降低系统性能;
- 破坏循环等待条件:对所有资源统一编号,所有进程对资源的请求必须按照序号递增的顺序提出,即只有占有了编号较小的资源才能申请编号较大的资源。反向是不允许得
动态地检测资源分配状态,以确保系统处于安全状态,只有处于安全状态时才会进行资源的分配。所谓安全状态是指:即使所有进程突然请求需要的所有资源,也能存在某种对进程的资源分配顺序,使得每一个进程运行完毕。
银行家算法 的关键思想是在分配资源之前进行安全性检查, 系统在运行时必须知道每种类型资源的总数量以及每个进程的最大资源需求。
- 当一个进程请求资源时,系统会检查是否有足够的资源可供分配。如果有足够的资源,则分配给进程,并且系统更新资源的可用数量。
- 如果没有足够的资源可供分配,系统会将请求的进程置于等待状态,直到有足够的资源可用。
- 当进程完成任务并释放其占用的资源时,系统会将这些资源返回到资源池,并通知等待的进程。
如何检测死锁:检测有向图是否存在环;
死锁解除的方法:
- 利用抢占:挂起某些进程,并抢占它的资源。但应防止某些进程被长时间挂起而处于饥饿状态;
- 利用回滚:让某些进程回退到足以解除死锁的地步,进程回退时自愿释放资源。要求系统保持进程的历史信息,设置还原点;
- 利用杀死进程:强制杀死某些进程直到死锁解除为止,可以按照优先级进行。
程序装入方式(装入确定的是物理地址)
- 绝对装入:编译器直接负责地址转换
- 可重定位装入(静态重定位):装入的时候将逻辑地址转换为物理地址
- 动态运行时装入(动态重定位):运行时才进行地址转换,需要设置重定位寄存器,执行到命令的时候由硬件进行转换
程序链接方式(链接确定的是逻辑地址)
- 动态链接库是位置无关代码,可以被存储在内存的任何位置而不影响执行
- 使用动态链接库,可以让用户在不更新整个程序文件的同时更新其用到的库函数
- 静态链接
- 在程序运行之前,将程序运行所需的各个模块连接成一个完整的可执行文件
- 装入时动态链接
- 也就是各个模块装入内存的时候边装入边链接
- 运行时动态链接
- 在用到某个模块的时候才将某个模块调入内存,调入内存的时候实现链接操作,能够提升堆内存的利用率
- 而且多个程序引用同一个动态库的时候,可以不在内存中拷贝形成副本
内部碎片和外部碎片
- 内部碎片,分配给某进程的内存区域中,有些部分没有用上
- 外部碎片,指内存中的某些空闲分区由于太小而难以利用
存储管理方式
内存管理方式分为连续分配管理和非连续分配管理。连续分配管理分为单一连续分配、固定分区分配、动态分区分配以及动态重定位分区分配四种;非连续分配管理又叫离散分配方式,如果离散分配的基本单位是页Page,则称为分页存储管理方式;如果离散分配的基本单位是段Segment,则称为分段存储管理方式。
连续分配管理方式允许一个用户程序分配一个连续的内存空间。
- 单一连续分配:多用于单用户单任务的操作系统中,是一种简单的存储管理方式。单一连续分配中,会将存储空间分配为系统区和用户区。系统区存放系统进程,用户区存放用户进程,这样的管理可以避免用户进程影响到系统进程。这种分配方式中,比如0-a的地址空间存放系统区,那么a+1-n的地址空间都存放用户区。
- 固定分区分配:是一种最简单的可运行多道程序的存储管理方式。固定分区分配首先要划分分区,之后进行内存分配。
- 内存分区分为分区大小相等和分区大小不等两种。分区大小相等的情况下,如果进程大小不相等容易造成内存浪费或者内存不够进程无法运行的问题,所以通常用于进程内存大小相等的情况中。分区大小不相等,就是根据常用的大小(较多的小分区,适量的中分区,少量的大分区)进行分区,这样就可以更好地利用内存空间,但是这样的方式需要维护一个分区使用表。
- 内存分配要维护一张分区使用表,通常按照进程大小进行排序。每次在进行内存分配的时候,要查看哪些分区能够容纳该进程。
动态分区分配:根据进程的需要,动态地分配内存空间。这样的分配方式涉及到分区中的数据结构、分区分配算法以及分区的分配和回收操作三个问题。
- 分区分配中的数据结构主要分为空闲分区表和空闲分区链两种。空闲分区表维护空闲分区的序号、空闲分区的起始地址以及空闲分区的大小数据。空闲分区链相当于一个双向链表,维护空闲分区;为了方便检索在分区尾部设置一个分区的状态位以及分区大小。
- 分区分配算法主要有五种方法。首次适应算法(First Fit),利用空闲分区链实现,将空闲分区按照地址递增(注意与后面的最佳适应区分,这里按照的是地址递增)进行排序,然后根据进程的大小从链首查找空闲分区链,第一个能够适应的就分配。循环首次适应算法(Next Fit),不是每次都从链首开始查找,而是从上一次找到的空闲分区的下一个空闲分区开始查找,直到查找到一个能够使用的分区,之后动态地分配内存(会导致后续大空闲分区变少)。最佳适应算法(Best Fit),将空闲分区链按照大小进行排序,找到第一个适应的空闲分区即可(最大限度利用空闲分区)。最坏适应算法(Worst Fit),与最佳适应相反,排序之后每次挑选最大的分区使用,对中小作业有利,不易产生碎片。快速适应算法(Quick Fit),根据大小将空闲分区进行分类,维护多个空闲分区链;这样的好处是可以加快检索,相比一条链能够更快地检索目标进程。
- 分区分配和回收:主要操作是内存的分配和内存的回收。内存分配中,若空闲分区内存大小-用户进程内存大小<=预设不可切分内存大小,则进行一次内存的分配;否则将剩余的内存空间放到空闲内存链中,继续下次的使用。内存回收,主要看是否和空闲分区相邻,如果相邻就直接合并;否则就建立新的表项,维护回收区的内存起始地址和大小。
动态重定位分区分配:在连续分配方式中,必须把系统进程或者用户进程装入一个连续的内存空间中。这个时候可能会因为程序的大小与分区的大小不一致的问题产生内存碎片。这个时候我们要想插入新的进程,即使碎片空间总和支持进程,也无法再分配空间,所以我们要把内存空间进行一个整理。
在程序装入内存的时候根据程序的大小动态分配内存,程序运行过程中可以动态修改程序在物理内存中的位置
整理内存地址,是将程序的内存地址整理成在物理上相邻的状态。程序使用的地址在分区装载之后仍然是相对地址,要想将相对地址转换为相邻物理地址,必然会影响到程序的执行。为了不影响程序的执行,需要在硬件上提供对程序的内存地址转换支持,于是引入重定位寄存器,用它来存放程序在内存中的起始地址。
非连续分配管理方式允许将一个程序分散地装入不相邻的内存分区。根据内存分区的大小分为分页式存储管理方式和分段式存储管理方式。
页存储:页存储首先需要将一个进程的逻辑内存空间划分为多个内存大小相等的页,然后将物理内存空间划分为相等的大小个数的物理块Block(页框Frame)。分配的时候将多个页面放入多个不相邻的物理块中。这样的划分之后,通常进程的最后一页存不满,会产生内存碎片——”页内碎片“。
页面大小:页面大小的选定需要适当。如果过小,虽然可以减少页内碎片的产生,但是需要更大的页表,而且页面切换更频繁;如果太大就会产生较大的内存碎片。所以大小的选择应该适中,通常为2的整数次幂,范围为512B至8KB。
页表:页表主要保存进程占用的页数,同时存储逻辑内存空间页到物理内存块的映射。
地址转换:页表要存储地址映射,那么首先得有一个地址变换机构。基本地址变换机构,主要用来建立逻辑地址到物理内存空间地址的映射。传统系统中,主要使用寄存器来存放页表(寄存器速度快,有利于变换地址),那么每一个页表都需要寄存器来存放,成本过高。进程数量过多的情况下,首先将页表的起始地址和页表长度存放在PCB中,之后在进程运行的时候再将数据读取到PTR(Page-Table Register,页表寄存器)。读取数据的时候,地址转换机构会将相对地址转换为页号以及页内地址两部分,之后根据页号检索页表。检索之前会将页号和页表长度进行比较,如果页号大于或等于页表长度,则说明出现了访问越界,会出现越界中断。
具有块表的地址转换机构:页表存放在内存中,那么将要先查页表,计算得出物理地址之后访问物理地址,是两次检索过程。
为了提高地址变换速度,可在地址变换机构中增设一个具有并行查寻能力的特殊高速 缓冲寄存器,用来存放最近放问过的页表项的副本,又称为“联想寄存器”(Associative Memory),或称为“快表”。
对于32位操作系统,使用两级页表结构式合适的;但是对于64位操作系统,建议采用多级页表。
段存储:使用段存储而不再使用页存储,第一个原因是提高内存的利用率,第二个原因是满足开发者在编程中的多种需求。主要是满足编程中的几个新的需求:方便编程(简化逻辑地址访问方式)、信息共享(段式信息的逻辑单位)、信息保护(对信息的逻辑单位实现保护)、动态增长(分段更加满足动态增长的需求)、动态链接(链接装载使用段更符合需求)。
- 分段:作业的地址空间被划分为若干个段,每个段定义了一组逻辑信息。
- 段表:为每个分段分配一个连续的分区,进程每个段可以离散地存放到不同分区中,所以也需要维护一张段表,使得进程能够查到逻辑地址对应的物理地址,这个表就是段映射表,简称段表。
- 地址变换机构:为了实现逻辑地址到物理地址的映射,系统设置了一个段表寄存器,用于存放段表地址和段表长度,如果段号大于段表长度,则表示发生了越界访问,产生越界中断;若未越界,找出该段对应段表项的位置,读出内存中的起始地址;再检查段内地址是否超过段长,如果超过段长,发出越界中断;否则将基址与段内地址相加,得出内存的物理地址。
- 分段和分页的主要区别:①页是信息的物理单位,段是信息的逻辑单位;②页的大小由系统决定,段的大小不固定;③分页作业地址空间是一维的,分段作业地址空间则是二维的。即页的地址空间可以用一个符号来标识,而段的地址空间不仅要知道段名还要知道段内地址。
段页存储:分页存储能提高内存利用率,分段存储能满足用户的更多需求。为了各取所长,产生了段页存储的管理方式。
- 基本原理:段页存储,就是既分段也分页,先分段再分页。地址结构由段号、段内页号、页内地址三部分组成。
- 地址变换过程:首先也需要准备一个段表寄存器,存放段表起始地址和段表长。首先将段号和段表长相比较是否越界;再利用段表起始地址和段表号来找到段表项在段表中的位置,从而得到页表地址;再之后使用段内页号来获取页表项的位置,找出物理块号,用物理块号和页内地址来构成物理地址。
分页和分段有什么区别?
- 页式存储:用户空间划分为大小相等的部分称为页(page),内存空间划分为同样大小的区域称为页框,分配时以页为单位,按进程需要的页数分配,逻辑上相邻的页物理上不一定相邻;
- 段式存储:用户进程地址空间按照自身逻辑关系划分为若干个段(segment)(如代码段,数据段,堆栈段),内存空间被动态划分为长度不同的区域,分配时以段为单位,每段在内存中占据连续空间,各段可以不相邻;
- 段页式存储:用户进程先按段划分,段内再按页划分,内存划分和分配按页。
- 分段对用户是可见的,但是分页对用户是不可见的,是系统自动分配的
- 段页式管理的地址结构是二维的,既要给出段号又要给出段内偏移量
- 用段表找页表,页表找页框
区别:
- 目的不同:分页的目的是管理内存,用于虚拟内存以获得更大的地址空间;分段的目的是满足用户的需要,使程序和数据可以被划分为逻辑上独立的地址空间;
- 大小不同:段的大小不固定,由其所完成的功能决定;页的大小固定,由系统决定;
- 地址空间维度不同:分段是二维地址空间(段号+段内偏移),分页是一维地址空间(每个进程一个页表/多级页表,通过一个逻辑地址就能找到对应的物理地址);
- 分段便于信息的保护和共享;分页的共享收到限制;
- 碎片:分段没有内碎片,但会产生外碎片;分页没有外碎片,但会产生少量的内碎片(一个页填不满)
单级页表和多级页表
- 假如只有单级的页表的话,必须使用一个连续的空间存储所有的页表项,但是很难分配
- 进程大部分时候都只需要访问部分页表,没必要整个页表常驻内存
- 可以将一定数量的页表项分为一组,每个组之间独立离散的存放在内存中,此时再建立一个上级页表用于寻找这些页表
- 利用地址的最高几位部分在顶层页表中找到自己属于哪个二级页表,再利用地址的中间几位在二级页表中找到自己属于哪个页框,然后利用地址的最低几位找到自己在页框中的偏移量
什么是虚拟内存?
每个程序都拥有自己的地址空间,这个地址空间被分成大小相等的页,这些页被映射到物理内存;但不需要所有的页都在物理内存中,当程序引用到不在物理内存中的页时,由操作系统将缺失的部分装入物理内存。这样,对于程序来说,逻辑上似乎有很大的内存空间,只是实际上有一部分是存储在磁盘上,因此叫做虚拟内存。
虚拟内存的优点是让程序可以获得更多的可用内存。
虚拟内存的实现方式、页表/多级页表、缺页中断、不同的页面淘汰算法:答案。
- 特点:
- 某个页面被访问但是这个页面尚且没有被调入内存,此时需要触发一个缺页中断使得系统将对应的页面调入内存供访问
- 是一种故障,可以被修复的
- 是一种内中断
CPU高速缓存
在一个多核CPU中,每个核心都有自己的一级(L1,几十几百kb)和二级(L2几MB)缓存,这些缓存只能被相应的核心访问。这种设计可以使每个核心快速访问其私有缓存中的数据,而不需要与其他核心争用资源。然而,这也意味着如果一个核心需要访问另一个核心的数据,它必须通过更慢的通道(比如主内存或者其他某种形式的互连)来获取。
此外,多核CPU通常还有一个三级(L3几十MB)缓存,这是一个共享缓存,所有的核心都可以访问。这使得一个核心可以相对快速地访问另一个核心的数据,但是这也意味着核心之间可能会发生资源争用。
如何实现缓存一致性
MESI(修改、独占、共享、无效):这是最常见的缓存一致性协议之一。每个缓存行可以处于四种状态之一:修改(Modified)、独占(Exclusive)、共享(Shared)或无效(Invalid)。当一个处理器需要写入数据时,它必须首先获取该数据的独占访问权限,这意味着所有其他处理器的相应缓存行都必须变为无效状态。
伪共享
- 伪共享(False Sharing)是多线程编程中的一个问题,它发生在多个线程并发地访问同一缓存行(Cache Line)中的不同数据时。尽管这些线程访问的数据在逻辑上是独立的,但由于它们位于同一缓存行中,CPU会将它们视为“共享”的,这可能导致性能显著下降
- 一个线程访问缓存行之前,需要先获取缓存行的所有权,并发访问会导致并行性能下降
- 每次缓存行的所有权改变时,CPU都必须将缓存行的内容写回到主内存,并从主内存中加载新的内容
- 避免伪共享,可以尽量确保并发访问的数据位于不同的缓存行中。例如,可以通过添加填充(Padding)来调整数据的布局
如何进行地址空间到物理内存的映射?
内存管理单元(MMU)管理着逻辑地址和物理地址的转换,其中的页表(Page table)存储着页(逻辑地址)和页框(物理内存空间)的映射表,页表中还包含包含有效位(是在内存还是磁盘)、访问位(是否被访问过)、修改位(内存中是否被修改过)、保护位(只读还是可读写)。逻辑地址:页号+页内地址(偏移);每个进程一个页表,放在内存,页表起始地址在PCB/寄存器中。
有哪些页面置换算法?
在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘中来腾出空间。页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。
- 最佳页面置换算法OPT(Optimal replacement algorithm):置换以后不需要或者最远的将来才需要的页面,是一种理论上的算法,是最优策略但是不可能实现
- 先进先出FIFO:置换在内存中驻留时间最长的页面。缺点:有可能将那些经常被访问的页面也被换出,从而使缺页率升高;
- 时钟算法 Clock:将所有存在在内存中的页面组织为一个循环链表,给访问过的页面置为1,需要替换的时候进行遍历,将遇到的非0页面置零,零页面则替换为新的页面,如果都是1,则再循环一次,此时必然可以找到可替换的页面
- 改进版,操作系统应该尽可能的考虑到没有被修改过的页面换出,因为这个页面换出不需要写回外存,先找没有被修改过的页面,再找被修改过的
- 最近未使用算法NRU(Not Recently Used):检查访问位R、修改位M,优先置换R=M=0,其次是(R=0, M=1);
- 最近最少使用算法LRU(Least Recently Used):置换出未使用时间最长的一页;实现方式:维护时间戳,或者维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。
- 最不经常使用算法LFU(Least Frequently Used):置换出访问次数最少的页面
- 时间上:最近被访问的页在不久的将来还会被访问;
- 空间上:内存中被访问的页周围的页也很可能被访问。
颠簸本质上是指频繁的页调度行为。进程发生缺页中断时必须置换某一页。然而,其他所有的页都在使用,它置换一个页,但又立刻再次需要这个页。因此会不断产生缺页中断,导致整个系统的效率急剧下降,这种现象称为颠簸。内存颠簸的解决策略包括:
- 修改页面置换算法;
- 降低同时运行的程序的数量;
- 终止该进程或增加物理内存容量。
缓冲区溢出问题
- 什么是缓冲区溢出
- C 语言使用运行时栈来存储过程信息。每个函数的信息存储在一个栈帧中,包括寄存器、局部变量、参数、返回地址等。C 对于数组引用不进行任何边界检查,因此对越界的数组元素的写操作会破坏存储在栈中的状态信息,这种现象称为缓冲区溢出。缓冲区溢出会破坏程序运行,也可以被用来进行攻击计算机,如使用一个指向攻击代码的指针覆盖返回地址。
缓冲区溢出的防范方式
防范缓冲区溢出攻击的机制有三种:随机化、栈保护和限制可执行代码区域。
- 随机化:包括栈随机化(程序开始时在栈上分配一段随机大小的空间)和地址空间布局随机化(Address-Space Layout Randomization,ASLR,即每次运行时程序的不同部分,包括代码段、数据段、栈、堆等都会被加载到内存空间的不同区域),但只能增加攻击一个系统的难度,不能完全保证安全。
- 栈保护:在每个函数的栈帧的局部变量和栈状态之间存储一个随机产生的特殊的值,称为金丝雀值(canary)。在恢复寄存器状态和函数返回之前,程序检测这个金丝雀值是否被改变了,如果是,那么程序异常终止。
- 限制可执行代码区域:内存页的访问形式有三种:可读、可写、可执行,只有编译器产生的那部分代码所处的内存才是可执行的,其他页限制为只允许读和写。
更详细的可以参考
磁盘调度
过程:磁头(找到对应的盘面);磁道(一个盘面上的同心圆环,寻道时间);扇区(旋转时间)。为减小寻道时间的调度算法:
- 先来先服务
- 最短寻道时间优先
- 电梯算法:电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。
字节对齐
- 从偏移为0的位置开始存储
- 如果没有定义
#pragma pack(n)- sizeof的最终结果必然是结构内部最大成员的整数倍,不够补齐
- 结构内部各个成员的首地址必然是自身大小的整数倍
- 如果定义了
#pragma pack(n)- sizeof的最终结果必然必然是min[n,结构内部最大成员]的整数倍,不够补齐
- 结构内部各个成员的首地址必然是min[n,自身大小]的整数倍
软连接和硬链接的区别
- 类似于文件本身和快捷方式的区别
- 软连接是个独立的文件
- 硬链接和它的文件指向相同的inode,所有硬链接被删除之后才会真的删除文件
- 软链接是否删除与文件无关
零拷贝
“零拷贝”(Zero Copy)是一种优化技术,用于在数据传输和处理过程中减少或消除数据的拷贝操作,以提高性能和降低资源消耗。零拷贝的核心思想是尽量减少不必要的数据拷贝,而不是在数据传输的过程中将数据从一个缓冲区复制到另一个缓冲区。
内存管理:在操作系统和运行时库中,零拷贝可以用于减少内存分配和释放操作。例如,内存池可以通过重用先前分配的内存块来避免频繁的内存分配和释放。
文件传输:在文件传输中,零拷贝可以通过直接将文件从磁盘映射到内存中
管道和套接字通信:在进程间通信中,零拷贝可以通过使用共享内存(Shared Memory)或使用 sendfile/sendfilev 等系统调用来避免将数据从一个进程的地址空间复制到另一个进程的地址空间。
Volatile
- 它用于告诉编译器不要对某个变量进行优化,以确保每次访问该变量都从内存中读取,而不是使用缓存中的值。
- 常见于硬件寄存器访问和多线程编程
malloc函数
- 底层是如何实现的,会不会分配内存空间 以及如何分配内存空间
后者是内存管理方式 - 内存池初始化:当程序启动时,操作系统会分配一块内存池(memory pool),用于后续的内存分配请求。这个内存池通常包含物理内存的一部分。
- 分配内存:当调用
malloc函数时,会先在自身的堆空间中找一个没有被分配的足够大小的内存块,如果没找到,会调用sbrk函数将堆向上生长,扩大堆的区域。这个内存块的大小通常要大于请求的大小,因为malloc可能会分配4字节内存用于管理(例如,存储分配的内存块的大小等信息,通过这个内存块的大小计算出下个内存块的起始位置),这个存储内存块信息的部分一般在申请到的内存块的头部之前紧邻的位置存放,类似地,申请到的一个内存块在尾部也会有一个存放相关信息的区域(4字节),类似于双向链表,方便查找。
- 分配内存:当调用
- 返回指针:一旦操作系统成功分配了内存块,
malloc会返回一个指向该内存块起始地址的指针。这个指针可以用于访问和操作这块内存。
- 返回指针:一旦操作系统成功分配了内存块,
- 释放内存:当程序调用
free函数来释放先前分配的内存块时,内存块将被标记为未分配,以便将来的`malloc“调用可以再次使用这块内存。注意,释放内存并不会立即返还给操作系统,而是由C/C+运行时库来管理。
- 释放内存:当程序调用
待完成
- IPC
- 进程同步问题:生产者-消费者问题…
- 银行家算法
- 文件与文件系统、文件管理?
看这俩有没有,然后试一下命令行输入