面试问题记录

图片记忆

虚函数表存放在哪里

C语言里int类型到底为多长?

分布式系列文章——Paxos算法原理与推导

C++11 之 lambda函数的详细使用

(1)[var] 表示值传递方式捕捉变量var
(2)[=] 表示值传递方式捕捉所有父作用域的变量(包括this)
(3)[&var] 表示引用传递捕捉变量var
(4)[&] 表示引用传递捕捉所有父作用域的变量(包括this)

k8s k3s kube


边缘存储的发展现状与挑战

一文看懂Go语言协程的设计与原理



后端面试之系统设计 - 用户密码如何储存在DB里

LevelDB的版本控制

1
2
3
4
5
6
7
8
9
10
struct FileMetaData {
int refs; // 当前有多少个version引用了这个sst文件
int allowed_seeks; // 在执行compact之前还允许seek多少次
uint64_t number; // 序列号,也就是sst文件的编号
uint64_t file_size; // sst文件的体积
InternalKey smallest; // 该sst文件记录的最小key
InternalKey largest; // 该sst文件记录的最大key

FileMetaData() : refs(0), allowed_seeks(1 << 30), file_size(0) { }
};




STL介绍以及其底层实现?包括vector, map, list

vector和list的区别

  1. vector底层实现是数组;list是双向链表。
  2. vector支持随机访问,list不支持。
  3. vector是顺序存储,list不是。
  4. vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
  5. vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

Linux—自旋锁spinlock、信号量semaphore、互斥锁mutex介绍及各自对应使用场景

mutex 只能在进程上下文中使用,谁给 mutex 加锁,就只能由谁来解锁。semaphore 的锁定与释放,并不限定为同一个进程。

linux文件系统基础–VFS中的file、dentry和inode–讲得非常透的一篇文章

二维码的原理是什么?


Linux内核块设备IO和buffer_head


Block multi-queue 架构解析(二)流程与机制

shell命令

chatgpt网站:poe.com

要找到Java进程的内存资源前十名

1
ps aux | grep java | sort -k 4 -nr | head -n 10
  • ps aux:列出当前系统上所有的进程信息。
  • grep java:从所有进程信息中筛选出包含”java”关键字的行,即Java进程。
  • sort -k 4 -nr:根据第四列(内存占用)进行逆序排序。
  • head -n 10:只显示前十行,即前十个占用内存最多的Java进程。

close_wait time_wait问题

close_wait

close_wait的危害在于,在一个进程上打开的文件描述符超过一定数量,(在linux上默认是1024,可修改),新来的socket连接就无法建立了,因为每个socket连接也算是一个文件描述符。

会报:Too many open files

1 服务端调用大数据,大数据关闭连接,(发起fin,服务器回ack)。此时,因为代码不严谨,服务端没有再向大数据发起close请求,

2 服务端接口耗时较长,客户端主动断开了连接,此时,服务端就会出现 close_wait。

服务器出现大量close_wait,我们来说说到底是怎么回事

tcp连接出现close_wait状态?可能是代码不够健壮

time_wait

TIME_WAIT状态存在的必要性。虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,比如丢包或者延迟到达,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文,并保证于此。

简单说timewait之所以等待2MSL的时长,是为了避免因为网络丢包或者网络延迟而造成的tcp传输不可靠,而这个time_wait状态则可以最大限度的提升网络传输的可靠性。

TIME_WAIT状态过多的危害

TIME_WAIT状态是TCP链接中正常产生的一个状态,但凡事都有利弊,TIME_WAIT状态过多会存在以下的问题:

(1)在socket的TIME_WAIT状态结束之前,该socket所占用的本地端口号将一直无法释放。这也是文章开头的提到问题的一个原因之一。

(2)在高并发(每秒几万qps)并且采用短连接方式进行交互的系统中运行一段时间后,系统中就会存在大量的time_wait状态,如果time_wait状态把系统所有可用端口 都占完了且尚未被系统回收时,就会出现无法向服务端创建新的socket连接的情况。此时系统几乎停转,任何链接都不能建立。

(3)大量的time_wait状态也会系统一定的fd,内存和cpu资源,当然这个量一般比较小,并不是主要危害

如何优化TIME_WAIT过多的问题

方式一:调整系统内核参数

方式二:调整短链接为长链接

短连接和长连接工作方式的区别:

短连接 连接->传输数据->关闭连接 HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。 也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。

长连接 连接->传输数据->保持连接 -> 传输数据-> 。。。->关闭连接。 长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。

从区别上可以看出,长连接比短连接从根本上减少了关闭连接的次数,减少了TIME_WAIT状态的产生数量,在高并发的系统中,这种方式的改动非常有效果,可以明显减少系统TIME_WAIT的数量。

如何优化高并发TCP链接中产生的大量的TIME_WAIT的状态

右值引用

详解C++右值引用
左值是一个指向某内存空间的表达式,并且我们可以用&操作符获得该内存空间的地址。右值就是非左值的表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<class T>
void swap(T& a, T& b)
{
T tmp(a);
a = b;
b = tmp;
}

X a, b;
swap(a, b);

template<class T>
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}

X a, b;
swap(a, b);

右值引用类型既可以被当作左值也可以被当作右值,判断的标准是,如果它有名字,那就是左值,否则就是右值。
std::move(x)的类型是右值引用,而且它也没有名字,所以它是个右值。因此std::move(x)正是通过隐藏名字的方式把它的参数变为右值。

天池比赛中使用的右值引用

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
bool LocalEngineEntity::write(const std::string &key, const std::string &value, bool use_aes) {

// 区分加密与非加密
std::string encrypt_value;
if (use_aes) {
m_engine_->encrypted(value, encrypt_value);
}

index_t index;
if (use_aes) {
index = m_cur_page_->insert(key, std::move(encrypt_value));
} else {
index = m_cur_page_->insert(key, value);
}
return true;
}

