tsy77 / blog

78 stars 2 forks source link

计算机系统有关的若干问(持续更新) #17

Open tsy77 opened 5 years ago

tsy77 commented 5 years ago
  1. 大小端模式

    大小端模式是内存存储数据高低位的方式,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。大小端只与CPU结构有关,与CPU硬件无关,CPU只负责程序运行,只有架构才管怎样取指令以及如何存储。 由于全局变量在编译阶段就会确定其内存分配,所以编译器还是需要知道是大端还是小端编译的,同理,操作系统也要知道大小端。

  2. 二进制机器码为什么与操作系统有关

    二进制的机器码按道理说是是CPU可以识别的指令,那么为什么同样的硬件环境,操作系统不同,二进制机器码不同呢?这是因为二进制机器码与OS的ABI相关,所以二进制机器码会随OS变化。

  3. 计算机为什么要用补码存储

    补码是在反码基础上加1得到的,反码的出现是为了解决“正负相加等于0”的问题,而补码的出现则是为了解决“只有一个0”的问题。

  4. 软硬件在虚拟内存上是如何配合的

    虚拟内存技术需要软硬件的协调,虚拟内存的这种缓存管理机制是通过操作系统内核,MMU(内存管理单元)中的地址翻译硬件和每个进程存放在主存中的页表(page table)数据结构来实现的。

    页表(page table)是存放在主存中的,每个进程维护一个单独的页表。它是一种管理虚拟内存页和物理内存页映射和缓存状态的数据结构。它逻辑上是由页表条目(Page Table Entry, PTE)为基本元素构成的数组,数组的索引号对应着虚拟页号,数组的值对应着物理页号。数组的值可以留出几位来表示有效位,权限控制位。有效位为1的时候表示虚拟页已经缓存。有效位为0,数组值为null时,表示未分配。有效位为0,数组值不为null,表示已经分配了虚拟页,但是还未缓存到具体的物理页中。权限控制位有可读,可写,是否需要root权限

  5. 为什么要有数据对齐

    许多计算机系统对基本数据类型的合法地址作出了限制,要求某种类型对象的地址必须是K的倍数。这种限制或者说是建议旨在提高内存系统的性能。比如一个处理器每次总是从内存取8个字节,如果我们保证double类型的地址都是8的倍数,那么我们每次一个内存操作就可以读或者写值了。编译器为了达到数据对齐,会在struct的存储上插入间隙或在结尾加一些填充。

  6. 栈帧中为什么要存储%rbp(基地址)

    定长的栈帧在编译阶段就可以知道栈帧的长度,但对于变长的栈帧来说(比如函数调用alloca,alloca是在栈(stack)上申请空间)就需要%rbp来记录栈帧的基地址,在函数末尾执行leave命令,将栈指针%rsp保存成%rbp的位置,将%rbp弹出到%rbp寄存器。

  7. 驱动程序原理

    驱动程序连接着操作系统和设备控制器,是操作系统内核和机器硬件之间的接口。

    用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是linux的设备驱动程序工作的基本原理。

    驱动程序调用int register_chrdev(unsigned int major, const char name, struct file_operations fops) 把厂商的接口函数fops存放到chrdevs这个数组中,上面提到的主设备号吗就是数组index,当操作系统访问某个设备时,就通过这个index找到驱动程序。

  8. 调试器断点原理

    普通断点原理:直接改写断点内存地址的第一个字节,替换为int3 (0xcc,软中断机制),并保存原始字节至OD维护的一张断点表处。程序运行到此处时会中断,抛出异常,OD通过捕获该异常,暂停程序运行至断点内存地址处(断点处指令仍未执行),当执行断点处指令时,并不是完全从内存中取指令,因为该断点内存中的第一个指令已经被改写为0xcc,因此,此时执行的指令是由断点表中保存的原始字节与后续的二进制数据自合而成。执行完断点处指令后,只要断点没有被删除,其内存中的第一个字节仍然是0xcc。当删除某个断点时,od会根据删除的断点地址在断点表中查找对应的原始字节,并恢复至对应的内存中。

    编译器在生成机器码时同时会生成相应的调试信息。调试信息代表了可执行程序与源代码之间的关系,并以一种提前定义好的格式,同机器码存放在一起。

  9. TCP/IP back_log参数

    back_log代表连接队列的大小,也就是connection fd的数量,连接队列包括了半连接和全连接队列。

    半连接状态为:服务器处于Listen状态时收到客户端SYN报文时放入半连接队列中,即SYN queue(服务器端口状态为:SYN_RCVD)。

    全连接状态为:TCP的连接状态从服务器(SYN+ACK)响应客户端后,到客户端的ACK报文到达服务器之前,则一直保留在半连接状态中;当服务器接收到客户端的ACK报文后,该条目将从半连接队列搬到全连接队列尾部,即 accept queue (服务器端口状态为:ESTABLISHED)。

  10. dlopen、dlsym原理

    dlopen、dlsym是linux提供的用来加载动态链接库的系统调用。

    dlopen该函数用来打开动态库,并将其加载到进程的地址空间,完成初始化过程,并返回全局符号表(一般存储在进程的数据段)的句柄给调用进程,dlopen有两种打开方式的选择(RTLD_LAZY和RTLD_NOW),其中RTLD_LAZY表示延迟动态链接的重定位,等到使用全局方法或变量时再做重定位,而RTLD_NOW则是立刻重定位,相比于RTLD_LAZY加载速度较慢。

    dlsym则根据dlopen返回的全局符号表的位置,找到指定符号的。

  11. 信号处理的时机

    信号在程序从内核态转换到用户态时进行处理,信号集合包括block集和pending集,处于pending集中的信号,在没有处理完成时,进程收到相同信号不会排队,而会直接丢弃。

  12. 信号量的PV操作

    信号量是一种特殊的变量,实际上就是用来控制进程状态的一个代表某一资源的存储单元。表现形式是一个整型S和一个队列.

    struct semaphore {
        raw_spinlock_t                lock;
        unsigned int              count;
        struct list_head   wait_list;
    };
    
    void P(semaphore s)
    {
        s.value--; // 申请资源,边界是value已经为0了,那么现在变-1,表示有一个进程在等待
        if(s.value < 0)
        {
            将此进程加入就绪队列,等待;
            block(s.L);
        }
    }
    
    void V(semaphore s)
    {
        s.value++;
        if(s.value <= 0)
        {
            将进程P从就绪队列中移出;
            wakeup(P);// 叫醒P,让它起来干活
        }
    }

    其中信号量结构中list_head属性是等待信号量的线程队列,链表每一项是一个进程或线程控制快。

    block操作将当前进程的将其状态转为阻塞状态,停止运行,把该PCB插入到相应事件的等待队列中去。

    wakeup操作从队列中取出起一个PCB,将其从等待队列中移出,并置其状态为就绪状态,把该PCB插入就绪队列中,等待调度程序调度。

  13. 多进程、IO复用、多线程

    多进程、IO复用、多线程是计算机系统中的三种并发模型。

    多进程模型中,父进程接受请求,子进程负责具体工作。子进程通过父进程fork()产生,各自拥有独立的虚拟内存空间,但不利于共享数据,共享数据必须要通过IPC;同时父子进程有时还会出现竞争的关系,需要通过原子方法来避免。

    IO复用模型在一个进程中创建并发逻辑流,自己来调度这些流,常见的有poll、epoll等模型,epoll通过在内核高速缓冲区中维护一个fd的红黑树,用户态逻辑通过epoll_wait得到发生事件(就绪)的fd的数组。IO复用模型由于需要自己调度,若出现长时间阻塞CPU的操作,调度就会被阻塞,这也是node不适合CPU密集型操作的原因;同时IO复用模型也不能充分利用多核处理器。

    基于线程的并发模型混合了上述内核调度和数据共享的特点,多线程模型通过内核来调度,同时运行在单一的进程中,可共享除了栈、寄存器外的其他空间(堆、代码段、数据段等)。多线程模型由于可共享进程中的变量,随即引入了同步错误,需要通过信号量对共享变量进行保护。多线程可利用多核,但当变量保护(变量同步)很多时,就会影响性能。

  14. 线程出现的动机

    主要是考虑到上下文切换开销少和共享变量的特点,同时线程不像进程那样按照父子关系来组织,在同一进程中的线程都是对等的。