您的位置 首页 golang

「linux后台开发」最新一线互联网大厂面试题总结

1、设计高并发系统数据库层面该如何设计? 数据库 有哪些类型?如何实现?

1. 分库分表: 同样量的数据平均存储在不同数据库相同表(或不同表)中,减轻单表压力,如果还是很大,就可以每个库在分多张表,根据hash取值或者其他逻辑判断将数据存储在哪张表中

2. 读写分离: 数据库原本就有主从数据库之分,查询在从服务器,增删改在主服务器,

3. 归档和操作表区分: 建一张归档表,将历史数据放入,需要操作的表数据单独存储

4. 索引啊之类的创建,对于数据量很大,百万级别以上的单表,如果增删改操作不频繁的话, 可以创建bitMap索引,速度要快得多

1. 共享锁:要等第一个人操作完,释放锁,才能操作

2. 更新锁:解决死锁,别人可以读,但不能操作

3. 排他锁:读写都被禁用

4. 意向锁(xlock): 对表中部分数据加锁,查询时,可以跳过

5. 计划锁: 操作时,别的表连接不了这张表,

2、一个 线程 池正在处理服务如果忽然断电该怎么办?

队列实现持久化储存,下次启动自动载入。

但是实际需要看情况,大体思路是这样。

添加标志位,未处理 0,处理中 1,已处理 2。每次启动的时候,把所有状态为 1 的, 置为 0。或者定时器处理

关键性的应用就给电脑配个 UPS。

3、顺序栈的表示和实现

顺序栈,即栈的顺序存储结构是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针top指示栈顶元素在顺序栈中的位置。通常的习惯做法以top=0表示空栈。一般来说,在初始化设空栈时不应限定栈的最大容量。一个较合理的做法:先为栈分配一个基本容量,然后在应用过程中,当栈的空间不足在进行扩展。

 #define STACK_INIT_SIZE 100
#define STACKINCREMENT 10
typedef struct SqStack
{
int *base;
int *top;
int stacksize;
}SqStack;
int InitStack(SqStack &S)
{
S.base=(int *)malloc(STACK_INIT_SIZE*sizeof(int));
if(!S.base)
exit(0);
S.top=S.base;
S.stacksize=STACK_INIT_SIZE;
return 0;
}
int GetTop(SqStack S,int &e)
{
if(S.top==S.base)
return 1;
e=*(S.top-1);
return 0;
}
int Push(SqStack &S,int e)
{
if(S.top-S.base>=S.stacksize)
{
S.base=(int*)realloc(S.base,(S.stacksize+STACKINCREMENT)*
sizeof(int));
if(!S.base)
return 1;
S.top=S.base+S.stacksize;
S.stacksize+=STACKINCREMENT;
}
*S.top=e;
S.top++;
return 0;
}
int Pop(SqStack &S,int &e)
{
if(S.top==S.base)
return 1;
--S.top;
e=*S.top;
return 0;
}
int StackEmpty(SqStack S)
{
if(S.top==S.base)
return 1;
return 0;
}
int DestroyStack(SqStack &S)
{
free(S.base);
S.top=NULL;
S.base=S.top;
return 0;
}  

4、 C++实现 线程安全 的单例模式

懒汉模式:

 class singleton
{
protected:
    singleton()
    {
        pthread_mutex_init(&mutex);
    }
private:
    static singleton* p;
public:
    static pthread_mutex_t mutex;
    static singleton* initance();
};
 
pthread_mutex_t singleton::mutex;
singleton* singleton::p = NULL;
singleton* singleton::initance()
{
    if (p == NULL)
    {
        pthread_mutex_lock(&mutex);
        if (p == NULL)
            p = new singleton();
        pthread_mutex_unlock(&mutex);
    }
    return p;
}  

5、 使用“反向 代理服务器 ”的优点是什么?

(1)提高访问速度

由于目标主机返回的数据会存在代理服务器的硬盘中,因此下一次客户再访问相同的站 点数据时,会直接从代理服务器的硬盘中读取,起到了缓存的作用,尤其对于热门站点 能明显提高请求速度。

