2019年4月

一、自旋锁

自旋锁和互斥量相似,通过加锁和解锁来保护对共享数据的保护。

和互斥量不同的是:互斥量在锁已经被占有的情况下,会阻塞等待,此时线程处于休眠状态,不占用CPU资源。而自旋锁此时虽然也会被阻塞,但是不会休眠,处于忙等的状态,不会交出CPU资源。

自旋锁适用于那些锁持有时间短不希望把时间浪费在CPU调度上的场景。

linux内核中,自旋锁被广泛的使用,因为内核模块特别是在网卡驱动中,时间精度要求很高,相比其他锁切换时间,自旋锁的忙等效率更高一些。

自旋锁的数据类型为pthread_spinlock_t,相关函数为:

#include <pthread.h>

// 初始化和销毁自旋锁
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

// 对自旋锁加锁和解锁
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

自旋锁的函数定义基本和其他锁一致,具体使用方法不再详细描述。

二、示例

#include <stdio.h>
#include <pthread.h>
#include "log.h"

static int n = 0;

pthread_spinlock_t spin_lock;

static void *f(void *args) {
    int idx = 0;
    while (idx++ < 1000) {
        pthread_spin_lock(&spin_lock);
        n++;
        pthread_spin_unlock(&spin_lock);
    }
    return 0;
}

int main() {
    int i;
    pthread_spin_init(&spin_lock, 0);

    pthread_t ts[50];
    for (i = 0; i < 50; i++) {
        pthread_create(&ts[i], 0, f, 0);
    }

    for (i = 0; i < 50; i++) {
        pthread_join(ts[i], 0);
    }
    pthread_spin_destroy(&spin_lock);

    info("n = %d", n);

    return 0;
}

编译运行:

一、ELF文件

ELF(Executable and Linkable Format)文件是linux下的二进制可执行文件,它同时兼容可执行文件和可链接文件。

一个ELF文件包含两个部分:一个固定长度的文件头和多个可扩展的数据块。其中,文件头是整个可执行文件的总地图,描述了整个文件的组织结构。可扩展数据块分为两类,对应着不同的视图——在链接视图下,数据块的单位是节(Section),用多个节区头索引所有内容;而在执行视图下,数据块的单位是段(Segment),用程序头(Program Header)索引所有的段。如下图所示:

通过readelf -S命令可以打印出文件的节区头部分,这里使用ln命令作为源程序分析:

[ma@localhost:~]$ readelf -S /bin/ls
There are 29 section headers, starting at offset 0x1a358:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400200  00000200
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             000000000040021c  0000021c
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             000000000040023c  0000023c
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400260  00000260
       0000000000000064  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002c8  000002c8
       0000000000000be8  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400eb0  00000eb0
       00000000000005c4  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000401474  00001474
       00000000000000fe  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000401578  00001578
       00000000000000a0  0000000000000000   A       6     3     8
  [ 9] .rela.dyn         RELA             0000000000401618  00001618
       00000000000001b0  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             00000000004017c8  000017c8
       0000000000000990  0000000000000018   A       5    12     8
  [11] .init             PROGBITS         0000000000402158  00002158
       0000000000000018  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000402170  00002170
       0000000000000670  0000000000000010  AX       0     0     4
  [13] .text             PROGBITS         00000000004027e0  000027e0
       000000000000fb68  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         0000000000412348  00012348
       000000000000000e  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         0000000000412360  00012360
       0000000000003b27  0000000000000000   A       0     0     32
  [16] .eh_frame_hdr     PROGBITS         0000000000415e88  00015e88
       00000000000006b4  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000416540  00016540
       0000000000001fdc  0000000000000000   A       0     0     8
  [18] .ctors            PROGBITS         0000000000619000  00019000
       0000000000000010  0000000000000000  WA       0     0     8
  [19] .dtors            PROGBITS         0000000000619010  00019010
       0000000000000010  0000000000000000  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000619020  00019020
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .data.rel.ro      PROGBITS         0000000000619040  00019040
       0000000000000a48  0000000000000000  WA       0     0     32
  [22] .dynamic          DYNAMIC          0000000000619a88  00019a88
       00000000000001d0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000619c58  00019c58
       0000000000000098  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000619cf0  00019cf0
       0000000000000348  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         000000000061a040  0001a040
       0000000000000200  0000000000000000  WA       0     0     32
  [26] .bss              NOBITS           000000000061a240  0001a240
       0000000000000d20  0000000000000000  WA       0     0     32
  [27] .gnu_debuglink    PROGBITS         0000000000000000  0001a240
       0000000000000010  0000000000000000           0     0     4
  [28] .shstrtab         STRTAB           0000000000000000  0001a250
       0000000000000101  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

