面试笔记(操作系统\c++\网络)

面试这么久了,感觉应该出个笔记,记录一下被问到的问题

操作系统

虚拟内存

为了对内存中的存储单元进行识别,内存中的每一个存储单元都必须有一个确切的地址。而一台计算机的处理器能访问多大的内存空间就取决于处理器的程序计数器,该计数器的字长越长,能访问的空间就越大。比如说,我的电脑是64位处理器,能访问的地址数目就是2^64=16G地址空间(处理器的寻址空间或寻址能力),但是计算机所配置内存的实际空间常常小于处理器的寻址范围,处理器的一部分寻址空间没有对应的物理存储单元,从而导致处理器寻址能力的浪费。

当应用程序比较大,计算机实际所配置的内存空间无法满足时,出现了如下方法:

  • 把要运行的那一段程序自辅存复制到内存中来运行,而其他暂时不运行的程序段就让它仍然留在辅存
  • 当需要执行另一段尚未在内存的程序段时,就把内存中程序段1的副本复制回辅存,在内存腾出必要的空间后,再把辅存中的程序段2复制到内存空间来执行

通过上述方法使得处理器看起来似乎是拥有了一个大于实际物理内存的内存空间。对于处理器运算器和应用程序设计人员来说,看到的只是虚拟内存空间和虚拟地址,而处理器片外的地址总线看到的只是物理地址空间和物理地址。

存储管理单元MMU操作系统的内存管理模块共同完成虚拟内存到物理内存的映射:

  • Linux把虚存空间分成若干个大小相等的存储分区(页)。物理内存也按相同大小分成若干个块。由于物理内存中的块空间是用来容纳虚存页的容器,所以物理内存中的块叫做页框(4K)
  • 存储单元原来的地址都被分成了两段,高位段叫做页码(页框码);低位段叫做页内偏移量(页框偏移量)
  • 使用页表记录页码和存放该页映像的页框码的映射关系,在由虚拟地址转化成物理地址的过程中,偏移值不变
    在这里插入图片描述

流程:
在这里插入图片描述
人们发现,系统一旦访问了某一个页,那么系统就会在一段时间内稳定地工作在这个页上。所以,为了提高访问页表的速度,系统还配备了一组正好能容纳一个页表的硬件寄存器,这样当系统再访问虚存时,就首先到这组硬件寄存器中去访问,系统速度就快多了。这组存放当前页表的寄存器叫做快表。
在这里插入图片描述
多级页表:

假设一个程序的虚拟空间为4GB,页表以4KB为一页,那么这个程序空间就是1M页。为了存储这1M页的页指针,这个页表的长度会变得很大。所以,最好对页表也进行分页存储,在程序运行时只把需要的页复制到内存,而暂时不需要的页就让它留在辅存中。为了管理这些页表页,还要建立一个记录页表页首地址的页目录表
在这里插入图片描述
Linux页表结构:

Linux系统使用了三级页表结构:页目录(Page Directory,PGD)、中间页目录(Page Middle Directory,PMD)、页表(Page Table,PTE)。

参考链接

软链接与硬链接

链接是一种文件共享的方式,是 POSIX 中的概念,主流文件系统都支持链接文件

类似Windows 中常见的快捷方式(或是 OS X 中的替身),Linux 中常用它来解决一些库版本的问题,通常也会将一些目录层次较深的文件链接到一个更易访问的目录中。

在这些用途上,我们通常会使用到软链接(也称符号链接)

区别:
从使用的角度讲,两者没有任何区别,都与正常的文件访问方式一样,支持读写,如果是可执行文件的话也可以直接执行
区别在于底层实现:

  • 给myfile创建硬链接
ln myfile hard

在这里插入图片描述
最左边一列,是文件的 inode 值,它指向了物理硬盘的一个区块。可以看到,这两个inode值一样,指向了同一区块。修改hard,再查看myfile,可以看到确实指向同一区块。
在这里插入图片描述

  • 给myfile创建软连接
ln -s myfile myfilesoft

在这里插入图片描述
inode值不一样了。其实,软链接的 inode 所指向的内容实际上是保存了一个绝对路径。

当删除myfile时,查看hard,仍然能看到这个区块上的内容,而soft已经无法查看了,这是因为这个绝对路径上的文件被删除了。再向soft里添加内容,可看到又出现了myfile文件,但是这时的myfile文件和hard不再是同一个inode

参考链接 顺带看了一眼链接里面的知乎问题,挺迷惑的…

大小端

