多路复用,epoll,poll
select 遍历整个文件描述符集合

poll 使用链表来表示文件描述符遍历整个文件描述符集合因此没有了监视文件数量的限制

epoll 使用回调方式,只返回就绪的文件描述符,而不需要遍历整个文件描述符集合。

虚拟内存是什么,为什么使用虚拟内存?

通过将磁盘空间用作临时的扩展内存来扩大实际内存的容量。每个程序都拥有自己的虚拟地址空间,而不需要关心实际物理内存的分配和管理。

更大的地址空间:虚拟内存可以使得每个程序拥有更大的地址空间,使得程序能够处理比实际物理内存更大的数据集。

隔离和保护:每个程序拥有自己的虚拟地址空间,这样可以有效地隔离和保护各个程序的内存空间,提高系统的安全性和稳定性。

简化内存管理:虚拟内存由操作系统负责管理,程序员无需手动管理内存,简化了内存管理的复杂度。

允许共享内存:虚拟内存使得多个程序可以共享同一个物理内存页面,从而节约内存的使用。

更好的内存管理:虚拟内存可以提供更灵活的内存分配和回收机制,能够更好地满足程序对内存的需求。

为什么会出现僵尸进程?

僵尸进程是指在一个进程终止后,其父进程没有及时处理该子进程的终止状态信息(称为退出状态),导致子进程的进程描述符依然存在,但是实际上已经结束了。

父进程未及时处理:当一个子进程完成任务后,如果其父进程没有显式地调用类似 wait() 或 waitpid() 的系统调用来获取子进程的退出状态信息,那么子进程就会成为僵尸进程。

父进程意外终止:如果父进程意外终止,而没有释放子进程的资源或者获取子进程的状态信息,那么子进程也有可能成为僵尸进程。

如何实现一个线程池?

确定线程池的参数: 首先确定线程池的基本参数,包括核心线程数、最大线程数、工作队列类型、任务超时设置等。

创建工作队列: 根据需要选择合适的工作队列,例如有界队列(如 ArrayBlockingQueue)或无界队列(如 LinkedBlockingQueue),用于存储待执行的任务。

创建线程池管理器: 创建一个线程池管理器的类,在该类中初始化线程池,并提供提交任务、关闭线程池等方法。

LRU (最近最少使用) 实现思路?手写实现?

LRU(Least Recently Used)是虚拟内存的一直页面置换算法。是一种缓存淘汰策略,根据最近使用的时间来淘汰缓存中最近最少使用的数据。

以下是一种基于哈希表和双向链表实现LRU缓存的思路:使用哈希表存储(key, value)键值对,以支持快速的查找操作。使用双向链表维护缓存中的数据顺序,链表头部表示最近访问的数据,链表尾部表示最久未被访问的数据。

如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。

如果此数据没有在缓存链表中,又可以分为两种情况:如果此时缓存未满,则将此结点直接插入到链表的头部;如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。


class DLinkedNode:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None
class LRUCache:
    def __init__(self, capacity: int):
        self.cache = {}
        self.head = DLinkedNode()
        self.tail = DLinkedNode()
        self.head.next = self.tail
        self.tail.prev = self.head
        self.capacity = capacity
        self.size = 0
    def _add_node(self, node):
        # 添加节点到头部
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node
    def _remove_node(self, node):
        # 从链表中移除节点
        prev = node.prev
        new = node.next
        prev.next = new
        new.prev = prev
    def _move_to_head(self, node):
        # 移动节点到头部
        self._remove_node(node)
        self._add_node(node)
    def _pop_tail(self):
        # 弹出尾部节点
        res = self.tail.prev
        self._remove_node(res)
        return res
    def get(self, key: int) -> int:
        # 获取节点的值并将其移到头部
        node = self.cache.get(key, None)
        if not node:
            return -1
        self._move_to_head(node)
        return node.value
    def put(self, key: int, value: int) -> None:
        # 若key存在,则更新value,并移到头部;若key不存在,插入新节点到头部
        node = self.cache.get(key)
        if not node:
            newNode = DLinkedNode(key, value)
            self.cache[key] = newNode
            self._add_node(newNode)
            self.size += 1
            if self.size > self.capacity:
                tail = self._pop_tail()
                del self.cache[tail.key]
                self.size -= 1
        else:
            node.value = value
            self._move_to_head(node)
    
