腾讯后台开发面试记录

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、合并链表

struct LinkNode *merge (struct LinkNode *firstLink, struck LinkNode *secondLink) {
    LinkNode *output = firstLink; // trace the next node
    LinkNode *head = firstLink; // record the head node
    if ((firstLink == NULL) || (secondLink == NULL)) {
        return firstLink == NULL ? secondLink : firstLink;
    }
    while (firstLink.next && secondLink.next) {
        if (firstLink.value < secondLink.value) {
            output.next = firstLink;
            output = output.next;
            firstLink = firstLink.next;
        }
        else {
            output.next = secondLink;
            output = output.next;
            secondLink = secondLink.next;
        }
    }
    return head;
}

注意考虑节点为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


1、TCP的三次握手

第一次:客户端发送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、关闭网络连接;

2、进程和线程

参考这篇博客
- 定义
进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位
线程:进程的一个实体,是CPU进行资源分配和调度的基本单位,它是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同一进程下的线程共享进程的所有资源
- 关系
一个线程可以创建和撤销另一个线程,同一个进程的多个线程之间可以并发执行
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列
- 区别
进程和线程的主要区别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,资源消耗大,效率差一些。但对于某些要求同时进行又需要共享变量的并发操作,只能用线程,不能用进程
1)简而言之,一个程序至少有一个进程,一个进程至少有一个线程
2)线程的划分尺度小于进程,使得多线程程序的并发性高
3)进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
4)线程在执行过程中与进程是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能独立执行,必须依存于应用程序中,由应用程序提供多个线程执行控制
5)从逻辑角度看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
- 优缺点
线程执行开销小,但不利于资源的管理和保护,进程正好相反
线程适合在SMP机器上运行,而进程则可以跨机器迁移

3、进程间的通信方式

参考这篇博客

(1)管道

通常指无名管道,是UNIX系统IPC最古老的形式
特点:
- 它是半双工的,也就是说数据只能在一个方向上流动,具有固定的读端和写端
- 它只能用于具有亲缘关系的进程之间通信,也就是父子进程或兄弟进程
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write函数,但它不是普通的文件,并不属于任何文件系统,只存在于内存中

(2)FIFO

也称为命名管道,它是一种文件类型
特点:
- 它可以在无关的进程间交换数据,与无名管道不同
- 它有路径名与之关联,以一种特殊设备的形式存在于文件系统中

(3)消息队列

是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
特点:
- 消息队列是面向记录的,其中的消息具有特定的格式和特定的优先级
- 消息队列独立于发送与接受进程,进程终止时消息队列及其内容不会删除
- 消息队列可以实现消息的随机查询,不一定要先进先出,也可以按类型查询

(4)信号量

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据
特点:
- 信号量用于进程间同步,若要在进程间传递数据需要结合内存共享
- 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作
- 每次对信号量的操作不限于加一或减一,可以加减任意正数
- 支持信号量组

(5)共享内存

指两个或多个进程共享一个给定的存储区
特定:
- 共享内存是最快的一种IPC,因为进程是直接对内存进行读取
- 因为多个进程可以同时操作,所以需要同步
- 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问

总结

1.管道:速度慢,容量有限,只有父子进程能通讯

2.FIFO:任何进程间都能通讯,但速度慢

3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

4.信号量:不能传递复杂消息,只能用来同步

5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

4、epoll

等看完再补充 # TODO

5、C++的vector和list的区别

vector和数组类似,拥有一段连续的存储空间,并且起始地址不变
因此能高效地进行随机存取,时间复杂度为O(1)
但因为内存是连续的,所以在进行插入和删除操作时会造成内存块的拷贝,时间复杂度为O(n)
另外,当数组中内存不够时,会重新申请一块内存空间并进行内存拷贝

list是由双向链表实现的,内存空间不连续
只能通过指针访问list的元素,所以list的随机存取很没有效率,时间复杂度为O(n)
但由于链表的特点,插入删除非常方便,时间复杂度为O(1)

vector拥有一段连续的内存空间,能很好的支持随机存取,
因此vector<int>::iterator支持“+”,“+=”,“<”等操作符。
list的内存空间可以是不连续,它不支持随机访问,
因此list<int>::iterator则不支持“+”、“+=”、“<”等
vector<int>::iteratorlist<int>::iterator都重载了“++”运算符。
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;
如果需要大量的插入和删除,而不关心随机存取,则应使用list。

6、用户态切换到内核态的方法

  • 系统调用
    这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断
  • 异常
    当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常
  • 外围设备的中断
    当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等

三种方式中,系统调用是主动的,异常和外围设备中断是被动的

猜你喜欢

转载自blog.csdn.net/github_39273626/article/details/81417056