(腾讯)后台开发面试题解答介绍
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
linux和os:
netstat:显示网络状态
tcpdump:主要是截获通过本机网络接口的数据,用以分析。
能够截获当前所有通过本机网卡的数据包。
它拥有灵活的过滤机制,可以确保得到想要的数据。
ipcs:检查系统上共享内存的分配
ipcrm:手动解除系统上共享内存的分配
(如果这四个命令没听说过或者不能熟练使用,基本上可以回家,通过的概率较小^_^,这四个命令的熟练掌握程度基本上能体现面试者实际开发和调试程序的经验)
cpu内存硬盘等等与系统性能调试相关的命令必须熟练掌握,设置修改权限tcp网络状态查看各进程状态抓包相关等相关命令必须熟练掌握
awk sed需掌握
共享内存的使用实现原理(必考必问,然后共享内存段被映射进进程空间之后,存在于进程空间的什么位置?共享内存段最大限制是多少?)
共享内存定义:共享内存是最快的可用IPC(进程间通信)形式。
它允许多个不相关的进程去访问同一部分逻辑内存。
共享内存是由IPC为一个进程创建的一个特殊的地址范围,它将出现在进程的地址空间中。
其他进程可以把同一段共享内存段“连接到”它们自己的地址空间里去。
所有进程都可以访问共享内存中的地址。
如果一个进程向这段共享内存写了数据,所做的改动会立刻被有访问同一段共享内存的其他进程看到。
因此共享内存对于数据的传输是非常高效的。
共享内存的原理:共享内存是最有用的进程间通信方式之一,也是最快的IPC
形式。
两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。
进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。
c++进程内存空间分布(注意各部分的内存地址谁高谁低,注意栈从高到低分配,堆从低到高分配)
ELF是什么?其大小与程序中全局变量的是否初始化有什么关系(注意未初始化的数据放在bss段)
可执行文件:包含了代码和数据。
具有可执行的程序。
可重定位文件:包含了代码和数据(这些数据是和其他重定位文件和共享的object文件一起连接时使用的)
共享object文件(又可叫做共享库):包含了代码和数据(这些数据是在连接时候被连接器ld和运行时动态连接器使用的)。
使创建共享库容易,使动态装载和共享库的结合更加容易。
在ELF下,在C++中,全局的构造函数和析构函数在共享库和静态库中用同样方法处理。
使用过哪些进程间通讯机制,并详细说明(重点)
makefile编写,虽然比较基础,但是会被问到
mkdir mf
cd mf
vim makefile
hello.o:hello.c hello.h
gcc–c hello.o-Lm
make
./hello
gdb调试相关的经验,会被问到
如何定位内存泄露?
内存泄漏是指堆内存的泄漏。
堆内存是指程序从堆中分配的、大小任意的(内存块的大小可以在程序运行期决定)、使用完后必须显示释放的内存。
应用程序一般使用malloc、realloc、new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块。
否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
C++程序缺乏相应的手段来检测内存信息,只能使用top指令观察进程的动态内存总额。
而且程序退出时,我们无法获知任何内存泄漏信息
使用Linux命令回收内存,可以使用ps、kill两个命令检测内存使用情况和进行回收。
在使用超级用户权限时使用命令“ps”,它会列出所有正在运行的程序名称和对应的进程号(PID)。
kill命令的工作原理是向Linux操作系统的内核送出一个系统操作信号和程序的进程号(PID)
动态链接和静态链接的区别
动态链接是指在生成可执行文件时不将所有程序用到的函数链接到一个文件,因为有许多函数在操作系统带的dll文件中,当程序运行时直接从操作系统中找。
而静态链接就是把所有用到的函数全部链接到exe文件中。
动态链接是只建立一个引用的接口,而真正的代码和数据存放在另外的可执行模块中,在运行时再装入;而静态链接是把所有的代码和数据都复制到本模块中,运行时就不再需要库了。
32位系统一个进程最多有多少堆内存
多线程和多进程的区别(重点面试官最最关心的一个问题,必须从cpu调度,上下文切换,数据共享,多核cup利用率,资源占用,等等各方面回答,然后有一个问题必须会被问到:哪些东西是一个线程私有的?答案中必须包含寄存器,否则悲催)
写一个c程序辨别系统是16位or32位
法一:int k=~0;
if((unsigned int)k>63356)cout<<"at least32bits"<<endl;
else cout<<"16bits"<<endl;
法二://32为系统
int i=65536;
cout<<i<<endl;
int j=65535;
cout<<j<<endl;
写一个c程序辨别系统是大端or小端字节序
用联合体:如char类型的,可以看他输出的是int的高字节还是低字节
信号:列出常见的信号,信号怎么处理?
i++是否原子操作?并解释为什么?
说出你所知道的linux系统的各类同步机制(重点),什么是死锁?如何避免死锁(每个技术面试官必问)
死锁的条件。
(互斥条件(Mutual exclusion):1、资源不能被共享,只能由一个进程使用。
2、请求与保持条件(Hold and wait):已经得到资源的进程可以
再次申请新的资源。
3、非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。
4、循环等待条件(Circular wait):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。
处理死锁的策略:1.忽略该问题。
例如鸵鸟算法,该算法可以应用在极少发生死锁的的情况下。
为什么叫鸵鸟算法呢,因为传说中鸵鸟看到危险就把头埋在地底下,可能鸵鸟觉得看不到危险也就没危险了吧。
跟掩耳盗铃有点像。
2.检测死锁并且恢复。
3.仔细地对资源进行动态分配,以避免死锁。
4.通过破除死锁四个必要条件之一,来防止死锁产生。
)
列举说明linux系统的各类异步机制
exit()与_exit()的区别?
_exit终止调用进程,但不关闭文件,不清除输出缓存,也不调用出口函数。
exit 函数将终止调用进程。
在退出程序之前,所有文件关闭,缓冲输出内容将刷新定义,并调用所有已刷新的“出口函数”(由atexit定义)。
‘exit()’与‘_exit()’有不少区别在使用‘fork()’,特别是‘vfork()’时变得很突出。
‘exit()’与‘_exit()’的基本区别在于前一个调用实施与调用库里用户状态结构(user-mode constructs)有关的清除工作(clean-up),而且调用用户自定义的清除程序
如何实现守护进程?
守护进程(Daemon)是运行在后台的一种特殊进程。
它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
守护进程是一种很有用的进程。
Linux的大多数服务器就是用守护进程实现的。
比如,Internet服务器inetd,Web服务器httpd等。
同时,守护进程完成许多系统任务。
比如,作业规划进程crond,打印进程lpd等。
守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同,造成不同Unix环境下守护进程的编程规则并不一致。
需要注意,照搬某些书上的规则(特别是BSD4.3和低版本的System V)到Linux会出现错误的。
下面将给出Linux下守护进程的编程要点和详细实例。
一.守护进程及其特性
守护进程最重要的特性是后台运行。
在这一点上DOS下的常驻内存程序TSR与之相似。
其次,守护进程必须与其运行前的环境隔离开来。
这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。
这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。
最后,守护进程的启动方式有其特殊之处。
它可以在Linux系统启动时从启动脚本
/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(shell)执行。
总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。
因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。
如果对进程有比较深入的认识就更容易理解和编程了。
二.守护进程的编程要点
前面讲过,不同Unix环境下守护进程的编程规则并不一致。
所幸的是守护进程的编程原则其实都一样,区别在于具体的实现细节不同。
这个原则就是要满足守护进程的特性。
同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。
编程要点如下;
1.在后台运行。
为避免挂起控制终端将Daemon放入后台执行。
方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
if(pid=fork())
exit(0);//是父进程,结束父进程,子进程继续
2.脱离控制终端,登录会话和进程组
有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。
登录会话可以包含多个进程组。
这些进程组共享一个控制终端。
这个控制终端通常是创建进程的登录终端。
控制终端,登录会话和进程组通常是从父进程继承下来的。
我们的目的就是要摆脱它们,使之不受它们的影响。
方法是在第1点的基础上,调用setsid()使进程成为会话组长:
setsid();
说明:当进程是会话组长时setsid()调用失败。
但第一点已经保证进程不是会话组长。
setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。
由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
3.禁止进程重新打开控制终端
现在,进程已经成为无终端的会话组长。
但它可以重新申请打开一个控制终端。
可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
if(pid=fork())exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
4.关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。
如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。
按如下方法关闭它们:
for(i=0;i关闭打开的文件描述符close(i);>
5.改变当前工作目录
进程活动时,其工作目录所在的文件系统不能卸下。
一般需要将工作目录改变到根目录。
对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir("/")
6.重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。
它可能修改守护进程所创建的文件的存取位。
为防止这一点,将文件创建掩模清除:umask(0);
7.处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。
但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。
如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。
如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。
在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。
这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。
三.守护进程实例
守护进程实例包括两部分:主程序test.c和初始化程序init.c。
主程序每隔一分钟向/tmp目录中的日志test.log报告运行状态。
初始化程序中的init_daemon 函数负责生成守护进程。
读者可以利用init_daemon函数生成自己的守护进程。
linux的内存管理机制是什么?
Linux虚拟内存的实现需要6种机制的支持:地址映射机制、内存分配回收机制、缓存和刷新机制、请求页机制、交换机制和内存共享机制
内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址。
当用户程序运行时,如果发现程序中要用的虚地址没有对应的物理内存,就发出了请求页要求。
如果有空闲的内存可供分配,就请求分配内存(于是用到了内存的分配和回
收),并把正在使用的物理页记录在缓存中(使用了缓存机制)。
如果没有足够的内存可供分配,那么就调用交换机制;腾出一部分内存。
另外,在地址映射中要通过TLB(翻译后援存储器)来寻找物理页;交换机制中也要用到交换缓存,并且把物理页内容交换到交换文件中,也要修改页表来映射文件地址。
linux的任务调度机制是什么?
标准库函数和系统调用的区别?
1、系统调用和库函数的关系
系统调用通过软中断int0x80从用户态进入内核态。
函数库中的某些函数调用了系统调用。
函数库中的函数可以没有调用系统调用,也可以调用多个系统调用。
编程人员可以通过函数库调用系统调用。
高级编程也可以直接采用int0x80进入系统调用,而不必通过函数库作为中介。
如果是在核心编程,也可以通过int0x80进入系统调用,此时不能使用函数库。
因为函数库中的函数是内核访问不到的。
2、从用户调用库函数到系统调用执行的流程。
1)假设用户调用ssize_t write(int fields,cont void*buff,size_t nbytes);库函数。
2)库函数会执行int0x80中断。
因为中断使得进程从用户态进入内核态,所以参数通过寄存器传送。
3)0x80中断对应的中断例程被称为system call handler。
其工作是:
i.存储大多数寄存器到内核堆栈中。
这是汇编代码写的。
ii.执行真正的系统调用函数――system call service routine。
这是C代码。
iii.通过ret_from_sys_call()返回,回到用户态的库函数。
这是汇编代码。
1、系统调用
系统调用提供的函数如open,close,read,write,ioctl等,需包含头文件unistd.h。
以write为例:其函数原型为size_t write(int fd,const void*buf, size_t nbytes),其操作对象为文件描述符或文件句柄fd(file descriptor),要想写一个文件,必须先以可写权限用open系统调用打开一个文件,获得所打开文件的fd,例如fd=open(/"/dev/video/",O_RDWR)。
fd是一个整型值,每新打开一个文件,所获得的fd为当前最大fd加1。
Linux系统默认分配了3个文件描述符值:0-standard input,1-standard output,2-standard error。
系统调用通常用于底层文件访问(low-level file access),例如在驱动程序中对设备文件的直接访问。
系统调用是操作系统相关的,因此一般没有跨操作系统的可移植性。
系统调用发生在内核空间,因此如果在用户空间的一般应用程序中使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。
事实上,即使在用户空间使用库函数来对文件进行操作,因为文件总是存在于存储介质上,因此不管是读写操作,都是对硬件(存储器)的操作,都必然会引起系统调用。
也就是说,库函数对文件的操作实际上是通过系统调用来实现的。
例如C库函数fwrite()就是通过write()系统调用来实现的。
这样的话,使用库函数也有系统调用的开销,为什么不直接使用系统调用呢?这是因为,读写文件通常是大量的数据(这种大量是相对于底层驱动的系统调用所实现的数据操作单位而言),这时,使用库函数就可以大大减少系统调用的次数。
这一结果又缘于缓冲区技术。
在用户空间和内核空间,对文件操作都使用了缓冲区,例如用fwrite写文件,都是先将内容写到用户空间缓冲区,当用户空间缓冲区满或者写操作结束时,才将用户缓冲区的内容写到内核缓冲区,同样的道理,当内核缓冲区满或写结束时才将内核缓冲区内容写到文件对应的硬件媒介。
2、库函数调用
标准C库函数提供的文件操作函数如fopen,fread,fwrite,fclose,fflush, fseek等,需包含头文件stdio.h。
以fwrite为例,其函数原型为size_t
fwrite(const void*buffer,size_t size,size_t item_num,FILE*pf),其操作对象为文件指针FILE*pf,要想写一个文件,必须先以可写权限用fopen 函数打开一个文件,获得所打开文件的FILE结构指针pf,例如
pf=fopen(/"~/proj/filename/",/"w/")。
实际上,由于库函数对文件的操作最终是通过系统调用实现的,因此,每打开一个文件所获得的FILE结构指针都有一个内核空间的文件描述符fd与之对应。
同样有相应的预定义的FILE指针:stdin-standard input,stdout-standard output,stderr-standard error。
库函数调用通常用于应用程序中对一般文件的访问。
库函数调用是系统无关的,因此可移植性好。
由于库函数调用是基于C库的,因此也就不可能用于内核空间的驱动程序中对设备的操作
ping命令所利用的原理是这样的:网络上的机器都有唯一确定的IP地址,我们给目标IP地址发送一个数据包,对方就要返回一个同样大小的数据包,根据返回的数据包我们可以确定目标主机的存在,可以初步判断目标主机的操作系统等。
补充一个坑爹坑爹坑爹坑爹的问题:系统如何将一个信号通知到进程?(这一题哥没有答出来)
c语言:
宏定义和展开(必须精通)
位操作(必须精通)
指针操作和计算(必须精通)
内存分配(必须精通)
sizeof必考
各类库函数必须非常熟练的实现
哪些库函数属于高危函数,为什么?(strcpy等等)
c++:
一个String类的完整实现必须很快速写出来(注意:赋值构造,operator=是关键)
虚函数的作用和实现原理(必问必考,实现原理必须很熟)
有虚函数的类内部有一个称为“虚表”的指针(有多少个虚函数就有多少个指针),这个就是用来指向这个类虚函数。
也就是用它来确定调用该那个函数。
实际上在编译的时候,编译器会自动加入“虚表”。
虚表的使用方法是这样的:如果派生类在自己的定义中没有修改基类的虚函数,就指向基类的虚函数;如果派生类改写了基类的虚函数(就是自己重新定义),这时虚表则将原来指向基类的虚函数的地址替换为指向自身虚函数的指针。
那些被virtual关键字修饰的成员函数,就是虚函数。
虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略。
每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址,
虚函数的效率低,其原因就是,在调用虚函数之前,还调用了获得虚函数地址的代码。
sizeof一个类求大小(注意成员变量,函数,虚函数,继承等等对大小的影响)指针和引用的区别(一般都会问到)
相同点:1.都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
区别:1.指针是一个实体,而引用仅是个别名;
2.引用使用时无需解引用(*),指针需要解引用;
3.引用只能在定义时被初始化一次,之后不可变;指针可变;
4.引用没有const,指针有const;
5.引用不能为空,指针可以为空;
6.“sizeof引用”得到的是所指向的变量(对象)的大小,而“sizeof指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
7.指针和引用的自增(++)运算意义不一样;
8.从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。
多重类构造和析构的顺序
先调用基类的构造函数,在调用派生类的构造函数
先构造的后析构,后构造的先析构
stl各容器的实现原理(必考)
STL共有六大组件
1、容器。
2、算法。
3、迭代器。
4、仿函数。
6、适配器。
序列式容器:
vector-数组,元素不够时再重新分配内存,拷贝原来数组的元素到新分配的数组中。
list-单链表。
deque-分配中央控制器map(并非map容器),map记录着一系列的固定长度的数组的地址.记住这个map仅仅保存的是数组的地址,真正的数据在数组中存放着.deque先从map中央的位置(因为双向队列,前后都可以插入元素)找到一个数组地址,向该数组中放入数据,数组不够时继续在map中找空闲的数组来存数据。
当map也不够时重新分配内存当作新的map,把原来map中的内容copy 的新map中。
所以使用deque的复杂度要大于vector,尽量使用vector。
stack-基于deque。
queue-基于deque。
heap-完全二叉树,使用最大堆排序,以数组(vector)的形式存放。
priority_queue-基于heap。
slist-双向链表。
关联式容器:
set,map,multiset,multimap-基于红黑树(RB-tree),一种加上了额外平衡条件的二叉搜索树。
hash table-散列表。
将待存数据的key经过映射函数变成一个数组(一般是vector)的索引,例如:数据的key%数组的大小=数组的索引(一般文本通过算法也可以转换为数字),然后将数据当作此索引的数组元素。
有些数据的key经过算法的转换可能是同一个数组的索引值(碰撞问题,可以用线性探测,二次探测来解决),STL是用开链的方法来解决的,每一个数组的元素维护一个list,他把相同索引值的数据存入一个list,这样当list比较短时执行删除,插入,搜索等算法比较快。
hash_map,hash_set,hash_multiset,hash_multimap-基于hash table。
extern c是干啥的,(必须将编译器的函数名修饰的机制解答的很透彻)volatile是干啥用的,(必须将cpu的寄存器缓存机制回答的很透彻)volatile的本意是“易变的”因为访问寄存器要比访问内存单元快的多,所以编译
器一般都会作减少存取内存的优化,但有可能会读脏数据。
当要求使用volatile 声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用volatile,则编译器将对所声明的语句进行优化。
(简洁的说就是:volatile 关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)
5.volatile的本质:
1>编译器的优化
在本次线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致。
当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。
当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致。
2>volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人。
static const等等的用法,(能说出越多越好)
数据结构或者算法:
《离散数学》范围内的一切问题皆由可能被深入问到(这个最坑爹,最重要,最体现功底,最能加分,特别是各类树结构的实现和应用)
各类排序:大根堆的实现,快排(如何避免最糟糕的状态?),bitmap的运用等等
hash,任何一个技术面试官必问(例如为什么一般hashtable的桶数会取一个素数?如何有效避免hash结果值的碰撞)
网络编程:
tcp与udp的区别(必问)。