节区头包含了我们经常见到的字段:.data/.text/.init等,这里只描述一下这几个常见节区的作用。

1.1 .text 节区

该节区存储了源文件编译生成的机器指令。对开发者来说,这可能是最重要的一个节区,所有的程序逻辑都放在这里。但开发者对该节区能做的控制却很少,能影响它的因素只有开发者写的程序逻辑,以及编译时使用的选项。比如,使用 -O1 优化选项编译程序可以生成尽量紧凑的 .text 节区,而用 -O2 优化选项会使编译器倾向于生成执行速度更快的指令组合,但有可能让 .text 节区的体积轻微地增大。

1.2 .rodata 节区

从字面上就能看出,ro表示read only,即不可写,因此该节区存储了程序中的常量数据,例如:

const char *p = "helloworld";

1.3 .data 节区

所有的全局和静态的已初始化变量会存放在这个节区中,这个节区是可读可写的。

1.4 .bss 节区

该节区存储了所有未初始化或初始化为 0 的全局和静态变量,该节区的设计初衷就是为了节省目标文件的存储空间。变量未被初始化,或者虽被初始化了,但值为 0,就没必要浪费空间,再在目标文件中存储大量的 0 值。

1.5 .got 和 .plt 节区

这两个节区存储了动态链接用到的全局入口表和跳转表。当程序中用到动态链接库中的某个函数时,会在该节区内记录相应的数据。

二、进程内存分布

2.1 虚拟地址空间

linux为每个进程都分配了4G(32位平台)的虚拟地址空间,虚拟空间可以认为是操作系统给每个进程准备的沙盒。就像电影《黑客帝国》中 Matrix 给每个人准备的充满营养液的容器一样。实际上,每个进程只存活在自己的虚拟世界里,却感觉自己独占了所有的系统资源(内存)。

当一个进程要使用某块内存时,它会将自己世界里的一个内存地址告诉操作系统,剩下的事情就由操作系统接管了。操作系统中的内存管理策略将决定映射哪块真实的物理内存,供应用使用。操作系统会竭尽全力满足所有进程合法的内存访问请求。一旦发现应用试图访问非法内存,它将会把进程杀死,防止它做“坏事”影响到系统或其他进程。

这样做,一方面为了安全,防止进程操作其他进程或者系统内核的数据;另一方面为了保证系统可同时运行多个进程,且单个进程使用的内存空间可以超过实际的物理内存容量。

该做法的另一结果则是降低了每个进程内存管理的复杂度,进程只需关心如何使用自己线性排列的虚拟地址,而不需关心物理内存的实际容量,以及如何使用真实的物理内存。

2.2 虚拟地址空间分布

虚拟内存的排布规则如下所示:

从下往上依次是0-4G的内存空间,分别分给了不同的区段。可以用一段程序来验证这一个观点:

#include <stdlib.h>

static const char *p = "HelloWorld";
static int s_init_var = 0;
static int s_not_init;

int g_init_var = 0;
int g_not_init;

int main() {
    const char *q = "abc";

    int a, b;
    int *pa = (int *)malloc(sizeof(int));
    int *pb = (int *)malloc(sizeof(int));

    printf("static variable address: %p %p\n",
            &s_init_var, &s_not_init);
    printf("global variable address: %p %p\n",
            &g_init_var, &g_not_init);
    printf("const variable:%p[static] %p\n",
            p, q);
    printf("a and b: %p %p\n", &a, &b);
    printf("pa and pb: %p %p\n", pa, pb);

    return 0;
}

代码分别创建了静态变量、全局变量以及临时变量等多种不同场景下的变量,打印出它们的地址。使用gcc编译运行:

static variable address: 0x600a0c 0x600a10
global variable address: 0x600a08 0x600a14
const variable:0x4006c8 0x4006d3
a and b: 0x7ffda942df14 0x7ffda942df10
pa and pb: 0xa82010 0xa82030

可以看到已经初始化的静态变量s_init_var和已经初始化的全局变量g_init_var两者地址紧紧相邻,间距刚好隔了4个字节,因为他们被初始化了,都放在了.data区域。而未初始化的两个虽然也是相连在一起的,但是和初始化了的变量还是隔了一段距离,但距离不是很大,因为.bss.data地址差别不大。

