复习笔记-操作系统

操作系统

文章目录

        • 操作系统
        • 1. 进程和线程的概念,进程和线程的用处(为什么要用进程和线程),进程和线程的区别,进程和线程的各自同步。
        • 2. Linux虚拟地址空间
        • 3. 操作系统中程序在内存中的结构
        • 4. 操作系统的缺页中断
        • 5. fork和vfork的区别
        • 6. 修改文件最大句柄数
        • 7. 并发(concurrency)和并行(parallelism)
        • 8. 查看并修改MySQL端口号
        • 9. 操作系统的页表寻址
        • 10. 有了进程为什么要有线程
        • 11. 单核机器上写多线程程序是否需要考虑加锁
        • 12. 线程需要哪些上下文,SP,PC,EAX寄存器的作用
        • 13. 线程间同步的方式,具体的系统调用
        • 14. 多线程和多进程的不同
        • 16. 游戏服务器应该为每个用户开辟一个线程还是一个进程?
        • 17. OS缺页置换算法
        • 19. 多线程和多进程的使用场景
        • 20. 死锁发生的条件和解决死锁的方法
        • 22. 操作系统的结构体对齐、字节对齐
        • 24. 虚拟内存置换的方式(补充#17)
        • 26. 互斥锁(mutex)机制,互斥锁,读写锁的区别
        • 28. 进程状态转换图,动态就绪,静态就绪,动态阻塞,静态阻塞
        • 29. A* a = new A; a->i=10内存分配
        • 30. 给一个类,里面有static,virtual,说说这个类的内存分布
        • 31. 软链接和硬链接的区别
        • 32. 大端和小端
        • 33. 静态变量初始化
        • 34. 内核态和用户态
        • 35. 设计server,使得能接收多个客户端的请求
        • 36. 死循环+来连接时新建线程的方法效率有点低,如何改进
        • 37. 如何唤醒被阻塞的socket线程
        • 38. 确定当前线程是繁忙还是阻塞
        • 40. 就绪状态的进程在等待什么
        • 41. 多线程同步,锁机制
        • 42. 两个进程访问临界区资源: 单核cpu,并且开了抢占可以造成这种情况。
        • 44. Windows消息机制
        • 45. c++锁:包括互斥锁,条件变量,自旋锁和读写锁
        • 49. 内存溢出和泄漏
        • 50. 【补充】 常用的线程模型
        • 51. 协程
        • 52. 系统调用
        • 54. 【补充】用户态到内核态的转化原理
        • 55. 源码到可执行文件的过程
        • 56. 微内核、宏内核
        • 57. 僵尸进程
        • 59. 5种IO模型
        • 60. 异步编程的事件循环
        • 61. 操作系统为什么分内核态和用户态:安全性
        • 62. 为什么要page cache,以及操作系统如何设计page cache
        • 62. server端监听端口,但还没客户端连接进来,此时进程处于什么状态
        • 69. 实现线程池
        • 70. linux下得到一个文件的100行到200行
        • 71. awk用法
        • 72. linux内核Timer定时器
        • 操作系统附加知识
        • 1. 线程池、内存池
      • 数据库基础
        • 1. 数据库事务,四个特性
        • 2. 数据库的三大范式
        • 4. 数据库索引
        • 2. 数据库的三大范式
        • 4. 数据库索引

1. 进程和线程的概念,进程和线程的用处(为什么要用进程和线程),进程和线程的区别,进程和线程的各自同步。

  • 概念:进程是对运行时程序的封装,是系统资源调度和分配的基本单位,实现OS的并发。线程是进程的子任务,是CPU调度和分派的基本单位,是操作系统可识别的最小执行和调度单位,每个线程独占一个虚拟处理器,独自的寄存器组,指令计数器和处理器状态。每个线程完成不同任务,但是共享同一个地址空间(同样的动态内存,映射文件,目标代码),打开的文件队列和内核资源。

  • 区别:

    • 线程属于进程,一个进程可以有多个线程,线程依赖于进程;
    • 进程拥有独立的内存单元,多个线程共享进程的内存、资源。资源:共享代码段(代码+常量),数据段(全局变量+静态变量),扩展段(堆存储),但是每个线程拥有自己的栈段(运行时段,用来存放局部变量和临时变量);
    • 进程是资源分配的最小单位,线程是CPU调度的最小单位;
    • 系统开销,进程创建和撤销的开销主要是分配和回收资源(内存空间、IO设备),进程切换的时候涉及进程CPU环境保存、新北调入的CPU环境的设置。但是线程切换只涉及少量寄存器。
    • 通信:由于同一个进程中多个线程具有相同的地址空间,这样他们之间的通信和同步实现相对较为简单。进程间IPC通信,线程间可以直接读写进程数据段来通信,期间需要进程同步和互斥
    • 进程开销大,但是调试简单,线程开销小,但是调试难;
    • 进程间互不干扰,但是若线程挂掉,会导致整个进程挂掉。
    • 进程适合多核多机,线程适合多核。
  • 进程间通信方式:

    进程间通信方式有:管道、系统IPC(消息队列、信号量、信号、共享内存等),套接字socket. --> Inter-Process Communication

    • 管道:普通管道PIPE,命名管道FIFO

    • 系统IPC

      • 消息队列,由标记符(即队列ID)来标记,详细队列面向记录,并且消息具有特定格式和优先级,消息队列独立于发送和接收进程,消息队列可以实现消息的随机查询。
      • 信号量semaphore,是一个计数器,用来控制多个进程对共享资源的访问,用于实现进程间的互斥和同步。信号量是基于操作系统的PV操作,P(通过)V(释放)
      • 信号signal,信号用于通知接收进程某个事件已经发生。
      • 共享内存(shared memory),多个进程访问同一块内存空间,不同进程可以及时看到对方的进程中对共享内存数据的更新,需要同步操作(互斥锁、信号量)
      • 特点:共享内存是最快的IPC;因为多个进程同时操作,因此需要同步;信号量+共享内存;
    • 套接字socket,用于各主机间进程的通信。

  • 线程间通信方式

    • 临界区,多线程串行化访问公共资源
    • 互斥量 synchronized/lock
    • 信号量semphare, 为控制具有有限数量的用户资源而设计的,允许同一时刻多个线程访问同一个资源,但是会限制同一时刻访问此资源的最大线程数量。
    • 事件(信号),wait/notify,通过通知操作的方式来保持多线程同步,并且也可以实现多线程的优先级。

2. Linux虚拟地址空间

目的:为了防止不同进程在同一时刻在物理内存中运行而对物理内存产生争夺和践踏。

虚拟地址空间和虚拟内存

虚拟内存技术:由于CPU(地址总线32位)寻址空间是4GB,因此每个进程在运行的过程中可以独占4GB的内存,每个进程只能将自己需要的虚拟内存空间在运行时动态的从外存加载到物理内存上。若物理内存上没找到程序对应的数据,会发生缺页异常(中断),然后通过存储器映射(虚拟内存与磁盘文件之间的映射)从磁盘上加载数据。

请求分页系统、请求分段系统、请求段页式系统都是针对虚拟内存,通过请求实现内存和外存的信息置换。

虚拟内存的好处:

  • 扩大地址空间。准确的说,32位的CPU地址空间是固定,4GB。这里扩大的是虚拟内存,将讲部分外存映射到虚拟内存上。
  • 内存保护,每个进程运行在各自的虚拟内存地址空间,互不干扰。
  • 公平分配内存,每个进程拥有相同大小的虚存空间。
  • 进程通信时可以通过共享虚存实现。
  • 不同进程可以在物理内存中共享相同的代码,只需通过虚存将地址都映射到这个物理内存地址上即可。
  • 虚拟内存适合在多道程序设计系统中使用。
  • 在虚拟地址空间分配连续地址,但是在物理内存中可以不连续,因此可以利用碎片。

虚拟内存的坏处:

  • 虚存管理需要建立很多数据结构,这些结构会占用额外内存
  • 虚拟地址到物理地址的转换,增加指令的执行时间
  • 页的换入换出需要磁盘IO----------->这点加上第二点其实是在讨论发生缺页中断时,需要将未命中的页从磁盘调人内存的过程。

3. 操作系统中程序在内存中的结构

一个程序本质上是由bss,data,text段三个组成,一个可执行程序在存储(没调入到内存时)时,分为代码段、数据区和未初始化数据区三个部分。

  • bss段(未初始化数据区):用来存放程序中未初始化的全局变量和静态变量的一块内存区域。bss段属于静态分配,程序结束后静态变量资源由系统自动释放。
  • 数据段:存放程序中已经初始化的全局变量的一块内存区域,数据段也属于静态内存分配。
  • 代码段:存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且属于只读区域,当然在代码段中也可能包含一些只读的常数变量。
  • text段和data段在编译时分配空间,bss不占可执行文件大小,由链接器获取内存
  • bss段(未进行初始化的数据)的内容并不存放在磁盘的程序文件中,由内核在程序开始运行前将他们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。
  • data段(已经初始化的数据)则为数据分配空间,数据保存在目标文件中。

可执行文件在运行时会多出两个区域:栈区和堆区;

  • 栈:由编译器自动释放,存放函数的参数和局部变量。
  • 堆:用于动态分配内存。堆是从低地址向高地址位增长,采用链式存储结构。

4. 操作系统的缺页中断

malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,但是没有分配虚拟内存对应的物理内存,当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。

缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的内存页不在内存中,会产生一次缺页中断。操作系统通过外存地址在外存地址在外存中找到所缺的页,将其调入内存。

缺页本身是一种中断,与一般中断一样,需要四个步骤:

  1. 保护CPU现场
  2. 分析中断原因
  3. 转入缺页中断处理程序进行处理
  4. 恢复CPU现场,继续执行

缺页中断是由于所访问的页面不在内存中,由硬件所产生的特殊中断。

2020年3月2日23:10:08

5. fork和vfork的区别

fork: 调用fork创建的新进程与父进程一模一样,然后使用exec载入二进制映像,替换当前进程的映像。

早期Unix系统,在创建进程时,内核会将所有的数据复制一份,复制进程的页表项,然后把父进程的地址空间的内容逐页复制到子进程的地址空间中。现在Unix/Linux系统采用写时复制的方法。

vfork:

  • 问题:fork后立即执行exec造成地址空间浪费。
  • vfork会挂起父进程直至子进程终止或者运行了一个新的可执行文件的映像。通过这个方式,vfork避免地址空间的按页复制,父进程和子进程共享相同的地址空间和页表项。vfork只完成一件事:复制内部的内核数据结构,这里子进程不能修改地址空间中的任何内存。

写时复制:

  • 目的:减少fork对父进程空间整体复制带来的开销。

  • 原理:若多个进程要读取资源时,每个进程保存一个指向这个资源的指针,当进程需要修改那份资源时,会复制那份资源,然后将复制的那份提给进程。复制的过程是透明的。

  • 好处:尽量推迟代价高昂的操作,直到必要时刻才会去执行。

  • 实现:在虚拟内存下,COW是以页为基础的,在内核里,内核页相关的数据结构被标记为只读和写时复制。当进程尝试修改页时,会产生缺页中断。内核处理这个缺页中断的方式是对该页进行一次透明复制,然后清除页面的COW属性,表示它不再被共享。

  • 现代计算机系统结构中在内存管理单元(MMU)提供了硬件级别的写时复制支持。

  • 调用fork时,大量fork之后会跟着执行exec,若复制整个父进程的内容到子进程的地址空间将完全是浪费时间,并且子进程会立刻执行新的二进制可执行文件映像,先前地址空间会被交换出去。而COW恰好对此场景做了优化。

  • fork和vfork的区别:

    • fork拷贝父进程的数据段+代码段,vfork:共享数据段;
    • fork父子进程的执行次序不确定,vfork保证子进程先运行(在调用exec或exit之前与父进程数据是共享的,在调用exec或exit之后父进程才能继续调度运行);可能会有deadlock;

6. 修改文件最大句柄数

linux默认最大文件句柄数1024。修改最大句柄数有两个方法:

  1. limit -n <文件数>只对当前进程有效
  2. 对所有进程有效,修改Linux系统参数;
vim /etc/security/limits.conf
soft nofile 65536
hard nofile 65536

7. 并发(concurrency)和并行(parallelism)

并发:宏观上两个程序同时运行,微观上两个程序的指令交织运行。并发不能提高计算机的性能,只提高了效率。

并行:严格物理上的同时执行,并行提高了计算机的效率和性能。

8. 查看并修改MySQL端口号

show global variables like 'port'查看端口,Mysql:3306, sqlserver:1433, oracle:1521, db2: 5000

修改端口,编辑/etc/my.cnf

9. 操作系统的页表寻址

页式内存管理,内存被分成固定长度的一个个页片。操作系统为每个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫做页表。页表的内容是该进程的虚拟地址到物理地址的一个映射。页表中每一项纪录这个页的基地址,通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,页基地址偏移一定长度就能得到最后的物理地址,偏移长度由逻辑地址的低位部分决定,(其实这里有点像FTL中的块映射。)

Linux最初的两级页表机制:两级分页机制将32位的虚拟空间分为三段,低12段表示页内偏移,高20段分别表示两级页表的偏移:

  • PGD:page global directory:高10位,全局页目录表索引
  • PTE:page table entry:中间10位,页表入口索引

从CR3寄存器中获取PGD物理地址(这个应该是基地址),获取虚拟地址的高10位作为偏移,即可定位到这个地址描述的pgd,即页表的物理地址。然后从虚拟地址中抽取中间10位作为偏移,即定位到pte,即可获取页表的入口索引。然后在虚拟地址中得到页内偏移,这样即可完成虚拟地址到物理地址的转换。对虚拟地址的分级解析过程实际上是不断深入页表层次的过程,逐渐定位到最终地址,page table work

Linux三级页表机制:CR3,PGD,PMD,PTE,page offset
Linux四级页表机制:PML4,PGD,PMD,PTE,page offset

10. 有了进程为什么要有线程

线程产生的原因:进程可以使多个程序并发执行,以提高资源的利用率和吞吐量,但是有以下缺点:

  1. 进程在同一时间只能干一种事;
  2. 进程在执行过程中如果被阻塞,整个进程就会被阻塞,即使进程中有些工作不依赖于等待资源,但是仍然不能执行;

操作系统引入比进程更小粒度的线程,作为并发执行的基本单位,线程的优势:

  1. 资源节俭。启动新的进程,操作系统必须给他分配独立的地址空间,建立众多的数据表维护它的代码段、堆栈段、数据段;
  2. 切换效率高。一个进程运行多个线程,多个线程使用相同的地址空间,线程间切换的时间远小于进程间切换的时间。
  3. 通信机制上讲,线程间更方便通信。不同进程拥有各自独立的地址空间,要进行数据传递,只能通过进程间通信的方式进行。而同一进程的不同线程共享地址空间,一个线程的数据可以直接被其他线程使用。

多线程程序作为一种多任务、并发的工作方式,有如下优点:

  1. 多CPU更加高效,当线程数少于CPU数量时,操作系统会保证不同线程运行在不同CPU上。
  2. 改善程序结构。一个长的进程可以拆分成读个线程,各自形成独立或者半独立的运行部分。

11. 单核机器上写多线程程序是否需要考虑加锁

单核机器上写多线程程序仍然需要线程锁。因为线程锁通常用来实现线程的同步和通信。在单核机器上运行多线程程序时,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,在不使用线程锁的情况下,可能会导致共享数据修改引起的冲突。

12. 线程需要哪些上下文,SP,PC,EAX寄存器的作用

线程在切换过程中需要保存当前线程的ID,线程状态、堆栈、寄存器(SP,PC,EAX)状态等信息.

  • SP: 堆栈指针,指向当前栈的栈顶地址
  • PC: 程序计数器,存储下一条将要执行的指令。
  • EAX: 累加寄存器,用于加法乘法的缺省寄存器。

13. 线程间同步的方式,具体的系统调用

  1. 信号量:一种特殊的变量,用于线程同步,只能取自然数,只支持两种操作:

    • P(SV):如果信号量SV大于0,则减1,如果SV等于0,则挂起该线程;

    • V(SV):如果有其他进程因为等待SV而挂起,则唤醒,然后SV+1,否则直接SV+1;

    • 系统调用:

      • sem_wait(sem_t *sem):以原子操作的方式将信号量减1,如果信号量为0,则sem_wait将被阻塞,直到这个信号量具有非0值
      • sem_post(sem_t *sem):以原子操作将信号量减1,当信号量大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。
  2. 互斥量:又称互斥锁,主要用于线程互斥,不能保证按序访问,可以和条件锁一起实现同步,当进入临界区时,需要获取互斥锁并且加锁,当离开临界区时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。系统调用如下:

    • pthread_mutex_init: 初始化互斥锁
    • pthread_mutex_destroy: 销毁互斥锁
    • pthread_mutex_lock: 以原子的方式给互斥锁加锁,如果互斥锁已经被上锁,pthread_mutex_lock调用将被阻塞,直到该互斥锁的占有者将其解锁;
    • pthread_mutex_unlock: 以原子操作的方式给互斥锁解锁。
  3. 条件变量:又称条件锁,机制:当共享数据达到某个值时,唤醒等待这个共享数据的一个或者多个线程,即当某个共享变量等于某个值时,调用singnal/broadcast。系统调用如下:

    • pthread_cond_init:初始化条件变量
    • pthread_cond_destroy:销毁条件变量
    • pthread_cond_signal:唤醒一个等待目标条件变量的线程。哪个线程被唤醒取决于调度策略和优先级。
    • pthread_cond_wait: 等待目标条件变量。该函数在进入wait状态前首先解锁,接收到信号后再加锁。

14. 多线程和多进程的不同

  1. 进程是资源分配的最小单位,线程是CPU调度的最小单位
  2. 多线程之间共享同一个进程的地址空间,线程间通信简单
  3. 线程的创建、销毁和切换简单,速度快、占用的内存少,适合于多核分布式系统,但是线程间会互相影响,一个线程意外终止会导致同一个进程的其他线程也终止,程序可靠性差;而多进程拥有各自独立的运行地址空间,进程间不会相互影响,但是进程创建、销毁和切换复杂,适合多核多机分布。

16. 游戏服务器应该为每个用户开辟一个线程还是一个进程?

游戏服务器应该为每个用户开辟一个进程。因为同一个进程的的线程会互相干扰,一个线程死掉会影响其他线程,从而导致进程崩溃。

17. OS缺页置换算法

缺页置换:当访问一个内存中不存在的页,并且内存已经满,则需要从内存中调出一个页,将数据送至磁盘对换区,替换一个页。常用的缺页置换算法:

  • 先进先出(FIFO):置换最先调入内存的页面。
  • 最近最少使用(LRU): 置换最近一段时间来最长时间未访问的页面。

19. 多线程和多进程的使用场景

多进程模型的优势是CPU,适用于CPU密集型,多机分布式场景,多机扩展。多线程间切换的代价小,适合IO密集型的工作场景,IO密集型的场景中经常会由于IO阻塞导致频繁的切换线程。同时,多线程模型也适用于单机多核分布式场景。

20. 死锁发生的条件和解决死锁的方法

死锁:两个或者两个以上进程在执行过程中,因争夺资源而造成相互等待的现象。死锁发生的4个必要条件:

  • 互斥条件:进程对分配到的资源不允许其他进程访问,若其他进程访问,只能等待直到占用该资源的进程使用完释放该资源;
  • 请求和保持条件:进程获得资源后,又对其他资源发出请求,但该资源可能被其他进程占有,此时请求阻塞,但进程不会释放自己已经占有的资源;
  • 不可剥夺条件:进程已获得的资源,在未完成使用前,不可剥夺,只能在使用后自己释放;
  • 环路等待条件:进程发生死锁后,一定存在一个进程-资源之间的环形链。

解决死锁的方法及破坏上述四个条件之一:

  • 资源一次性分配,从而剥夺请求和保持的条件;
  • 可剥夺资源,当进程新的资源未得到满足时,释放已经占有的资源,从而破坏不可剥夺的条件;
  • 资源有序分配法,系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则想发,从而破坏环路等待的条件;

22. 操作系统的结构体对齐、字节对齐

  • 对齐原因:

    • 平台原因(移植原因):不是所有硬件平台都能访问任意地址上的任意数据,某次硬件平台智能在某些地址处取,某些特定类型的数据,否则抛出硬件异常;
    • 性能原因:数据结构(尤其是栈)应该尽可能在自然边界上对齐。因为为了访问未对齐的内存,处理器需要做两次访问内存操作,而对齐的内存访问只需要一次。
  • 规则:

    • 数据成员对齐规则:结构(struct)或者联合(union)的数据成员,第一个数据成员放在offset为0处,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中较小的那个进行;
    • 结构(联合)整体的对齐规则:在数据成员完成各自对齐之后,结构(联合)本身也要进行对齐,按照#pragma pack和最大数据成员长度中较小的对齐;
    • 结构体作为成员,结构体成员要从其内部最大元素的大小的整数倍地址开始存储。
    • 定义结构体对齐;

24. 虚拟内存置换的方式(补充#17)

常见的内存替换算法:FIFO,LRU,LFU,LRU-K,2Q

  1. FIFO,无法体现页面冷热信息

  2. LFU(最不经常访问淘汰算法)

    • 思想:如果数据过去被访问过多次,那么将来被访问的频率也更高。
    • 实现:每个数据块一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序,每次淘汰队尾数据块。
    • 缺点:排序开销大
  3. LRU(最近最少使用替换算法)

    • 思想:如果数据最近被访问过,那么将来被访问的几率也更高。
    • 实现:使用一个栈,新页面或者命中的页面移动到栈底,每次替换栈顶的缓存页面;
    • 优点:LRU算法对热点数据命中率很高;
    • 缺点:缓存颠簸、缓存污染(突然大量偶发性数据访问时,内存中存放大量冷数据);
  4. LRU-K(LRU-2,LRU-3)

    思想:最久未使用K次淘汰算法。

    LRU-K的K表示最近使用的次数,LRU-K的主要目的是为了解决LRU算法"缓存污染"的问题。

    相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有将数据访问次数达到K次时,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距离当前时间最大的数据。

    实现:

    1. 数据第一次被访问,加入到访问历史列表;
    2. 如果数据在访问历史列表中没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;
    3. 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列中删除,将数据移动到缓存队列中,并缓存此数据,缓存队列重新按照时间排序。
    4. 缓存数据队列中被再次访问后,重新排序;
    5. 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即淘汰"倒数第K次访问离现在最久的"数据。
  5. 2Q

    类似LRU-2,使用FIFO队列和LRU队列

    实现:

    1. 新访问的数据插入到FIFO队列;
    2. 如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;
    3. 如果数据在FIFO队列中被再次访问,则将数据移动到LRU队列头部;
    4. 如果数据在LRU队列再次被访问,则将数据移动到LRU队列头部;
    5. LRU队列淘汰末尾的数据

    针对的问题:LRU的缓存污染

26. 互斥锁(mutex)机制,互斥锁,读写锁的区别

  • 互斥锁与读写锁的区别

    • 互斥锁:mutex,用于保证在任意时刻只能有一个线程访问对象,当获取锁失败时,线程会进入睡眠, 等待锁释放时被唤醒;

    • 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获取读操作。但是同一个时刻只能有一个线程可以获得写锁。其他获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其他线程获取,写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。读写锁适合于读取数据的频率远远大于写数据的频率的场合.

    • 区别:

      1. 读写锁区分读和写,互斥锁不区分
      2. 互斥锁同一个时间内只能允许一个线程访问该对象,无论读写;读写锁同一时刻只允许一个写者,但是允许多个读者同时读对象;
    • Linux的4中锁机制:

      1. 互斥锁:mutex
      2. 读写锁
      3. 自旋锁:spinklock在任何时刻同样只能有一个线程访问对象,但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下回极大提高效率。但是如果加锁时间过程,则会浪费CPU
      4. RCU: read-copy-update,在修改写数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后再将老数据update成新的数据。在使用RCU时,读者几乎不需要同步开销,因为不需要获取锁,也不使用原子指令,因此不会导致锁竞争,并且也不会有死锁的问题,但是写者的同步开销非常大,因为需要复制被修改的数据,还必须使用锁机制同步其他写者的修改操作,RCU适合少量写大量读的场景下.

28. 进程状态转换图,动态就绪,静态就绪,动态阻塞,静态阻塞

进程的5种状态:

  • 创建状态:进程正在被创建;
  • 就绪状态:进程被加入到就绪队列中等待CPU调度运行;
  • 执行状态:进程正在被运行
  • 等待阻塞状态:进程因为某种原因,i.e.,等待IO,等待设备,暂时不能运行
  • 终止状态:进程运行完毕

交换技术(进程的挂起状态):

  • 现象:当多个进程竞争内存资源时,会造成内存资源紧张,如果此时没有就绪进程,处理机会空闲,IO速度比处理机速度慢得多,可能出现全部进程阻塞等待IO
  • 解决方法:交换技术(换出一部分进程到外存,腾出内存空间),虚拟存储技术(每个进程只能装入一部分程序和数据)
  • 在交换技术中,将内存暂时不能运行的进程或者暂时不用的数据和程序换出到外存,来腾出足够的内存空间,把已经具备运行条件的进程,或者进程所需的数据和程序换入内存,这里会出现进程的挂起状态.

活动阻塞,静止阻塞,活动就绪,静止就绪

  • 活动阻塞:进程在内存由于某种原因被阻塞了
  • 静止阻塞:进程在外存,同时由于某个原因被阻塞了
  • 活动就绪:进程在内存,处于就绪状态,只要给CPU和调度就能直接运行;
  • 静止就绪:进程在外存,处于就绪状态,只要调到内存,给CPU和调度就能直接运行;

状态:

  • 活动就绪—>静止就绪(内存不够,调到外存);
  • 活动阻塞—>静止阻塞(内存不够,调到外存);
  • 执行—>静止就绪(时间片用完)

29. A* a = new A; a->i=10内存分配

30. 给一个类,里面有static,virtual,说说这个类的内存分布

  • static修饰符:

    • static修饰成员变量,对于非静态数据成员,每个类对象都有自己的拷贝,静态数据成员被当做类的成员,无论类被定义了多少个,静态数据成员都只有一份拷贝,为该类型的所有对象所享用(包括派生类),所以,静态数据成员的值对每个对象都是一样的,它的值可以更新。静态数据成员在全局数据区分配内存,属于本类所有对象共享,所以它不属于特定的类对象,在没有产生类对象前就可以使用
    • static修饰成员函数,与普通的成员函数相比,静态成员函数由于不是与任何对象相联系,因此它不具备this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,只能调用其他的静态成员函数。static修饰的成员函数在代码区分配内存
  • c++继承和虚函数

    c++多态分为静态多态和动态多态。静态多态是通过重载和模板技术实现的,在编译时候确定,动态多态通过虚函数和继承关系来实现,执行动态绑定,在运行时确定;

    动态多态实现的条件:

    • 虚函数;
    • 一个基类指针或者引用指向派生类的对象;
  • virtual修饰符

    如果一个类是局部变量,则该类数据存储在栈区,如果一个类是通过new/malloc动态申请的,则该类数据存储在堆区。

    如果这个类是virtual继承而来的子类,则该类的虚函数表指针和该类其他成员一起存储。虚函数表指针指向只读数据段中的类虚函数表,虚函数表中存放着一个个函数指针,函数指针指向代码段中的具体函数。

31. 软链接和硬链接的区别

为了解决文件共享的问题,linux引入软链接和硬链接、软/硬链接还可以隐藏文件路径、增加权限安全及节省存储等好处。若一个inode号对应多个文件名,则为硬链接,即硬链接就是同一个文件使用了不同的别名,使用ln创建。若文件用户数据块中存放的内容是另一个文件的路径名指向,则文件是软链接。软链接是一个普通文件,有自己独立的inode,数据块内容比较特殊。

32. 大端和小端

大端是指低字节存储在高地址,小端是低字节存储在低地址。可以用联合体来判断系统是大端还是小端,因为联合体变量是从低地址存储。

33. 静态变量初始化

静态变量存储在虚拟地址空间的数据段和bss段,c语言在代码执行前初始化,属于编译期初始化。而c++由于引入对象,对象生成必须调用构造函数,因此c++规定全局或者局部静态对象当且仅当对象首次用到时进行构造。

34. 内核态和用户态

用户态和内核态是操作系统的两种运行级别。两者最大的区别是特权级别不同。用户态拥有最低的特权级,内核态拥有较高的特权级。运行在用户态的程序不能直接访问操作系统内核数据结构和程序。内核态和用户态之间的转换方式主要包括:系统调用,异常,中断

35. 设计server,使得能接收多个客户端的请求

多线程,线程池,IO复用

线程池#last

36. 死循环+来连接时新建线程的方法效率有点低,如何改进

提前创建线程池,用生产者消费者模型,创建任务队列,队列作为临界资源,有新的连接,就挂到任务链表上,队列为空所有线程睡眠。改进死循环的方法:使用select epoll技术

37. 如何唤醒被阻塞的socket线程

给阻塞时缺少的资源。

38. 确定当前线程是繁忙还是阻塞

使用ps命令查看

40. 就绪状态的进程在等待什么

被调度使用的CPU运行权

41. 多线程同步,锁机制

同步的时候用一个互斥量,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。在互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去再次等待他重新变为可用。在这种方式下,每次只有一个线程可以向前执行.

42. 两个进程访问临界区资源: 单核cpu,并且开了抢占可以造成这种情况。

44. Windows消息机制

当用户有操作(鼠标,键盘等)时,系统会将这些事件转化为消息,每个打开的进程系统都会为其维护一个消息队列,系统会将这些消息放到进程的消息队列中,而应用程序会循环从消息队列中取出消息,完成对应的操作.

45. c++锁:包括互斥锁,条件变量,自旋锁和读写锁

49. 内存溢出和泄漏

  1. 内存溢出

    • 概念:程序申请内存时,没有足够的内存供申请者使用,内存溢出:申请的内存空间超过了系统实际分配的空间,系统无法满足,会报内存溢出错误.
    • 原因:内存中加载的数据量过于庞大,e.g.,一次从数据库中取出过多的数据,集合类中有对对象的引用,使用完后未清空,使得不能收回,代码死循环产生过多重复的对象实体;第三方软件中的BUG
  2. 内存泄漏

    • 概念:程序未能释放掉不再使用的内存

    • 分类

      • 堆内存泄漏(heap leak):malloc/realloc/new从堆中分配的内存,没有用free/delete删掉.
      • 系统资源泄漏(resource leak):程序使用系统分配的资源,比如bitmap,handle,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费。

50. 【补充】 常用的线程模型

  1. future模型

    该模型需要结合callable接口配合使用,future把结果放在将来获取,当前主线程并不急于获取处理结果。允许子线程先进行处理一段时间,处理结束后把结果保存下来,当主线程需要使用的时候再向子线程索取。

    callable类似runnable接口,call方法类似run方法,不同的是run方法不能抛出受检异常和没有返回值,而call方法可以抛出受检异常并可设置返回值,两者的方法体都是线程执行体.

  2. fork&join模型

    该模型包含递归思想和回溯思想,递归用来拆分任务,回溯用来合并结果。可以用来处理一些可以进行拆分的大任务。主要是把一个大任务逐级拆分为多个子任务,然后分别在子线程中执行,当每个子线程执行结束之后逐级回溯,返回结果进行汇总合并,最终得到想要的结果。

  3. actor模型

    actor模型属于一种基于消息传递机制并行任务处理思想,它以消息的形式来进行线程间数据传输,避免全局变量的使用(免掉数据同步的隐患).actor在接收到消息后自己进行处理,也可以继续传递(分发)给其他actor进行处理

  4. 生产者消费者模型

    使用一个缓存保存任务,开启一个/多个线程来生产任务,然后再开启一个/多个来从缓存中取出任务进行处理。好处:任务的生成和处理分隔开,生产者不需要处理任务,只负责生产任务并保存到缓存,消费者只需要从缓存中取出任务进行处理。使用的时候可以根据任务的生成情况和处理情况开启不同的线程来处理.

  5. master-worker模型

    任务分发策略,开启一个master线程接收任务,然后在master中根据任务的具体情况分发给其他worker子线程,然后由子线程处理任务.

51. 协程

  • 概念:微线程,协程看上去也是子程序,但在执行过程中,子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行.
  • 协程和线程的区别:协程最大的优势是协程执行效率极高(子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销,和多线程相比,线程数据越多,协程的性能优势越明显)。协程不需要锁机制,因为协程只有一个线程.
  • 多进程+协程可以充分利用多核,又能发挥协程的高效率,可获得极高的性能.

52. 系统调用

概念:运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供了用户程序与操作系统之间的接口

操作系统的状态分为管态(核心态)和目态(用户态)。大多数系统交互式操作需要在内核态执行,e.g.,设备IO,进程间通信。特权指令:一类只能在核心态下运行而不能再用户态下运行的特殊指令。

54. 【补充】用户态到内核态的转化原理

种类:

  1. 系统调用:系统调用机制的核心依然是使用了操作系统为用户特别开放的中断来实现的,Linux的ine 80h中断
  2. 异常:当CPU在执行运行在用户态的程序时,发现某些事件不可知的异常,会触发当前运行进程切换到此处理,异常会转换到内核相关程序中处理,也即内核态,比如缺页中断.
  3. 外围设备的中断:外围设备完成用户请求的操作之后,会向CPU发出相应的中断信号,CPU会暂停执行下一条将要执行的指令,转而执行中断信号的处理程序.

方式:三种涉及的关键步骤完全一样.

55. 源码到可执行文件的过程

  1. 预编译:主要处理源代码文件中的以#开头的预编译指令,处理规则如下:

    • 删除所有的#define,展开所有宏定义
    • 处理所有条件预编译指令,#if,#endif,#ifdef,#elif,#else
    • 处理#include预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件
    • 删除所有的注释,// /**/
    • 保留#pragma编译器指令,编译器需要用到他们,如:#pragma once是为了防止有文件被重复引用
    • 添加行号和文件标识,便于编译器在编译时产生调试用的行号信息
  2. 编译

    把预编译之后生成的xxx.i或者xxx.ii文件进行词法分析、语法分析、语义分析、优化生成相应的汇编代码文件;

    • 词法分析:利用类似于"有限状态机"的算法,将源代码程序输入到扫描机中,将字符序列分割成一系列记号;
    • 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树,由语法分析器输出的语法树是一种以表达式为节点的树;
    • 语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义(编译期间确定的语义),相对应的动态语义是运行期才能确定的语义.
    • 优化:源代码级别的优化过程
    • 目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列–>汇编语言表示
    • 目标代码优化:目标代码优化器对上述机器指令代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余指令.
  3. 汇编

    将汇编代码转变成机器可执行的指令(机器码文件),汇编器汇编过程:根据汇编指令和机器指令的对照表一一翻译,由汇编器as完成。经过汇编之后产生的目标文件(与可执行文件格式基本一样)

  4. 链接:将不同的源文件产生的目标文件进行链接,从而形成可执行文件,链接分为动态链接和静态链接:

    • 静态链接

      • 概念:函数和数据被编译进一个二进制文件。在使用静态库的情况下,编译链接可执行文件时,链接器从库中复制这些函数和数据,并把他们和应用程序的其他模块组合起来创建最终的可执行文件
      • 空间浪费:因为每个可执行程序对所有需要的目标文件都要有一个副本,如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本
      • 更新困难:每当库函数代码修改,这个时候就需要重新进行编译链接形成可执行文件
      • 运行速度快:静态链接时,可执行程序中已经具备了所有执行程序所需要的任何东西,执行的时候速度快
    • 动态链接

      • 概念:把程序按照模块拆分成各个相对独立的部分,在程序运行时才将他们链接在一起形成完整的程序
      • 共享库:即使每个库都依赖同一个库,这些程序在执行时也是共享同一份副本的
      • 更新方便:更新时只需要更换原来的目标文件,程序运行时,新版本的目标文件自动加载到内存并链接起来
      • 性能损耗:链接推迟到运行时

56. 微内核、宏内核

* 宏内核:除最基本的进程、线程管理、内存管理外,将文件系统,驱动,网络协议也都集成到了内核里面

    - 优点:效率高
    - 缺点:稳定性差,开发过程中出现bug经常会导致系统挂掉

* 微内核: 内核只有最基本的调度、内存管理。驱动、文件系统都是用户态的守护进程实现

57. 僵尸进程

  1. 正常进程:正常情况下,子进程是通过父进程创建的,子进程再创建新的进程,子进程的结束和父进程的运行是一个异步的过程,父进程永远无法预测子进程何时结束。当一个进程完成它的工作终止后,它的父进程需要调用wait或者waitpid系统调用取得子进程的终止状态(进程ID,退出状态,运行时间)

  2. 孤儿进程:一个父进程退出,它的一个或者多个子进程还在运行,那么这些子进程将成为孤儿进程,孤儿进程被init进程(进程id为1)所收养,并由Init进程完成状态收集

  3. 僵尸进程:一个进程使用fork创建的子进程,如果子进程退出,而父进程并没有调用wait或者waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保持在系统中。

    • 僵尸进程是每个进程必然经过的过程(发生在进程结束时)
    • 子进程在exit之后,父进程没来及处理,ps命令看到的子进程状态是Z
    • 危害:如果进程不调用wait或者waitpid,那么保留的那段信息不会释放,其进程号就会一直被占用着,系统所能使用的进程号是有限的
  4. 外部消灭:通过kill发送SIGTERM或者SIGKILL信号消灭僵尸进程的进程,他产生的僵尸进程就变成孤儿进程,孤儿进程由init接管

  5. 内部解决:

    • 子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号,信号处理函数中调用wait处理僵尸进程
    • fork两次,将子进程变成孤儿进程,其父进程转为init进程

59. 5种IO模型

  1. 阻塞IO:调用者调用某个函数,等待这个函数返回,期间什么都不做,不停检测函数是否返回,必须等待函数返回才能进行下一步
  2. 非阻塞IO:非阻塞等待,每隔一段时间检测IO事件是否就绪,没就绪就可以做其他事
  3. 信号驱动IO:linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号,然后处理IO事件
  4. IO复用/多路转接IO:linux用select/poll函数实现IO复用,这两个函数会使进程阻塞,可以阻塞多个IO操作,对多个读操作和写操作的IO函数进行检测,知道有数据可读或者可写时才真正调用IO操作函数
  5. 异步IO:linux中,调用aio_read函数告诉内核描述字缓存…

60. 异步编程的事件循环

事件循环:不停等待事件发生,然后将这个事件的所有处理器,以及他们订阅的这个事件的时间顺依次执行完毕之后,事件循环就会开始继续等待下一个事件的触发,不断往复。

61. 操作系统为什么分内核态和用户态:安全性

62. 为什么要page cache,以及操作系统如何设计page cache

加快从磁盘读取文件的速率。page cache中有一部分磁盘文件的缓存,因为从磁盘中读取文件比较慢,所以读取文件先去page cache中查找,如果命令,则不需要去磁盘上读取,大大加快读取速度,在linux内核中,文件的每个数据块最多只能对应一个page cache项,通过两个数据结构来管理这些cache项,一个是radix tree,另一个是双向链表,radix tree是一种搜索树,linux内核利用这个数据结构来通过文件内偏移快速定位cache项.

62. server端监听端口,但还没客户端连接进来,此时进程处于什么状态

普通模型,处于阻塞状态,使用epoll,select等IO复用的模型,处于运行状态

69. 实现线程池

  1. 设置一个生产者消费者队列,作为临界资源
  2. 初始化n个线程,并让其运行起来,加锁去队列取任务运行
  3. 当任务队列为空时,所有线程阻塞
  4. 当生产者队列来了一个任务后,先对队列加锁,把任务挂在队列上,然后使用条件变量去通知阻塞中的一个线程

70. linux下得到一个文件的100行到200行

sed -n '100,200p' <inputfile>
awk 'NR>=100&&NR<=200{print}' <inputfile>
head -200 <inputfile>|tail -100

71. awk用法

  1. 作用:样式扫描和处理语言,它允许创建简短的程序,读取输入文件,为数据排序、处理数据、对输入执行计算以及生成报表
  2. awk [-F field-sep] 'command'
  3. 内置变量…

72. linux内核Timer定时器

  • 低精度时钟

    linux2.6.16之前只支持低精度时钟,内核定时器工作方式:

    1. 系统启动后,会读取时钟源设备(rtc,hpet,pit…)初始化当前系统的时间
    2. 内核会根据HZ(系统定时器频率,节拍率)参数值,设置时钟事件设备,启动tick(节拍)中断,HZ表示1秒钟产生多少个时钟硬件中断,tick表示连续两个中断的间隔时间
    3. 设置时钟事件设备后,时钟事件设备会定时产生一个tick中断,触发时钟中断处理函数,更新系统时钟,并检查timer wheel,进行超时时间处理

    在linux2.6.16之前,内核软件定时器采用timer wheel多级时间轮的实现机制,维护操作系统的所有定时事件,timer wheel触发是基于系统tick周期性中断

    旧内核机制:依赖系统定时器硬件定期的tick,基于tick,内核会扫描timer wheel处理超时事件,更新jiffies,wall time(墙上时间,现实时间),process的使用时间

  • 高精度时钟

    linux2.6.16内核支持高精度时钟,内核采取新的定时器hrtimer,其实现逻辑与之前的定时器逻辑区别是:

    • hrtimer采用红黑树进行高精度定时器的管理,而不是时间轮,高精度时钟定时器不再依赖系统tick中断,而是基于事件触发;
    • 新内核不再支持周期性tick,新内核定时器框架采用基于事件触发(而不是周期性触发)

    hrtime工作原理:通过将高精度时钟硬件触发时间设置为红黑树中最早到期的Timer时间,时钟到期后从红黑树中得到下一个Timer的到期时间,并设置硬件,如此往复;

    其他:在高精度时钟模式下,操作系统内核仍然需要周期性tick中断,以便刷新内核的一些任务。hrtimer是基于事件的,不会周期性触发tick中断,为了实现周期性tick中断(dynamic tick):系统创建一个模拟tick时钟的特殊hrtimer,将其超时时间设置为一个tick时长,在超时到达后,完成对应的工作,然后再次设置下一个tick的超时时间,以此达到周期性tick中断的需求.

    新内核对相关时间硬件设备进行了统一的封装,主要有两个结构:

    • 时钟源设备(clock source device):抽象那些能够提供计时功能的系统硬件,e.g.,RTC(Real Time Clock),TSC(Time Stamp Counter),HPET,ACPI PM-Timer,PIT.不同时钟源提供的精度不一样。
    • 时钟事件设备(clock event device):系统中可以触发one-shot(单次)或者周期性中断的设备

    注:当前内核同时存在新旧timer wheel和hrtimer两套timer的实现,内核启动后会进行从低精度模式到高精度时钟模式的切换,hrtimer模拟的tick中断将驱动传统的低精度定时器系统(基于时间轮)和内核进程调度.

操作系统附加知识

1. 线程池、内存池


数据库基础

1. 数据库事务,四个特性

  • 事务:一系列对系统中数据进行访问和更新的操作所组成的一个程序执行逻辑单元。事务DBMS中最基础的单位,事务是不可分割的。

  • 基本特征:原子性(A),一致性©,隔离性(I),持久性(D)

    • 原子性:事务包含的所有操作要么全部成功,要么全部失败回滚
    • 一致性:事务必须使数据库从一个一致性状态变换到另一个一致性状态,转账过程,总和一定
    • 隔离性:当多个用户并发访问数据库时,操作同一张表,数据库为每个用户开启的事务,不能被其他事务的操作干扰,多个并发事务之前要相互隔离,隔离等级:Read Uncommitted(读取未提交),Read Committed(读取提交),Repeated Read(可重复读),Serialization(可串行化)
    • 持久化: 数据提交了就能写成功

2. 数据库的三大范式

  • 第一范式:关系模式R的所有属性都不能再分解为更基本的数据单位,属性不可分
  • 第二范式:在满足第一范式下,R的所有非主属性都完全依赖于R的每一个候选关键属性
  • 第三范式:R满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖R的任意一个候选关键字,非主属性不传递依赖于键码

4. 数据库索引

索引是对数据库表中一列或者多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。加快检索数据.

数据库事务,四个特性

  • 事务:一系列对系统中数据进行访问和更新的操作所组成的一个程序执行逻辑单元。事务DBMS中最基础的单位,事务是不可分割的。

  • 基本特征:原子性(A),一致性©,隔离性(I),持久性(D)

    • 原子性:事务包含的所有操作要么全部成功,要么全部失败回滚
    • 一致性:事务必须使数据库从一个一致性状态变换到另一个一致性状态,转账过程,总和一定
    • 隔离性:当多个用户并发访问数据库时,操作同一张表,数据库为每个用户开启的事务,不能被其他事务的操作干扰,多个并发事务之前要相互隔离,隔离等级:Read Uncommitted(读取未提交),Read Committed(读取提交),Repeated Read(可重复读),Serialization(可串行化)
    • 持久化: 数据提交了就能写成功

2. 数据库的三大范式

  • 第一范式:关系模式R的所有属性都不能再分解为更基本的数据单位,属性不可分
  • 第二范式:在满足第一范式下,R的所有非主属性都完全依赖于R的每一个候选关键属性
  • 第三范式:R满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖R的任意一个候选关键字,非主属性不传递依赖于键码

4. 数据库索引

索引是对数据库表中一列或者多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。加快检索数据.

你可能感兴趣的:(面试)