计算机硬件有两种储存数据的方式:大端字节序(big endian)和小端字节序(little endian)。举例来说,数值0x12345678共有四个字节,从高到低为12 34 56 78

大端:高位字节在低地址
小端:高位字节在高地址
参考链接参考链接

  • 大端:基于其存储特点,符号位在所表示的数据的内存的第一个字节中,便于快速判断数据的正负和大小(CPU做数值运算时从内存中依顺序依次从低位地址到高位地址取数据进行运算,大端就会最先拿到数据的(高字节的)符号位))
  • 小端:基于其存储特点,内存的低地址处存放低字节,所以在强制转换数据时不需要调整字节的内容(比如,把int—4字节强制转换成short—2字节,就可以直接把int数据存储的前两个字节给short就行,因为其前两个字节刚好就是最低的两个字节,符合转换逻辑;另外CPU做数值运算时从内存中依顺序依次从低位地址到高位地址取数据进行运算,开始只管取值,最后刷新最高位地址的符号位就行,这样的运算方式会更高效一些)
    参考链接
int main(){
    int x = 0x12345678;
    short y = short(x);
    cout << y << endl; //输出22136,即0x5678
    return 0;
}
  • 为什么要有大小端
    不同的CPU厂商并没有达成一致:
  1. x86,MOS Technology 6502,Z80,VAX,PDP-11等处理器为Little endian
  2. Motorola 6800,Motorola 68000,PowerPC 970,System/370,SPARC(除V9外)等处理器为Big endian
  3. ARM, PowerPC (除PowerPC 970外), DEC Alpha, SPARC V9, MIPS, PA-RISC and IA64的字节序是可配置的