而被const修饰的两个变量地址也是一样,处在同一个地址空间,和上面的静态变量以及全局变量相比,地址比它们小,这也就说明了.rodata是在.data.bss下面的。

最后的a/bpa/pb则验证了栈区地址和堆区地址的位置,堆是处在靠下的地址,而栈从地址位数上来说就已经很高了。

这里还能看出一点:局部变量b在a后面申请,但是它的地址比a要小;而pb同样也是在pa后面申请,地址却比pa大。
这说明了栈的地址空间是从上往下申请的,而堆则正常由下往上。

三、参考文档

文章部分摘抄于:攻克 Linux 系统编程

原文写得很不错,有兴趣可自行参考。

一、题目

设计一个类,我们只能生成该类的一个实例

二、思路

单例模式是一种软件设计模式,表示单例对象设计的类只能允许一个实例存在。

单例模式的基本解题思路为:

  1. 将类的构造函数设为私有,外部无法直接通过默认构造函数来生成新的实例,只提供一个公共的静态接口函数返回该实例。
  2. 当外部没有引用当前实例时,创建并返回该实例。如果该实例已经存在了,直接返回。

单例模式共有两种实现方法:

  1. 饿汉模式:饿汉是指在实例初始化时就创建好,后面直接使用。
  2. 懒汉模式:懒汉模式是实例在使用了,万不得已的情况下才创建。

三、饿汉模式实现

#include <iostream>
using namespace std;

class CSingleTon {
private:
    CSingleTon() {
        cout << "CSingleTon" << endl;
    }

    static CSingleTon *instance;
public:
    static CSingleTon *getInstance() {
        return instance;
    }
    static void destoryInstance() {
        if (instance != NULL) {
            delete instance;
            instance = NULL;
        }
    }
};

CSingleTon *CSingleTon::instance = new CSingleTon();

int main() {
    cout << "helloworld" << endl;

    CSingleTon *s1 = CSingleTon::getInstance();
    CSingleTon *s2 = CSingleTon::getInstance();
    CSingleTon *s3 = CSingleTon::getInstance();

    cout << s1 << "\n"
        << s2 << "\n"
        << s3 << endl;

    s1->destoryInstance();

    return 0;
}

编译运行:

注意:CSingleTon是在helloworld的前面打印出来的,说明程序在创建时就已经生成了实例。并且三个实例的地址都是相同的。

四、懒汉模式实现

#include <iostream>

using namespace std;

class CSingleTon {
private:
    CSingleTon() {
        cout << "CSingleTon" << endl;
    }

    static CSingleTon *instance;
public:
    static CSingleTon *getInstance() {
        if (instance == NULL) {
            instance = new CSingleTon();
        }
        return instance;
    }
    static void destoryInstance() {
        if (instance != NULL) {
            delete instance;
            instance = NULL;
        }
    }
};

CSingleTon *CSingleTon::instance = NULL;

int main() {
    cout << "helloworld" << endl;

    CSingleTon *s1 = CSingleTon::getInstance();
    CSingleTon *s2 = CSingleTon::getInstance();
    CSingleTon *s3 = CSingleTon::getInstance();

    cout << s1 << "\n"
        << s2 << "\n"
        << s3 << endl;

    s1->destoryInstance();

    return 0;
}

编译运行,和上面的不同,helloworld先打印出来了,并且三个实例的地址也都是相同的:

关于斐波那契数列的描述:
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

一、面试题

求斐波那契数列的第n项:写一个函数,输入n,输出斐波那契数列的第n项。

二、解法一

求斐波那契数列最简单的解法就是用递归,大部分书籍上介绍递归的时候都会用斐波纳契数列做例子,递归很容易写:

long long Fibonacci_Solution1(unsigned int n)
{
    if(n <= 0)
        return 0;

    if(n == 1)
        return 1;

    return Fibonacci_Solution1(n - 1) + Fibonacci_Solution1(n - 2);
}

这个解法并不是最优的,因为中间充斥了很多重复的运算,应该还不足以应付面试。例如计算f(10)的过程中:

f(6)/f(7)/f(8)都被计算了多次,当数据越大时,重复计算的次数也越来越多,因此并不是最优的解法。

三、解法二

上面的递归之所以慢,是因为重复的计算太多,我们可以考虑从如何减少重复计算下手。例如可以选择从2开始,保存前两位的值,依次向后计算:

