其他分享
首页 > 其他分享> > 操作系统与计组面试复习

操作系统与计组面试复习

作者:互联网

操作系统

目录

基本知识

用户态与内核态

在实际运行过程中,处理机会在系统态和用户态间切换。相应地,现代多数操作系统将 CPU 的指令集分为

两个状态的切换

陷入指令(又称为访管指令,因为内核态也被称为管理态,访管就是访问管理态)

该指令给用户提供接口,用于调用操作系统的服务。

同步异步

并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步,指需要阻塞等待事件的完成才能进行下一步操作

异步是指不需要等待事件完成,可以在这期间做点别的事件,即非阻塞

并发并行

多个程序、交替执行的思想,就有 CPU 管理多个进程的初步想法;虽然单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发

死锁

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,于是两者都不能执行而处于永远等待状态

产生死锁的的四个条件如下

解决死锁

预防死锁

避免死锁

加锁顺序:当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。

加锁时限:加上一个超时时间,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。

死锁检测

银行家算法:

  1. 银行家算法是从当前状态出发,按照系统各类资源剩余量逐个检查各进程需要申请的资源量,找到一个各类资源申请量均小于等于系统剩余资源量的进程P1。
  2. 然后分配给该P1进程所请求的资源,假定P1完成工作后归还其占有的所有资源,更新系统剩余资源状态并且移除进程列表中的P1,进而检查下一个能完成工作的客户,......。
  3. 如果所有客户都能完成工作,则找到一个安全序列,银行家才是安全的。若找不到这样的安全序列,则当前状态不安全。

进程

我们编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」

进程是计算机中的程序关于某数据集合上的一次运行活动

一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。

进程控制块PCB

在操作系统中,是用进程控制块process control block,PCB)数据结构来描述进程的。

PCB是进程存在的唯一标志!

PCB内容

进程描述信息:

进程控制和管理信息:

资源分配清单:

CPU 相关信息:

底层结构

通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:

上下文切换

进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

同一进程的线程的上下文切换只需要切换线程的独立一套的寄存器和栈不共享的数据

线程Thread

线程是进程当中的一条执行流程,同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程都有独立一套的寄存器和栈,这样可以确保线程的控制流是相对独立的。

线程的优点:

线程的缺点:

线程与进程

线程与进程最大的区别在于:进程是资源拥有的基本单位,线程是调度的基本单位

线程的实现

主要有三种线程的实现方式:

线程上下文切换

所以,线程的上下文切换相比进程,开销要小很多。

资源竞争

线程是非独立的,同一个进程里线程是数据共享的,当各个线程访问数据资源时会出现竞争状态即:
数据几乎同步会被多个线程占用,造成数据混乱

线程安全

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

如何保证线程安全?

  1. 共享的资源加把,保证每个资源变量每时每刻至多被一个线程占用。

  2. 让线程也拥有资源,不用去共享进程中的资源。如: 使用threadlocal可以为每个线程的维护一个私有的本地变量。

  3. 让共享资源只能看,不能改

线程不安全就是两条都满足:共享和可变,只要打破其中一条即可。

单例模式

单例模式指在整个系统生命周期里,保证一个类只能产生一个实例,确保该类的唯一性

单例类特点

单例模式分为两种

在C++11内部静态变量的方式里是线程安全的,只会创建了一次实例

一般new会经过三个步骤

  1. malloc分配内存
  2. 强制类型转换
  3. 调用构造函数

由于编译器的优化以及运行时优化等等原因,使得instance虽然已经不是nullptr但是其所指对象还没有完成构造函数,这种情况下,另一个线程如果调用getInstance()就有可能使用到一个不完全初始化的对象。

C++11没有出来的时候,只能靠插入两个memory barrier(内存屏障)来解决这个错误,但是C++11引进了memory model,提供了atomic实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。

三种解决方法

//----------1.懒汉非线程安全----------
class SingleInstance
{
private:
    // 唯一单实例对象指针
    static SingleInstance *m_SingleInstance;
private:
	Singleton();
	~Singleton();
	Singleton(const Singleton&);
	Singleton& operator=(const Singleton&);
	
	void SingleInstance::deleteInstance();
public:
	SingleInstance* SingleInstance::GetInstance();

};
//初始化静态成员变量
SingleInstance *SingleInstance::m_SingleInstance = NULL;

SingleInstance* SingleInstance::GetInstance()
{

	if (m_SingleInstance == NULL)
	{
		m_SingleInstance = new (std::nothrow) SingleInstance;  // 没有加锁是线程不安全的,当线程并发时会创建多个实例
	}

    return m_SingleInstance;
}

void SingleInstance::deleteInstance()
{
    if (m_SingleInstance)
    {
        delete m_SingleInstance;
        m_SingleInstance = NULL;
    }
}

//---------2.加锁懒汉线程安全---------
。。。类内
private:
    // 唯一单实例对象指针
    static SingleInstance *m_SingleInstance;
    static std::mutex m_Mutex;
};

//初始化静态成员变量
SingleInstance *SingleInstance::m_SingleInstance = NULL;
std::mutex SingleInstance::m_Mutex;