多个线程抢占时锁升级的过程是怎样的?

在多线程环境下,当多个线程竞争同一个锁时,可能会出现锁的升级过程。锁的升级是指从低级别的锁升级为高级别的锁,这个过程通常是为了提高并发性能。

  1. 偏向锁:初始状态下,锁处于偏向锁状态,即被第一个获取锁的线程所偏向。如果有其他线程尝试获取该锁,会触发偏向锁撤销,将锁升级到轻量级锁。
  2. 轻量级锁:当多个线程竞争同一个锁时,偏向锁会升级到轻量级锁。此时,各个线程会通过自旋操作来尝试获取锁,避免进入操作系统的阻塞状态。如果自旋操作仍然无法获取到锁,则锁会升级到重量级锁。
  3. 重量级锁:如果自旋操作不断尝试后仍未获取到锁,锁会最终升级为重量级锁。这时候线程会进入阻塞状态,等待操作系统的调度来解除阻塞并获取锁。
什么是I/O多路复用?

I/O多路复用是一种通过单一线程处理多个I/O操作的机制。它允许一个程序可以同时监听多个文件描述符(sockets、pipes等)上的I/O事件,当其中任何一个文件描述符准备好进行I/O操作时,就通知程序进行相应的读取或写入操作。

常见的I/O多路复用技术包括select、poll和epoll等,在不同的操作系统上有不同的实现方式。

通过I/O多路复用,程序可以避免使用多线程或多进程来同时监听多个I/O事件,从而降低了系统资源的开销,并提高了程序的并发性能。这种机制在网络编程中特别有用,因为一个服务器经常需要同时处理多个客户端的连接请求,以及读写操作。

总之,I/O多路复用允许一个程序可以高效地管理多个I/O事件,提高了程序对并发请求的处理效率,是提升系统性能的重要技术之一。

进程有哪几种类型?

前台进程、后台进程、守护进程、孤儿进程、僵尸进程

有哪几种情况进程会发生阻塞?

I/O阻塞:当进程需要进行I/O操作(如读取文件、从网络接收数据等)时,如果所需的I/O资源尚未就绪,进程将被阻塞,直到I/O操作完成或者数据可用。

同步阻塞:当进程等待某个条件的满足时,如果条件尚未满足,则进程会被阻塞。例如,一个进程请求获取一个信号量,但是当前信号量的值不满足进程的请求,这时候进程就会被阻塞。

计算阻塞:在进行复杂的计算任务时,如果任务执行时间较长,会导致进程在执行过程中处于阻塞状态,无法执行其他操作。

管道阻塞:当使用管道进行进程间通信时,如果管道已满(写端写入速度快于读端读取速度),写入端的进程将被阻塞。

Linux下有哪些IO模型?
  • 阻塞 I/O:当应用程序发起一个I/O操作后,如果操作没有完成,程序将会被阻塞,直到I/O操作完成。在此期间,程序无法执行其他任务。
  • 非阻塞 I/O:应用程序通过设置为非阻塞模式后,可以立即返回,而不必等待I/O操作完成。如果I/O操作尚未完成,则程序可以执行其他任务。但是,非阻塞I/O需要持续轮询状态以检查I/O是否完成,这可能会导致CPU资源浪费。
  • I/O 多路复用:通过使用select、poll、epoll等机制,一个线程可以同时监视多个文件描述符(sockets、pipes等)上的I/O事件。当其中任何一个文件描述符准备好进行I/O操作时,通知程序进行相应的读取或写入操作,从而避免了持续轮询状态的缺点。
  • 信号驱动 I/O:应用程序在发起一个I/O操作后,可以继续执行其他任务。当I/O操作完成时,内核发送一个信号通知应用程序完成了I/O操作。
  • 异步 I/O:应用程序发起一个I/O操作后,可以继续执行其他任务,当I/O操作完成时,应用程序会得到通知,这种模型需要操作系统和硬件的支持。