long long Fibonacci_Solution2(unsigned n)
{
    int result[2] = {0, 1};
    if(n < 2)
        return result[n];

    long long  fibNMinusOne = 1;
    long long  fibNMinusTwo = 0;
    long long  fibN = 0;
    for(unsigned int i = 2; i <= n; ++ i)
    {
        fibN = fibNMinusOne + fibNMinusTwo;

        fibNMinusTwo = fibNMinusOne;
        fibNMinusOne = fibN;
    }

     return fibN;
}

注意事项:传参的n的类型为unsigned int,保存值以及返回值的数据类型是long long,避免int溢出。

ps:虽然long long也会溢出,至少比int大一些。

一、两者对比

进程是最小的资源分配单位,线程是最小的执行单位:

  • 每个进程至少有一个线程,任务的执行都是由线程来完成,也就是说,线程时进程运行时的实体。
  • 线程运行时依赖进程中分配的资源,一个进程可以有多个线程,但是每个线程只属于一个进程。

进程之间相互独立,线程之间数据共享:

  • 每个进程拥有自己独立的地址空间:堆、栈等。
  • 线程之间大部分数据是共享的,它只拥有自己独立的栈,局部堆以及单独的信号屏蔽字等。

进程创建和切换的开销比线程要大:

  • 操作系统切换进程的时候,需要保存进程上下文(线程也属于进程上下文中的内容)。创建进程时,子进程需要拷贝父进程的数据内容,遵循读时共享、写时复制的原则。
  • 线程切换只需要切换线程所有的栈空间,只需要移动栈指针即可,开销远小于进程切换。

进程间通信比线程间通信麻烦:

  • 进程间通信依赖IPC通信方式:共享内存、管道以及socket等,实现复杂。
  • 线程间通信只需要通过定义全局变量即可,各线程都能共享。

进程比线程稳定:

  • 多进程间,有各自独立的数据段,一个进程的崩溃不会影响到其他进程。
  • 多线程间数据共享,一个线程的崩溃往往会影响其他线程。

表格对比

对比点进程线程
定义进程时运行中的程序,是最小的资源分配单位线程是进程执行的实体,是最小的执行单位
资源占用占用资源多占用资源较少
内存共享数据不共享,遵循读时共享、写时复制原则大部分数据共享,只有私有的栈区不共享
通信依赖IPC通信方式,机制复杂可直接通过全局变量通信,实现简单
上下文切换耗时耗时长耗时短,更轻量
稳定性进程崩溃不会影响另外进程线程崩溃可能导致他线程也崩溃

二、多进程和多线程的选择

  • 需要频繁创建销毁的优先用线程
  • 需要进行大量计算的优先使用线程
  • 可能要扩展到多机分布的用进程,多核分布的用线程

三、参考文档

Difference between Process and Thread

编程思想之多线程与多进程系列

多线程还是多进程的选择及区别

一、编程和语言

1.1 C/C++

指针数组、数组指针、函数指针的概念

什么是结构体对齐

静态数组和动态数组区别

​ 静态数组是编译时就分配好大小的数组,内存在栈区(全局数组在全局区),能分配的大小有限,受限于栈的大小。动态是程序运行后动态调用malloc或者new生成的,内存在堆区,可分配大小相较于栈区较大。

static的作用

sizeof的作用

C++

  • 重载和重写的区别
    答:重载时同一个函数的不同实现方式,通过入参的不同调用不同的函数(重载函数不能以返回值为区别)。重写是指子类覆盖父类的函数,隐藏父类实现达到多态的机制。
  • 运算符重载
  • C++中virtual关键字的作用
    virtual是实现多态的前提,对函数使用virtual将在类中产生虚函数表。对类使用virtual该类将变成虚基类。
  • 菱形继承:B和C继承A,D继承B和C)时每个对象的空间结构分布,比如问D有几份虚表,D中B和C的成员空间排布。
  • 指针和引用的区别
  • new/delete/new[]/delete []/malloc/free的区别
  • RAII/pimpl的惯用方式
  • 构造函数的调用顺序
  • 静态类静态方法
  • 智能指针的实现原理

STL

  • vecotr 和list 的区别,适用情况
  • vector的扩容机制
  • set承载结构体
  • 迭代器的使用方式,迭代器失效场景

其他

  • 程序的内存布局:(堆、栈、静态/全局/局部变量)
  • 函数的堆栈,调用函数时参数如何入栈。
  • 内存泄漏的原因及避免措施
    内存泄漏的原因是因为申请了内存没有被释放掉,写代码时应有良好的编程习惯,对申请的内存一定要即时释放
  • gdb的使用方法(断点、查看内存、执行跟踪、了解CPU主要寄存器作用)

1.2 Go

  • 协程实现原理
  • 管道实现原理

