您的位置:首页 > 职场人生

百度质量部面试体验之 二面

2014-03-25 11:34 92 查看
千辛万苦过了一面,谢谢老天保佑。

接下来的二面,主要是专访3班吴大神后加上一丁点自己的东西的成果。

=======================================

1.简单介绍下你的项目?

我做了一个基于XEN虚拟化环境的病毒管理系统,系统是以Struts2架构,主要功能有:用户管理,样本管理,虚拟机管理,虚拟机的控制。
目前实现了用户管理和虚拟机管理以及控制。
在这个项目中用到了技术如下:libvirt的API调用,js的ajax异步技术,进程间的通信。

学到的什么:一是网站的ajax和js编码能力的提高,二是对以前的涉及到的通信,TCP/IP协议理解更深了。

2.TCP的三次握手和四次挥手

TCP的这个问题其实经常考,三次握手是什么?四次挥手是什么?为什么是三次和四次的关系。

三次握手:

======================================================

Client    ---------->        Server

Syn=1  Seq=a                                       此时Client进入了SYN_SENT状态

======================================================

Client  <----------        Server

            Ack=a+1 Syn=1  Seq = b        此时服务器进入了SYN_REVC状态

======================================================

Client  ------------>        Server

Ack=b+1   Seq = z                                两者同时进入了ESTABLISHED状态

======================================================

细心的同学能够发现,只有前两次涉及到syn包了,那就涉及到什么是syn包了?

syn包是TCP连接发起的第一个信息包,它很小很小(也基于此有一个syn floor攻击),它表示握手信号。

如果提到了syn攻击:黑客发送syn包后,服务器在Syn_RECV状态,它在等待,等收到ACK后,服务器转入ESTABLISHED状态,如果没有呢?就白等了,如果白等很多回,就会耗尽服务器资源。

如果提到了怎么防范:SynAttackProtect保护机制、SYN cookies技术、在服务器增大最大连接数、缩短超时时间。

四次挥手:

假设是Client发起的关闭(其实谁都可以先关闭的):

======================================================

Client ---------------------->        Server

Fin=1       Ack=z       Seq=x         告诉Server,Client不会再传输数据过来了

======================================================

Client <----------------------      Server

                Ack=x+1   Seq = z       告诉Client,Server收到

======================================================

Client  <---------------------       Server

   Fin=1   Ack=x       Seq=y          告诉Client,Server再也不会传输数据过去了             

======================================================、

Client  ---------------------->     Server

         Ack=y       Seq=x          告诉Server,Client收到          

================================= =====================

为啥握手是三次、挥手是四次?

看看本质吧,握手是:
1.Client说:我准备好连接了
2.Server说:收到 and 我也装备好连接了
3.Client说:收到 。

再看看挥手:
1.Client说:我要关闭了
2.Server说:收到
3.Server说,我也要关闭了
4.Client说:收到

那么问题就是变成:挥手的2,3为什么不能一起说? 
其实很简单:因为2说完后,Server端可能有数据没传完,它得先传完啊,然后才能说:我也要关闭!
所以,2,3才要分开说!

3.进程之间的通信有几种方法?它们的优缺点是什么?

3.1.其实,至今只会一种:SOCKET通信。

优点是:只要遵循协议,可以跨操作系统,可以网络间的通信。

Server:
ss = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ss.bind((HOST,PORT))
ss.listen(5) //限定连接数为5
while True:
cs,addr = ss.accept()
cs.send("200 Connected!")
rev_data = cs.recv(BUF_SIZE)
cs.close()


Client:
cs = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
cs.connect((HOST,PORT))
rev_data = cs.recv(BUF_SIZE)
cs.send("Hi,Server,I am Client!")
cs.close()


也了解过其它的;

3.2  使用共享内存方式(Shared Memory)

优点:效率很高。

a.设定一块共享内存区域

HANDLE CreateFileMapping(HANDLE,LPSECURITY_ATTRIBUTES, DWORD, DWORD, DWORD,  LPCSTR)
产生一个file-mapping核心对象
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD  dwDesiredAcess,
DWORD  dwFileOffsetHigh,
DWORD  dwFileOffsetLow,
DWORD  dwNumberOfBytesToMap
);
得到共享内存的指针


b.找出共享内存

决定这块内存要以点对点(peer to peer)的形式呈现。

每个进程都必须有相同的能力,产生共享内存并将它初始化。
每个进程都应该调用CreateFileMapping(),然后调用GetLastError()。
如果传回的错误代码是ERROR_ALREADY_EXISTS,那么进程就可以假设这一共享内存区域已经被别的进程打开并初始化了,否则该进程就可以合理的认为自己排在第一位,并接下来将共享内存初始化。

   