在Python中,如何实现不同模型的IO操作?
  • 阻塞 I/O:Python 中的常规 I/O 操作(如文件读写、套接字通信)默认是阻塞的。当调用类似 file.read() 或 socket.recv() 的方法时,程序将会等待 I/O 完成后再继续执行。
  • 非阻塞 I/O:可以通过设置文件描述符或套接字为非阻塞模式来实现非阻塞 I/O。例如,使用 fcntl 模块或 setblocking() 方法将文件描述符设置为非阻塞模式。
  • I/O 多路复用:Python 提供了 select、selectors 模块以及第三方库 gevent、eventlet 来实现 I/O 多路复用。这些工具允许程序同时监听多个文件描述符上的 I/O 事件,并在有事件发生时进行处理。
  • 信号驱动 I/O:在 Python 中,可以使用 signal 模块来处理信号。然而,Python 并没有直接提供信号驱动 I/O 的功能,因此需要结合其他机制来实现。
  • 异步 I/O:Python 3.4 引入了 asyncio 标准库,它提供了对异步 I/O 的完整支持。使用 async 和 await 关键字,可以编写基于协程的异步 I/O 代码。此外,还有一些第三方库(如 Tornado、Twisted 等)也提供了异步 I/O 支持。
什么是用户态和内核态?

在计算机系统中,用户态和内核态是操作系统管理和控制硬件资源的两种不同特权级别。

用户态:在用户态下运行的程序或进程只能访问有限的系统资源,并且无法直接操作系统底层的硬件设备。

内核态:内核态是操作系统的特权模式,具有对全部系统资源和硬件设备的完全访问权限。

用户态和内核态之间的转换是通过系统调用和异常中断来实现的。

系统调用:当运行在用户态下的程序需要访问操作系统提供的特权指令或资源时,它会通过系统调用向操作系统发起请求。系统调用是一种特殊的函数调用,它将控制权从用户态切换到内核态,使得操作系统可以代表应用程序执行需要特权级别的操作。通常,系统调用的触发是通过调用类似于 int 0x80 或 syscall 指令。

异常/中断处理:在硬件层面,当发生诸如缺页错误、除零错误、时钟中断等异常或中断事件时,CPU 会自动切换到内核态,并跳转到相应的异常处理程序或中断服务例程。这样,操作系统在内核态下可以对异常或中断进行处理。