1.3 设计模式

    • OO思想、常见设计模式(单例模式、工厂设计模式、装饰者模式、Builder模式、生产者消费者模式、策略模式等)
    • Observer模式的思想,策略模式的思想,两者区别,优缺点,适用情况
    • OOP特性,通过哪些机制实现的

二、linux

2.1 系统编程

  • 多线程相关的API:创建、等待、终止以及获取线程ID等
  • 进程相关的API:创建、等待、终止以及获取进程ID等。
  • 线程同步方式:互斥体、读写锁、屏障、信号量、条件变量、自旋锁
  • 多线程和多进程的区别,为什么选用多线程或者是多进程
  • 线程和进程里面各有什么资源,都有什么用处?
    进程间都是相互独立的,产生一个新的进程,大部分的数据都被独立开来,每个进程拥有自己的堆区、栈区等数据,遵循“读时共享、写时复制”的原则。多个线程间大部分数据是共享的,如全局变量、堆等,都是共享的,线程只有自己独立的栈以及线程ID等少量资源,线程还拥有自己独立的信号屏蔽字。
  • forkwait有什么作用,fork进程时的操作。
    fork会产生一个子进程,在子进程中,fork返回0,父进程中,返回新创建的子进程id。wait是父进程用来回收子进程资源用的,避免子进程不回收处在僵尸态。fork后子进程和父进程采用“读时共享、写时复制”的方式来运行。
  • 父子进程、孤儿进程以及僵尸进程,僵尸进程如何产生和消除?
    在进程中调用fork函数会产生一个子进程,当前进程即为新进程的父进程,父进程调用fork返回子进程ID,子进程返回0。孤儿进程是指某个进程的父进程执行完退出了,但是子进程还在执行,此时子进程就是孤儿进程,会被1号进程init回收。僵尸进程是指子进程运行完毕退出了,父进程没有调用wait或者waitpid回收子进程资源,导致子进程处于僵尸态。消除僵尸进程的方法是杀掉父进程,使子进程被1号进程收养并回收掉。
  • 进程间通信:共享内存、匿名管道、有名管道、消息队列、信号量和socket。
  • linux应用层常见的锁,什么是死锁、如何避免死锁。
    互斥量、读写锁、自旋锁、信号量、屏障以及条件变量等。死锁是指进程间由于加锁不当导致程序hang住的现象,死锁只会发生在一个线程试图锁住另一个线程以相反顺序锁住的互斥量以及同一个进程对同一把锁重复加锁。
    避免死锁:锁住的区域尽量少,加完即使释放,尽量不多多个进程同时加多把锁,加锁时注意顺序。
  • 守护进程,操作以及实现原理。

2.2 网络编程

  • socket API:connect/accept/bind/listen/send/sendto/recv/recvfrom/select/gethostbyname
  • 异步IO框架:select/poll/epoll
  • 什么是阻塞模式和非阻塞模式,两种模式下的socket在connectsend以及recv等行为上的区别,如何将socket设置为非阻塞的
    阻塞模式:函数没有达到触发状态时,会一直卡在这里处于等待状态,直到又事件发生为止。
    将socket设置为非阻塞的只要使用fcntl函数给文件描述符的状态加上O_NONBLOCK标记即可。如果是文件描述符,直接在打开时也可以设置非阻塞。
  • 大端小端问题,主机是什么字节序
  • epoll水平触发与边缘触发
    边沿触发(ET模式):epoll中产生可读事件后,如果没有读完缓冲数据域,剩余的数据部分将会在下次有数据过来时才能继续读。水平触发(LT模式):epoll产生可读事件后,如果没有一次没有读完,epoll_wait()继续返回可读事件。
    正常来说,ET模式比LT模式效率更高,因为它在epoll产生的触发事件更少,更多情况下都采用“ET+非阻塞”模式。
  • libeventreactor模式。
    libevent的核心思想便是reactor,当一个socket产生可读事件时,读取数据,然后设置可写事件,再对socket写数据。
    工作流程:socket --> 添加到epoll --> 可读 --> 从epoll中摘下,读数据 --> 设置可写事件 --> 放回红黑树
  • 为什么select有文件句柄限制,pollepoll没有。
    select的文件句柄限制是写死在头文件里面的,编译时就已经固定了。poll和select工作原理类似,它的出现就是为了解决select文件句柄限制的问题。
    而epoll基于事件触发,和前面两者不同,它底层通过红黑树来存储文件描述符,插入删除的效率非常高。
  • epoll_event结构中的epoll_data_t的fd与ptr的使用场景。
    以reactor模式为例,可以把fd设置为当前的文件描述符,ptr设置为函数指针,当socket有可读或者可写事件的时候,自动调用函数指针执行,从而达到异步IO的效果。
  • select函数可以检测网络异常吗
    可以用过select设置超时时间来检测网络异常,很多时候可以通过非阻塞socket+select来对函数添加超时逻辑。
  • send/recv以及read/write返回值大于0、等于0、小于0的区别,错误码EINTR如何处理。
    大于0表示读到n字节的数据,等于0表示对端已经关闭连接,小于0表示错误,需要判断错误码。
    常见的错误码EINTR表示遇到中断了,重新读。EAGAIN也要重新读。
  • 发送数据缓冲区与接收数据缓冲区如何设计,如何编写正确的收数据代码与发数据代码。
  • shutdown与优雅关闭
    close会直接关闭两端的socket连接,并销毁内存对象。shutdown只会关掉本机的连接,对向还可以传输数据。也就是TCP的瓣关闭状态
  • 如何解决tcp粘包问题。
    添加协议,最简单的就是添加一个数据包头,然后写明该条数据包的长度n。读取时根据长度来读数据。
  • http代理、socks4代理与socks5代理如何编码实现。
  • gethostbyname阻塞与错误码获取问题
    gethostbyname默认是阻塞的
  • 如何清除无效的死链(端与端之间的线路故障)
    长连接,持续发送心跳包检测。
  • 长连接和短连接
    长连接表示连接一直处于打开状态,并且定时发送心跳包,当一定时间内心跳包如果发送失败或者没有收到回包,就认为连接发生故障。长连接的目的就是为了实时检测网络故障,避免出现连接异常断开,但是另一端并不知情还处在打开的状态。