主机字节序不管是大端还是小端并没有关系。问题是,网络的出现使得计算机可以通信了。通信,就意味着相处,相处必须得有共同语言啊,得说普通话,要不然就容易会错意,下了一个小时的小电影发现打不开,理解错误了!(参考链接 这个作者说话害挺有意思

TCP/IP协议规定使用“大端”字节序为网络字节序,其他不使用大端的计算机发送数据的时候必须要将自己的主机字节序转换为网络字节序(即“大端”字节序),接收到的数据再转换为自己的主机字节序。这样就与CPU、操作系统无关了,实现了网络通信的标准化。BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl

用户态vs内核态

在这里插入图片描述

  • 内核态

内核是一种特殊的软件程序,能够控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行。

  • 用户态

用户态就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O。内核必须提供一组通用的访问接口,这些接口就叫系统调用。

  • 系统调用

系统调用是操作系统的最小功能单位。根据不同的应用场景,不同的Linux发行版本提供的系统调用数量也不尽相同,大致在240-350之间。这些系统调用组成了用户态跟内核态交互的基本接口。

参考链接

上下文切换

在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮它设置好CPU 寄存器和程序计数器

  • CPU上下文
    CPU 寄存器:CPU 内置的容量小、但速度极快的内存
    程序计数器:用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置
    CPU 寄存器和程序计数器就是 CPU 上下文,因为它们都是 CPU 在运行任何任务前,必须的依赖环境

  • CPU上下文切换
    先把前一个任务的 CPU 上下文保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来

  • CPU 上下文切换的类型

  1. 进程上下文切换

进程的运行空间分为内核空间和用户空间,内核空间(Ring 0)具有最高权限,可以直接访问所有资源;用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。

进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。

  1. 线程上下文切换

进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的。线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

  1. 中断上下文切换

为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。

参考链接

linux常用命令

更改文件权限chmod

  • chmod (数字 文件)
    操作文件或目录的用户,有3种不同类型:文件所有者、群组用户、其他用户
    文件或目录的权限分为3种:只读、只写、可执行。
    在这里插入图片描述
    依照上面的表格,权限组合就是对应权限值求和,如下:
    7 = 4 + 2 + 1 读写运行权限
    5 = 4 + 1 读和运行权限
    4 = 4 只读权限

例子:
chmod 754 filename
将filename文件的读写运行权限赋予文件所有者,把读和运行的权限赋予群组用户,把读的权限赋予其他用户

参考链接

  • chmod +x 文件
    给文件给执行权限

  • chmod +x 和 chmod a+x
    在这里插入图片描述

参考链接

重定向

cmd > file  #以擦写的模式重定向至
cmd >> file  # 以追加的模式重定向至
# 1 代表stdout标准输出
# 2 代表stderr标准错误
cmd &1> file  #只把标准输出重定向到file
cmd &2> file   #只把标准错误重定向到file
cmd > file 2>&1 #把stderr标准错误重定向到stdout标准输出,然后两个并在一起再重定向。其中&只是区分1是代表stdout而不是代表一个文件名
command &> output.txt #把stderr标准错误和stdout标准输出都重定向到ouput

参考链接

僵尸进程与孤儿进程

  • 当父进程退出时,它的子进程们(一个或者多个)就成了孤儿进程了。父进程退出后,子进程被一个进程 ID 为 1 的进程领养,进程 id 为 1 的进程是 init 进程,每当有孤儿进程出现时,init 进程就会收养它并成为它的父进程。(无危害)
  • 子进程先退出,而父进程又没有去处理回收释放子进程的资源,这个时候子进程就成了僵尸进程。(占用不了什么资源。但是通常系统的进程数量都是有限制的,如果有大量的僵尸进程占用进程号,会导致新的进程无法创建)

参考链接

死锁

  • 发生死锁的必要条件:
  1. 互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。
  2. 请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。
  3. 非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。
  4. 循环等待条件(Circular wait):系统中若干进程组成环路,改环路中每个进程都在等待相邻进程正占用的资源。
  • 预防
  1. 破坏不可剥夺条件
    允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程(会降低系统性能)
  2. 破坏请求与保持条件
    可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程(资源利用率低,降低了进程的并发性)
  3. 破坏循环等待条件
    实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁(给系统中所有资源合理编号也是件困难事,并增加了系统开销)
  • 避免

c/c++

list用法

参考链接 很全面

多态&虚函数&虚函数表

C++多态通过虚函数来实现的,虚函数允许子类重新定义成员函数,子类重新定义父类的做法称为覆盖(override),或者称为重写。

声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,进行动态绑定。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数

例子:

#include <bits/stdc++.h>
using namespace std;

class shape{
public:
    float a;
    float h;
    shape(){
        cout << "调用基类无参构造函数" << endl;
    }
    shape(float _a, float _h) : a(_a), h(_h){
        cout << "调用基类带参构造函数" << endl;
    }
    virtual ~shape(){
        cout << "调用基类析构函数" << endl;
    }
    void fun(){
        cout << "调用基类fun函数" << endl;
    }
    virtual void show(){
        float area = a * h;
        cout << "shape area: " << area << endl;
    }
};

class triangle: public shape{
public:
    triangle(){
        cout << "调用子类无参构造函数" << endl;
    }
    triangle(float _a, float _h): shape(_a, _h){
        cout << "调用子类带参构造函数" << endl;
    }
    ~triangle(){
        cout << "调用子类析构函数" << endl;
    }
    void fun(){
        cout << "调用子类fun函数" << endl;
    }
    void show(){
        float area = a * h / 2;
        cout << "triangle area: " << area << endl;
    }
};

int main() {
    shape* shp = new shape(3, 5);
    shape *trig = new triangle(5, 9);
    triangle *trig2 = new triangle(3, 9);
    shape *trig3 = new triangle();
    shp->fun();
    shp->show();
    trig->fun();
    trig->show();
    trig2->fun();
    trig2->show();
    trig3->fun();
    trig3->show();
    delete (shp);
    delete (trig);
    delete (trig2);
    delete (trig3);
}

在这里插入图片描述

涉及的点:

a. 基类与子类的构造函数

  • 基类未声明构造函数、
  1. 子类未声明构造函数,均由编译器生成默认的构造函数
  2. 子类声明构造函数,可以写成任何形式。创建子类对象时,先调用父类默认的构造函数(编译器自动生成),再调用子类的构造函数
  • 基类声明了构造函数
  1. 基类只声明无参构造函数
    子类可以隐式调用基类构造函数
  2. 基类只声明带参构造函数
    子类必须显示调用基类构造函数,即上图的triangle(float _a, float _h): shape(_a, _h){}
  3. 基类声明了无参和带参构造函数
    子类实现一个构造函数即可。如果子类的构造函数没有显示地调用父类的构造函数(无参或带参),则默认调用父类的无参构造函数

参考链接

b. 基类与子类的析构函数

  • 当基类没有子类时,析构函数不是虚函数
  • 当基类有子类时,析构函数必须是虚函数,这样子类才能重写析构函数,在析构类时,先析构子类,再析构基类

c. 虚函数表

mmap函数

除了标准的文件 IO,例如 open, read, write,内核还提供接口允许应用将文件 map 到内存。使得内存中的一个字节与文件中的一个字节一一对应。
使用场景:通常在需要对文件进行频繁读写时使用,用内存读写取代I/O读写,以获得较高的性能;

#include <sys/mman.h>

//addr:这个参数是建议地址(hint),没有特别需求一般设为0
//len:文件长度。
//prot:权限
//flags:描述了映射的类型。
//MAP_FIXED:开启这个选项,则 addr 参数指定的地址是作为必须而不是建议。如果由于空间不足等问题无法映射则调用失败。不建议使用。
//MAP_PRIVATE:表明这个映射不是共享的。文件使用 copy on write 机制映射,任何内存中的改动并不反映到文件之中。也不反映到其他映射了这个文件的进程之中。如果只需要读取某个文件而不改变文件内容,可以使用这种模式。
//MAP_SHARED:和其他进程共享这个文件。往内存中写入相当于往文件中写入。会影响映射了这个文件的其他进程。与 MAP_PRIVATE冲突
//fd:文件描述符。进行 map 之后,文件的引用计数会增加。因此在 map 结束后关闭 fd,进程仍然可以访问它。当我们 munmap 或者结束进程,引用计数会减少
//offset:文件偏移,从文件起始算起
//返回:一个实际 map 的地址。
void * mmap (void *addr,
             size_t len,
             int prot,
             int flags,
             int fd,
             off_t offset);
   
int munmap(void *addr, size_t length);

在这里插入图片描述

参考链接
参考链接, 这里有几个使用mmap的实例

内存释放free函数如何知道要释放多少内存

malloc如何工作

使用malloc分配内存时候根据参数指定的大小,分配一块内存,然后返回这块内存的起始位置给调用者(返回给调用者一个指针p)。但这个指针并不是真正的起始位置,真正的指针在malloc返回指针 p 的前面,内存分配器在 p 的前面用两个字节的空间来存放分配的内存大小信息。

参考链接
从参考链接内容可以看到,在p的前面16字节处存放分配的内存大小

因此内存释放free函数可以直接通过该值释放相应的内存

程序如何根据变量名在内存中找到存放这个变量的地址

  • C++对变量名不作存储,在汇编以后不会出现变量名;
  • 变量名作用只是用于方便编译成汇编代码,方便人阅读

内联函数和宏定义有什么区别

  1. 内联函数在编译时展开,而宏在预编译时展开
  2. 在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换
  3. 内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能
  4. 宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。

线程安全

当多个线程同时去访问一个对象时,就可能会出现线程安全问题。
多个线程访问了相同的资源,如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。

STL如何解决线程安全


  1. 多读少写的场景可以用读写锁(也叫共享独占锁)

进程间通信方式

  1. 共享内存:最快的一种 IPC,结合信号量使用

在这里插入图片描述

//成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数
//调用失败返回-1
int shmget(key_t key, size_t size, int shmflg);

//shm_id是由shmget()函数返回的共享内存标识
//shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址
//shm_flg是一组标志位,通常为0
//调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1
void *shmat(int shm_id, const void *shm_addr, int shmflg);

//ipcs -m可得到上述创建的共享内存的shm_id

//shm_id是由shmget()函数返回的共享内存标识
//command是要采取的操作,它可以取下面的三个值 
int shmctl(int shm_id, int command, struct shmid_ds *buf);

参考链接

  1. 消息队列
  • 消息队列是面向记录,其中的消息具有特定的格式以及特定的优先级
  • 消息队列独立于发送和接收进程,进程终止时,消息队列机器内容并不会被删除
  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取
  • 有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息
  • 消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点
//创建消息队列
//key表示指定的物理地址
//msgflg选项如下:
//IPC_CREATE(如果消息队列对象不存在,则创建。否则打开) 
//IPC_EXCL(与IPC_CREATE一起使用时用|连接,如果消息对象不存在创建,否则产生一个错误并返回)
//0644 (为消息队列赋予的权限值,与IPC_CREATE一起使用时用|连接)
//返回值 非-1:成功(消息队列标示符) -1:失败
int msgget(key_t key,int msgflg);

//将消息送入消息队列
//msgid表示消息队列对象标示符
//msgb表示发送消息所在内存
//msgb_sz表示发送消息的长度
//msgflg 0:表示阻塞方式 ,IPC_NOWAIT表示非阻塞方式
//返回值 0:成功 -1:失败
int msgsnd(int msgid,struct msgbuf* msgb,int msgb_sz,int msgflg);

//从消息队列中读出一条新消息
//msgid表示消息队列对象标示符
//ptr表示消息缓冲区指针
//ptr_sz表示消息长度
//type表示从队列中返回哪条消息
//            =0 返回消息队列中第一条消息
//            >0 返回消息队列中等于mtype 类型的第一条消息。
//            <0 返回mtype<=type 绝对值最小值的第一条消息。
//msgflg 0:表示阻塞方式 ,IPC_NOWAIT表示非阻塞方式
//返回值 0:成功 -1:失败
int msgrcv(int msgid,void *ptr,size_t ptr_sz,long type,int mgsflg);

//控制对消息队列的操作
//msgid表示消息队列对象标示符
//cmd表示对消息队列的操作,选项如下:
//IPC_STAT 取出系统保存的消息队列的msqid_ds 数据,并将其存入参数buf 指向的msqid_ds 结构中
//IPC_SET  设定消息队列的msqid_ds 数据中的msg_perm 成员。设定的值由buf 指向的msqid_ds结构给出
//IPC_RMID 将队列从系统内核中删除。
//buf表示msqid_ds 数据
//返回值 0:成功 -1:失败
int msgctl(int msgid,int cmd,struct msqid_ds * buf);

参考链接

  1. 管道

匿名管道(父子进程)

#include <unistd.h>

//filedes[0]用于读出数据,读取时必须关闭写入端,即close(filedes[1]);
//filedes[1]用于写入数据,写入时必须关闭读取端,即close(filedes[0])。
int pipe(int filedes[2]);
int main(void)
{
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];
   
    if(pipe(fd) != 0){                 /* 先建立管道得到一对文件描述符 */
        exit(0);
    }

    if((pid = fork()) < 0)            /* 父进程把文件描述符复制给子进程 */
        exit(1);
    else if(pid > 0){                /* 父进程写 */
        close(fd[0]);                /* 关闭读描述符 */
        write(fd[1], "\nhello world\n", 14);
    }
    else{                            /* 子进程读 */
        close(fd[1]);                /* 关闭写端 */
        n = read(fd[0], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }

    exit(0);
}

有名管道:可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *filename, mode_t mode);
int mknode(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0 );
#include <stdio.h>  
#include <stdlib.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
      
