levelDB里缓存实现

leveldb的缓存机制

leveldb采用LRU机制, 利用键的哈希值前n位作为索引, 将要插入的键值对分派到指定的缓存区, 当缓存区的使用率大于总容量后, 优先淘汰最近最少使用的缓存, 独立的缓存区总量为2^n .

初始化ShardedLRUCache

  • 设置初始缓存容量, 并设置16个子分区的容量.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    static const int kNumShardBits = 4;
    static const int kNumShards = 1 << kNumShardBits;
    explicit ShardedLRUCache(size_t capacity)
    : last_id_(0) {
    const size_t per_shard = (capacity + (kNumShards - 1)) / kNumShards;
    for (int s = 0; s < kNumShards; s++) {
    shard_[s].SetCapacity(per_shard);
    }
    }

新建缓存

  • 由key的hash值的前kNumShardBits位作为子缓存区索引, 默认为前4位的索引位, 索引到指定的区, 子缓存区LRUCache接管新建缓存的任务.
    1
    2
    3
    4
    5
    6
    7
    class ShardedLRUCache : public Cache {
    ...
    virtual Handle* Insert(const Slice& key, void* value, size_t charge,
    void (*deleter)(const Slice& key, void* value)) {
    const uint32_t hash = HashSlice(key);
    return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
    }
  • LRUCache结构
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // A single shard of sharded cache.
    class LRUCache {
    public:
    LRUCache();
    ~LRUCache();

    // Separate from constructor so caller can easily make an array of LRUCache
    void SetCapacity(size_t capacity) { capacity_ = capacity; }

    // Like Cache methods, but with an extra "hash" parameter.
    Cache::Handle* Insert(const Slice& key, uint32_t hash,
    void* value, size_t charge,
    void (*deleter)(const Slice& key, void* value));
    Cache::Handle* Lookup(const Slice& key, uint32_t hash);
    void Release(Cache::Handle* handle);
    void Erase(const Slice& key, uint32_t hash);

    private:
    void LRU_Remove(LRUHandle* e);
    void LRU_Append(LRUHandle* e);
    void Unref(LRUHandle* e);

    // Initialized before use.
    size_t capacity_;

    // mutex_ protects the following state.
    port::Mutex mutex_;
    size_t usage_;

    // Dummy head of LRU list.
    // lru.prev is newest entry, lru.next is oldest entry.
    LRUHandle lru_;

    HandleTable table_;
    };

LRUCache才是真正具有缓存功能的结构, capacity_表示它的最大容量, mutex_规范各线程互斥地访问, usage_标志缓存中已用容量, lru_作为哑节点, 它的前节点是最新的缓存对象, 它的后节点是最老的缓存对象, table_是用来统计对象是否被缓存的哈希表.

  • LRUCache缓存生成

    1. 缓存的基础节点是LRUHandle, 储存节点的(键, 值, 哈希值, next, prev节点, 销毁处理函数等)信息. 综合上面的LRUCache结构也看到, 实际上, 缓存节点是双向列表存储, LRUHandle lru_这个哑节点用来分隔最常更新的节点和最不常更新节点.
    2. 当要将缓存节点插入缓存区, 先由哈希表判断缓存是否已存在, 若存在, 将其更新至最常更新节点; 若不存在, 则插入为最常更新节点. 同时更新哈希表HandleTable table_.
    3. 这里的HandleTable实现并没特殊之处, 我看是采用键哈希策略进行哈希, 如果键冲突则以链表进行存储.
    4. 利用二级指针对链表进行插入.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    LRUHandle* Insert(LRUHandle* h) {
    LRUHandle** ptr = FindPointer(h->key(), h->hash);
    LRUHandle* old = *ptr;
    h->next_hash = (old == NULL ? NULL : old->next_hash);
    *ptr = h;
    if (old == NULL) {
    ++elems_;
    if (elems_ > length_) {
    // Since each cache entry is fairly large, we aim for a small
    // average linked list length (<= 1).
    Resize();
    }
    }
    return old;
    }
  • 二级指针
    在HandleTable这个哈希表实现里, 有个FindPointer方法用来查找此对象是否已存在, 并返回LRUHandle**, 代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Return a pointer to slot that points to a cache entry that
    // matches key/hash. If there is no such cache entry, return a
    // pointer to the trailing slot in the corresponding linked list.
    LRUHandle** FindPointer(const Slice& key, uint32_t hash) {
    LRUHandle** ptr = &list_[hash & (length_ - 1)];
    while (*ptr != NULL &&
    ((*ptr)->hash != hash || key != (*ptr)->key())) {
    ptr = &(*ptr)->next_hash;
    }
    return ptr;
    }

可见, 如果已存在节点, 则返回这个节点的LRUHandle**; 如果不存在, 返回的是可以保存这个LRUHandle*的地址.

  • LRUCache缓存查找
    如果缓存存在, 则将其放置到哑节点lru_的prev位置, 即最近使用节点, 相当于提升它的优先级, 便于下次快速查找; 如果不存在这个缓存, 返回NULL.

    小结

    看完leveldb的缓存实现, 在实现思路上, 是传统的lru算法, 使用哈希表判重, 根据缓存的请求时间提升缓存的优先级. 里面的细节例如使用哈希值的前n位进行路由, 路由到2^n个独立的缓存区, 各个缓存区维护自己的mutex进行并发控制; 哈希表在插入节点时判断空间使用率, 并进行自动扩容, 保证查找效率在O(1).

levelDB里integer编码

leveldb的数字编码方法

  • EncodeFixed32
    按照机器的字节存储顺序(大端, 小端), 原样存储到一块32位内存

  • EncodeFixed64
    按照机器的字节存储顺序(大端, 小端), 原样存储到一块64位内存

  • PutFixed32
    将EncodeFixed32处理过的内存块追加到目的地址

  • PutFixed64
    同PutFixed32

  • EncodeVarint32
    将32位的无符号整数以7个有效位, 1个标志位的形式编码.

对于Varint这种整数编码, 是为了减短数值较小的整数所占的存储空间. 实现方法是每8位, 低7位存储有效数值和高1位存储标记位(用来标记是否还有后续的数值), 以127和128这两个数为例:
127 => 0111 1111
第一个字节低7位是有效数值127, 最高位位0, 所以表示没有后续的数值, 于是算出它所代表的数值是127.

128 => 1111 1111 0000 0001
第一个字节低7位是有效数值127, 最高位位1, 所以表示有后续的数值, 相同计算方法算出后续值1, 最高位标志0表示结束, 127 + (1<<7) = 128, 因为每8位实际表示的是7位, 所以要将1左移7位, 若有更多位, 计算方式相同.

leveldb里的32位编码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
char* EncodeVarint32(char* dst, uint32_t v) {
// Operate on characters as unsigneds
unsigned char* ptr = reinterpret_cast<unsigned char*>(dst);
static const int B = 128;
if (v < (1<<7)) {
*(ptr++) = v;
} else if (v < (1<<14)) {
*(ptr++) = v | B;
*(ptr++) = v>>7;
} else if (v < (1<<21)) {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = v>>14;
} else if (v < (1<<28)) {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = (v>>14) | B;
*(ptr++) = v>>21;
} else {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = (v>>14) | B;
*(ptr++) = (v>>21) | B;
*(ptr++) = v>>28;
}
return reinterpret_cast<char*>(ptr);
}

用了5个if来判断这个数落在哪个数值区间里, 32位的数值使用变长编码能使用1-5位来表示

leveldb里的64位编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// leveldb implementation
char* EncodeVarint64(char* dst, uint64_t v) {
static const int B = 128;
unsigned char* ptr = reinterpret_cast<unsigned char*>(dst);
while (v >= B) {
*(ptr++) = (v & (B-1)) | B;
v >>= 7;
}
*(ptr++) = static_cast<unsigned char>(v);
return reinterpret_cast<char*>(ptr);
}

// My own implementation
char* EncodeVarint64(char* dst, uint64_t v) {
unsigned char* ptr = reinterpret_cast<unsigned char*>(dst);
static const int B = 128;
int flag = 1;
for (int offset = 0; flag > 0 && offset < 8; offset++) {
*(ptr++) = ((v >> (offset * 7)) & 127) | B;
flag = v >> (offset * 7);
}
return reinterpret_cast<char*>(ptr);
}

比较了我和leveldb的实现之后, 发现leveldb更简洁高效, 是因为我的实现维护了一个offset来记录当前的偏移, flag用来记录是否继续计算, 每次都需要维护这个offset和flag, leveldb直接使用v作为变量, 通过每次对它的修改, 来记录是否继续, 同时它只需要用低7位, 省去了记录偏移的麻烦, 简洁有力!

levelDB里自定义的状态

LevelDB状态码

LevelDB里专门定义了Status类,自定义了常见的状态码,用于函数的返回。

状态码类型

在Status类中以枚举类型定义Code:

0 => 处理成功
1 => 未找到
2 => 崩溃
3 => 不支持
4 => 无效的参数
5 => IO错误

并且有6个静态方法用来生成对应状态码的Status对象, 和4个判断方法来方便确认状态。

存储方式

状态码包括状态信息都是利用私有字符串变量state_存储,存储方式如下:

0 ~ 4字节:消息长度(消息1的长度len1+分割符的长度+消息2的长度len2)
4 ~ 5字节:状态码
5 ~ 5+len1:消息1的长度
(若消息2不为空)
5+len1 ~ 7+len1:分隔符的长度(分隔符为2个字符,分别为冒号和空格)
7+len1 ~ 7+len1+len2:消息2的长度

这种存储方式,另得所有操作都是基于这个state_变量的下标索引操作。

小结

定义异常在其它代码遇到错误时可以抛出适当的异常, 这样的实现方式我认为好处是初始化,销毁和复制操作简单,并且这些内容在内存中的位置紧密,有利于缓存,不知道是不是这样考虑才这么做的,但分成3个变量也无伤大雅。

MIT 6.824 Lab2, part1小结

关于MIT 6.824课程的Lab 2, part 1

从昨晚在宾馆到今天飞机上,都在过这几个test case,终于最后有点讨巧的过了。

View service是MIT介绍paxos之前用来入门的,它的原理是以时间先后的顺序确定主备服务器,由定期的心跳更新此关系。之所以放在单独的service里获取主备关系,是为了将主备关系从K-V系统中解耦,从而K-V系统不用关心键值操作之外的事情,让更加上层的系统进行处理。

遇到的问题:

  • view service当前的view,和下一个未确认的view在哪些时间点更新?
    • 怎么认为这个服务器作为主,又怎么确认其为Backup,Idle?
    • 什么时候能将这些认识更新到服务器当前view上?
  • 服务器看到的view service的当前view是怎样的?
    • 不同服务器查询当前view状态,获得的结果是一样的吗?

我用复杂难看的if-else语句最后实现这些逻辑,但不可否认,实现的漏洞很多,我理想的解决方案还是使用状态机,可以更加清晰容易更新(用图表展现)。

levelDB里skiplist的实现代码赏析

跳表的原理就是利用随机性建立索引,加速搜索,并且简化代码实现难度。具体的跳表原理不再赘述,主要是看了levelDB有一些实现细节的东西,凸显自己写的实现不足之处。

  • 去除冗余的key

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    template<typename Key, class Comparator>
    struct SkipList<Key,Comparator>::Node {
    explicit Node(const Key& k) : key(k) { }

    Key const key;

    // Accessors/mutators for links. Wrapped in methods so we can
    // add the appropriate barriers as necessary.
    Node* Next(int n) {
    assert(n >= 0);
    // Use an 'acquire load' so that we observe a fully initialized
    // version of the returned Node.
    return reinterpret_cast<Node*>(next_[n].Acquire_Load());
    }
    void SetNext(int n, Node* x) {
    assert(n >= 0);
    // Use a 'release store' so that anybody who reads through this
    // pointer observes a fully initialized version of the inserted node.
    next_[n].Release_Store(x);
    }

    // No-barrier variants that can be safely used in a few locations.
    Node* NoBarrier_Next(int n) {
    assert(n >= 0);
    return reinterpret_cast<Node*>(next_[n].NoBarrier_Load());
    }
    void NoBarrier_SetNext(int n, Node* x) {
    assert(n >= 0);
    next_[n].NoBarrier_Store(x);
    }

    private:
    // Array of length equal to the node height. next_[0] is lowest level link.
    port::AtomicPointer next_[1];
    };

    这里使用一个Node节点表示所有相同key,不同高度的节点集合,仅保留了key和不同高度的向右指针,并且使用NewNode来动态分配随即高度的向右指针集合,而next_就指向这指针集合。这也是c/c++ tricky的地方。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <stdio.h>
    struct Node {
    char str[1];
    };
    int main() {
    char* mem = new char[4];
    for (int i = 0; i < 4; i++) {
    mem[i] = i + '0';
    }
    Node* node = (Node*)mem;
    char* const pstr = node->str;
    for (int i = 0; i < 4; i++) {
    printf("%c", pstr[i]);
    }
    return 0;
    }

    就像上面这个简单的sample,成员str可以作为指针指向从数组下标0开始的元素,并且不受申明时的限制,不局限于大小1,索引至分配的最大的内存地址。

  • 简易随机数生成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    uint32_t Next() {
    static const uint32_t M = 2147483647L; // 2^31-1
    static const uint64_t A = 16807; // bits 14, 8, 7, 5, 2, 1, 0
    // We are computing
    // seed_ = (seed_ * A) % M, where M = 2^31-1
    //
    // seed_ must not be zero or M, or else all subsequent computed values
    // will be zero or M respectively. For all other values, seed_ will end
    // up cycling through every number in [1,M-1]
    uint64_t product = seed_ * A;

    // Compute (product % M) using the fact that ((x << 31) % M) == x.
    seed_ = static_cast<uint32_t>((product >> 31) + (product & M));
    // The first reduction may overflow by 1 bit, so we may need to
    // repeat. mod == M is not possible; using > allows the faster
    // sign-bit-based test.
    if (seed_ > M) {
    seed_ -= M;
    }
    return seed_;
    }

可以看到,他使用A和M对种子进行运算,达到一定数据范围内不会重复的数集,而里面对于(product % M),使用(product >> 31) + (product & M)进行运算优化,考虑右移和与操作的代价远小于取余操作。

  • 简洁清晰的私有帮助方法,帮助寻找小于指定key的节点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    template<typename Key, class Comparator>
    typename SkipList<Key,Comparator>::Node*
    SkipList<Key,Comparator>::FindLessThan(const Key& key) const {
    Node* x = head_;
    int level = GetMaxHeight() - 1;
    while (true) {
    assert(x == head_ || compare_(x->key, key) < 0);
    Node* next = x->Next(level);
    if (next == NULL || compare_(next->key, key) >= 0) {
    if (level == 0) {
    return x;
    } else {
    // Switch to next list
    level--;
    }
    } else {
    x = next;
    }
    }
    }

初次使用golang channel

goroutine之间的同步

goroutine是golang中在语言级别实现的轻量级线程,仅仅利用go就能立刻起一个新线程。多线程会引入线程之间的同步问题,经典的同步问题如生产者-消费者问题,在c,java级别需要使用锁、信号量进行共享资源的互斥使用和多线程之间的时序控制,而在golang中有channel作为同步的工具。

1. channel实现两个goroutine之间的通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "strconv"
import "fmt"

func main() {
taskChan := make(chan string, 3)
doneChan := make(chan int, 1)

for i := 0; i < 3; i++ {
taskChan <- strconv.Itoa(i)
fmt.Println("send: ", i)
}

go func() {
for i := 0; i < 3; i++ {
task := <-taskChan
fmt.Println("received: ", task)
}
doneChan <- 1
}()

<-doneChan
}
  • 创建一个channel,make(chan TYPE {, NUM}), TYPE指的是channel中传输的数据类型,第二个参数是可选的,指的是channel的容量大小。
  • 向channel传入数据,CHAN <- DATA, CHAN指的是目的channel即收集数据的一方,DATA则是要传的数据。
  • 启动一个goroutine接收main routine向channel发送的数据,go func(){ BODY }()新建一个线程运行一个匿名函数。
  • 从channel读取数据,DATA := <-CHAN,和向channel传入数据相反,在数据输送箭头的右侧的是channel,形象地展现了数据从‘隧道’流出到变量里。
  • 通知主线程任务执行结束,doneChan的作用是为了让main routine等待这个刚起的goroutine结束,这里显示了channel的另一个特性,如果从empty channel中读取数据,则会阻塞当前routine,直到有数据可以读取。

上面这个程序就是main routine向另一个routine发送了3条int类型的数据,当3条数据被接收到后,主线程也从阻塞状态恢复运行,随后结束。

2. 不要陷入“死锁”

我一开始用channel的时候有报过*”fatal error: all goroutines are asleep - deadlock! “*的错误,真实的代码是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"

func main() {
ch := make(chan int)
ch <- 1 // I'm blocked because there is no channel read yet.
fmt.Println("send")
go func() {
<-ch // I will never be called for the main routine is blocked!
fmt.Println("received")
}()
fmt.Println("over")
}

我的本意是从main routine发送给另一个routine一个int型的数据,但是运行出了上述的错误,原因有2个:

  • 当前routine向channel发送/接收数据时,如果另一端没有相应地接收/发送,那么当前routine则会进行休眠。
  • 这个程序的main routine先行在ch <- 1进入休眠状态,程序的余下部分根本来不及运行,那么channel里的数据永远不会被读出,也就不能唤醒main routine,进入“死锁”。

解决这个“死锁”的方法可是是设置channel的容量大小大于1,那么channel就不会因为数据输入而阻塞主程; 或者将数据输入channel的语句置于启动新的goroutine之后。

3. channel作为状态转移的信号源

我跟着MIT的分布式计算课程做了原型为Map-Reduce的课后练习,目的是实现一个Master向Worker分派任务的功能:Master服务器去等待Worker服务器连接注册,Master先将Map任务和Reduce任务分派给这些注册Worker,等待Map任务全部完成,然后将Reduce任务再分配出去,等待全部完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Initialize a channel which records the process of the map jobs.
mapJobChannel := make(chan string)

// Start a goroutine to send the nMap(the number of the Map jobs) tasks to the main routine.
go func() {
for m := 0; m < nMap; m++ {
// Send the "start a Map job <m>" to the receiver.
mapJobChannel <- "start, " + strconv.Itoa(m)
}
}()

// Main routine listens on this mapJobChannel for the Map job task information.
nMapJobs := nMap

// Process the nMap Map tasks util they're all done.
for nMapJobs > 0 {
// Receive the Map tasks from the channel.
taskInfo := strings.Split(<-mapJobChannel, ",")
state, mapNo := taskInfo[0], taskInfo[1]

if state == "start" {
// Assign a Map task with number mapNo to a worker.
go func() {
// Wait for a worker to finish the Map task.
ok, workNo := assignJobToWorker("Map", mapNo)
if ok {
// Send a task finish signal and set the worker's state to idle.
mapJobChannel <- "end, " + mapNo
setWorkerState(workNo, "idle")
} else {
// Restart this task and set the worker's state to finished.
mapJobChannel <- "start, " + mapNo
setWorkerState(workNo, "finished")
}
}()
} else {
nMapJobs--
}
}

以上是我截取自己写的代码,关于用channel来传递当前Map任务的进度信息,用类似信号的方式标注当前的任务执行状态。

  • 当从channel中读取到”start, {NUM}”时找一个空闲的Worker去执行Map任务,并且等待它的完成,完成成功则向channel中发送”end, {NUM}”信号,表明任务完成,如果失败,就重发”start, {NUM}”信号。
  • 从channel中读取到”end, {NUM}”时,把剩余任务数减1。
    这种信号触发的方式,触发Master的状态转移,并且可以通过增加信号以及信号处理的方式,拓展业务处理的情况,暂时还能处理这个需求情景。

代码文件github地址
MIT分布式系统课程

笔试面试带来什么?

最早的笔试是大二下半学期的时候支付宝的校园招聘,那时候自己刚看完csapp,终于给了自己一个基本计算机概念,没那么不知所措了。。

被marine鼓励了一下,参加了这个笔试,这笔试考的是linux使用的一些基础知识,比如指令,shell脚本的一些编写,还有一些IQ问题、过了几天,alipay让我去面试,人生的第一次面试就这么诞生了,犹记得在那晚等着电话的我,单曲循环曹方姐姐的门这首歌,在微冷的租的小房间里突然响起铃声,激动地拿起手机,听到的是hr姐姐的悦耳的声音,就算在现在我始终觉得这声音是听过的hr的最好听的了、 面试那天,傻乎乎地给了妈妈和奶奶打了电话,也算分享我当时激动的心情!面试的前奏是紧张的,可是反而在看见那个面试的哥哥的时候,我没那么紧张了,可能是因为他胖胖的,没那么严肃,给我一点放松的心情,他问了我什么知识,比较懂,我说我看过csapp,所以计算机的基础知识,比较扎实,我了个擦,他直接问我中断有哪几种,我当时直接懵了,我没有刻意地去记忆这些东西,他突然发问,我就呆了,毕竟这些知识和code的多少有关系,我当时总共学习一年,code量少,确实被开始就打击了!中断的具体种类在我之后关心操作系统的实现的时候,曾经关注过,可是我现在还是记不太住,可能对我来说,我的缓存确实比较小,不能存到不经常用的内容!像什么缺页中断,是由内存缺页引起的,我会记得比较牢、 被灭了1盏灯之后,他问了两道算法题,现在具体什么内容记不清了,反正是炸鸡了! 人生中第一次收到回家等消息吧、、第一次回学校认真地等待着消息,印证了第一次的默拒、

大三下,暑期实习的机会我没有特别积极,原因是大三这年的精神状态极差,有一段时间处于讨厌学校的情绪中,陷入自我怪圈!干的事情确实比较少,所以在暑假的时候想自己写点代码,于是有了scheme解释器的小项目,不写不知道,写了才知道,代码功力确实不够,在写的时间里,会出现一个个在看书出现不了的问题,比如,多文件之间全局变量的引用,static修饰符作为修饰变量的可见性,出现文件互相依赖的时候,如何解决?如何使用函数指针? 如何编写Makefile ?这些只有在实践中才会遇见的有意思的问题,慢慢被自己解决,慢慢终于自己的解释器,初见轮廓,这个暑假确实是对我有意义,督促我不能光看不练,只动嘴皮子、

今年10月份的秋季校园招聘,如火如荼地开展,我发送了许多的简历出去,几乎所有的大公司都投过,结果也比较美好,ms,google,amazon,oracle,tencent,alibaba,baidu…都给我笔试机会,这些笔试,我只过了alibaba一家,除了baidu没去,其他全都没过!baidu没去的原因,是我本身对baidu没太大热情,并且次次下沙到玉泉的公交让我有种崩溃的感觉,我非常讨厌坐车,因为下沙到玉泉要坐1.5-2小时,到了玉泉,人都傻了,记得腾讯那天是早上9点笔试,我6:30和djh出发,中途坐车时肚子突然闹了一下,非常尴尬地到处在街上找公共厕所,问了个门卫大叔,问有没有厕所,他很强硬地回了门卫哪有厕所,我真是脑子都炸了,那他们不用解决吗? 幸好忍到了厕所的拯救、真是记忆犹新!结果考了1个小时,就出来了,甚至觉得题目太简单,自信满满的觉得妥了,结果我是想多了的节奏、ms的题目设置是不能蒙,宁愿不答也不要蒙,可是我真是自己不会的软件工程题目,自己手贱蒙了个答案上去,乱七八糟的,甚至有道题我觉得题目错了,都是乱蒙,结果确实是题目错了、真是非常缺乏自信,太慌了、 这些公司的笔试,难度正常,但是你要有良好的心理素质、我觉得我的问题是考试经历少,心理素质差!

对自己的表现非常失望,基本上是自己放弃了这些公司的,很大原因是自信心不足,没有一往无前的决心,怨自己!!

但是当我lose the chances的时候我才发现,我的机会越来越少,在第二波校园招聘来临的时候,我只能硬着头皮把简历投到自己连了解都不了解的公司,这些公司在我的学校里有招聘会,我去了4家做笔试,第一家简历被拒,我没想通、 第二家笔试过后被拒,自觉笔试挺好的、第三家第四家同第二家,我甚至怀疑我是不是被加入了这些公司的黑名单、、

知乎上有个人发了条message说”我baidu笔试被刷了,我觉得人生灰暗。。求开导” ,我非常蛋疼地已自己的经历来鼓励他,甚至有些自嘲,他没放在心上,以为是我的笑谈、

可是我并没有放弃,我仍在找寻着机会,marine帮我找了很多信息,让我去尝试,鼓励我说,我可以的!于是我尝试了知乎,知乎的jobs网页做的非常酷,网页上的各职位需求用该职位所需的技术来作为媒介,显示需求什么样的人,我当时就很兴奋,觉得cool!于是投了简历,知乎的哥哥很快给了回应,并且给了我两个笔试题,一道是实现server监视特定端口的用户发言请求,请求是json格式,你要做的是判断它是否是垃圾发言,1分钟内不能多余10条,不能发10条重复的发言!

我花了几天,实现了一个简易的server和client测试程序,我很蛋疼地用c实现,代码量还是挺大的,不过确实server的一些知识,重新复习了一下,比如,套接字如何稳定地读写,因为read,write是不阻塞的,所以你必须通过检查返回值来判断是否成功的读写! 而且read,write返回值为-1时,还需判断是否是被中断引起的、

等我交了之后,那个哥哥下午给我个电话,和我进行了一次电话面试,一开始就劈头盖脸地问我,为什么用K&R写法,问我对它有多少了解,这种写法是一时兴起,我就如实说了,他反问了一句笔试觉得好玩吗? 额。。我真的被震住了,太强势了,我当时非常怯懦地说没想清楚,我错了、 然后的电话就已他问我一些关于c的问题,和我还有别的技术兴趣等,可是我被开始K&R问题弄得有点凌乱,似乎我觉得有股火在烧!是的,感觉很不爽,但是觉得自己没想清楚是有点虚,所以有种有火无处发的感觉!期间很插曲地手机没电了,关了5分钟机,我觉得炸鸡了、、

补完剩下的电话之后,我有种虚脱的感觉,和marine谈论自己对于那个哥哥对我K&R写法的强烈打击,marine的话当时就给了我一道灵光!我瞬间觉得人生充满了光明!他说,”我喜欢”是一个非常好的理由,K&R是c语言最早的一代的写法,那时候混沌初开,但是充满了黑客的精神,以前的开源项目都是K&R写法,我这么写首先是喜欢开源文化,黑客文化,其次那么多人那么写,我想和别人不一样,我想要不一样的写法,我想做一个cool的人,程序员不就是要追求cool,不就是要炫技吗,编译器支不支持是另一回事,这表明我的一种态度!

我非常赞同他说的话,好像说出了心声,但是既喜且悲,悲的是因为自己和人的打交道太少,自己又怯懦,不能强势地表达出自己的想法,我觉得他们不要我也是情理之中,一个不敢说出自己意见的人,怎么配说自己是程序员哪!我顿时有种自惭、不过也为我发现自己这个巨大的缺失而高兴!

突然面试的机会又来了,以前投过ibm,前两天ibm跟我说要给我视频面试,我很高兴,又有机会来找到自己的不足!ibm的哥哥人很好,问了我一下面试的基础问题,问了我一下和团队合作时,对队友的一些看法,最后的时候还给我了建议:1. 下次面试让室友知道,别让他们光着膀子乱走! 2. 准备自我介绍  3. 加强表达能力、 三个建议很中肯!很感谢他!

程序水平对我反而是其次了,和人的沟通才是重点,毕竟合作是大项目的基础!!!

生活仍在继续—改善—自信—才能获取生存的幸福感!!!

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×