2.3 操作系统

2.4 linux命令和shell

  • crontabiptables等命令的的使用
  • 进程、线程状态查看命令top/strace/pstack
  • 内存状态查看命令memstat/free
  • IO状态查看命令iostat/df/du
  • linux文件的权限、用户、时间(ctime/mtime/atime)和inode
  • 文件传输命令scp/rz/sz命令
  • 文件定位命令find/whereis命令
  • 硬链接和软链接,ln命令的用法,硬链接和软连接区别
  • lsof命令
  • kill用法,某个进程杀不掉的原因(进入内核态,忽略kill信号)
  • 系统管理命令(如查看内存使用、网络情况)
  • 管道的使用|
  • grep的使用
  • 文本处理命令awk/sed/grep
  • 本编辑工具vi/vim
  • 了解shell基本语法、变量操作、函数、循环/条件判断等程序结构
  • linux环境下的几种文件类型
  • linux常用端口和网络命令

三、计算机网络

3.1 基础网络知识

  • TCP/UDP报头格式,tcp和ip包头常见有哪些字段。
  • TCP/IP协议栈层次结构。
  • TCP三次握手和四次挥手,各种状态的细节点:CLOSE_WAIT、TIME_WAIT、2MSL。
  • TCP与UDP的区别与适用场景
  • nagle / keepalive / linger等选项的区别
  • 拥塞控制慢开始、拥塞避免、快重传、滑动窗口协议、停止等待协议、超时重传机制
  • IP地址子网划分
  • DNS解析过程
  • 地址解析协议ARP
  • 交换机和路由器的区别
  • http和https区别,https在请求时额外的过程,https是如何保证数据安全的
  • SESSION机制、cookie机制
    session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。 session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中, 与此相对的,cookie需要将所有信息都保存在客户端。因此cookie存在着一定的安全隐患,例如本地cookie 中保存的用户名密码被破译,或cookie被其他网站收集(例如:1. appA主动设置域B cookie,让域B cookie获取;2. XSS,在appA上通过javascript获取document.cookie,并传递给自己的appB)。
  • HTTP请求方法(GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE)
  • 熟悉tcpdump命令,windows下wireshark使用

3.2 常见问题

  • A与B建立了正常连接后,从未相互发过数据,这个时候B突然机器重启,问A此时的tcp状态处于什么状态?如何消除服务器程序中的这个状态
  • 聊天用什么协议(UDP),为什么使用UDP
  • 微信qq用什么协议
  • 跟多个人聊天怎么实现的(多线程),多线程怎么判断和哪个人聊天,需要设置什么全局变量

四、数据库