SingleInstance *&SingleInstance::GetInstance()
{

    //  这里使用了两个 if判断语句的技术称为双检锁;
    //  避免每次调用 GetInstance的方法都加锁(如果加锁放在外面的话),锁的开销毕竟还是有点大的。
    if (m_SingleInstance == NULL) 
    {
    	//如果不双重加锁,线程A到这被中断,线程B进来加锁,实例化;此时线程A恢复现场,重新执行
    	//若没有再次判断,则线程A也加锁,实例化,那么就有两个单例了;
    	//若有再次判断,则线程A直接跳出这里;
        std::unique_lock<std::mutex> lock(m_Mutex); // 加锁
        if (m_SingleInstance == NULL)
        {
            m_SingleInstance = new (std::nothrow) SingleInstance;
        }
    }

    return m_SingleInstance;
}

void SingleInstance::deleteInstance()
{
    std::unique_lock<std::mutex> lock(m_Mutex); // 加锁
    if (m_SingleInstance)
    {
        delete m_SingleInstance;
        m_SingleInstance = NULL;
    }
}

//--------3.atomic懒汉线程安全-----------
atomic<Widget*> Widget::pInstance{ nullptr };
Widget* Widget::Instance() {
    Widget* p = pInstance;//用临时指针减少了atomic的开销
    if (p == nullptr) { 
        lock_guard<mutex> lock{ mutW }; 
        if ((p = pInstance) == nullptr) { 
            pInstance = p = new Widget(); 
        }
    } 
    return p;
}


//--------4.内部局部静态懒汉线程安全-----------只能在C++11后使用
Single &Single::GetInstance()
{
    // 局部静态特性的方式实现单实例
    static Single signal;
    return signal;
}


//----------5.饿汉式单例线程安全------------
private:
    // 唯一单实例对象指针
    static Singleton *g_pSingleton;
};

// 代码一运行就初始化创建实例 ,本身就线程安全
Singleton* Singleton::g_pSingleton = new (std::nothrow) Singleton;

Singleton* Singleton::GetInstance()
{
    return g_pSingleton;
}

void Singleton::deleteInstance()
{
    if (g_pSingleton)
    {
        delete g_pSingleton;
        g_pSingleton = NULL;
    }
}

STL容器线程安全

STL 语义上不提供任何强度的线程安全保证,以vector为例

多线程访问vector分为两种情况

调度

进程调度

页面调度

缺页异常(缺页中断)

当 CPU 访问的页面不在物理内存时,便会产生一个缺页中断,请求操作系统将所缺页调入到物理内存。那它与一般中断的主要区别在于:

preview

页面置换算法的功能是,当出现缺页异常,需调入新页面而内存已满时,选择被置换的物理页面,也就是说选择一个物理页面换出到磁盘,然后把需要访问的页面换入到物理页。

算法目标则是,尽可能减少页面的换入换出的次数

磁盘置换

磁盘调度算法的目的就是为了提高磁盘的访问性能

进程间通信

由于每个进程的用户空间都是独立的,不能相互访问,这时就需要借助内核空间来实现进程间通信,原因很简单,每个进程都是共享一个内核空间。

同一台主机

管道

特点:写入与获取的数据都是缓存在内核,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。

缺点:管道的通信方式是效率低的,因此管道不适合进程间频繁地交换数据。

消息队列

特点:解决管道不适合频繁沟通的问题

缺点:一是通信不及时,二是附件也有大小限制

共享内存

特点:可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,大大提高了通信的速度,享有最快的进程间通信方式之名

缺点:多进程竞争同个共享资源会造成数据的错乱

信号量

特点:保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问

信号

特点:进程间通信机制中唯一的异步通信机制,可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件

捕捉信号:当信号发生时,我们就执行相应的信号处理函数。

不同主机

socket

特点:Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信

根据创建 Socket 的类型不同,分为三种常见的通信方式,

线程间通信

同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信

所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步:

内存管理

早期,程序直接运行在物理内存上,直接操作物理内存,导致三个问题

虚拟内存

计算机系统里任何问题都可以靠引入一个中间层来解决,内存管理就在程序和物理内存之间引入了虚拟内存的概念;对进程地址和物理地址进行隔离;

进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存

虚拟地址空间

物理地址空间是有限的,虚拟地址空间可以是任意大小;

每个进程都有自己的虚拟空间,而物理内存只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术,把不常使用的内存暂时存放到硬盘(换出),在需要的时候再装载回物理内存(换入)。

程序可以通过操作虚拟地址,把虚拟地址空间映射到物理地址空间; Linux通过缺页中断和调度机制,实现虚拟地址映射;

虚拟地址优点:

分页和分段

虚拟地址和物理地址,主要通过分段分页技术,进行映射;

程序地址:段号+页号+页内偏移;

段和页的区别:

分段:将程序分为代码段、数据段、堆栈段等;

分页:将段分成均匀的小块,通过页表映射物理内存;

演变过程

页表实际上存储在 CPU 的内存管理单元MMU) 中

当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。

内存碎片

内存碎片:回收内存时,将内存块放入空闲链表中; 因内存越分越小,内存块小而多;当需要一块大内存时,尽管此时空闲内存综合可能满足需求,但过于零散,没有一个合适的内存块