index_t insert(const std::string &key, const std::string &value) {
m_cur_size_ += 16 + value.size(); // 加上key的长度
m_key_[m_cur_index_] = key;
m_value_[m_cur_index_] = value;
return m_cur_index_++; // 返回当前索引并加一
}

index_t insert(const std::string &key, std::string &&value) {
m_cur_size_ += 16 + value.size(); // 加上key的长度
m_key_[m_cur_index_] = key;
m_value_[m_cur_index_] = std::move(value);
return m_cur_index_++; // 返回当前索引并加一
}

报文头部

LwIP 协议栈之 udp 协议解析

UDP报文头部:8字节
伪首部完全是虚拟的,它并不会和用户数据报一起被发送出去,只是在校验和的计算过程中会被使用到,伪首部主要来自于运载 UDP 报文的 IP 数据报首部,将源 IP 地址和目的 IP 地址加入到校验和的计算中可以验证用户数据报是否已经到达正确的终点。

UDP报文理论最大长度:(IP报文长度为16位)65536- 1 - 8(UDP头部) - 20(IP头部) = 65507字节
在下层数据链路层最大传输单元是1500字节的情况下,要想IP层不分包,那么UDP数据包的最大大小应该是1500字节 – IP头(20字节) – UDP头(8字节) = 1472字节。不过鉴于Internet上的标准MTU值为576字节,所以建议在进行Internet的UDP编程时,最好将UDP的数据长度控制在 (576-8-20)548字节以内

IP分片

分片:就是当一个skb包长度大于传输设备或者链路上物理设备的mtu时,会根据一定的方式进行切割,从而使报文得以发送出去。但是这里需要说明,分片又分为IP和TCP分片两种,由于tcp报文有自己的机制去分片,不需要依赖IP层分片;而对于udp或者icmp等报文,只能依赖IP层去分片。

分片与重组关系:IP协议理论上允许的最大IP数据报为65535字节(16位来表示包总长)。但是因为协议栈网络层下面的数据链路层一般允许的帧长远远小于这个值,例如以太网的MTU(即Maximum Transmission Unit,最大传输单元)通常在1500字节左右。假如我们需要传输一个字节为4600的数据,则我们需要将其分为四片。所以较大的IP数据包会被分片传递给数据链路层发送,分片的IP数据报可能会以不同的路径传输到接收主机,接收主机通过一系列的重组,将其还原为一个完整的IP数据报,再提交给上层协议处理。

identification(标识符): 唯一的标识主机发送的报文。 通常与标记字段和分片偏移字段一起用于IP报文的分片。 IP软件在储存器中维护一个计数器,每产生一个数据报,计数器就+1,并将此值赋值给标识字段。

MAC帧格式、IPV4数据报格式、TCP报文格式、UDP数据报格式

18字节报文首部与尾部

c++与go对比

Golang和C++简单对比

goroutine原理分析

实时操作系统

Linux和实时操作系统的疑问及解答

RTOS的实时性通过哪些措施来实现?

分布式块存储如何使用raft

基于 Raft 构建的大规模分布式存储


实践案例丨基于Raft协议的分布式数据库系统应用

grpc+protobuf

在客户端层面进行批处理与缓存

MVCC

检错码纠错码纠删码

检错纠错码(奇偶校验码 CRC循环冗余校验码 海明码)



xp的分布式系统系列教程之: Erasure-Code: 工作原理, 数学解释, 实践和分析.



linux 网络

如何选择Linux内核网络协议栈相关资料?




主要模块:
套件字和虚拟文件系统(Socket/VFS)
路由子系统(IP Routing)
报文过滤和防火墙模块(Netfilter,IPTable和Connection Tracking)
邻居子系统(ARP和Neighbor Discovery)
流量控制模块(Quality of Service)








智能指针是线程安全的吗?

c++ 11 的shared_ptr多线程安全?
C++ 智能指针线程安全的问题
C++ STL容器如何解决线程安全的问题?


shared_ptr实现细节

线程安全:
1 引用计数修改 线程安全
To satisfy thread safety requirements, the reference counters are typically incremented using an equivalent of std::atomic::fetch_add with std::memory_order_relaxed (decrementing requires stronger ordering to safely destroy the control block).

2 修改指向时是否是线程安全
同一个shared_ptr不安全,不是同一个shared_ptr的对象安全

3 shared_ptr所管理数据的线程安全性不由shared_ptr保证

leveldb锁机制

深入浅出leveldb之基础知识
深入浅出leveldb之MemTable
深入浅出leveldb之高性能中锁的使用

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
// 前方高能,请读者注意
{
// 此处解锁了,也就意味着其他线程可以同时读取或者写入leveldb,写入肯定会被放到writers_队尾等待
// 那读取该如何保证线程安全性,解锁后毕竟还是要操作MemTable的,这个后面章节会有详细说明
// 这里要说明的是,因为写入操作要有文件写入,必要时还有文件同步,如果不解锁性能肯定很低
mutex_.Unlock();
// log_可以想象为一个文件,AddRecord可以想象为文件的追加(append)操作,就是所有记录是先追加到日志文件
// 因为写入的这个流程设计永远都保证一个线程操作这个日志文件,所以不会出现乱序问题,而读取是访问MemTable
// 所以不需要在加锁状态先执行
status = log_->AddRecord(WriteBatchInternal::Contents(updates));
// 如果用户配置了同步选项,那么就要执行文件同步操作,可以想象为flush(),这样比较安全,但是性能会第一点
bool sync_error = false;
if (status.ok() && options.sync) {
status = logfile_->Sync();
if (!status.ok()) {
sync_error = true;
}
}
// 写入日志文件成功,那么就要把数据同步到MemTable中了
if (status.ok()) {
status = WriteBatchInternal::InsertInto(updates, mem_);
}
// 耗时的操作都完事了,重新进入加锁状态,后面要操作共享资源
mutex_.Lock();
// 如果上面的过程失败了,那么就要唤醒所有的线程报错
if (sync_error) {
RecordBackgroundError(status);
}
}