(2)防火墙作用

由于所有的客户机请求都必须通过代理服务器访问远程站点,因此可在代理服务器上设 限,过滤某些不安全信息。

(3)通过代理服务器访问不能访问的目标站点

互联网上有许多开发的代理服务器,客户机可访问受限时,可通过不受限的代理服务器 访问目标站点,通俗说,我们使用的翻墙浏览器就是利用了代理服务器,可直接访问外网。

需要更多面试题或C/C++ linux 高级服务器架构师学习资料后台私信“资料”(包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL, Redis ,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker, TCP /IP,协程,DPDK,ffmpeg等)

6、 Epoll在LT和ET模式下的区别以及注意事项

1.ET模式:

因为ET模式只有从unavailable到available才会触发,所以

1)、读事件:需要使用while循环读取完,一般是读到EAGAIN,也可以读到返回值小 于缓冲区大小;

如果应用层读缓冲区满:那就需要应用层自行标记,解决OS不再通知可读的问题

2)、写事件:需要使用while循环写到EAGAIN,也可以写到返回值小于缓冲区大小

如果应用层写缓冲区空(无内容可写):那就需要应用层自行标记,解决OS不再通知 可写的问题。

3).正确的 accept ,accept 要考虑 2 个问题

(1) 阻塞模式 accept 存在的问题

考虑这种情况:TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发 送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻 塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接 为止。但是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其他描述符 都得不到处理。

解决办法是把监听套接口设置为非阻塞,当客户在服务器调用accept之前中止某个连 接时,accept调用可以立即返回-1,这时源自Berkeley的实现会在内核中处理该 事 件,并不会将该事件通知给epool,而其他实现把errno设置为ECONNABORTED或者 EPROTO错误,我们应该忽略这两个错误。

(2)ET模式下accept存在的问题

考虑这种情况:多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由 于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列 中剩下的连接都得不到处理。

解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退 出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno 设置为EAGAIN就表示所有连接都处理完。

综合以上两种情况,服务器应该使用非阻塞地accept,accept在ET模式下的正确使用 方式为:

 while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,
(size_t *)&addrlen)) > 0) {
    handle_client(conn_sock);
}
if (conn_sock == -1) {
    if (errno != EAGAIN && errno != ECONNABORTED &&
errno != EPROTO && errno != EINTR)
    perror("accept");
}  

2.LT模式:

因为LT模式只要available就会触发,所以:

1)、读事件:因为一般应用层的逻辑是“来了就能读”,所以一般没有问题,无需while 循环读取到EAGAIN;

如果应用层读缓冲区满:就会经常触发,解决方式如下;

2)、写事件:如果没有内容要写,就会经常触发,解决方式如下。

LT经常触发读写事件的解决办法:修改fd的注册事件,或者把fd移出epollfd。

总结:

LT模式的优点在于:事件循环处理比较简单,无需关注应用层是否有缓冲或缓冲区是 否满,只管上报事件。缺点是:可能经常上报,可能影响性能。

7、线程与进程的区别和联系?线程是否具有相同的堆栈?d是否有独立的堆栈?

进程是死的,只是一些资源的集合,真正的程序执行都是线程来完成的,程序启动的时候操作系统就帮你创建线程。每个线程有自己的堆栈。 DLL 中有没有独立的堆栈,这个问题不好回答,或者说这个问题本身是否有问题。因为DLL中的代码是被某些线程所执行;只有线程拥有堆栈,如果DLL中的代码是EXE中的线程所调用,那么这个时候是不是说这个DLL没有自己独立的堆栈?如果DLL中的代码是由DLL自己创建的线程所执行,那么是不是说DLL有独立的堆栈?

以上讲的是堆栈,如果对于堆来说,每个DLL有自己的堆,所以如果是从DLL中动态分配的内存,最好是从DLL中刪除,如果你从DLL中分配内存,然后在EXE中,或者另外一个DLL中删除,很有可能导致程序崩溃.

8、画出OSI和TCP/IP协议栈的对应关系.