4.1 MySql

  • 数据表结构设计(三范式、字段属性)
  • 查询优化(索引的概念与创建、sql优化)
  • 事务四大特性(ACID)
  • 数据库隔离级别,每个级别会引发什么问题,mysql默认是哪个级别
  • MYSQL的两种存储引擎区别(事务、锁级别等等),各自的适用场景
  • 数据库的优化(从sql语句优化和索引两个部分回答)
  • 索引有B+索引和hash索引,各自的区别
  • B+索引数据结构,和B树的区别
  • 索引的分类(主键索引、唯一索引),最左前缀原则,哪些情况索引会失效
  • 聚集索引和非聚集索引区别
  • 有哪些锁(乐观锁悲观锁),select时怎么加排它锁
  • 关系型数据库和非关系型数据库区别
  • 数据库的主从复制
  • long_query怎么解决
  • 使用explain优化sql和索引
  • 内连接、外连接、交叉连接、笛卡儿积等
  • MVCC机制
  • 死锁怎么解决
  • varchar和char的使用场景。
  • mysql并发情况下怎么解决(通过事务、隔离级别、锁)
  • 字典树
  • 平时怎么写数据库的模糊查询

4.2 Redis

  • redis基本数据结构
  • redis队列应用场景
  • redis和Memcached的区别(支持数据持久化)
  • 分布式使用场景(储存session等)
  • 发布/订阅使用场景
  • redis的数据存储结构、rehash;
  • redis的事务与集群。
  • redis的持久化
  • redis热备
  • redis和的hash和string选择的优劣
  • redis单线程设计的原因
  • redis淘汰策略

五、数据结构和算法

5.1 排序和查找

5.2 树

  • AVL和红黑树的区别和原理实现
  • 哈夫曼树,哈夫曼编码。哈夫曼译码
  • AVL树和B树的概念、细节
  • 红黑树的概念、平均算法复杂度、最好最坏情况下的算法复杂度,左右旋转、颜色变换
  • 层次遍历、求深度、求两个节点距离、翻转二叉树、前中后序遍历
  • B+树和B树的区别,插入节点怎么分裂

5.3 哈希表

  • 建表以及解决哈希冲突
  • 哈希表的冲突检测、哈希函数常用实现、算法复杂度
  • 哈希表插入元素算法,元素类型是任意类型
  • hashmap的实现原理 采用什么方法能保证每个bucket中的数据更均匀

5.4 链表

  • 删除一个节点
  • 单链表倒转
  • 两个链表找相交的部分
  • 插入节点、链表逆置、使用链表进行大数字的加减,双向链表实现队列、寻找链表中的环

5.5 堆

  • 大量数据中寻找最大N个数字几乎每次都会问
  • 堆在插入时进行的调整

5.6 栈和队列

  • 两个栈实现队列

5.6 其他

  • KMP算法描述

5.7 常见问题

  • 单纯反转,hello world! :!!dlrow olleh 剑指offer原题。然后问,如果不用栈怎么做。指针找空格,后往前反转
  • 1000万个数,取出top100 堆实现和二分
  • 有100W个数字存放在一个文件中,然后让你随机生成100个数字不要与文件中的数字重复。(关键代码实现)典型的大文件处理
  • 两个有序数组,a[],b[],大小分别为m,n,求第k 大的数
  • 在一组数里面找到一个数,比它左右相邻的数都要大
  • 一个文本文件中每一行中有一个URL,最多一万行,统计每一个URL的次数,输出到另外一个文件中,每一行前面是URL,后面是个数。
  • 一个函数实现给定字符串,去除前面和后面的空格,比如“ ab cd ”,最后得到的结果是”ab cd”,不能改变字符串的地址
  • 对比cookie和session,有一个值错误则不正确
  • 查找10的阶乘后面有几个0
  • 字符串移位,给出字符串abc##dfg##gh,实现将所有#移至字符串串头。输出####abcdfggh(个人认为可以用后向移位,减少移位次数)
  • 给出一颗二叉树,两个叶节点,找到这两个叶节点互连通的一条最短路径
  • 两个日期计算天数差
  • 100个有序数组合并
  • 在一个字符串中找出第一个字符出现的位置,保证高效

alpine是一个轻量级的linux系统,由于太轻量了,目前被广泛用于docker镜像的制作上了(最新版的docker镜像才5M)。

体积小就有缺点了,缺点就是内部除了基本的命令以外,大部门的功能都不支持,默认连bash都没有。需要自己手动添加并重新构建。

1. 基于alpine:3.8添加bash并设置阿里云源

FROM alpine:3.8

MAINTAINER MaQian