1 通过std::deque实现写入线程按照调用时间先后顺序执行,这就保证了写入操作的线程安全性;
2 实际写入是在无锁状体下执行的,意味着执行文件和MemTable写入的同时可以读取和继续向std::deque中追加,这样的设计就为合并多次写入操作提供了条件;比如在写第一条数据的时候因为在加锁状态下无法合并第二条及以后的数据(因为此时线程因为锁而挂起),但是第一条数据写入文件过程中第二、三、四……条数据已经在std::deque中排队了,那么开始写入第二条数据的时候就可以合并第三、四….条数据了;
3 因为数据的合并写入,使得写入性能相比于逐条写入提升不少,尤其是在有同步选项的时候;

1
2
3
4
5
6
7
x = NewNode(key, height);
for (int i = 0; i < height; i++) {
// NoBarrier_SetNext() suffices since we will add a barrier when
// we publish a pointer to "x" in prev[i].
x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
prev[i]->SetNext(i, x);
}

Memory Order浅析(以LevelDB中的跳表为例)

write系统调用流程

《Linux内核源代码情景分析》文件读与写摘录

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*
* Keep mostly read-only and often accessed (especially for
* the RCU path lookup and 'stat' data) fields at the beginning
* of the 'struct inode'
*/
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;

const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;

} __randomize_layout;

struct address_space {
struct inode *host; /* owner: inode, block_device */
struct radix_tree_root i_pages; /* cached pages */
atomic_t i_mmap_writable;/* count VM_SHARED mappings */
struct rb_root_cached i_mmap; /* tree of private and shared mappings */
struct rw_semaphore i_mmap_rwsem; /* protect tree, count, list */
/* Protected by the i_pages lock */
unsigned long nrpages; /* number of total pages */
/* number of shadow or DAX exceptional entries */
unsigned long nrexceptional;
pgoff_t writeback_index;/* writeback starts here */
const struct address_space_operations *a_ops; /* methods */
unsigned long flags; /* error bits */
spinlock_t private_lock; /* for use by the address_space */
gfp_t gfp_mask; /* implicit gfp mask for allocations */
struct list_head private_list; /* for use by the address_space */
void *private_data; /* ditto */
errseq_t wb_err;
} __attribute__((aligned(sizeof(long)))) __randomize_layout;

/*
* Historically, a buffer_head was used to map a single block
* within a page, and of course as the unit of I/O through the
* filesystem and block layers. Nowadays the basic I/O unit
* is the bio, and buffer_heads are used for extracting block
* mappings (via a get_block_t call), for tracking state within
* a page (via a page_mapping) and for wrapping bio submission
* for backward compatibility reasons (e.g. submit_bh).
*/
struct buffer_head {
unsigned long b_state; /* buffer state bitmap (see above) */
struct buffer_head *b_this_page;/* circular list of page's buffers */
struct page *b_page; /* the page this bh is mapped to */

sector_t b_blocknr; /* start block number */
size_t b_size; /* size of mapping */
char *b_data; /* pointer to data within the page */

struct block_device *b_bdev;
bh_end_io_t *b_end_io; /* I/O completion */
void *b_private; /* reserved for b_end_io */
struct list_head b_assoc_buffers; /* associated with another mapping */
struct address_space *b_assoc_map; /* mapping this buffer is
associated with */
atomic_t b_count; /* users using this buffer_head */
};


之前汇报的PPT






进程线程协程

聊聊Linux中线程和进程的联系与区别!
Linux进程是如何创建出来的?

对于内核任务来说,无论有多少个任务,其使用地址空间都是同一个。所以一般都叫内核线程,而不是内核进程。

协程的定义是什么?

子程序,或者称为函数,在所有语言中都是层级调用。子程序调用是通过栈实现的,一个线程同一时间就是执行一个子程序。
协程本质上也是子程序,协程作为子程序最大的特征是可中断可恢复。

消息队列多生产者多消费者锁优化

Linux 内核:RCU机制与使用

RCU(Read-Copy Update),是 Linux 中比较重要的一种同步机制。顾名思义就是“读,拷贝更新”,再直白点是“随意读,但更新数据的时候,需要先复制一份副本,在副本上完成修改,再一次性地替换旧数据”。这是 Linux 内核实现的一种针对“读多写少”的共享数据的同步机制。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static enum audit_state audit_filter_task(struct task_struct *tsk)
{

struct audit_entry *e;
enum audit_state state;

rcu_read_lock();

/* Note: audit_netlink_sem held by caller. */

list_for_each_entry_rcu(e, &audit_tsklist, list) {
if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
rcu_read_unlock();
return state;
}
}

rcu_read_unlock();
return AUDIT_BUILD_CONTEXT;
}


static inline int audit_del_rule(struct audit_rule *rule,
struct list_head *list)
{

struct audit_entry *e;

/* Do not use the _rcu iterator here, since this is the only
* deletion routine. */

list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
list_del_rcu(&e->list);
call_rcu(&e->rcu, audit_free_rule, e);
return 0;
}
}

return -EFAULT; /* No matching rule */
}

static inline int audit_add_rule(struct audit_entry *entry,
struct list_head *list)
{

if (entry->rule.flags & AUDIT_PREPEND) {
entry->rule.flags &= ~AUDIT_PREPEND;
list_add_rcu(&entry->list, list);
} else {
list_add_tail_rcu(&entry->list, list);
}

return 0;

}

网络编程相关知识学习

数据库索引优化学习

iostat介绍

【Linux系列-2】iostat命令详解
iostat命令是Linux系统上查看I/O性能最基本的工具,其全称为 I/O statistics。iostat能统计磁盘活动情况,也能统计CPU使用情况。
iostat有以下缺陷:

  • iostat的输出结果大多数是一段时间内的平均值,因此难以反映峰值情况
  • iostat仅能对系统整体情况进行分析汇报,却不能针对某个进程进行深入分析。
  • iostat未单独统计IO处理信息,而是将IO处理时间和IO等待时间合并统计,因此包括await在内的指标并不能非常准确地衡量磁盘性能表现。