无论是通过系统调用还是异常/中断处理,从用户态切换到内核态都需要经过以下步骤:

  • 保存当前用户态的上下文信息(如寄存器状态、栈指针等
  • 将 CPU 的特权级别设置为内核态
  • 跳转到操作系统内核的特定入口点(如系统调用处理程序或异常处理程序)
  • 在内核态下执行相关操作;
  • 最后返回用户态时,恢复之前保存的用户态上下文信息,将 CPU 的特权级别设置回用户态。
线程安全是什么?

线程安全(Thread Safety)指的是当多个线程同时访问某个对象或变量时,不会出现意外的、不可预知的结果。具体来说,线程安全通常包括以下两个方面:

  • 原子性:要么一个操作完全执行并且对其他线程隐藏这个操作,要么就不执行这个操作。在并发环境中,如果一个操作是不可分割的,那它就是原子的。
  • 可见性:当一个线程修改了共享变量的值时,其他线程能够立即看到这个改动。
如何实现线程安全?

使用同步机制(如锁、信号量等)来控制对共享资源的访问,确保在同一时间只有一个线程可以访问共享资源。

使用原子操作或原子数据类型,确保针对共享资源的操作是不可分割的,从而避免竞态条件的发生。

使用线程安全的数据结构,如线程安全的队列、Map等,以减少需要手动处理同步的工作。

什么是饥饿进程?怎么解决饥饿进程?

饥饿进程是指在多任务系统中,由于资源分配不合理或调度算法不公平,导致某些进程长时间无法获得所需的资源而无法执行的情况。具体来说,饥饿进程通常表现为以下几个特征:长时间等待资源:某些进程长时间无法获得所需的 CPU 时间、内存、I/O 资源等,处于等待状态。优先级逆置:高优先级的进程总是能够获得资源,而低优先级的进程却长时间无法获得资源,这种情况称为优先级逆置。持续性问题:饥饿进程所面临的问题通常是持续性的,而不是偶发性的。

造成饥饿进程的原因可能包括不公平的调度算法、资源分配策略不当、进程优先级设置不合理等。

使用公平的调度算法,确保每个进程都有机会获得CPU时间和其他所需资源。例如,在操作系统的进程调度中,可以使用轮转调度算法(Round-Robin Scheduling)或者基于优先级的调度算法来避免某些进程长时间占用资源。

进程间通信方式的方式有哪些?

匿名管道(pipe)、具名管道(FIFO)、POSIX消息队列、共享内存、信号、Sockets

线程间同步有哪些方式?

互斥锁、条件变量、信号量、事件等待

死锁怎么产生的?举个例子, 那怎么解决死锁?

多线程编程对共享资源进行并发访问,需要加锁。死锁就是线程陷入僵局状态,一直等待资源被释放。

为避免死锁的发生,可以采取一些措施,如尽量减少互斥资源的使用时间、按序申请资源、破坏循环等待条件等。在实际编程和系统设计中,需要谨慎考虑资源的分配和进程间的同步,以避免死锁的发生。

怎么实现共享变量?

进程共享变量就是可以使用共享内存。线程本就是共享整个地址空间的,要对共享变量加锁。

写一段死锁的代码

# 伪代码示例,可能导致死锁
lock1 = Lock()
lock2 = Lock()
def function1():
    lock1.acquire()
    # 假设这里需要访问共享资源 A
    lock2.acquire()  # 尝试获取另一个锁
    # 进行一些操作
    lock2.release()
    lock1.release()
def function2():
    lock2.acquire()
    # 假设这里需要访问共享资源 B
    lock1.acquire()  # 尝试获取另一个锁
    # 进行一些操作
    lock1.release()
    lock2.release()
# 创建两个线程分别调用 function1 和 function2
thread1 = Thread(target=function1)
thread2 = Thread(target=function2)
# 启动线程
thread1.start()
thread2.start()
# 等待两个线程结束
thread1.join()
thread2.join()
    

上面的示例展示了两个线程分别尝试获取两个不同的锁,但是由于获取锁的顺序不同,可能会导致死锁的发生。在实际编程中,需要避免出现这样的情况,例如通过统一获取锁的顺序、避免嵌套锁等方式来预防死锁的发生。

锁有多少种?
管道有多少种?
协程相比线程到底好在哪里?

协程是轻量级线程,全部都在用户态,因此系统消耗资源非常低,协程没有了线程切换的开销,非常高效。不像线程一样是内核线程,由cpu调度,造成上下文切换,浪费资源。协程的效率比较高。

线程实现数据共享的方式是共享内存,需要加锁,避免线程安全问题。而协程是单一个线程,不会有线程安全问题,不需要加锁,避免了锁竞争。

协程更加轻量,一个线程栈可能要1M左右,创建一个协程可能只要几K或者几十K。

协程的劣势是什么?

协程不好控制

  • 进程的状态

    处理器通过异常跳转表执行异常处理程序进行切换。进程就是一个执行中的程序的实例。进程的上下文:存放在存储器中的程序的代码和数据,它的站栈、通用寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。进程的地址空间。

  • 进程与线程的区别

    进程是操作系统对一个正在运行的程序的一种抽象,只有的单一的控制流。一个进程实际上可由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。

    每个进程都有地址空间是独立的。

    进程有自己的地址空间,两个进程是内存隔离的。eg:a进程0x40000处的内存值跟b进程0x40000处的内存值可以不同。

    线程是实际执行的基本单位,用于分配cpu执行时间,每个线程有自己的寄存器状态,拓展后可能还有一些tls,用于保存线程相关的变量。

    是CPU调度和分派的基本单位,他是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源。在运行时,只是暂用一些计数器、寄存器和栈 。

  • 什么是context switch,触发它的代价是什么。

    操作系统内核使用上下文切换的异常控制流来实现多任务。内核为每一个进程维持一个上下午。上下文就是内核重启一个被抢占的进程所需的状态。包括:通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核的数据结构(页表、进程表、文件表)。调度,保存上下文、恢复某个先前被抢占的进程被保存的上下文、将控制传递给这个新恢复的进程。

    影响性能。对于中断处理程来说高速缓存是冷不命中的。程序所需的数据都不在高速缓存中。

  • 僵尸进程

    已终止的子进程。waitpid

  • IPC进程间通讯的方式,为什么用进程间通讯

    • 匿名管道(pipe)

    • 具名管道(FIFO)

    • POSIX消息队列

    • 共享内存

    • 信号

    • Sockets

    共享内存效率高,但是受网络宽带延迟限制,不能高效的共享两台物力机器的内存。

    TCP sockets和pipe都是操作文件描述符,用来收发字节流。但TCP是双向的,Linux的pipe是单向的。进程间通信得两个文件描述符,并不方便。而且进程要有父子关系才能用pipe。

    进程间通信首选TCP Sockets:①由一个进程独占,系统会自动回收,即使程序以外退出,也不会留下垃圾。②对端挂掉了感知。③可以跨主机,具有伸缩性④tcpdump和Wireshark可以记录和重现。⑤TCP可以跨语言。共享内存不能跨语言。⑥数据流协议,在此智商可构建RPC/HTTP/SOAP邓上层通信协议。广播协议,可构建可观可控的分布式系统。

  • 如何实现同步?

    • 互斥量(mutex)

    • 条件变量(condition variable)

    • 读写锁(reader-writer lock)

    • 文件锁(record locking)

    • 信号量(semaphore)

    互斥锁和互斥量在我的理解里没啥区别,不同叫法。广义上讲可以值所有实现互斥作用的同步机制。狭义上讲指的就是mutex这种特定的二元锁机制。

    互斥锁的作用就是互斥,mutual exclusive,是用来保护临界区(critical section)的。所谓临界区就是代码的一个区间,如果两个线程同时执行就有可能出问题,所以需要互斥锁来保护。

    信号量(semaphore)是一种更高级的同步机制,mutex可以说是semaphore在仅取值0/1时的特例。Semaphore可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

    自旋锁是一种互斥锁的实现方式而已,相比一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock)则是不断循环并测试锁的状态,这样就一直占着cpu。

    同步锁好像没啥特殊说法,你可以理解为能实现同步作用的都可以叫同步锁,比如信号量。

    最后,不要钻这些名词的牛角尖,更重要的是理解这些东西背后的原理,叫什么名字并没有什么好说的。这些东西在不同的语言和平台上又有可能会有不同的叫法,其实本质上就这么回事

    不用读写锁和信号量。读写锁容易出错,读锁是可重入的,写锁是不可重入的,写锁通常会阻塞后来的reader lock, reader lock在重入时可能会死锁。条件变量配合互斥器可以完全替代信号量的功能了,而且不易出错。

  • 线程共享

  • mutex锁是如何实现的?一个lock操作的侧滑盖那本是什么,是否block的成本是不一样的?

    锁采用信号量实现的,只有0和1两种状态。以提供互斥位目的的二元信号量常称为互斥锁。执行P操作称为对互斥锁加锁,执行V操作成为对互斥锁解锁。对一个互斥锁加了锁,但是还没有解锁的线程称为占用这个互斥锁。P和V操作的结合创建了一组状态,叫做禁止区,禁止区完全包括乐不安全区。信号量确保了对临界区的互斥访问。

  • mutex和spin_lock的区别?以及优缺点。

    自旋锁是一种互斥锁的实现方式而已,相比一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock)则是不断循环并测试锁的状态,这样就一直占着cpu。

    自旋锁**(spin lock)与互斥量(mutex)**的比较

    自旋锁是一种非阻塞锁,也就是说,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。

    互斥量是阻塞锁,当某线程无法获取互斥量时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥量后,操作系统会激活那个被挂起的线程,让其投入运行。

    两种锁适用于不同场景:

    如果是多核处理器,如果预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,使用自旋锁是划算的。

    如果是多核处理器,如果预计线程等待锁的时间较长,至少比两次线程上下文切换的时间要长,建议使用互斥量。

    如果是单核处理器,一般建议不要使用自旋锁。因为,在同一时间只有一个线程是处在运行状态,那如果运行线程发现无法获取锁,只能等待解锁,但因为自身不挂起,所以那个获取到锁的线程没有办法进入运行状态,只能等到运行线程把操作系统分给它的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。

    如果加锁的代码经常被调用,但竞争情况很少发生时,应该优先考虑使用自旋锁,自旋锁的开销比较小,互斥量的开销较大。

    自旋锁就是通过原子操作实现的呀

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    lock = 1;
    while(lock){
    }
    // lock=0 now
    // critical code
    
    或者
    lock = 1;
    while(lock){
    check_environment_and_set_lock_to_zero_or_continue;
    }
    // lock=0 now
    //critical code
    
  • 什么是race?

    一个程序的正确性依赖于一个线程在另一个线程到达y点之前到达它的控制流x点时,就会发生竞争。线程化的程序必须对任何可行的轨迹线都正确工作。

    为了消除竞争,可以动态地为每个整数ID分配一个独立的块,并且传递给每个线程例程一个指向这个块的指针。线程例程必须释放这些块以避免存储器泄露。

  • 消息队列

  • 共享内存

  • 栈/堆区别,大概的位置,各往哪个方向生长

    栈向下,堆向上、中间共享库

  • 各类变量存储在哪个区域

    已初始化的全局变量在.data段,为初始化的全局变量在.bss段,函数里的对象在栈、动态分配的对象在堆

  • 用户态和内核态的区别

    处理器提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。在某个控制寄存器中模式位来提供这种功能。

    一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问任何存储器位置。没有设置模式位时,进程就运行在用户模式中。不允许执行特殊指令,如停止处理器,改变模式位,发起I/O操作等等。必须通过系统调用接口间接的访问内核代码和数据。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障、陷入系统调用这样的异常。

  • 虚拟存储器和物理存储器分别是什么?

  • 内存换页算法

  • 死锁的条件与算法

    死锁就是一组线程被阻塞了,等待一个永远也不会为真的条件。

  • 可重入性:

    可重入函数被多个线程调用的时候,不会引用任何共享数据。

  • 什么是协程

    协程切换不过是保存寄存器+跳转到另一个函数的半中间。在此之上可以构建个调度器,也可以不构建。

    假设你有10个文件,你想把10个文件拼接起来模拟成一个大文件。但是你内存又没那么大,那只好读一点让用户消耗一点了。所以你得写个filecat这样的adaptor。

    同步接口

    用户仿佛是在读同一个文件,但自己不好实现。得实时检查读到第几个文件了,上面读好了没。后面还有没文件。要维护一堆状态。

    异步接口

    实现很简单,直接把用户的回调挨个传到不同的文件读写调用里去。要是用户想读三行,停一停去做些别的(比如处理下这三行),是很麻烦的,因为你这个adaptor往用户端塞数据塞得根本停不下来。所以用户得手动维护这些状态。

    协程

    解决办法就是协程。你会发现无论是同步还是异步,都有这种“手动维护状态”的问题。要想让adaptor和用户都开心,解法自然是协程。我现在就用代码来表示状态,执行一行就是状态的转移。但是两头的状态要交替变化,这边做三行,轮到那边做了;那边做完了,又回到这边来,这跳来跳去的,就是协程了。

  • 动态库和静态库的区别,动态库映射在进程模型的哪里

  • 动态链接和静态链接的优缺点?

  • 从源程序到可执行文件整个过程

  • linux常用命令:查看进程、查看网络、权限修改

  • proc文件系统常识

  • 32位机器和64位极其在函数调用上的区别

  • 64位CPU指的是什么

    通用寄存器的宽度或者数据总线的宽度。

  • 不定参数函数的实现

  • valgrind动态监测内存泄露的原理

  • 如何设置任务调度。

    参考进程调度的方法,先进先出,最短作业优先或者设置优先级。

    用滑动窗口设置优先级,在规定的时间粒度内按照各种资源的需求、i/o或者cpu做一些公式的技术,然后作为优先级的依据。