OSI/RM共分为七层,TCP/IP分为四层。TCP/IP中的网络接口层相当于OSI的物理层和数据链路层,TCP的应用层相当于OSI的应用层、表示层和会话层。其余层次基本对应。见图,其中外围深颜色的是OSI层次,内部白颜色的是TCP层次。

9、分布式服务接口请求的顺序性如何保证?

①首先,一般来说,从业务逻辑上最好设计系统不需要这种顺序的保证,因为一旦引入顺序性保障,会导致系统复杂度的上升,效率会降低,对于热点数据会压力过大等问题。

②操作串行化。

首先使用一致性hash负载均衡策略,将同一个id的请求都分发到同一个机器上面去处理,比如订单可以根据订单id。如果处理的机器上面是 多线程 处理的,可以引入内存队列去处理,将相同id的请求通过hash到同一个队列当中,一个队列只对应一个处理线程。

③最好能将多个操作合并成一个操作。

10、 智能指针 是线程安全的吗?哪些地方需要考虑线程安全?

1.智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或者–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2,这样引用计数就乱了,有可能造成资源未释放或者程序崩溃的风险。所以说智能指针中++或–的操作是需要加锁的,也就是说引用计数的操作是线程安全的

2.智能指针的对象存放在堆上,两个线程同时去访问,就会造成线程安全问题.

std::shared_ptr循环引用

 struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){  cout  << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}  

node1和node2两个智能指针对象指向两个节点,引用计数变为1,我们不需要手动delete

node1的_next指向node2,node2的_prev指向node1,引用计数变成2

node1和node2析构,引用计数减到1,但是_next还指向下一个节点,_prev指向上一个节点

也就是说_next析构了,node2释放了

也就是说_prev析构了,node1释放了

但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁都不会释放

解决方案

在引用计数的场景下,把shared_ptr换成weak_ptr就可以了

原理就是,node1->_next = node2; 和 node2->_prev = node1; 时weak_ptr的_next和_prev不会增加node1和node2的引用计数

 struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}  

如果不是new出来的空间如何用智能指针管理呢?

其实shared_ptr设计了一个删除器来解决这个问题

 // 仿函数的删除器
template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main()
{
FreeFunc<int> freeFunc;
shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
return 0;
}  

11、TCP三次握手的过程, accept发生在三次握手哪个阶段?

accept发生在三次握手之后。

第一次握手:客户端发送syn包(syn=j)到服务器。

第二次握手:服务器收到syn包,必须确认客户的sY(ack=j+1),同时自己也发送一个ASK包(ask=k)。

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1)。

握手完成后,客户端和服务器就建立了tcp连接。这时可以调用 accept函数获得此连接。

12、redis的主从复制怎么做的?

Redis旧版复制功能只有同步和命令传播。新版复制功能加入了部分同步的功能。

1)同步:

2)命令传播:

当主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。

3)部分同步:(断线后重复制)

复制偏移量:通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态。

复制积压缓冲区:主服务保存最近的写命令到复制积压缓冲区,是一个先进先出队列

服务器运行ID:从服务器记录上次同步的主服务器的Id。

13、程序什么时候应该使用线程,什么时候单线程效率高。

1 耗时的操作使用线程,提高应用程序响应

2 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求。

3 多CPU系统中,使用线程提高CPU利用率

4 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

其他情况都使用单线程。

14、设计DNS服务器中cache的数据结构。

要求设计一个DNS的Cache结构,要求能够满足每秒5000以上的查询,满足IP数据的快速插入,查询的速度要快。(题目还给出了一系列的数据,比如:站点数总共为5000万,IP地址有1000万,等等)

DNS服务器实现域名到IP地址的转换。

每个域名的平均长度为25个字节(估计值),每个IP为4个字节,所以Cache的每个条目需要大概30个字节。

总共50M个条目,所以需要1.5G个字节的空间。可以放置在内存中。(考虑到每秒5000次操作的限制,也只能放在内存中。)

可以考虑的数据结构包括hash_map,字典树, 红黑树 等等。

15、linux中软连接和硬链接的区别.