用户程序崩溃转储

一文读懂 | coredump文件是如何生成的

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
int do_coredump(long signr, int exit_code, struct pt_regs *regs)
{
char corename[CORENAME_MAX_SIZE + 1];
struct mm_struct *mm = current->mm;
struct linux_binfmt *binfmt;
struct inode *inode;
struct file *file;
int retval = 0;
int fsuid = current->fsuid;
int flag = 0;
int ispipe = 0;

binfmt = current->binfmt; // 当前进程所使用的可执行文件格式(如ELF格式)

...
// 1. 判断当前进程可生成的 coredump 文件大小是否受到资源限制
if (current->signal->rlim[RLIMIT_CORE].rlim_cur < binfmt->min_coredump)
goto fail_unlock;

...
// 2. 生成 coredump 文件名
ispipe = format_corename(corename, core_pattern, signr);

...
// 3. 创建 coredump 文件
file = filp_open(corename, O_CREAT|2|O_NOFOLLOW|O_LARGEFILE|flag, 0600);

...
// 4. 把进程的内存信息写入到 coredump 文件中
retval = binfmt->core_dump(signr, regs, file);

fail_unlock:
...
return retval;
}

core-dump文件未生成原因

root用户 ulimit限制 kernel.core_pattern对应目录没有创建
内核转储的设置

Linux 中信号是一种异步事件处理的机制,每种信号都有其对应的默认操作,你可以在 signal(7) 查看 Linux 系统提供的信号以及默认处理。
默认操作主要包括:终止进程(Term)、忽略该信号(Ing)、终止进程并发生核心转储(Core)、暂停进程(Stop)、继续运行被暂停的进程(Cont)。

io_uring介绍

图解原理|Linux I/O 神器之 io_uring


使用条件变量与互斥锁实现信号量

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
40
41
42
43
44
45
#include <bits/stdc++.h>
#include <unistd.h>
using namespace std;

class Sem {
public:
Sem(int count) : count_(count) {}
~Sem() { unique_lock<mutex> lk(mu_); }
void Down() {
unique_lock<mutex> lk(mu_);
count_--;
if (count_ < 0) {
cv_.wait(lk);
}
}
void Up() {
unique_lock<mutex> lk(mu_);
count_++;
if (count_ <= 0) {
cv_.notify_one();
}
}

private:
mutex mu_;
condition_variable cv_;
int count_;
};
int main() {
Sem sem(0);
auto fun1 = [&]() {
cout << "call down op start" << endl;
sem.Down();
cout << "call down op end" << endl;
};
auto fun2 = [&]() {
sleep(10);
sem.Up();
};
thread t1(fun1);
thread t2(fun2);
t1.join();
t2.join();
}

使用互斥锁与条件变量实现读写锁

cmu15445 读写锁实现

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* Reader-Writer latch backed by std::mutex.
*/
class ReaderWriterLatch {
using mutex_t = std::mutex;
using cond_t = std::condition_variable;
static const uint32_t MAX_READERS = UINT_MAX;

public:
ReaderWriterLatch() = default;
~ReaderWriterLatch() { std::lock_guard<mutex_t> guard(mutex_); }

DISALLOW_COPY(ReaderWriterLatch);

/**
* Acquire a write latch.
*/
void WLock() {
std::unique_lock<mutex_t> latch(mutex_);
while (writer_entered_) {
reader_.wait(latch);
}
writer_entered_ = true;
while (reader_count_ > 0) {
writer_.wait(latch);
}
}

/**
* Release a write latch.
*/
void WUnlock() {
std::lock_guard<mutex_t> guard(mutex_);
writer_entered_ = false;
reader_.notify_all();
}

/**
* Acquire a read latch.
*/
void RLock() {
std::unique_lock<mutex_t> latch(mutex_);
while (writer_entered_ || reader_count_ == MAX_READERS) {
reader_.wait(latch);
}
reader_count_++;
}

/**
* Release a read latch.
*/
void RUnlock() {
std::lock_guard<mutex_t> guard(mutex_);
reader_count_--;
if (writer_entered_) {
if (reader_count_ == 0) {
writer_.notify_one();
}
} else {
if (reader_count_ == MAX_READERS - 1) {
reader_.notify_one();
}
}
}

private:
mutex_t mutex_;
cond_t writer_;
cond_t reader_;
uint32_t reader_count_{0};
bool writer_entered_{false};
};

我的简单实现

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <bits/stdc++.h>
#include <unistd.h>
using namespace std;

const int kMaxReader = 5;
class RWLock {
public:
RWLock() = default;
~RWLock() { lock_guard<mutex> lk(mu_); }

void WLock() {
unique_lock<mutex> lk(mu_);
while (writer_enter_) {
cv_.wait(lk);
}
writer_enter_ = true;
while (reader_count_ > 0) {
cv_.wait(lk);
}
}
void WUnlock() {
unique_lock<mutex> lk(mu_);
writer_enter_ = false;
cv_.notify_all();
}
void RLock() {
unique_lock<mutex> lk(mu_);
while (writer_enter_ || reader_count_ >= kMaxReader) {
cv_.wait(lk);
}
reader_count_++;
}
void RUnlock() {
unique_lock<mutex> lk(mu_);
reader_count_--;
cv_.notify_all();
}

private:
mutex mu_;
condition_variable cv_;
bool writer_enter_{false};
int reader_count_{0};
};

