AI岗的竞争实在是太激烈了,想转开发,中午投了腾讯没想到这么快就笔试+面试……自己完全没有准备好,痛定思痛,就从这次记录开始。
笔试部分:
1、实现C++中的memcpy函数
2、两个有序的单链表,将它们合并
3、一个双链表,实现删除功能
4、有编号1-300000的员工,现在有rand()函数可以生成[0, 65535]的随机数,如何在30w人中抽出10w个中奖用户
面试部分:
1、TCP三次握手,TCP和UDP的区别
2、讲讲进程和线程
3、进程通信的方式
4、用过epoll没有
5、C++中vector和list的区别
6、用户如何调用内核
1、memcpy实现
void *memcpy(void *dest, const void *src, size_t count)
{
char *d;
const char *s;
if (dest > (src+count)) || (dest < src))
{
d = dest;
s = src;
while (count--)
*d++ = *s++;
}
else /* overlap */
{
d = (char *)(dest + count - 1); /* offset of pointer is from 0 */
s = (char *)(src + count -1);
while (count --)
*d-- = *s--;
}
return dest;
}
要注意地址可能有重复的情况,此时从高位往低位拷贝
2、合并链表
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if (pHead1 == NULL)
return pHead2;
if (pHead2 == NULL)
return pHead1;
if (pHead1 -> val <= pHead2 -> val) {
pHead1 -> next = Merge(pHead1 -> next, pHead2);
return pHead1;
} else {
pHead2 -> next = Merge(pHead2 -> next, pHead1);
return pHead2;
}
}
};
注意考虑节点为NULL的情况
3、双链表删除节点
void remove(struct LinkNode *todelete) {
LinkNode * tmp = head;
if (tmp == NULL) {
return;
}
if (todelete == head) {
tmp.next.prev = NULL;
delete head;
return;
}
while (tmp.next != todelete) {
tmp = tmp.next;
}
tmp.next = todelete.next;
todelete.next.prev = tmp;
delete todelete;
return;
}
这里也是要注意讨论节点为NULL和删除的节点为头结点的情况
4、暂时还没想到特别好的方法,# TODO
第一次:客户端发送syn包(syn=j)到服务器端,并进入SYN_SENT状态,等待服务器确认。syn:同步序列号
第二次:服务器端接收到syn包,必须确认客户的syn(ack=j+1),同时自己也发送一个syn包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态
第三次:客户端接收到syn+ack包,向服务器发送ack包(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成握手
TCP与UDP的区别
1、基于连接与非连接
2、基于流模式和数据报模式
3、TCP要求系统资源较多,UDP要求较少
4、TCP保证数据正确性,UDP不保证
5、TCP保证数据顺序,UDP不保证
6、UDP没有拥塞控制,因此网络出现拥塞不会使源主机发送速率降低
7、TCP首部开销20字节,UDP首部开销8字节
8、TCP的逻辑通信通道是全双工的可靠通道,UDP的是半双工的不可靠通道
9、TCP只能一对一,UDP可以一对一,一对多,多对一,多对多
具体编程时的区别
1、socket()的参数不同
2、UDP server不需要调用listen和accept
3、UDP收发用sendto和recvfrom函数
4、TCP地址信息在connect / accept时确定
5、UDP在sendto和recvfrom函数中每次都需要指定地址信息
6、UDP:shutdown函数无效
TCP:
TCP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt(); * 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、开启监听,用函数listen();
5、接收客户端上来的连接,用函数accept();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
8、关闭监听;
TCP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置要连接的对方的IP地址和端口等属性;
5、连接服务器,用函数connect();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
UDP:
与之对应的UDP编程步骤要简单许多,分别如下:
UDP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、循环接收数据,用函数recvfrom();
5、关闭网络连接;
UDP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置对方的IP地址和端口等属性;
5、发送数据,用函数sendto();
6、关闭网络连接;
参考这篇博客
- 定义
进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位
线程:进程的一个实体,是CPU进行资源分配和调度的基本单位,它是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同一进程下的线程共享进程的所有资源
- 关系
一个线程可以创建和撤销另一个线程,同一个进程的多个线程之间可以并发执行
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列
- 区别
进程和线程的主要区别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,资源消耗大,效率差一些。但对于某些要求同时进行又需要共享变量的并发操作,只能用线程,不能用进程
1)简而言之,一个程序至少有一个进程,一个进程至少有一个线程
2)线程的划分尺度小于进程,使得多线程程序的并发性高
3)进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
4)线程在执行过程中与进程是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能独立执行,必须依存于应用程序中,由应用程序提供多个线程执行控制
5)从逻辑角度看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
- 优缺点
线程执行开销小,但不利于资源的管理和保护,进程正好相反
线程适合在SMP机器上运行,而进程则可以跨机器迁移
参考这篇博客
通常指无名管道,是UNIX系统IPC最古老的形式
特点:
- 它是半双工的,也就是说数据只能在一个方向上流动,具有固定的读端和写端
- 它只能用于具有亲缘关系的进程之间通信,也就是父子进程或兄弟进程
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write函数,但它不是普通的文件,并不属于任何文件系统,只存在于内存中
也称为命名管道,它是一种文件类型
特点:
- 它可以在无关的进程间交换数据,与无名管道不同
- 它有路径名与之关联,以一种特殊设备的形式存在于文件系统中
是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
特点:
- 消息队列是面向记录的,其中的消息具有特定的格式和特定的优先级
- 消息队列独立于发送与接受进程,进程终止时消息队列及其内容不会删除
- 消息队列可以实现消息的随机查询,不一定要先进先出,也可以按类型查询
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据
特点:
- 信号量用于进程间同步,若要在进程间传递数据需要结合内存共享
- 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作
- 每次对信号量的操作不限于加一或减一,可以加减任意正数
- 支持信号量组
指两个或多个进程共享一个给定的存储区
特定:
- 共享内存是最快的一种IPC,因为进程是直接对内存进行读取
- 因为多个进程可以同时操作,所以需要同步
- 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
1.管道:速度慢,容量有限,只有父子进程能通讯
2.FIFO:任何进程间都能通讯,但速度慢
3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
4.信号量:不能传递复杂消息,只能用来同步
5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存
等看完再补充 # TODO
vector和数组类似,拥有一段连续的存储空间,并且起始地址不变
因此能高效地进行随机存取,时间复杂度为O(1)
但因为内存是连续的,所以在进行插入和删除操作时会造成内存块的拷贝,时间复杂度为O(n)
另外,当数组中内存不够时,会重新申请一块内存空间并进行内存拷贝
list是由双向链表实现的,内存空间不连续
只能通过指针访问list的元素,所以list的随机存取很没有效率,时间复杂度为O(n)
但由于链表的特点,插入删除非常方便,时间复杂度为O(1)
vector拥有一段连续的内存空间,能很好的支持随机存取,
因此vector
支持“+”,“+=”,“<”等操作符。
list的内存空间可以是不连续,它不支持随机访问,
因此list
则不支持“+”、“+=”、“<”等
vector
和list
都重载了“++”运算符。
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;
如果需要大量的插入和删除,而不关心随机存取,则应使用list。
三种方式中,系统调用是主动的,异常和外围设备中断是被动的