后台开发面试题总结

来源:互联网 发布:以下哪种立体匹配算法 编辑:程序博客网 时间:2024/05/21 21:44


1、系统调用与函数调用的区别;

2、Linux内存模型、布局

3、怎样用O(1)的时间复杂度实现拒绝1秒超过百次访问的IP

4、TCP模型

5、后台架构是怎样的;

6、怎样实现负载均衡;

7、怎样进行服务发现;

8、O(n)时间复杂度实现删除字符串的全部空格,不允许申请空间;

9、用非递归的方式实现二叉树左右子树的交换;


1、系统调用与函数调用的区别;

Linux下对文件操作有两种方式:系统调用(system call)和库函数调用(Library functions)。系统调用实际上就是指最底层的一个调用,在linux程序设计里面就是底层调用的意思。面向的是硬件。而库函数调用则面向的是应用开发的,相当于应用程序的api,采用这样的方式有很多种原因,第一:双缓冲技术的实现。第二,可移植性。第三,底层调用本身的一些性能方面的缺陷。第四:让api也可以有了级别和专门的工作面向。


  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库的,因此也就不可能用于内核空间的驱动程序中对设备的操作。


  ※ 函数库调用 VS 系统调用




原文出自【比特网】,转载请保留原文链接:http://soft.chinabyte.com/os/258/12424258.shtml



2、Linux内存模型、布局

0. 内存基本知识

        我们通常称 linux的内存子系统为:虚拟内存子系统(virtual memory system),为何这样称谓呢?

        其实这个是个很牛的设计。linux充分利用了程序的局部性原理,结合线性地址的概念(虚拟地址)使得运行于操作系统上的每个进程都可以使用所有用户空间主存。而且虚拟内存还解决了内存不连续和碎片的问题(因为在程序来说线性地址都是连续的);每个进程都有各自的页表,虚拟地址空间都各自独立,互补干扰;

        那么我们的程序里申请的内存的时候,linux内核其实只分配一个虚拟内存( 线性地址),并没有分配实际的物理内存。只有当程序真正使用这块内存时,才会分配物理内存。这就叫做延迟分配和请页机制。释放内存时,先释放线性区对应的物理内存,然后释放线性区;

        什么时候内核为进程划分物理内存的呢? 当进程执行时,申请的内存只是一块虚拟内存区域,而不是实际的物理内存,只是获得了一块虚拟内存区域上线性地址区间的使用权。实际的物理内存只有当进程真的去访问新获得的虚拟地址时,才会由"请页机制"产生"缺页"异常,从而进入分配实际页框的例程。此异常会告诉内核去真正为进程分配物理页,并建立对应的页表。这之后虚拟地址才实实在在的映射到了物理内存上了。"请页机制"将物理内存的分配延后了,这样是充分利用了程序的局部性原来,节约内存空间,提高系统吞吐;

        那么cpu执行指令访存,使用的都是物理内存地址,而我们的编译器生成的二进制码实际上分配的都是逻辑内存(逻辑地址);那么线性地址是如何转换为物理内存地址的呢?我们知道内存模型里有,段,页机制来寻址内存的;(物理内存也是划分为页为单位划分的) ;我们的程序主要分为数据段,代码段;数据段存放代码里已初始化数据,代码段存放可执行代码指令;linux通过段机制将我们程序的逻辑地址转换为线性地址,又通过页机制将线性地址转换为物理地址。

 

1. 内存布局

     我们编写的程序是如何在内存中布局的呢?

     我们知道Linux内核启动起来时,如果是4G内存,那么会有大约1G被内核占用。其他3G会被用户进行使用。我们稍后会讲解内核内存如何和用户内存通信。

     一个进程对应的内存空间包含一下5个区:

     代码段 :存放可执行文件的操作指令;

      数据段: 存放可执行文件申请已经初始化的全局变量;

     BSS段:   存放未初始化的全局变量;

     堆:       存放用户程序运行中,动态申请的内存空间;

     栈:       存放用户程序运行中,临时创建的局部变量;我们知道CPU有寄存器是直接可以访问栈的,所以栈比堆快多了。


那么既然每个进程都有各自的虚拟内存空间,各自互不相干,那么进程间如何共享内存,内核又是如何向进程空间传递数据?  都是通过映射实现的,通过将内核的虚拟内存映射到当前进程用户空间的虚拟内存,当然映射时,要新建一个页表;  

2. 虚拟内存管理

    简单的说linux的虚拟内存管理技术:让每个进程看上去可以使用整个用户空间主存。通过 线性地址加上swap机制; swap机制:如果一个正在被cpu执行的进程恰巧和另外一个进程的线性地址指向了同一块物理内存。那么Linux通过swap机制,将这块内存写到磁盘上,叫做唤出。被唤出的数据,在使用时,又被换入;

    linux还通过cache+buffer机制: 将最近使用过的数据尽量cache,buffer起来,以便稍后会使用到;这就是说我们的可用内存=free + buffer + cache;


3、怎样用O(1)的时间复杂度实现拒绝1秒超过百次访问的IP

4、TCP模型

5、后台架构是怎样的;

6、怎样实现负载均衡;

7、怎样进行服务发现;

8、O(n)时间复杂度实现删除字符串的全部空格,不允许申请空间;

#include<conio.h>
#include<stdio.h>
#include<string.h>
char *fun(char *str)
{
    int i = 0 ;
    int j = 0 ;
    for( ; i < strlen(str) ; i++ )
    {
        if( str[i] != ' ' )
        {
            str[j++] = str[i] ;
        }
    }
    str[j] = '\0' ;
    return str ;
}
void main()
{
    char s[81],*ds;
    printf("\nPlease enter a string:");
    gets(s);
    ds=fun(s);
    printf("\nResult:%s\n",ds);

9、实现二叉树左右子树的交换;

递归:

BiNode* Exchange(BiNode* T)
{
 BiNode* p;
 if(NULL==T || (NULL==T->lchild && NULL==T->rchild))
  return T;
 p = T->lchild;
 T->lchild = T->rchild;
 T->rchild = p;
 if(T->lchild)
 {
  T->lchild = Exchange(T->lchild);
 }
 if(T->rchild)
 {
  T->rchild = Exchange(T->rchild);
 }
 return T;
}

非递归:

void NonRecursive_Exchange(BiNode* T)
{
 Stack s;
 BiNode* p;
 if(NULL==T)
  return;
 InitStack(&s);
 Push(&s,T);
 while(!isEmpty(&s))
 {
  T = Pop(&s);
  p = T->lchild;
  T->lchild = T->rchild;
  T->rchild = p;
 
  if(T->rchild)
   Push(&s,T->rchild);
  if(T->lchild)
   Push(&s,T->lchild); 
 } 
 DestroyStack(&s); 
}

0 0