int main() {
RWLock lock;
auto reader = [&](int i) {
lock.RLock();
printf("%d reader\n", i);
sleep(1);
lock.RUnlock();
};
auto writer = [&](int i) {
lock.WLock();
printf("%d writer\n", i);
sleep(1);
lock.WUnlock();
};
vector<thread> workers;
for (int i = 0; i < 10; i++) {
workers.emplace_back(thread(reader, i));
}
for (int i = 0; i < 10; i++) {
workers.emplace_back(thread(writer, i));
}
for (auto& worker : workers) {
worker.join();
}
}

跳表之所以高效的原因在哪里?怎样做到越上层的节点越稀疏?


跳表实际为一种多层的有序链表,跳表的每一层都为一个有序链表,且满足每个位于第 i 层的节点有 p的概率出现在第 i+1 层,其中 p为常数。

相关代码

1
2
3
4
5
6
7
int get_random_level() {
int level = 1;
while (level < kMaxLevel && rand() % 100 < kProb) {
level++;
}
return level;
}

memtable转换成sstable过程,相同key的合并在哪里发生的?


memtable to sstable

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
/*
MakeRoomForWrite
MaybeScheduleCompaction
BGWork
BackgroundCall
BackgroundCompaction
CompactMemTable
WriteLevel0Table
BuildTable
*/
// 省略部分代码
Status BuildTable(const std::string& dbname, Env* env, const Options& options,
TableCache* table_cache, Iterator* iter, FileMetaData* meta) {
Status s;
meta->file_size = 0;
iter->SeekToFirst();

std::string fname = TableFileName(dbname, meta->number);
WritableFile* file;
s = env->NewWritableFile(fname, &file);

TableBuilder* builder = new TableBuilder(options, file);
meta->smallest.DecodeFrom(iter->key());
Slice key;
// 将跳表各个节点添加到sst文件中,并没有相同key合并过程
for (; iter->Valid(); iter->Next()) {
key = iter->key();
builder->Add(key, iter->value());
}
if (!key.empty()) {
meta->largest.DecodeFrom(key);
}

// Finish and check for builder errors
s = builder->Finish();
return s;
}

sstable合并过程

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*
MakeRoomForWrite
MaybeScheduleCompaction
BGWork
BackgroundCall
BackgroundCompaction
DoCompactionWork
*/
// 省略大部分代码
Status DBImpl::DoCompactionWork(CompactionState* compact) {
input->SeekToFirst();
Status status;
ParsedInternalKey ikey;
std::string current_user_key;
bool has_current_user_key = false;
SequenceNumber last_sequence_for_key = kMaxSequenceNumber;
while (input->Valid()) {
// Handle key/value, add to state, etc.
bool drop = false;
ParseInternalKey(key, &ikey)
if (!has_current_user_key || user_comparator()->Compare(ikey.user_key, Slice(current_user_key)) != 0) {
// First occurrence of this user key
current_user_key.assign(ikey.user_key.data(), ikey.user_key.size());
has_current_user_key = true;
last_sequence_for_key = kMaxSequenceNumber;
}

if (last_sequence_for_key <= compact->smallest_snapshot) {
// Hidden by an newer entry for same user key
drop = true; // (A)
} else if (ikey.type == kTypeDeletion && ikey.sequence <= compact->smallest_snapshot && compact->compaction->IsBaseLevelForKey(ikey.user_key)) {
// For this user key:
// (1) there is no data in higher levels
// (2) data in lower levels will have larger sequence numbers
// (3) data in layers that are being compacted here and have
// smaller sequence numbers will be dropped in the next
// few iterations of this loop (by rule (A) above).
// Therefore this deletion marker is obsolete and can be dropped.
drop = true;
}
last_sequence_for_key = ikey.sequence;


if (!drop) {
// Open output file if necessary
if (compact->builder == nullptr) {
status = OpenCompactionOutputFile(compact);
if (!status.ok()) {
break;
}
}
if (compact->builder->NumEntries() == 0) {
compact->current_output()->smallest.DecodeFrom(key);
}
compact->current_output()->largest.DecodeFrom(key);
compact->builder->Add(key, input->value());

// Close output file if it is big enough
if (compact->builder->FileSize() >= compact->compaction->MaxOutputFileSize()) {
status = FinishCompactionOutputFile(compact, input);
if (!status.ok()) {
break;
}
}


input->Next();
}
return status;
}

首先,对于相同的key,序列号越大,排序越小,越靠前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int InternalKeyComparator::Compare(const Slice& akey, const Slice& bkey) const {
// Order by:
// increasing user key (according to user-supplied comparator)
// decreasing sequence number
// decreasing type (though sequence# should be enough to disambiguate)
int r = user_comparator_->Compare(ExtractUserKey(akey), ExtractUserKey(bkey)); // 先使用user key比较器进行比较
if (r == 0) { // 若user key相等则比较序列号
const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8);
const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8);
if (anum > bnum) { // 按序列号降序排序
r = -1;
} else if (anum < bnum) {
r = +1;
}
}
return r;
}

对于相同的key,序列号最大的最先出现,那么其后的key理应被它覆盖掉,但leveldb支持快照阅读(传入一个序列号,查找其前的key-value信息),故需保留最小快照号之后的相同key,对于删除key,将其删除的额外要求是其更高层没有相同key,要不然把删除标记删除了,但读操作在更高层找到了相同key,造成一个误读。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (last_sequence_for_key <= compact->smallest_snapshot) {
// Hidden by an newer entry for same user key
drop = true; // (A)
} else if (ikey.type == kTypeDeletion && ikey.sequence <= compact->smallest_snapshot && compact->compaction->IsBaseLevelForKey(ikey.user_key)) {
// For this user key:
// (1) there is no data in higher levels
// (2) data in lower levels will have larger sequence numbers
// (3) data in layers that are being compacted here and have
// smaller sequence numbers will be dropped in the next
// few iterations of this loop (by rule (A) above).
// Therefore this deletion marker is obsolete and can be dropped.
drop = true;
}