所有的进程都应该使用
HANDLE OpenFileMapping(DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName);

再调用MapViewOfFile(),取得共享内存的指针。

c.同步处理(Mutex)

d.清理(Cleaning up) 

BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
CloseHandle();


3.3 信号量

优点:在通信的时候解决了同步互斥问题。

什么是信号量?
它的数据结构是一个指针和一个值,指针指向下一个等待信号量的进程,值表示目前资源的消耗情况。

信号量的作用?
主要是进程间的同步与互斥,PV操作属于进程间的低级通信。

主要是点到了PV操作,P操作是信号量减一,消耗一个资源,V操作是信号量加一,释放一个资源。

P操作,如果遇到了无法消耗的时候,就排入等待队列(好理解啊,都没家产让你挥霍了,等你爹赚到钱吧。)
V操作,信号量S=S+1后,如果依旧是 s<0,那么释放等待队列的第一个进程。
为什么V操作后,S依旧可能是负数呢?
因为操作系统设计P操作,如果遇到无资源的情况,先减一再排入队列中,此时S就是负数了,负数的绝对值就是等待队列中等待信号量的进程数。
(好理解啊,就是即使你没钱了,也让你赊账,等赚到钱再还给我。)

下面给面试官讲讲经典的生产者消费者问题:
/*
一组生产者,一组消费者,公用n个环形缓冲区。
empty——表示缓冲区是否为空,初值为n。
full——表示缓冲区中是否为满,初值为0。
mutex1——生产者之间的互斥信号量,初值为1。
mutex2——消费者之间的互斥信号量,初值为1。
*/

//生产者进程
while(TRUE){
生产一个产品;
P(empty);
P(mutex1);
产品送往buffer(in);
in=(in+1)mod n;
V(mutex1);
V(full);
}

//消费者进程
while(TRUE){
P(full)
P(mutex2);
从buffer(out)中取出产品;
out=(out+1)mod n;
V(mutex2);
V(empty);
消费该产品;
}


3.4 消息管道(Message Pipe)

关于管道,我的理解是:它能像用文件一样来使用它。

管道的类型有两种:匿名管道和命名管道。

匿名管道是不命名的,它最初用于在本地系统中父进程与它启动的子进程之间的通信。

命名管道更高级,它由一个名字来标识,以使客户端和服务端应用程序可以通过它进行彼此通信。

而且,Win32命名管道甚至可以在不同系统的进程间使用,这使它成为许多客户/服务器应用程序的理想之选。

就像水管连接两个地方并输送水一样,软件的管道连接两个进程并输送数据。

一个管道一旦被建立,它就可以象文件一样被访问,并且可以使用许多与文件操作同样的函数。

可以使用CreateFile函数获取一个已打开的管道的句柄,或者由另一个进程提供一个句柄。

使用WriteFile函数向管道写入数据,之后这些数据可以被另外的进程用ReadFile函数读取。

管道是系统对象,因此管道的句柄在不需要时必须使用CloseHandle函数关闭。

3.5 Internet通信

服务器上,应用程序就能上传下载文件。

4.设计模式

这里大概给他讲两个模式,因为自己也就涉及到这两个模式:单例模式和工厂模式。

4.1单例模式