int main()  
{  
    int res = mkfifo("/tmp/my_fifo", 0777);  
    if (res == 0)  
    {  
        printf("FIFO created/n");  
    }  
     exit(EXIT_SUCCESS);  
}

参考链接

  1. socket

在这里插入图片描述

线程间通信方式(锁)

同一进程的不同线程共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段,所以线程之间可以方便、快速地共享信息。

  1. 互斥锁
#include <pthread.h>

pthread_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
...
pthread_mutex_unlock(&mutex);
pthread_mutex_detroy(&mutex);
  • 死锁

一个线程需要访问两个或者更多不同的共享资源,而每个资源又有不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就可能发生死锁。 死锁就是指多个线程/进程因竞争资源而造成的一种僵局(相互等待),若无外力作用,这些进程都将无法向前推进。

死锁的处理策略:
1、预防死锁:破坏死锁产生的四个条件:互斥条件、不剥夺条件、请求和保持条件以及循环等待条件。
2、避免死锁:在每次进行资源分配前,应该计算此次分配资源的安全性,如果此次资源分配不会导致系统进入不安全状态,那么将资源分配给进程,否则等待。算法:银行家算法。
3、检测死锁:检测到死锁后通过资源剥夺、撤销进程、进程回退等方法解除死锁。

  1. 读写锁