leveldb 合并过程笔记
DoCompactionWork
简单概括合并过程的执行就是:

  • 如果 imm_ 非空,则使 MemTable 落盘到 L0 并退出此轮合并
  • 对所有输入文件的 iterator 多路合并生成一个合并 iterator
  • 迭代此 iterator,过滤掉过期的键值,通过 TableBuilder 写入新的文件

过滤键值时需要考虑到多版本 Snapshot 的存在:

  • 如果遍历得到同一个 key 的多个版本,则过滤掉其中 sequence 号小于 smallest_snapshot (当前活跃的最早的 snapshot)的键值
  • 如果一个键被标记为删除,作为 tombstone 被清理时是要谨慎的,需要 IsBaseLevelForKey 判定更下层的没有重叠的 sst 才可以清理(这可能意味着往往需要在最下层才能跑到 tombstone 的清理),也需要等它的 sequence 号小于 smallest_snapshot 值才能删

LRU算法实现

在各个操作前加锁进行并发保护,使用find保证只进行一次遍历

PV拉起流程

用户创建一个PVC对象,声明存储资源声明,如果系统中存在与PVC需求相符合的PV,就将PVC和该PV绑定起来,若不存在则根据sc中指定的存储提供商创建对应大小的PV并与该PVC绑定。用户pod只需指定PVC与挂载点,而后k8s会将PVC绑定的PV挂载到对应pod中,最终挂载到特定容器中。

容器编排系统K8s之PV、PVC、SC资源

SC是StorageClass的缩写,表示存储类;这种资源主要用来对pv资源的自动供给提供接口;所谓自动供给是指用户无需手动创建pv,而是在创建pvc时对应pv会由persistentVolume-controller自动创建并完成pv和pvc的绑定;使用sc资源的前提是对应后端存储必须支持restfull类型接口的管理接口,并且pvc必须指定对应存储类名称来引用SC;简单讲SC资源就是用来为后端存储提供自动创建pv并关联对应pvc的接口;

面试前了解岗位的相关知识,针对性地准备

回表

什么是 MySQL 的“回表”?

img

  1. B-Tree 中,所有节点都会带有指向具体记录的指针;B+Tree 中只有叶子结点会带有指向具体记录的指针。

  2. B-Tree 中不同的叶子之间没有连在一起;B+Tree 中所有的叶子结点通过指针连接在一起。

  3. B-Tree 中可能在非叶子结点就拿到了指向具体记录的指针,搜索效率不稳定;B+Tree 中,一定要到叶子结点中才可以获取到具体记录的指针,搜索效率稳定。

  4. B+Tree 中,由于非叶子结点不带有指向具体记录的指针,所以非叶子结点中可以存储更多的索引项,这样就可以有效降低树的高度,进而提高搜索的效率。

  5. B+Tree 中,叶子结点通过指针连接在一起,这样如果有范围扫描的需求,那么实现起来将非常容易,而对于 B-Tree,范围扫描则需要不停的在叶子结点和非叶子结点之间移动。

16*1024/(8+6)=1170 存储主键(页号+偏移量)

B+Tree 的高度一般为 2-4 层,这就可以满足千万级的数据的存储

  • 主键索引的叶子结点存储的是一行完整的数据。
  • 非主键索引的叶子结点存储的则是主键值。

次搜索 B+Tree 拿到主键值后再去搜索主键索引的 B+Tree,这个过程就是所谓的回表。

如果查询的列本身就存在于索引中,那么即使使用二级索引,一样也是不需要回表的。

三大准则11个技巧,手把手教你创建数据库索引

准则1. 基于您的工作负载创建索引

准则2. 基于单个SQL的结构创建索引

  • 快速定位数据
  • 避免排序
  • 避免回表
  • 实现行级锁(MySQL,另文讨论)
  • 实现唯一性约束

最左前缀原则

最左前缀原则指的是,如果查询的时候等值的查询条件精确匹配索引的左边连续一列或几列,则此列就可以被用到,同时遇到范围查询(>、<、between、like)就会停止匹配,包括范围条件。

对于联合索引lineitem(l_shipdate,l_quantity),下面的SQL中前两个符合最左前缀原则,可以使用该索引。最后一个不满足最左前缀原则,无法使用该索引。

1
2
3
select * from lineitem where l_shipdate = date '2021-12-01' and l_quantity = 100; -- 可以使用索引
select * from lineitem where l_shipdate = date '2021-12-01'; -- 可以使用索引
select * from lineitem where l_quantity = 100; -- 不满足最左前缀原则,无法使用该索引

准则3. 创建索引时的约束条件

索引列取舍:通过对列的单值选择率的评估,在过滤效果最好的列上建立索引; 通过对工作负载的分析,避免在频繁更新的列上建立索引。
索引取舍:通过对工作负载的分析,在最重要的SQL或是使用频率最高的查询上提供索引。
索引合并:索引满足组最左前缀匹配原则,所以可以通过设计索引列的排列顺序,达到一个索引加速多个SQL的查询。
索引删除:通过命令或工具定期采集索引的使用情况,将不再使用的索引进行删除。

索引创建的过程可以抽象化为基于以上的约束条件,定义索引的收益,使用启发式算法,计算在满足特定约束条件下,整个工作负载收益最大的索引集合

死锁条件

互斥条件:一个资源每次只能被一个进程占用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不释放。
不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

进程通信方式

进程间通讯的7种方式

  1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  3. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  4. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  5. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
  7. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

Innodb

一文了解InnoDB存储引擎

黑马MySQL数据库InnoDB存储引擎教程,详解InnoDB引擎架构及原理

innodb官方文档

《mysql》之Innodb三大特性

【MySQL】Innodb三大特性之两次写(double write)

C++ 默认参数

