最近正是求职季,自己也经历了多次面试,网上看了许多面经。整个过程中,记录了很多,学到了很多,开个博客,分享给大家,祝大家Offer多多!
该系列博客会从几个不同的大方向,对常见问题进行分类,同时内容为我日常整理方便自己快速回忆,复习,并且为了尽可能覆盖更多的知识点,所以每个问题只有简短关键的回答,如果对相关知识点不熟悉的,建议多查阅一些相关资料。
系列文章目录
常规后端面试准备
操作系统常见问题:线程进程相关知识,Linux常规命令等
Linux常用指令
查看所有进程
ps -ax | less
- -a代表列出所有进程
- x代表列出没有tty(控制终端)的进程
- | linux指令里面的管道,利用管道将前一个程序的输出导入后一个程序(ps —> less)
拓展:ps是如何实现的?简述原理
Linux的/proc目录下面包含了所有进程的信息(ls /proc/
),每个进程都有一个对应的文件夹,名字为进程号。所以实现ps基本原理就是遍历改目录,获取所有的进程信息
文档内查找内容
cat file.txt | grep test
如何查看操作系统是几位的
uname -m
— x86_64 or x86
进程,线程,协程
- 进程:进程是资源分配的最小单位(分配内存等)
- 进程的fork都是先复制自身,然后再修改新进程的内容 —> init是所有进程的“老祖宗”
- 线程:线程是CPU调度的最小单位(CPU “fetch”一个线程,然后执行)
- 协程:“用户态的线程”,协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行(以同步的方式,在一个线程内执行任务)
- 进程 v.s. 线程
- 每个进程最少包含一个线程,每个线程必定属于某个进程
- 进程要比线程消耗更多的计算机资源,不同进程间的隔离度也更高(i.e. 一个进程终止一般不会影响到另一个进程),但是一个线程终止可能会导致该线程所属的整个进程同时终止
- 多进程:允许多个任务同时执行
- 多线程:允许一个任务的不同部分同时执行(embarrassingly Parallel)
- 进程内部所有线程共享内存空间
- 线程 v.s. 协程
- 线程切换:操作系统完成,overhead较大(e.g. 保存当前寄存器,flush,重新加载)
- 协程切换:用户完成,overhead较小(还是在同一个线程内)
进程的几种状态
- R(TASK_RUNNING)— 可执行状态
- ready和running都属于这个状态
- S(TASK_INTERRUPTIBLE) — 可中断的睡眠状态
- 处于该状态的进程,通常由于等待某事件的发生(如socket),而被挂起(task_struct被放入对应事件的等待队列中);
- 对应事件发生时,对应进程会被唤醒
- 如果使用ps查看进程,会有很多进程处于该状态(CPU能承受多进程的原因!)
- D(TASK_UNINTERRUPTIBLE) — 不可中断的睡眠状态
- 存在的意义是有些系统操作是不可打断的,如操作系统对某些硬件进行交互的时候,需要用这个进行保护(kill信号无法杀死这个状态的进程)
- T(TASK_STOPPED or TASK_TRACED) — 暂停状态或跟踪状态
- 如断点调试
- Z(TASK_ZOMBIE) — 退出状态,进程成为僵尸进程
- 进程在退出过程中,所有资源都被回收,除了task_struct结构(和少数资源)外,此时线程处于僵尸进程
- 父进程可以通过wait来使得僵尸进程退出
- X(TASK_DEAD) — 退出状态,进程即将被销毁
- 该状态通常很短暂(接下来就会销毁该进程),ps一般看不到
线程的几种状态
- new — 创建
- ready — 创建成功,可以被执行
- running — 正在执行
- waiting — 等待,由于IO或者有更高优先级的事情(如处理中断)
- terminated — 结束,释放资源
为什么需要协程?
- 节省CPU,协程是用户态的线程,不存在线程切换的开销;
- 节约内存,系统会给每个线程分配一定大小的栈内存(e.g. 8M)和堆内存(64M),线程数量有瓶颈;而协程的栈通常只有十几K,并且是从线程的堆里面分配出来的,数量受限小;
- 稳定性,不存在线程安全问题,因为协程是在一个线程内部,以同步的方式读写数据;
- 开发效率,可以利用协程将一些耗时操作异步化;
线程(进程)间的数据共享/通信
- 线程间通信
- 共享内存,共享变量,更需要考虑的是race condition
- 进程间通信
- 管道(pipe),单向通信,只能在具有亲属关系(多为父子关系)的进程间使用
- 命名管道(FIFO),可在任意两个进程间使用
- 信号量(semophore),一个计数器,多用于控制多进程访问共享资源
- 消息队列(message queue),储存在内核中的一个消息链表
- 信号(signal),类似于中断的概念
- 共享内存(shared memory)
- socket!!!(甚至可以做到分布式)
进程有哪些字段
- PID
- Stack
- Heap
- Execution context
- Program counter (PC)
- Stack pointer (SP)
- 寄存器(Registers)
- 代码
- 数据
- 独立内存空间
多线程如何做到thread-safe
- thread local变量(直接避免了race condition)
- 原子量(atomic)和原子操作(test-and-set,compare-and-swap)
- 临界区(critical section)
- 互斥锁(mutex)
- 信号量(semaphore)
- 事件对象(event)
活锁 V.S. 死锁
- 死锁:两个或两个以上的线程,因争夺资源而陷入互相等待(阻塞),会一直持续下去
- 可能是由于多个线程加锁的顺序不一样
- 活锁:线程在执行,但是由于某些条件未满足,所以都是无用功
- 形象的比喻,死锁是lock,活锁是try_lock(一直try),两者都是没有进度,但死锁是阻塞状态,活锁是运行状态
- 饥饿:某个线程“长期”处于等待资源状态,比如由于优先级不够高,系统一直不执行
僵尸进程 V.S. 孤儿进程
- 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用
wait
或waitpid
获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程- 任何一个进程退出后都会经历“僵尸进程”这个阶段,只有等到父进程调用
wait
或者waitpid之后
,进程才会真正结束 - 大量的僵尸进程可能会耗尽系统的进程号资源
- 任何一个进程退出后都会经历“僵尸进程”这个阶段,只有等到父进程调用
- 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(pid=1)所收养,并由init进程对它们完成状态收集工作
- 孤儿进程不会有什么危害,因为孤儿进程会被init“收养”并正常退出
- 如何避免僵尸进程?
- 1)通过信号机制;子进程退出时会向父进程发送SIGCHILD信号,父进程可以捕捉这个信号进行
wait
处理 - 2)fork两次(将子进程变为孤儿进程);先fork一次得到子进程1,然后在1里面再fork一次,成功后将1结束,从而使得子进程2变为孤儿进程,并被init进程“收养”
- 1)通过信号机制;子进程退出时会向父进程发送SIGCHILD信号,父进程可以捕捉这个信号进行
阻塞 V.S. 非阻塞,异步 V.S. 同步
- 阻塞:指调用结果返回之前,调用者会进入阻塞状态(线程状态,此时该线程可能会进入wait状态)等待。只有在得到结果之后才会返回(e.g. socket的recv函数)
- 非阻塞:指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回(e.g. socket的send函数,数据写到缓冲区之后立刻返回)
- 同步:在发出一个同步调用时,在没有得到结果之前,该调用就不返回(程序状态,如调用一个函数,此时该线程还可以在CPU上运行)
- 异步:在发出一个异步调用后,调用者不会立刻得到结果,该调用就返回了
- 同步 V.S. 阻塞
- 表面上看都是“卡住了”,但是阻塞指的是该线程被阻塞(CPU不再运行该线程);同步指的是程序/调用被阻塞,但是该线程可能还在CPU上面运行
- 两两组合
- 同步阻塞调用:得不到结果不返回,线程进入阻塞态等待;
- 同步非阻塞调用:得不到结果不返回,线程不阻塞一直在CPU运行;
- 异步阻塞调用:去到别的线程,让别的线程阻塞起来等待结果,自己不阻塞;
- 异步非阻塞调用:去到别的线程,别的线程一直在运行,直到得出结果;
大端,小端和名字的由来
- 名字由来 — 格列佛游记
- 大端 — 数据的高字节保存在内存的低地址中
- 小端 — 数据的低字节
字节对齐
- 基本的数据类型对齐,int 4,char 1,double 8…
- 结构体,类的长度对齐
- 原因:CPU加载数据常常是从对齐地址开始加载的;CPU一次不是加载1byte,而是多byte
memcpy和memmove
-
memcpy比memmove效率更高,更快
-
memcpy不会检查有无地址有无重叠,所以可能会出错
-
memmove会检查地址有无重叠,若有重叠会把src先拷贝到tmp再拷贝到dst
-
实现:
void *memcpy(void *dest, const void *src, size_t n) {
// 注意要先转换为char*
char * srcC = (char*) src;
char * dstC = (char*) dest;
while (n >= 0) {
*srcC++ = *dstC++;
}
return dest;
}
堆 vs 栈
- 分配方式不同 — 堆,程序申请,自行管理;栈,操作系统分配
- 管理方式不同 — 堆,程序管理;栈,操作系统管理
- 运行效率不同 — 栈的效率比堆高