Managing Concurrency

Threads, Processes, and Dispatching

进程线程协程,相关的应用场景

协程切换

线程和进程的区别和联系

知道协程吗?

COW (copy on write) 如何实现

Processes

僵尸进程和孤儿进程的危害

僵尸进程和孤儿进程。

线程池满了处理策略

进程,线程和协程

线程的几种状态

如何停止线程

多线程

进程和线程的概念

进程和线程的同步方式

进程间通信 匿名管道和命名管道的区别

进程和线程区别,线程之间通讯怎么实现?

进程的内存管理、内存分布

Threads

线程共享哪些东西,为什么还需要通信?(不太会)

多线程之间数据是怎么共享的

子线程结束,主线程才能结束,这个是怎么控制的?

线程同步一个进程的什么?

Dispatching

用户态和内核态的区别

什么是用户态和内核态

什么时候会切换到内核态

用户空间和内核空间

怎么样从用户态到内核态,举例说明中断的例子

ctrl c用什么实现

当你读取一个文件 fail了,这个过程是怎么样的?

Process Creation

Concurrency

Independent and Cooperating Threads

Atomic Operations

The “Too Much Milk” Problem

Interprocess communication

Synchronization

线程同步(只答了信号量,没答锁)

信号量的实际应用

ReentrantLock 与 synchronized 区别