C++默认参数

  1. 如果某个参数是默认参数,那么它后面的参数必须都是默认参数
  2. 默认参数可以放在函数声明或者定义中,但只能放在二者之(通常我们都将默认参数放在函数声明中,因为如果放在函数定义中,那么将只能在函数定义所在地文件中调用该函数。)
  3. 函数重载时谨慎使用默认参数值

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
40
41
42
// 堆排序
class Solution {
public:
void maxHeapify(vector<int>& nums, int i, int len) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
// 若子节点值大于父节点,找到较大者
if (left <= len && nums[left] > nums[largest]) {
largest = left;
}
if (right <= len && nums[right] > nums[largest]) {
largest = right;
}
if (largest != i) { // 与较大者交换,继续下沉操作
swap(nums[i], nums[largest]);
maxHeapify(nums, largest, len);
}
}
void buildMaxHeap(vector<int>& nums, int len) {
for (int i = len / 2; i >= 0; i--) { // 将父节点依次下沉
maxHeapify(nums, i, len);
}
}
void heapSort(vector<int>& nums) {
heap_size_ = nums.size() - 1;
buildMaxHeap(nums, heap_size_); // 建立大顶堆
for (int i = nums.size() - 1; i > 0; i--) {
swap(nums[i], nums[0]); // 将最大值移至末尾
heap_size_--;
maxHeapify(nums, 0, heap_size_); // 进行下沉操作,维持大顶堆性质
}
}
vector<int> sortArray(vector<int>& nums) {
heapSort(nums);
return nums;
}

private:
int heap_size_;
};

E-R图

ER图(实体关系图)怎么画?

img

1)矩形框:表示实体,在框中记入实体名。

2)菱形框:表示联系,在框中记入联系名。

3)椭圆形框:表示实体或联系的属性,将属性名记入框中。

4)连线:实体与属性之间;实体与联系之间;联系与属性之间用直线相连,并在直线上标注联系的类型。

友元

【C++】友元函数和友元类(作用及优缺点)

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 友元函数
#include <iostream>
using namespace std;

class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(){}
Date(int year, int month, int day): _year(year), _month(month), _day(day)
{}

private:
int _year;
int _month;
int _day;
};

ostream& operator<<(ostream& _cout, const Date& d)
{
_cout<<d._year<<"-"<<d._month<<"-"<<d._day;
return _cout;
}

istream& operator>>(istream& _cin,Date& d)
{
_cin>>d._year;
_cin>>d._month;
_cin>>d._day;
return _cin;
}

int main()
{
Date d;
cin>>d;
cout<<d<<endl;
return 0;
}

// 友元类
class Date; // 前置声明
class Time
{
friend class Date;
// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour, int minute, int second): _hour(hour), _minute(minute), _second(second)
{}

private:
int _hour;
int _minute;
int _second;
};

class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1): _year(year),
_month(month),_day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t.second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};

存储过程

存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*in 输入参数的存储过程 输入id查询的行*/
DELIMITER $$
CREATE PROCEDURE b(IN pr_id INT)
BEGIN
SELECT * FROM a WHERE id=pr_id;

END$$
DELIMITER $$

/*out 的存储过程的说法*/
DELIMITER $$
CREATE PROCEDURE c(OUT pr_name VARCHAR(20))
BEGIN

SELECT NAME INTO pr_name FROM a WHERE id=2;

END$$

DELIMITER $$

HTTP 报文

http报文内容

img

1
2
3
4
5
6
7
POST /user HTTP/1.1      //请求行
Host: www.user.com
Content-Type: application/x-www-form-urlencoded
Connection: Keep-Alive
User-agent: Mozilla/5.0. //以上是首部行
(此处必须有一空行) //空行分割header和请求内容
name=world 请求体
1
2
3
4
5
6
7
<status-line>   //状态行

<headers> //消息报头

<blank line> //空行

<response-body> //响应体

CPU调度方法

CPU 的工作原理是什么?
非常有趣的一系列回答,建议看一下

“假设我们已经造出来了这么个机器,造了好几个,我们这么连起来…诶纸不够大,我写不下了。”小明一听,赶紧从书桌膛里翻出来一本草稿纸,生怕同桌变成费马。

为了计算黑板上那道题,四人一共制作了32个一位全加器,将它们连接后,一个三十二位加法器便诞生了。他们历经艰辛,踩着自然规律和人类智慧的肩膀,把自己从枯燥的加法计算中解放出来!
窗外的蝉鸣渐渐平息,头顶的吊扇不再转动。“刘老师,答案是250999!”小明站了起来,声若洪钟大吕,震慑天地。他和小红、小刚、小兰分别对视了一眼,收获了坚定的目光–这目光,连同面前的32位加法器,如同新的转机和闪闪星斗,正在缀满没有遮拦的天空。

刘老师仍未停下,黑板快被写满了:
103. 方程ζ(s)=0的所有有意义的解都在一条直线上吗?
104. 大于2的偶数都可以写成两个质数的和吗?
……
134. 生命,宇宙及所有事物的答案?
刘老师放下了粉笔,半截粉笔已经变成硬币的厚度。“这些问题,我们能造个机器回答么?”小明撑着头,喃喃自语。


FIFO(First In First Out,先进先出)调度算法以原理简单,容易实现著称,它先调度首先到达的任务直至结束,然后再调度下一个任务,以此类推。

FIFO调度策略在任务运行时间差异较大的场景下,容易出现任务饿死的问题!

SJF(Shortest Job First,最短任务优先)从相同到达时间的多个任务中选取运行时长最短的一个任务进行调度,接着再调度第二短的任务,以此类推。

我们在协作式调度的SJF算法的基础上,加上抢占式调度算法,就演变成了STCF算法(Shortest Time-to-Completion First,最短时间完成优先),调度原理是当运行时长较短的任务到达时,中断当前的任务,优先调度运行时长较短的任务。

RR(Round Robin,轮训)算法给每个任务分配一个时间片,当任务的时间片用完之后,调度器会中断当前任务,切换到下一个任务,以此类推。

MLFQ(Multi-Level Feedback Queue,多级反馈队列)调度算法的目标如下:
1 优化周转时间。
2 降低交互类任务的响应时间,提升用户体验。