RUN echo "https://mirrors.aliyun.com/alpine/v3.8/main/" > /etc/apk/repositories

RUN apk update \
        && apk upgrade \
        && apk add --no-cache bash bash-doc bash-completion \
        && rm -rf /var/cache/apk/* \
        && /bin/bash

2. 基于最新版alpine并设置阿里云源

相比上面的多了一个获取当前版本的逻辑,Dockerfile如下:

from alpine:latest

MAINTAINER MaQian

RUN alpine_version=`cat /etc/issue | head -1 | awk '{print $5}'` \
    && echo "https://mirrors.aliyun.com/alpine/v${alpine_version}/main/" > /etc/apk/repositories   \
    && apk update && apk upgrade && apk add --no-cache bash bash-doc bash-completion \
    && rm -rf /var/cache/apk/*

CMD ["/bin/bash"]

一、描述

前阵子硬盘出了问题,导致之前的vmware esxi系统坏了,重装一直报错,搞了几次搞不定干脆换成XenServer了。

XenServer相对vmware来说不是特别友好,第一步在创建新虚拟机的时候就卡了很久,主要是创建时虚拟机的镜像文件无法找到。很奇葩,竟然不能直接从本地导入上传的,只能从远端的NAS或者其他文件系统中获取,对于windows来说最简单的就是设置共享文件夹。

二、步骤

2.1 XenServer创建新虚拟机

首先,在界面上选择New VM按钮进入新建虚拟机向导页面:

选择好自己的系统类型,然后继续下一步,我这里是CentOS 7

然后选择系统镜像位置,这里不能直接从本地上传,也正是卡壳的地方。要点击右边的新建iso仓库选项先创建一个仓库:

选择仓库类型为Windows File Sharing,下一步:

如果已有共享文件夹,直接输入共享文件夹地址和账号密码即可,没有的的话参考2.2节先配置windows共享文件夹目录:

不出意外镜像仓库就添加成功了,只要把镜像放在这个目录下就行了。正常情况下这里是有意外的,有意外的话看下面的错误解决方案。

注意事项
iso文件一定要放在共享文件夹的当前目录下,不能在子目录下边。
例如共享目录为\\domain\b,则iso文件一定要在\\domain\b目录下,不能在\\domain\b\c\下边,否则会找不到文件

找到的iso文件后后面就比较顺畅了,和其他虚拟化产品一样,依次设置内存、磁盘已经网络等信息,一直下一步即可:

设置CPU配置:

设置磁盘,默认10G,可能较小,适当调整:

设置网络,网络比较重要,这里不描述:

设置完成:

2.2 windows开启共享文件夹

首先在需要共享的文件夹内右键属性,进入共享TAB页面:

PIC20190406_231541.png

选择共享,添加允许访问的用户:

PIC20190406_231606.png

选好后,确认,返回到第一层页面,点击高级共享,设置共享的名字,这个名字是后续需要访问的地址的一部分:

加上windows名字为maqian,设置的共享名为share,开启共享后访问的路径就是\\maqian\share

PIC20190406_231624.png

设置读写权限,然后确定退出当前页面:

PIC20190406_231624.png

再点击第一层页面的网络共享中心,关闭密码访问:

PIC20190406_231731.png

共享文件夹设置完成。

三、错误问题解决

3.1 DNS找不到

DNS找不到会报以下错误:

此时需要进入到XeNserver的后台,用SSH登陆,修改hosts文件,把主机指向当前设备的IP。

例如我的windows名字为maqian,共享文件夹的路径为\\maqian\share,正常情况下maqian只在windows下生效,XenServer是不知道这个主机的IP是哪里的,因为没有DNS解析。

要做的事情就是把maqian添加解析,windows下通过ipconfig命令看到网卡IP为192.168.1.3,添加到XenServer的hosts文件:

echo "192.168.1.3 maqian" > /etc/hosts

然后ping maqian,如果能通表明正常,否则需要检查网络问题。

3.2 认证问题

如果出现以下错误,说明windows的账号和密码不对:

检查当前的windows登入账号或者密码,这个账号密码是windows共享文件夹共享时添加的用户的账户和密码。

一、问题现象

使用golang编译了一个二进制程序,在CentOSUbuntu的镜像上运行是可以的,但是在Alpine运行就不行,使用./运行报错:

/bin/sh: ./saas_server: not found

二、解决方案

编译时添加参数CGO_ENABLED=0,关闭CGO就可以了:

CGO_ENABLED=0 go build

三、参考文档

Installed Go binary not found in path on Alpine Linux Docker