races

线程之间的冲突如何解决,会有一些锁,比如互斥锁了,读写锁了

如果线程间抢占资源,会有什么后果?

inconsistency

Locks

读写锁和互斥锁的概念用法

读写锁和互斥锁

信号锁和互斥锁

跳表的实现是怎样的?

跳表(Skip List)是一种数据结构,它通过在有序链表的基础上增加多层索引来实现快速的查找。以下是跳表的简单实现思路:

  • 节点结构:跳表由一个或多个层级组成,每个节点包含一个值和多个指向下一个节点的指针数组。最底层的节点形成一个普通的有序链表。
  • 插入操作:为了插入一个新元素,首先从顶层开始,按照链表中的顺序找到合适的位置,并且在插入的同时决定是否要将这个元素“升级”到更高层的索引中。这种“升级”的概率是可以调控的,一般通过随机算法来决定节点是否会升级到更高的索引层。
  • 删除操作:删除操作需要在所有层级上进行更新,以保证数据结构的完整性。
  • 查找操作:从最顶层的节点开始,根据索引逐层向下查找目标元素,直到找到目标元素或者确定它不存在为止。

跳表的实现相对较为复杂,需要处理节点之间的关系、节点的插入、删除、查找等操作,并且需要考虑索引的更新策略。通过合理的索引设计,跳表可以达到与平衡二叉树类似的时间复杂度,具有较好的查找性能。