原理上,硬链接和源文件的inode节点号相同,两者互为硬链接。软连接和源文件的inode节点号不同,进而指向的block也不同,软连接block中存放了源文件的路径名。

实际上,硬链接和源文件是同一份文件,而软连接是独立的文件,类似于快捷方式,存储着源文件的位置信息便于指向。

使用限制上,不能对目录创建硬链接,不能对不同文件系统创建硬链接,不能对不存在的文件创建硬链接;可以对目录创建软连接,可以跨文件系统创建软连接,可以

对不存在的文件创建软连接。

16、linux内核调度详细说一下

1.1、调度策略

定义位于linux/include/uapi/linux/sched.h中

SCHED_NORMAL:普通的分时进程,使用的fair_sched_class调度类

SCHED_FIFO:先进先出的实时进程。当调用程序把CPU分配给进程的时候,它把该进程描述符保留在运行队列链表的当前位置。此调度策略的进程一旦使用CPU则一直运行。如果没有其他可运行的更高优先级实时进程,进程就继续使用CPU,想用多久就用多久,即使还有其他具有相同优先级的实时进程处于可运行状态。使用的是rt_sched_class调度类。

SCHED_RR:时间片轮转的实时进程。当调度程序把CPU分配给进程的时候,它把该进程的描述符放在运行队列链表的末尾。这种策略保证对所有具有相同优先级的SCHED_RR实时进程进行公平分配CPU时间,使用的rt_sched_class调度类

SCHED_BATCH:是SCHED_NORMAL的分化版本。采用分时策略,根据动态优先级,分配CPU资源。在有实时进程的时候,实时进程优先调度。但针对吞吐量优化,除了不能抢占外与常规进程一样,允许任务运行更长时间,更好使用高速缓存,适合于成批处理的工作,使用的fair_shed_class调度类

SCHED_IDLE:优先级最低,在系统空闲时运行,使用的是idle_sched_class调度类,给0号进程使用

SCHED_DEADLINE:新支持的实时进程调度策略,针对突发型计算,并且对延迟和完成时间敏感的任务使用,基于EDF(earliest deadline first),使用的是dl_sched_class调度类。

1.2、调度触发

调度的触发主要有两种方式,一种是本地定时中断触发调用scheduler_tick函数,然后使用当前运行进程的调度类中的task_tick,另外一种则是主动调用schedule,不管是哪一种最终都会调用到__schedule函数,该函数调用pick_netx_task,通过rq->nr_running ==rq->cfs.h_nr_running判断出如果当前运行队列中的进程都在cfs调度器中,则直接调用cfs的调度类(内核代码里面这一判断使用了likely说明大部分情况都是满足该条件的)。如果运行队列不都在cfs中,则通过优先级stop_sched_class->dl_sched_class->rt_sched_class->fair_sched_class->idle_sched_class遍历选出下一个需要运行的进程。然后进程任务切换。

处于TASK_RUNNING状态的进程才会被进程调度器选择,其他状态不会进入调度器。系统发生调度的时机如下:

à调用cond_resched()时

à显式调用schedule()时

à从中断上下文返回时

当内核开启抢占时,会多出几个调度时机如下:

à在系统调用或者中断上下文中调用preemt_enable()时(多次调用系统只会在最后一次调用时会调度)

à在中断上下文中,从中断处理函数返回到可抢占的上下文时

2、CFS调度

该部分代码位于linux/kernel/sched/fair.c中

定义了const struct sched_classfair_sched_class,这个是CFS的调度类定义的对象。其中基本包含了CFS调度的所有实现。

CFS实现三个调度策略:

1> SCHED_NORMAL这个调度策略是被常规任务使用

2> SCHED_BATCH 这个策略不像常规的任务那样频繁的抢占,以牺牲交互性为代价下,因而允许任务运行更长的时间以更好的利用缓存,这种策略适合批处理

3> SCHED_IDLE 这是nice值甚至比19还弱,但是为了避免陷入优先级导致问题,这个问题将会死锁这个调度器,因而这不是一个真正空闲定时调度器

CFS调度类:

n enqueue_task(…) 当任务进入runnable状态,这个回调将把这个任务的调度实体(entity)放入红黑树并且增加nr_running变量的值

n dequeue_task(…) 当任务不再是runnable状态,这个回调将会把这个任务的调度实体从红黑树中取出,并且减少nr_running变量的值

n yield_task(…) 除非compat_yield sysctl是打开的,这个 回调函数 基本上就是一个dequeue后跟一个enqueue,这那种情况下,他将任务的调度实体放入红黑树的最右端

n check_preempt_curr(…) 这个回调函数是检查一个任务进入runnable状态是否应该抢占当前运行的任务

n pick_next_task(…) 这个回调函数选出下一个最合适运行的任务

n set_curr_task(…) 当任务改变他的调度类或者改变他的任务组,将调用该回调函数

n task_tick(…) 这个回调函数大多数是被time tick调用。他可能引起进程切换。这就驱动了运行时抢占

2.1、CFS调度

Tcik 中断,主要会更新调度信息,然后调整当前进程在红黑树中的位置。调整完成以后如果当前进程不再是最左边的叶子,就标记为Need_resched标志,中断返回时就会调用scheduler()完成切换、否则当前进程继续占用CPU。从这里可以看出CFS抛弃了传统时间片概念。Tick中断只需要更新红黑树。

红黑树键值即为vruntime,该值通过调用update_curr函数进行更新。这个值为64位的变量,会一直递增,__enqueue_entity中会将vruntime作为键值将要入队的实体插入到红黑树中。__pick_first_entity会将红黑树中最左侧即vruntime最小的实体取出。

17、什么是协程?

协程是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个 线程也可以拥有多个协程。

最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户 态执行)。

这样的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

其中 yield 是python当中的语法。当协程执行到yield关键字时,会暂停在那一行, 等到主线程调用send方法发送了数据,协程才会接到数据继续执行。

但是,yield让协程暂停,和线程的阻塞是有本质区别的。协程的暂停完全由程序控制, 线程的阻塞状态是由操作系统内核来进行切换。

因此,协程的开销远远小于线程的开销。

18、确保线程安全的几种方法?

确保线程安全的方法有这几个:竞争与原子操作、同步与锁、可重入、过度优化。

1.竞争与原子操作

多个线程同时访问和修改一个数据,可能造成很严重的后果。出现严重后果的原因是很 多操作被操作系统编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一 半就被调度系统打断了而去执行别的代码了。一般将单指令的操作称为原子的 (Atomic),因为不管怎样,单条指令的执行是不会被打断的。

因此,为了避免出现多线程操作数据的出现异常,Linux系统提供了一些常用操作的原 子指令,确保了线程的安全。但是,它们只适用于比较简单的场合,在复杂的情况下就 要选用其他的方法了。

2.同步与锁

为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对 同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得 对同一个数据进行访问。

同步的最常用的方法是使用锁(Lock),它是一种非强制机制,每个线程在访问数据或 资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取 锁时,线程会等待,直到锁重新可用。

二元信号量是最简单的一种锁,它只有两种状态:占用与非占用,它适合只能被唯一一 个线程独占访问的资源。对于允许多个线程并发访问的资源,要使用多元信号量(简称 信号量)。

3.可重入

一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进 入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。 可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。

4.过度优化

在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可 能对代码进行过度的优化以确保线程安全。

我们可以使用volatile关键字试图阻止过度优化,它可以做两件事:第一,阻止编译 器为了提高速度将一个变量缓存到寄存器而不写回;第二,阻止编译器调整操作 volatile变量的指令顺序。

在另一种情况下,CPU的乱序执行让多线程安全保障的努力变得很困难,通常的解决办 法是调用CPU提供的一条常被称作barrier的指令,它会阻止CPU将该指令之前的指 令交换到barrier之后,反之亦然。

文章来源:智云一二三科技

文章标题:「linux后台开发」最新一线互联网大厂面试题总结

文章地址:https://www.zhihuclub.com/101843.shtml

关于作者: 智云科技

热门文章

网站地图