阿里云秋招C++一面(自答)
阿里云秋招C++一面一帖的个人回答
- 指针和数组啥区别 内存里怎么访问的
答:指针是独立的对象,是存储在内存中连续的4/8个字节,其中存储另个虚拟内存位置的地址(虚拟地址),其值允许被修改;数组是编译器层面的概念,但是编译后也只有一个地址,与指针不同的是存储的地址不可改变。除此之外,编译器维护数组的大小信息,sizeof(数组)会得到数组所有元素的大小之和,sizeof(指针)只得到指针本身的大小,sizeof值都是在编译时确定的,汇编代码里都是字面常量。CPU使用寄存器基址变址寻址访问数据元素,AT&T风格的汇编可能是这样:
mov %ebx, ARRAY_LABEL ;ARRAY_LABEL是一个int数组,基址放入ebx寄存器 mov %esi, $3 ;要访问第三个元素 mov %eax, $0(%ebx, %esi, $4) ;类似于eax=ARRAY_LABEL[3]
寄存器基址变址寻址格式为 :offset(base, index, scale),计算出地址 %ebx + 4 * %esi + offset
- 内存访问一个int 和访问一个int数组啥区别 x86下向量化指令SIMD用过没(见过没写过) arm访存指令
答:第一次访问一个int应该会直接读一次内存,之后可能直接从寄存器里读;读int数组至会发生两次内存访问,第一次读数组基址,计算出元素地址后在进行第二次内存访问。SIMD指的是 单指令多数据(Single Instruction Multiple Data)是一种数据级别并行的手段,基本思想是使用更宽的寄存器,在单条机器指令中处理多个数据元素,从而实现更快的计算速度。代表的指令有MMX指令(使用80位的浮点寄存器执行并行计算);SSE(流式啥啥啥扩展,记不清了)3/4,使用128位的寄存器;AVX指令(Advanced Vector Extension)使用512位的寄存器。ARM不懂,不过x86架构里无论是访存还是读寄存器,都是mov指令,根据操作数宽度,有movl(32bit)、movq(64bit)、movw(16bit)、movb(8bit)
- 声明一个数组 在内存里咋样
答:分成三种情况说:全局作用域中的数组,如果有初始值,会放在ELF可执行文件里的.data节或者.rodata节,在虚拟地址空间里放在数据区或者只读数据区;如果没有初始值,则放在.bss节,不占用EFL可执行文件的空间,映射方式是匿名映射,缺页时请求二进制0的页面,所以全局作用域的数组即使没有初始值,也会有0作为初值。局部作用域的数组直接在栈帧里分配,栈帧的大小一般在编译时就确定了,栈帧的分配就是esp/rsp寄存器直接减小一个常量。栈帧通常都是对齐的,所以可能分配比局部变量所实际需要更多的空间,此外canary机制也会占用一个局部变量,用于在运行时坚持局部缓冲区溢出。
- voliate关键字
答:编译器可能在检测到函数中从未修改过一个变量,而只在第一次读改变量时访问内存,由于认为变量不会被修改,可能优化为:以后每次读变量,都从寄存器里读。如果改变量会被其他线程(或者其他因素)修改,则当前函数就读不到最新的变量。volitale告诉编译器,该变量可能由未知因素修改,每次访问都去读内存,不要使用寄存器优化!
- 结构体 在内存中 怎么存放的
在内存中就是一个连续的字节块,访问时是先拿到基址,再计算要访问的成员的偏移量,最后发生访问。内存对其有这么几个含义:假设k是常量,内存块的基址必须是k的整数倍;结构体的每个成员相对于机构体的基址的偏移量必须是k的整数倍;结构体的总大小必须是k的整数倍。内存对其的意义在于:1.为了可移植性考虑,有的体系结构必须内存对齐,访问未对其的地址是不允许的错误行为;2.为了内存访问的效率考虑,不对齐的元素,CPU要拿到元素值可能需要连续两次访存;3.为了提高Cache命中率,一个元素放在一个Cache单元里,避免一个元素占用两个Cache单元
- pthread_create
答:POSIX线程API中创建线程的系统调用,接受四个参数,分别是线程ID的指针,线程属性结构体的指针,线程执行函数指针,待执行函数参数的指针。待执行函数的原型必须是void* (func)(void);返回值为int类型,为0表示线程创建成功,大于0表示出现错误,和errno具有相同的意义。该系统调用最终也会调用clone()复制task_struct,但是不会复制mm(表示进程虚拟内存空间的结构体指针)和页表
- epoll 内核原理
答:这个理解不深,先不说了
- 建堆 时间复杂度
答:建堆的过程就是不断插入元素,所以是O(nlogn)的
- malloc sbrk的内存里机制 mmap 文件映射 匿名映射 munmap 为啥munmap能直接释放一段内存 内核怎么实现的
答:malloc()预先使用系统调用(brk),在数据区的尾部分配一定量的空间,brk和srk都可以用于扩展数据区的上届,不同的是brk是系统调用,sbrk是库函数,brk必须指定新的边界地址,成功扩展后返回旧的边界地址;sbrk能以步长(step)为参数扩展数据区上界,这也是sbrk中s的意义,sbrk使用brk实现的。malloc()可能使用隐式空闲连表组织预分配的空间,当有新的分配请求时,顺序查找空闲链表,如果找到可满足请求的空闲区域,则分配(可能伴随着区域的分裂);如果找不到,就会使用sbrk()申请扩展上届。当调用free()释放空间时,也就是有释放请求时,直接标记相应的区块为空闲(可能伴随着区域的合并),至于是否收缩数据区上届,则依赖于具体的实现。
mmap文件映射需要在使用第一个参数指定文件的fd,第二个参数指定映射的起始位置(在文件中的起始位置),映射本身并不做任何从磁盘到内存的数据复制,只需要修改当前进程的页表项和vma链表。基本思想是将当前进程的一块虚拟内存区域和磁盘页面关联起来,内核只需要维护相关的数据结构,记录映射关系和映射区域权限,读文件映射区域会引发缺页中断,由缺页中断服务程序负责将页面从磁盘还入物理内存;写文件映射区将会导致页面被标记为“脏”,可能在页面换出时写回磁盘。如果mmap不指定有效fd,则为匿名映射,缺页时会请求二进制0的页面。munmap能卸载一个映射区域,涉及到的数据结构包括进程的页表项和vma链表,内核需要为被映射文件维护引用计数,当最后一个映射的进程munmap时,销毁数据结构。
- 线程进程
答:内核按照进程分配系统资源,但是按照线程执行调度,一个线程就是一个独立的控制流。Linux中,线程和进程都使用task_struct结构体表示,进程只是多个共享了虚拟地址空间的多个task_struct。线程独立的数据只有:寄存器组的状态和栈区,共享的区域包括:内核区、代码段、只读数据读、数据段和共享映射区,进程之间虚拟内存空间一般是完全独立的,但是执行同一个可执行文件的进程可能共享代码段和只读数据段。进程特有的信息还包括:进程的状态、进程的标识信息(所属用户、组,实际用户、组,有效用户、组,进程id)、进程的文件描述符表、进程的之间的关系(父进程指针、子进程链表、兄弟进程指针)、进程的统计信息(静态、动态优先级,当前是否获得CPU)、进程的命名空间和文件系统信息、进程的资源限制....还有好多,估计上百项了,实在记不住。进程和线程可以帮助程序员利用多核CPU实现并行,如果需要扩展到多机环境运行,那么设计之初就应该考虑选择多进程,如果没有充分使用多进程的理由,则应该使用多线程,理由如下:线程创建、撤销、涉及到的数据结构有限(主要是不用复制页表和虚拟内存相关的数据结构),很多资源都是共享的,更快;线程切换只需要切换CPU,调度起来更快;进程切换会导致TLB实效,影响地址翻译的速度;进程间数据共享需要通过内核,纵然共享内存已经足够高效。
- x86 调用寄存器 被调用寄存器
答:没太看明白问题。64位架构通用寄存器高达16个,函数调用者负责参数准备工作,如果超过8个参数,则超过的参数逆序入栈,6个以内的参数(第1~6个参数)分别放在rdi、rsi、rdx、rcx、r8、r9寄存器里,执行call指令将返回地址入栈,控制来到被调函数。被调函数执行push %rbp
保存旧栈帧的基址,执行mov %rsp, %rbp
, 设置新的栈帧基址,之后rbp减一个常量为局部变量分配空间,最后使用leave指令恢复rsp和rbp(也可以手动恢复,具体看编译器),调用ret从栈中取出返回地址,放在rip寄存器,控制来到call指令的后一条指令处。
- 讲了讲jemalloc结构 为啥jemalloc分配和释放快
答:不懂啥是jemalloc,只知道ptmalloc(glibc的使用的)和tcmalloc(google提供的一种实现)
- gdb 命令 (讲了gdb调试clang) 有没有用gdb调过汇编
答:attach pid,run,continue,next,step,/x(memory examnation,看内存状态),disassembly(反汇编当前函数),info 查看寄存器/当前线程的状态,break 行号,finish,print,diable(好像是禁用所有断点),pt打印栈轨迹。暂时想到这么多
- git 命令 rebase用过没 git机制 怎么实现的
答:用过,能产生比merge更干净的提交树。git怎么实现的,通过记录时间戳和diff结果吧,不太懂这个
- strace ;perf 原理是啥
strace用于跟踪系统调用,具体不懂;perf。。。没听过阿
最后,欢迎补充、指正