Condition Variables

Monitors

Implementing Locks

Deadlock

死锁怎么产生的?举个例子, 那怎么解决死锁?

Scheduling

CPU调度算法,我说了先进先上和最短的先上、IO密集型的先上

Memory management

内存管理

虚拟内存和物理内存

虚拟内存是什么

LINUX的内存管理系统

起几个线程死循环 cpu会爆吗?

Linking (static and dynamic)

Dynamic Linking

Dynamic Storage Management

Stack Allocation

Heap Allocation

Storage Reclamation

Virtual Memory

Dynamic Address Translation

Base and Bound Translation

Multiple segments

Paging

Translation Lookaside Buffers (TLBs)

Miscellaneous Topics

Demand Paging

Page Faults

Page Fetching

Page Replacement

问 LRU 和 LFU,这个我知道啊,内存置换算法

Magnetic Disks (Hard Drives)

Communicating with I/O Devices

磁盘转一次要多久

阻塞是啥?send recv 阻塞非阻塞的区别?阻塞的线程占用CPU吗?为什么vim一个超大的文件CPU会卡?

File systems

File Systems

如何判断一个文件有没有被修改过

Inodes

Block Cache

Free Space Management

Block Sizes

Disk Scheduling

Working directories

File System Crash Recovery

Approach #1: check consistency during reboot, repair problems

Approach #2: ordered writes

Approach #3: write-ahead logging

Remaining problems

Protection

Authentication

Authorization

Access Enforcement

Rights Amplification

Managing Flash Memory

Virtual machines

用过虚拟机, 知道虚拟机配网络的有哪几种模式吗",“有桥接, nat, host-only(面试官追问它们的区别时候, nat, host-only没分清楚,这块儿只是用过, 没深入了解)

Implementing hypervisors

History/usage of virtual machines

操作系统

1 select,poll和epoll

其实所有的I/O都是轮询的方法,只不过实现的层面不同罢了.

这个问题可能有点深入了,但相信能回答出这个问题是对I/O多路复用有很好的了解了.其中tornado使用的就是epoll的.

selec,poll和epoll区别总结

基本上select有3个缺点:

  1. 连接数受限
  2. 查找配对速度慢
  3. 数据由内核拷贝到用户态

poll改善了第一个缺点

epoll改了三个缺点.

关于epoll的: http://www.cnblogs.com/my_life/articles/3968782.html