单例模式的双重检查锁定:
public sealed class Singleton
{
static Singleton instance=null;
static readonly object padlock = new object();

Singleton()
{
}

public static Singleton Instance
{
get
{
if (instance==null)
{
lock (padlock)
{
if (instance==null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
}
解决了同步问题,以及限制只能产生一个实例。
缺点是无法延迟初始化。

单例模式的静态初始化:
public sealed class Singleton
{
static readonly Singleton instance=new Singleton();

static Singleton()
{
}

Singleton()
{
}

public static Singleton Instance
{
get
{
return instance;
}
}
}
}
这样子在应用程序一跑起来就初始化成功了。
优点是:不用每个实例都加锁去判断,占用太多系统资源。
缺点是无法延迟初始化。

单例模式的静态初始化and延迟初始化版本:
public sealed class Singleton
{
Singleton()
{
}

public static Singleton Instance
{
get
{
return Nested.instance;
}
}

class Nested
{
static Nested()
{
}

internal static readonly Singleton instance = new Singleton();
}
}
实际上,该版本也是目前大多数应用程序用到的方法。
这个方法其实是集合了上面两个方法的优点:
1是用到了才实例化。
2是静态初始化。
原理是:如果Singleton类中有除instance外的static变量xx,而xx比instance先被引用,那么在xx被引用的时候instance也会被初始化。

4.2简单工厂模式

上手一个例子,就能明白了,主要是根据需求不同,调用工厂类中的不同方法。
/*
CPU 工厂类
*/
public interface Cpu {
public void calculate();
}

public class IntelCpu implements Cpu {
private int pins = 0;
public  IntelCpu(int pins){
this.pins = pins;
}
@Override
public void calculate() {
System.out.println("Intel CPU的针脚数:" + pins);
}
}

public class AmdCpu implements Cpu {
private int pins = 0;
public  AmdCpu(int pins){
this.pins = pins;
}
@Override
public void calculate() {
System.out.println("AMD CPU的针脚数:" + pins);
}
}

public class CpuFactory {
public static Cpu createCpu(int type){
Cpu cpu = null;
if(type == 1){
cpu = new IntelCpu(755);
}else if(type == 2){
cpu = new AmdCpu(938);
}
return cpu;
}
}

/*
主板 工厂类
*/
public interface Mainboard {
public void installCPU();
}

public class IntelMainboard implements Mainboard {
private int cpuHoles = 0;
public IntelMainboard(int cpuHoles){
this.cpuHoles = cpuHoles;
}
@Override
public void installCPU() {
System.out.println("Intel主板的CPU插槽孔数是:" + cpuHoles);
}
}

public class AmdMainboard implements Mainboard {
private int cpuHoles = 0;
public AmdMainboard(int cpuHoles){
this.cpuHoles = cpuHoles;
}
@Override
public void installCPU() {
System.out.println("AMD主板的CPU插槽孔数是:" + cpuHoles);
}
}

public class MainboardFactory {
public static Mainboard createMainboard(int type){
Mainboard mainboard = null;
if(type == 1){
mainboard = new IntelMainboard(755);
}else if(type == 2){
mainboard = new AmdMainboard(938);
}
return mainboard;
}
}

/*
使用工厂类来测试CPU和主板是不是兼容
*/
public class ComputerEngineer {
private Cpu cpu = null;
private Mainboard mainboard = null;

public void makeComputer(int cpuType , int mainboard){
//1:首先准备好装机所需要的配件
prepareHardwares(cpuType, mainboard);
//2:组装机器
//3:测试机器
//4:交付客户
}

private void prepareHardwares(int cpuType , int mainboard){
this.cpu = CpuFactory.createCpu(cpuType);
this.mainboard = MainboardFactory.createMainboard(mainboard);

//测试配件是否好用
this.cpu.calculate();
this.mainboard.installCPU();
}
}

优点是;简单。
缺点是:没有考虑CPU和主板之间的关系。对象之间是需要相互匹配的。

4.3抽象工厂模式

public interface AbstractFactory {
//创建CPU对象
public Cpu createCpu();

//创建主板对象
public Mainboard createMainboard();
}

public class IntelFactory implements AbstractFactory {
@Override
public Cpu createCpu() {
// TODO Auto-generated method stub
return new IntelCpu(755);
}

@Override
public Mainboard createMainboard() {
// TODO Auto-generated method stub
return new IntelMainboard(755);
}
}

public class AmdFactory implements AbstractFactory {
@Override
public Cpu createCpu() {
// TODO Auto-generated method stub
return new IntelCpu(938);
}

@Override
public Mainboard createMainboard() {
// TODO Auto-generated method stub
return new IntelMainboard(938);
}
}

public class ComputerEngineer {
private Cpu cpu = null;
private Mainboard mainboard = null;

public void makeComputer(AbstractFactory af){
//1:首先准备好装机所需要的配件
prepareHardwares(af);
//2:组装机器
//3:测试机器
//4:交付客户
}
private void prepareHardwares(AbstractFactory af){
//直接找相应的工厂获取
this.cpu = af.createCpu();
this.mainboard = af.createMainboard();

//测试配件是否好用
this.cpu.calculate();
this.mainboard.installCPU();
}
}

public class Client {
public static void main(String[]args){
//创建装机工程师对象
ComputerEngineer cf = new ComputerEngineer();
//客户选择并创建需要使用的产品对象
AbstractFactory af = new IntelFactory();
//告诉装机工程师自己选择的产品,让装机工程师组装电脑
cf.makeComputer(af);
}
}
因为简单工厂模式,随着产品的增多,平级结构随之增多,整个框架就很冗余。
用抽象工厂,变成了inter工厂和amd工厂。在各个具体的工厂里面再取出cpu,主板来操作。
确保了装机的时候产品之间的兼容性。

优点是:分离接口和实现、使切换产品族变得容易。
缺点是:如果需要给整个产品族添加一个新的产品,那么就需要修改抽象工厂,这样就会导致修改所有的工厂实现类。

5.如果快速给出关键字在一组文件中出现的位置,频率

lucene倒排索引啊!

这个涉及到lucene(读作卢森),一个java全文检索引擎工具包。

1.

建立倒排索引:关键词 && 频度 && 位置

2.

取得关键词

文章1的内容为:Tom lives in Guangzhou,I live in Guangzhou too.

文章2的内容为:He once lived in Shanghai.

分词加过滤后:

文章1的所有关键词为:[tom] [live] [guangzhou] [i] [live] [guangzhou]

文章2的所有关键词为:[he] [live] [shanghai]

3.

建立倒排索引

步骤2中是:“文章号”对应“文章中所有关键词”

倒排索引成:“关键词”对应“文章号”

倒排后:
关键词          文章号
guangzhou        1
he               2
i                1
live             1,2
shanghai         2
tom              1


4.

修改索引结构
关键词            文章号[出现频率]           出现位置
guangzhou           1[2]                      3,6
he                  2[1]                      1
i                   1[1]                      4
live                1[2]                      2,5
2[1]                      2
shanghai            2[1]                      3
tom                 1[1]                      1


lucene使用二元搜索算法快速定位关键词。

6.自然数a = b * c,怎么确定b+c的最小和

比如说:
12 = 3 x 4
12 = 2 x 6
12 = 1 x 12
最小的必然是12 = 3 x 4这一组合的3+4 = 7 最小。
12 = 2 x 6 = 2 x 2 x 3,这么一转,也能找到最小的。

所以这道题转化成如何确定一个数的所有质因子。

记得当年写ACM的时候用过:素数筛选法!
int *prime = int[100];

count = 0;
for (i = 2; i * i <= n; i ++) {
if(n % i == 0) {
while (n % i == 0) {
prime[count] = i;
count ++;
n /= i;
}
}
}


貌似有更快的算法,用到了分解多项式,具体不清楚。

7.一串数字,有一个数字占了一半以上,怎么确定这个数?

我的思想是:
用一个堆排,排序时间复杂度(nlgn),然后取中间那个就是了。

但是听面试官能更快的,陷入了思考:
定义2个变量:cnt,flag。
初始化flag = 0,遇到第一个数flag = 1,记录下cnt = array[i] 。
然后遇到下一个数,如果不同,flag = 0,cnt = array[i],
如果相同,flag 依旧为1,然后cnt = array[i]。

思想就是每次抓取两个不同的数出来,一轮循环之后,cnt记录的那个数就是我们要找的数,时间复杂度为n。
 
追加问题:如果这个数字不确定是否存在?若存在,请给出这个数字,若不存在,请说明!
 
问题转换成:怎么判定一个数字是否存在了。若存在,就用上面的方法。
 
这个问题,一开始就想到用一个循环去跑一遍,时间复杂度为n。
结果面试官说能降低,也就是lgn。
然后我们就陷入了死一般的沉默。请原谅我这个算法渣。。
 
到底怎么二分呢?
 
在电脑前我坐了5分钟,我想到了。
设置一个flag = array[n/2]
然后把array变成两段:A = array[0,n/2] 和 B = array[n/2+1,n ]
分别对这两段用二分法猜数字的思想。
猜到这A 的 flag 所在index 为 a
猜到这B 的 flag 所在index 为 b
用b-a 就得出长度了。
根据长度,我们就能判断这个数字是否存在了。

 

8.请对百度文库的“登录”进行测试

安全性:密码复杂度?明文否?是否MD5加密?登录次数限制?cookie是否有效?输入是否正常,是否带有SQL语句等?
用户友好性:是否支持PC,APP同时登录?是否要设置异地登录通知?是否用户ID唯一性?

9.请对一个3楼的电梯进行测试,该电梯每层只有一个up按钮,down按钮

从计算机的角度:6个变量的输入,调度是否正确。
从用户安全角度:是否有紧急呼叫?是否非密闭空间?
从用户体验角度:是否有摄像头?是否有灯?

  

10.请对百度搜索框的搜索建议进行测试

数目是否在控制范围内:10?

文字大小?颜色?

长度?

内容相关程度?

是否即时更新?

是否有纠正功能?

是否是百度产品优先出现?

是否是敏感词汇?

当时我就只想到这么多。。有更多的请回复。我加上。

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  面试