CFS并非以优化周转时间和响应时间为目标,而是希望将CPU公平地均分给每个任务。当然,CFS也提供了给进程设置优先级的功能,让用户/管理员决定哪些进程需要获得更多的调度时间。基本原理大部分调度算法都是基于固定时间片来进行调度,而CFS另辟蹊径,采用基于计数的调度方法,该技术被称为virtual runtime。CFS给每个任务都维护一个vruntime值,每当任务被调度之后,就累加它的vruntime。比如,当任务A运行了5ms的时间片之后,则更新为vruntime += 5ms。CFS在下次调度时,选择vruntime值最小的任务来调度

TCP与UDP区别

TCP面向字节流,UDP面向报文

img

https://www.zhihu.com/question/47378601

对于上层来说:TCP就像一个水管,交给上层协议时,前面的水流一定是排在前面的;UDP就像瓶装水,交给上层的时候,那一瓶先到了,那一瓶丢了,没有任何保证。

是否面向连接 :UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。

是否是可靠传输远地主机在收到 UDP 报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP 提供可靠的传输服务,TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。

是否有状态 :这个和上面的“是否可靠传输”相对应。TCP 传输是有状态的,这个有状态说的是 TCP 会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此 ,TCP 需要维持复杂的连接状态表。而 UDP 是无状态服务,简单来说就是不管发出去之后的事情了(这很渣男!)。

传输效率 :由于使用 TCP 进行传输的时候多了连接、确认、重传等机制,所以 TCP 的传输效率要比 UDP 低很多。

传输形式 :TCP 是面向字节流的,UDP 是面向报文的。

首部开销 :TCP 首部开销(20 ~ 60 字节)比 UDP 首部开销(8 字节)要大。

是否提供广播或多播服务 :TCP 只支持点对点通信,UDP 支持一对一、一对多、多对一、多对多;

用户态与内核态区别

内核态(Kernel Mode):运行操作系统程序,操作硬件

用户态(User Mode):运行用户程序

  • 处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的
  • 处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。

系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。

用户态和内核态的区别

中断与异常的区别

中断和异常

相同点:都是CPU对系统发生的某个事情做出的一种反应。

区别:中断由外因引起,异常由CPU本身原因引起。

img

面试考点——中断和异常的区别

流量控制与拥塞算法

流量控制是端到端的控制,例如A通过网络给B发数据,A发送的太快导致B没法接收(B缓冲窗口过小或者处理过慢),这时候的控制就是流量控制,原理是通过滑动窗口的大小改变来实现。
拥塞控制是A与B之间的网络发生堵塞导致传输过慢或者丢包,来不及传输。防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不至于过载。拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及与降低网络性能有关的所有因素。

img

img

流量控制与拥塞控制区别

自旋锁与内核抢占的关系

1
2
3
在单cpu,不可抢占内核中,自旋锁为空操作。
在单cpu,可抢占内核中,自旋锁实现为“禁止内核抢占”,并不实现“自旋”。
在多cpu,可抢占内核中,自旋锁实现为“禁止内核抢占” + “自旋”。

自旋锁在抢占(或非抢占)单核和多核中的作用

实习记录

1 使用nvme format命令清空磁盘
2 node-disk-manager构建block device
3 查看operator日志获取未成功启动的原因
4 在operator中动态修改malloc bdev size
首先在annotation中添加自定义参数,而后在代码中获取参数信息或者将其设置为环境变量,而后使用环境变量
5 测试机无法git clone spdk,先通过虚拟机将其spdk与子模块打包成zip上传至自身仓库,而后在测试机通过git clone拉取对应zip
6 编译spdk时显示找不到openssl/kdf.h文件,原因是openssl版本过低,可以考虑回退spdk版本
7 当pvc无法删除时可以考虑删除webhook
8 fio测试硬盘速度时记得指定direct=1保证不使用缓存
9 kubelet启动失败问题:kubelet cgroup与docker cgroup不一致 cgroupfs与systemd
10 使用journalctl -xefu kubelet命令用于查看kubernet集群中kubelet服务的日志
11 不要轻易重启机器,重置k8s机器,谨慎操作,确保不会搞坏机器,影响到其他人
12 kubeadm config images list –kubernetes-version=v1.21.8 获取k8s集群初始化所需镜像
13 设置base环境变量时注意对应的用户,使用sudo和root用户是不一样的(k8s的KUBECONFIG)
14 在出现问题时,除去running的其他pod原因:pend container create之类的,并查看所依赖的硬盘状态
15 注意先安装nvme-fabrics再安装nvme-tcp驱动
16 如果pod一直未调度到某个节点,查看对应污点状态
17 docker build docker push构造镜像并上传镜像
18 将/var/lib/docker移植其他硬盘以节省空间,注意关闭docker与docker.sock服务

19 搭建集群要点:删除.kube文件夹,去除污点,删除/etc/cni/net.d与/opt/cni/bin文件夹
20 安装pytorch,通过docker hub下载现成的镜像,直接下载apex相关pytorch的镜像避免下载apex
21 kubectl logs -p查看core-dump容器日志,设置/proc/sys/kernel/core_pattern,将coredump放在指定目录而后通过进入容器,gdb加载core-dump文件得到调用栈
22 无法访问外网记得设置默认网关 route add default gw xxx
23 编写代码时上层模块不应该依赖下层,应该将对应逻辑下沉
24 阅读项目代码时可先将代码划分为各个模块,而后阅读特点模块代码,熟悉后再使用gdb查看调用栈将各个模块串联起来
25 当怀疑软件出现问题时,可通过在最上层接口打印错误返回值,定位是否是软件发生问题,而后通过gdb附加进程打印调用栈与阅读代码的方式找到问题原因
26 bash与fish有很大的不同,复制命令时使用bash,自己写命令时用fish
27 ocf snprintf function misuse problem report
28 重启后挂在nvme盘,设置默认网关,加载nvme驱动