读写锁有3种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

当读写锁是写加锁状态时,在这个锁被解锁之前,所有线程对这个锁加锁都会被阻塞
当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止

pthread_rwlock_t q_lock;
pthread_rwlock_init(&q_lock, NULL);
pthread_rwlock_rdlock(&q_lock);
...
pthread_rwlock_unlock(&q_lock);
pthread_rwlock_detroy(&q_lock);
  1. 条件变量

互斥量用于上锁,条件变量则用于等待,并且条件变量总是需要与互斥量一起使用,运行线程以无竞争的方式等待特定的条件发生

pthread_mutex_t mutex;
pthread_cond_t cond;
...
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
...
pthread_mutex_unlock(&mutex);
  1. 信号量

信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量就增加;公共资源减少的时候,信号量就减少;只有当信号量的值大于0的时候,才能访问信号量所代表的公共资源

#include <semaphore.h>

sem_t sem_event;
int sem_init(sem_t *sem, int pshared, unsigned int value);//初始化一个信号量 
int sem_destroy(sem_t * sem);//销毁信号量
int sem_post(sem_t * sem);//信号量增加1
int sem_wait(sem_t * sem);//信号量减少1
int sem_getvalue(sem_t * sem, int * sval);//获取当前信号量的值

参考链接

struct和union有什么区别