2 调度算法

  1. 先来先服务(FCFS, First Come First Serve)
  2. 短作业优先(SJF, Shortest Job First)
  3. 最高优先权调度(Priority Scheduling)
  4. 时间片轮转(RR, Round Robin)
  5. 多级反馈队列调度(multilevel feedback queue scheduling)

常见的调度算法总结:http://www.jianshu.com/p/6edf8174c1eb

实时调度算法:

  1. 最早截至时间优先 EDF
  2. 最低松弛度优先 LLF

3 死锁

原因:

  1. 竞争资源
  2. 程序推进顺序不当

必要条件:

  1. 互斥条件
  2. 请求和保持条件
  3. 不剥夺条件
  4. 环路等待条件

处理死锁基本方法:

  1. 预防死锁(摒弃除1以外的条件)
  2. 避免死锁(银行家算法)
  3. 检测死锁(资源分配图)
  4. 解除死锁
    1. 剥夺资源
    2. 撤销进程
  5. 互斥锁

死锁概念处理策略详细介绍:https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/10.html

4 程序编译与链接

推荐: http://www.ruanyifeng.com/blog/2014/11/compiler.html

Bulid过程可以分解为4个步骤:预处理(Prepressing), 编译(Compilation)、汇编(Assembly)、链接(Linking)

以c语言为例:

1 预处理

预编译过程主要处理那些源文件中的以“#”开始的预编译指令,主要处理规则有:

  1. 将所有的“#define”删除,并展开所用的宏定义
  2. 处理所有条件预编译指令,比如“#if”、“#ifdef”、 “#elif”、“#endif”
  3. 处理“#include”预编译指令,将被包含的文件插入到该编译指令的位置,注:此过程是递归进行的
  4. 删除所有注释
  5. 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告时可显示行号
  6. 保留所有的#pragma编译器指令。

2 编译

编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。这个过程是整个程序构建的核心部分。

3 汇编

汇编器是将汇编代码转化成机器可以执行的指令,每一条汇编语句几乎都是一条机器指令。经过编译、链接、汇编输出的文件成为目标文件(Object File)

4 链接

链接的主要内容就是把各个模块之间相互引用的部分处理好,使各个模块可以正确的拼接。 链接的主要过程包块 地址和空间的分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等步骤。

5 静态链接和动态链接

静态链接方法:静态链接的时候,载入代码就会把程序会用到的动态代码或动态代码的地址确定下来 静态库的链接可以使用静态链接,动态链接库也可以使用这种方法链接导入库

动态链接方法:使用这种方式的程序并不在一开始就完成动态链接,而是直到真正调用动态库代码时,载入程序才计算(被调用的那部分)动态代码的逻辑地址,然后等到某个时候,程序又需要调用另外某块动态代码时,载入程序又去计算这部分代码的逻辑地址,所以,这种方式使程序初始化时间较短,但运行期间的性能比不上静态链接的程序

6 虚拟内存技术

虚拟存储器是指具有请求调入功能和置换功能,能从逻辑上对内存容量加以扩充的一种存储系统.

7 分页和分段

分页: 用户程序的地址空间被划分成若干固定大小的区域,称为“页”,相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配。

分段: 将用户程序地址空间分成若干个大小不等的段,每段可以定义一组相对完整的逻辑信息。存储分配时,以段为单位,段与段在内存中可以不相邻接,也实现了离散分配。

分页与分段的主要区别

  1. 页是信息的物理单位,分页是为了实现非连续分配,以便解决内存碎片问题,或者说分页是由于系统管理的需要.段是信息的逻辑单位,它含有一组意义相对完整的信息,分段的目的是为了更好地实现共享,满足用户的需要.
  2. 页的大小固定,由系统确定,将逻辑地址划分为页号和页内地址是由机器硬件实现的.而段的长度却不固定,决定于用户所编写的程序,通常由编译程序在对源程序进行编译时根据信息的性质来划分.
  3. 分页的作业地址空间是一维的.分段的地址空间是二维的.

8 页面置换算法

  1. 最佳置换算法OPT:不可能实现
  2. 先进先出FIFO
  3. 最近最久未使用算法LRU:最近一段时间里最久没有使用过的页面予以置换.
  4. clock算法

9 边沿触发和水平触发

边缘触发是指每当状态变化时发生一个 io 事件,条件触发是只要满足条件就发生一个 io 事件