介绍
本文记录的是2020.5月份应聘蔚来汽车linux 开发岗位的经历。
BOSS 岗位薪资:30K ~60K
总结:本次岗位应聘总共经历了4次面试,三次电话面试,一次视频面试。总体感觉难度中等,但是由于个人在最后一面视频面试中比较紧张,并且手写算法比较生疏,导致表现不如意,很遗憾最终没能进入该公司。虽然两个月后HR又请我再次面试,但是我已经入职新公司,并且与新同事相处愉快,就婉拒了。
希望本文能够帮助有需要的朋友,还请关注、点赞、收藏三连~~~
请参考上部分:【面试心经】——蔚来汽车Linux 岗位开发01-CSDN博客
面试内容
4. 三次握手和四次握手的过程?
TCP的三次握手:
- 首先由Client发出请求连接即 SYN=1 ACK=0 (请看头字段的介绍), TCP规定SYN=1时不能携带数据,但要消耗一个序号,因此声明自己的序号是 seq=x
- 然后 Server 进行回复确认,即 SYN=1 ACK=1 seq=y, ack=x+1,
- 再然后 Client 再进行一次确认,但不用SYN 了,这时即为 ACK=1, seq=x+1, ack=y+1.
为什么要进行三次握手呢(两次确认)?
我们首先要知道TCP三次握手的过程,确认两个条件:
- 双方都知道彼此做好发送数据的准备
- 双方协商初始序号知道对方的通信能力,比如mtu,设置window大下等
如果只进行两次确认。服务器返回的第二个SYN包,客户端没有收到。那么服务器认为客户端准备好了。但是客户端其实别没有准备好(因为他并不知道服务器的序号),导致服务端给客户端发送消息,客户端一直无法进行响应,浪费资源。
TCP 四次挥手
一次挥手:数据传输完毕,客户端想要释放连接(没有数据需要传输给服务端了),于是向服务端发送一段TCP报文请求释放连接,然后进入终止等待状态一。并且停止在客户端到服务端方向上发送数据,但是客户端仍然能接收从服务端传输过来的数据。FIN=1 表示请求释放连接, u为随机生成的起始报文段序号(FIN=1,seq=u)
第二次挥手:服务端收到连接释放报文,立即发出确认报文,表示接收到客户端发送的释放连接的请求,并进入关闭等待状态。(ACK=1,seq=v,ack=u+1)
第三次挥手:服务端自从发出ACK确认报文之后,经过了关闭等待阶段,做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文表示已经准备好释放连接了(没有数据需要传输给客户端了),然后进入最后确认状态。(FIN=1,ACK=1,seq=w,ack=u+1)
第四次挥手:客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,于是再次向服务端发送报文表示接收到服务端准备好释放连接的信号,并进入TIME-WAIT阶段等待2MSL ( 最大报文生存时间) 后再断开连接,服务端收到最终确认报文后立即断开连接,双方断开TCP连接。(ACK=1,seq=u+1,ack=w+1)
5. TCP 和 UDP 的区别?
- TCP是面向连接的,UDP是无连接的。就是在发送消息前不需要建立连接
- TCP提供可靠的服务,他能保证传送的数据无差错,不丢失,不重复,按序到达。UDP提供不可靠的服务。TCP是通过校验和,序号标识,确认应答实现可靠传输。如丢包时的重发控制
- UDP拥有较好的实时性,工作效率比TCP要高。适用于对高速传输和实时性较高的通行或者广播通信。TCP适用于安全要求较高的场景。
6. 如何解决hash冲突?
hash 冲突的解决方式一般有以下几种:
- 开放地址
- 链地址
- 再哈希法
7. 字节对齐(char,char,short,int; char,short,char,int)?
结构体中(char,char, short,int) 占8字节;(char,short,char,int)占12字节。
8. static 和 volatile关键字的作用?
volatile 关键子修饰的变量。表示该变量可能会被意想不到的改变。避免编译器在编译过程中进行优化。访问该变量时,需要从该变量的地址中去访问。应用场景有如下几种:
- 硬件寄存器
- 一个中断服务子程序中会访问到的非自动变量
- 多线程应用中被几个任务共享的变量。
static 关键字修改的变量会修改其作用域以及生命周期,以及存储地址。
- static 修饰的全局变量或者函数,他们的作用域仅在当前文件生效,别的文件无法直接访问。
- static 修饰的局部变量,只会初始化一次,并且函数结束后,不会被释放。
- static 修饰的局部变量保存在全局数据区。
9. 线程和进程的区别?
进程是操作系统资源分配的最小单元,线程是cpu调度和执行的最小单元。 它们的区别主要有以下:
- 资源的开销:每个进程都有自己的独立的程序上下文,程序之间的切换消耗较大;同一个进程,线程之间的是共享代码段和数据空间,但是每一个线程都有自己独立的运行栈和程序计数器。线程之间的切换消耗较小。
- 内存分配:线程共享本进程中的地址空间和资源,进程之间的地址空间和资源都是相互独立的。
- 影响关系:一个进程崩溃之后,在保护模式下,不会对其它进程产生影响。但是线程一旦崩溃整个进程都会崩溃。
从上面的区别我们可知,在不同的情况下采取适当的方式:
- 需要频繁进行创建和销毁的优先使用线程(常见的就是web服务器,来一个连接就新建一个线程,创建和销毁的代价较少)
10. 堆和栈的理解?
- 数据结构 :堆是先进先出、栈是先进后出
- 空间分配:栈是由操作系统自动分配释放,存放函数的参数值,局部变量的等;堆一般是有程序员分配释放,若程序员不释放,则会造成内存泄漏。或者是当程序结束,可能由OS回收。
- 空间大小:一般32位主机的情况下,堆内存可以达到近4G,因而几乎没有什么限制。但是栈空间是有限制的,一般为8M。当超过该限制,就会出现栈溢出错误。
- 生长方向:栈地址向下生长;堆地址向上生长
- 碎片问题: 频繁的进行申请/释放堆空间,会造成内存空间的不连续。从而造成大量的碎片,使程序效率降低。栈则不会出现这种问题。
11. select 和 epoll 的区别?
select 和epoll都是I/O 多路复用的机制。I/O多路复用就是通过一种机制,可以监听多个描述符。一旦某个描述符就绪,就会通知相应的程序进行读写。
select 和epoll 相对比,理论上,epoll的效率高于select。其原因如下:
- select 每一次调用都会将所有的fd 拷贝到内核中,当fd 很多时将会是一个很大的开销。而epoll 只会在epoll_ctl中将事件注册到内核中,而不是在epoll_wait时重复拷贝,因此epoll只会对fd 进行一次拷贝。
- select在等待时,是在内核中遍历传进来的所有的fd,查看哪个fd处于就绪状态。将就绪的fd 拷贝到fd_set中,再传到用户空间。当fd很多时,也会造成很大的开销。而epoll通过epoll_ctl将fd加入内核fd队列时,就已经为每个fd指定了回调函数,当fd就绪时,则会触发回调函数,将就绪fd拷贝到内核就绪fd队列中。而epoll_wait实际就是查看该就绪fd队列是否为空
因此当具有大量fd,交互频繁的应用场景,我们应该使用epoll;但是在fd 较少,并且交互不频繁的应用场景,我们使用select 的效率比epoll更高。因为epoll 使用了回调,相对比,性能优化的不多。
12. 什么是死锁?如何避免死锁?
死锁一般是由于竞争资源导致多个进程或线程通信阻塞。四个必要条件:
互斥使用:当资源被一个线程使用时,别的线程不能使用。
不可抢占:资源请求者不能从资源占有者中夺取资源,资源只能由资源占有者释放
请求和保持:当一个资源请求者请求其他资源时,保持对原有资源的占有
循环等待:即存在一循环坏等待队列
死锁避免的思路就是让其中四个必要条件之一不成立即可:
- 避免互斥
- 避免一个线程获取同时占有多个资源
- 尝试使用定时锁
13. int 类型在32位主机和64位主机上分别占有几个字节?int类型的指针分别在32位主机和64位主机上分别占几个字节?
变量占字节大小是由编译器决定的。编译器可以根据自身硬件来选择合适的大小。但是至少要满足以下约束:short和int 类型至少16bit位,long 类型至少为32,并且short型长度并不能超过int 型,int 型长度不能超过long 型。这就说明变量的大小是由编译器决定的。
目前主流的编译器,在32位机器或者64位机器中,int 类型的都是4字节
指针变量表示的是一个变量的地址,而所谓的32位机器或者64位机器就是寻址位数。因此,在32位机器中,指针变量占4字节,64位机器中,指针变量占8字节。
14. 网络编程的流程?
socket 编程根据传输协议,分为TCP/UDP两种:
Plain Text TCP服务器端 1、创建套接字(socket) 2、将套接字绑定到一个本地地址和端口上(bind) 3、将套接字设为监听模式,准备接收客户端请求(listen) 4、等待客户请求到来,当请求到来后,接收连接请求,返回一个新的对应于此次连接的套接字(accept) 5、用返回的套接字和客户端进行通信(send/recv) 6、返回,等待另一客户请求 7、关闭套接字
TCP客户端 1、创建套接字(socket) 2、向服务器发出连接请求(connect) 3、和服务器端进行通信(send/recv) 4、关闭套接字
UDP服务器端 1、创建套接字(socket) 2、将套接字绑定到一个本地地址和端口上(bind) 3、等待接收数据(recvfrom) 4、关闭套接字
UDP客户端 1、创建套接字(socket) 2、向服务器发送数据(sendto) 3、关闭套接字 |
16. 线程池的概念?
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
编程题
1. 创建三个多线程,依次输出A,B,C 10次?
#include #include #include char flag = 'A'; char num = 10; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void * func(void * arg) { char *tmp = arg; printf("----%c\n",*tmp); while(num != 0) { pthread_mutex_lock(&mutex); if(flag == *tmp) { printf("%c\n",*tmp); if(*tmp == 'A') { flag = 'B'; }else if(*tmp == 'B') { flag = 'C'; } else if( *tmp == 'C') { flag = 'A'; num --; } } pthread_mutex_unlock(&mutex); } }
int main() { pthread_t id[3] = {0}; pthread_create(&id[0],NULL,func,"A"); pthread_create(&id[1],NULL,func,"B"); pthread_create(&id[2],NULL,func,"C"); pthread_join(id[2],NULL); return 0; } |
2. 将两个有序的链表,合并为一个有序链表?
class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if (l1 == null) { return l2; } else if (l2 == null) { return l1; } else if (l1.val < l2.val) { l1.next = mergeTwoLists(l1.next, l2); return l1; } else { l2.next = mergeTwoLists(l1, l2.next); return l2; } } } |
3. 判断出大小端?
小端:低地址存放低位;大端:高地址存放低位
C union Endian { int a; char s[4]; } e; e.a = 0x12345678; if(e.s[0] == 0x78) printf("little"); else if(e.s[0] == 0x12) printf("big"); |