字节对齐

内存泄漏vs野指针vs内存溢出

  • 正常使用内存流程:

当程序向系统申请一块内存空间时候,程序会把申请号好的这块内存空间的首地址赋给变量a,当这块空间使用完成被free释放了之后,这块空间的使用权又被程序还给了系统。 将指针变量赋值给Null。

  • 内存泄漏:内存记录上可以使用的内存的记录缺少了一块,这一块本来是应该在记录里面的

当变量a申请一块内存的时候, 需要malloc函数去分配内存空间,这样变量就可以使用了,当时使用完成了之后,也就是这个变量已经销毁了,但是它对应的a内存并没有释放,这个时候a对应的内存所有权还是归为应用程序,所以,这个内存不会被其他的变量所访问。系统并没有获得这个变量的所有权,浪费了内存,导致可以使用的内存减少

  • 野指针:内存交还给系统的时候,指向这块内存的变量还没有置为NULL

当变量a申请一块内存的时候, 使用malloc函数去分配内存空间,这样变量就可以使用了,当时使用完成了之后,这个变量已经销毁了,对应的a内存已经释放,应用程序将这块内存的所有交回给了系统,但是变量a所获得的内存地址还是原来的对应的内存地址,并没有置位NULL;导致我们再次使用a这个变量的时候,应用程序并没有获得访问这个地址的所有权,或者说这个变量已经不能够访问了。需要将变量所指向的地址为NULL

  • 内存溢出

程序申请内存时,没有足够的内存供申请者使用。
还有一种情况时缓冲区溢出,比如我定义了一个数组,然后使用strcpy函数进行复制(strcpy函数会一直复制内存直到遇到’\0’)

int MAX_LENGTH = 16;
char destination[MAX_LENGTH];
string src = ...
strcpy(destination, src.c_str());

如果source长于16字节,那么就会修改到destination之外的内存。很多平台的栈变量是按地址顺序倒着分配的。所以destination溢出以后可能会修改先前定义的变量,这也是缓冲区溢出攻击。

参考链接-内存泄漏与野指针
参考链接-内存溢出

网络

输入一个url,发生了什么

get和post有什么区别

接口请求的六种常见方式

  1. Get
    向特定资源发出请求(请求指定页面信息,并返回实体主体)
  2. Post
    向指定资源提交数据进行处理请求(提交表单、上传文件),又可能导致新的资源的建立或原有资源的修改
  3. Put
    向指定资源位置上上传其最新内容(从客户端向服务器传送的数据取代指定文档的内容)
  4. Head
    与服务器索与get请求一致的相应,响应体不会返回,获取包含在小消息头中的原信息(与get请求类似,返回的响应中没有具体内容,用于获取报头)
  5. Delete
    请求服务器删除request-URL所标示的资源*(请求服务器删除页面)
  6. opions
    返回服务器针对特定资源所支持的HTML请求方法 或web服务器发送*测试服务器功能(允许客户端查看服务器性能)

GET,POST,PUT,DELETE可看作对资源的查 ,改 ,增 ,删 4个操作

参考链接

get与post的区别

  • GET
    “读取“一个资源。比如Get到一个html文件。反复读取不应该对访问的数据有副作用,比如”GET一下,用户就下单了,返回订单已受理“,这是不可接受的。没有副作用被称为“幂等“(Idempotent)。GET是读取,可以对GET请求的数据做缓存。这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),或者做到server端(用Etag,至少可以减少带宽消耗)
  • POST
    在页面里< form > 标签会定义一个表单。点击其中的submit元素会发出一个POST请求让服务器做一件事。这件事往往是有副作用的,不幂等的。意味着不能随意多次执行,因此也就不能缓存。比如通过POST下一个单,服务器创建了新的订单,然后返回订单成功的界面。这个页面不能被缓存。试想一下,如果POST请求被浏览器缓存了,那么下单请求就可以不向服务器发请求,而直接返回本地缓存的“下单成功界面”,却又没有真的在服务器下单。

参考链接 看了这个链接还是有点懵

OSI七层协议,传输层网络层都有哪些协议

其他

如果在一个页面输入注册信息,返回错误,再在这个页面输入注册信息时显示账户已注册,可能原因有哪些

用一个vector存储一个歌单,想要实现随机播放,有哪些方式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值