内存碎片产生原因:分配内存时,不能将相邻内存合并;

解决内存碎片的方法:

大端模式和小端模式

"大端"和"小端"表示多字节值的哪一端存储在该值的起始地址处

//大端模式:高字节存储在低地址中,低字节存放在高地址中。符合人正常逻辑
低地址 -----> 高地址
0x12 | 0x34 | 0x56 
//小端模式:与大端相反,符合计算机逻辑    
低地址 -----> 高地址
0x56 | 0x34 | 0x12 

一开始是由于不同架构的CPU处理多个字节数据的顺序不一样

后来互联网流行,TCP/IP协议规定为大端模式;

大小端模式各有优势:

判断大小端

基本思想室根据数据截断来判断是大端还是小端

//根据强制类型转换
BOOL IsBigEndian()
 {
 	short a = 0x1234;
 	char b = *(char*)&a;
 	if(0x12 == b)
 	{
 	    return TRUE;
 	}
 	
	return FALSE;
  }
//利用联合体共享内存的特性,截取低地址部分
 BOOL IsBigEndian()
  {
 	union NUM
 	{
 	    short  a;
 	    char b;
 	}num;
 	
 	num.a = 0x1234;
 	
 	if(0x12 == num.b)
 	{
 	    return TRUE;
 	}
 	
 	return FALSE;
  }

大小端转换

通信协议中的数据传输、数组的存储方式、数据的强制转换等这些都会牵涉到大小端问题。如果字节序不一致,就需要转换

对于16位字数据

#define BigtoLittle16(A) (( ((uint16)(A) & 0xff00) >> 8) | \ (( (uint16)(A) & 0x00ff) << 8))

对于32位字数据

#define BigtoLittle32(A) ((( (uint32)(A) & 0xff000000) >> 24) | \ (( (uint32)(A) & 0x00ff0000) >> 8) | \ (( (uint32)(A) & 0x0000ff00) << 8) | \ (( (uint32)(A) & 0x000000ff) << 24))

TCP/IP采用大端字节序

由于不同的处理器可以配置成大端或者小端,使得不同主机之间的通信变得复杂。

如果存在数据网络传输,如果大小端模式不一致,如果不经过转换,必然会导致数据不致,出现错误。

为此,网络协议指定了字节序。TCP/IP协议栈采用大端字节序,所以应用程序有时需要再处理器的字节序与网络的字节序之间进行转换。

对于TCP/IP应用程序,提供了以下四个通用函数进行转换:

#include <arpa/inet.h>
uint16_t ntohs(n)     // 16位数据类型网络字节顺序到主机字节顺序的转换  
uint16_t htons(n)     // 16位数据类型主机字节顺序到网络字节顺序的转换  
uint32_t ntohl(n)     // 32位数据类型网络字节顺序到主机字节顺序的转换  
uint32_t htonl(n)     // 32位数据类型主机字节顺序到网络字节顺序的转换

计组

机器码

机器码分为原码,反码,补码, 0只有在补码中表示形式才是唯一的

真值->原码:转换为二进制,加符号位(纯小数的符号位为小数点左边原个位),0为正,1为负

真值->反码:先转换为原码,正数反码=原码,负数反码=原码除符号位取反

真值->补码:正数补码=原码,负数补码=反码+1

意义

通过将符号位也参与运算的方法。我们知道,根据运算法则减去一个正数等于加上一个负数,即:1-1 = 1 + (-1) = 0, 所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单了。

为了解决原码做减法的问题, 出现了反码:

于是补码的出现,解决了0的符号问题以及0的两个编码问题:

补码运算(+,-,*,/)与溢出判断

1.补码加减法:两个数均用补码表示,符号位也参与运算

当XY两个数异号,实际上做加法运算,不会溢出;当同号且结果为正且超过最大整数,为正溢,反之为负溢

2.补码乘除法

3.十进制整数的加法运算

1.8421码:逢二进一,当和大于9,+6校正

2.余三码:逢二进一,当和无进位则-3,有进位则+3

4.浮点数加减运算

  1. 对阶:阶码小的数的尾数右移,每右移一位,其阶码+1,直到两数的阶码相等位置
  2. 尾数加/减
  3. 尾数结果规格化:左规可以多次,右规只能一次
  4. 舍入
  5. 溢出判断:右规后,根据阶码符号
    1. 上溢,机器停止运算,做溢出中断处理
    2. 下溢,浮点数趋于零,机器按机器零处理

溢出检测方法

加减法:1.采用一个符号位 :X,Y为两个数的符号位,S为结果的符号位

溢出=XYS+XY~S

2.采用进位位 : Cs-符号位是否产生进位 C1-最高数值位产生的进位

溢出=Cs⊕C1

3.采用变形补码(双符号位补码)

S1S2=00:结果正数,无溢出 S1S2=01:结果正溢

S1S2=10:结果负溢 S1S2=11:结果负数,无溢出

标签:操作系统,内核,复习,SingleInstance,线程,内存,进程,CPU,计组
来源: https://www.cnblogs.com/AMzz/p/14731642.html