0%

[TOC]

概述

上下文切换(以下简称CS)的定义,http://www.linfo.org/context_switch.html 此文中已做了详细的说明,这里我又偷懒不详细解释了:) 只提炼以下几个关键要点:

  • context(这里我觉得叫process context更合适)是指CPU寄存器和程序计数器在任何时间点的内容
  • CS可以描述为kernel执行下面的操作
      1. 挂起一个进程,并储存该进程当时在内存中所反映出的状态
      2. 从内存中恢复下一个要执行的进程,恢复该进程原来的状态到寄存器,返回到其上次暂停的执行代码然后继续执行
      3. CS只能发生在内核态(kernel mode)
      4. system call会陷入内核态,是user mode => kernel mode的过程,我们称之为mode switch,但不表明会发生CS(其实mode switch同样也会做很多和CS一样的流程,例如通过寄存器传递user mode 和 kernel mode之间的一些参数)
      5. 一个硬件中断的产生,也可能导致kernel收到signal后进行CS

进程上下文切换开销都有哪些

直接开销

直接开销就是在切换时,cpu必须做的事情,包括:

  • 1、切换页表全局目录

  • 2、切换内核态堆栈

  • 3、切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)

    • ip(instruction pointer):指向当前执行指令的下一条指令
    • bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
    • sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址
    • cr3:页目录基址寄存器,保存页目录表的物理地址
    • ……
  • 4、刷新TLB

  • 5、系统调度器的代码执行

间接开销

间接开销主要指的是虽然切换到一个新进程后,由于各种缓存并不热,速度运行会慢一些。如果进程始终都在一个CPU上调度还好一些,如果跨CPU的话,之前热起来的TLB、L1、L2、L3因为运行的进程已经变了,所以以局部性原理cache起来的代码、数据也都没有用了,导致新进程穿透到内存的IO会变多。 其实我们上面的实验并没有很好地测量到这种情况,所以实际的上下文切换开销可能比3.5us要大。

为什么说线程太多,cpu切换线程会浪费很多时间?

cpu在执行代码的时候【以下说明只在linux平台上,win我不会】。该程序已经是ELF executable file 且该文件内部按ELF格式存储了机器指令+数据。同时该文件必须引用linux 的核心api库【动态库】libc.so 及linux-x86-64.so 核心文件。启动的时候操作系统会识别该文件的ELF文件头信息【引入的api库提供了核心的execve函数用来执行程序】,进行判断,如果是ELF executable file就会把机器指令+数据装载到内存中去运行。

如果该文件不是elf executable file就会读取该文件的第一行数据并当作解释器来运行。

当程序启动时[如/bin/java demo],会读取后面的源码文件【如java 会execve(bin/java…) 再read 该的elf信息】,后面会打开demo.class文件并读取内容【做各种所谓的骚处理】其它语言【go,py,php】同理。

启动后就是一个进程了并且默认是从主线程开始执行,主线程结束,整个进程结束。如果开启了多个线程【每个线程都有一个入口函数】,当线程数量小于或等于cpu核心数时,理论上是并发执行,否则则是模拟”并发执行“。

当cpu切换到当前进程时执行某个或是某几个【多核时】线程时,可能会原因阻塞,锁等情况,被其它线程抢占运行,那么当前的线程的现场执行的上下文数据就要缓存起来以备切换回来时要能还原运行,而这些数据就要暂存到寄存中,如果线程数量过多,切换频繁,数据来回读写,那么当前进程的一堆线程执行性能就会慢慢下降,这些代码在执行的时候是执行机器指令,大家在源码里的代码虽然可能是一行,但是机器指令却是多条,执行到一半,cpu就切换到其线程中了,那当前线程执行到哪,现在的数据是什么总得存起来,以便后面切换时恢复,来回折腾这样好吗?寄存器不会发火吗?

多线程开得越多,cpu都忙在切换(切换还比较耗时,耗时在 3us 级别)上面了,代码执行的时间就会越来越少,执行一条指令立马被人偷袭抢占切换,当前进程启动的多线程执行时间就会越来越少,等半天才执行几条指令。

一旦线程让出cpu, 调度器就需要判断ready 队列中的所有线程哪些需要支持。当前选择哪个执行。

系统调用

触发条件

从用户态到内核态切换可以通过三种方式,或者说会导致从用户态切换到内核态的操作:

  • 系统调用,这个上面已经讲解过了,在我公众号之前的文章也有讲解过。其实系统调用本身就是中断,但是软件中断,跟硬中断不同。系统调用机制是使用了操作系统为用户特别开放的一个中断来实现,如 Linux 的 int 80h 中断。
  • 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,会触发由当前运行进程切换到处理此异常的内核相关进程中
  • 外围设备中断:外围设备完成用户请求的操作之后,会向CPU发出中断信号,这时CPU会转去处理对应的中断处理程序。

开销

当程序中有系统调用语句,程序执行到系统调用时,首先使用类似int 80H的软中断指令,保存现场,去的系统调用号,在内核态执行,然后恢复现场,每个进程都会有两个栈,一个内核态栈和一个用户态栈。当执行int中断执行时就会由用户态,栈转向内核栈。系统调用时需要进行栈的切换。而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。

当发生用户态到内核态的切换时,会发生如下过程(本质上是从“用户程序”切换到“内核程序”)

  • 设置处理器至内核态。
  • 保存当前寄存器(栈指针、程序计数器、通用寄存器)。
  • 将栈指针设置指向内核栈地址。
  • 将程序计数器设置为一个事先约定的地址上,该地址上存放的是系统调用处理程序的起始地址。

而之后从内核态返回用户态时,又会进行类似的工作。

如何避免频繁切换

用户态和内核态之间的切换有一定的开销,如果频繁发生切换势必会带来很大的开销,所以要想尽一切办法来减少切换。这也是面试常考的问题。

3.1 减少线程切换

因为线程的切换会导致用户态和内核态之间的切换,所以减少线程切换也会减少用户态和内核态之间的切换。那么如何减少线程切换呢?

  • 无锁并发编程。多线程竞争锁时,加锁、释放锁会导致比较多的上下文切换。(为什么加锁和释放锁会导致上下文切换,看文末的补充解释)
  • CAS算法。使用CAS避免加锁,避免阻塞线程
  • 使用最少的线程。避免创建不需要的线程
  • 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

参考

进程上下文切换 – 残酷的性能杀手(上)

http://www.linfo.org/context_switch.html

为什么说线程太多,cpu切换线程会浪费很多时间?

进程/线程上下文切换会用掉你多少CPU?

系统调用

[TOC]

JIT概述

JIT是“Just In Time”的首字母缩写。每当一个程序在运行时创建并运行一些新的可执行代码,而这些代码在存储于磁盘上时不属于该程序的一部分,它就是一个JIT。

我认为JIT技术在分为两个不同的阶段时更容易解释:

阶段1:在程序运行时创建机器代码。

阶段2:在程序运行时也执行该机器代码。

第1阶段是JITing 99%的挑战所在,但它也是这个过程中不那么神秘的部分,因为这正是编译器所做的。众所周知的编译器,如gcc和clang,将C/C++源代码转换为机器代码。机器代码被发送到输出流中,但它很可能只保存在内存中(实际上,gcc和clang / llvm都有构建块用于将代码保存在内存中以便执行JIT)。第2阶段是我想在本文中关注的内容。

stackoverflow

A JIT compiler runs after the program has started and compiles the code (usually bytecode or some kind of VM instructions) on the fly (or just-in-time, as it’s called) into a form that’s usually faster, typically the host CPU’s native instruction set. A JIT has access to dynamic runtime information whereas a standard compiler doesn’t and can make better optimizations like inlining functions that are used frequently.

This is in contrast to a traditional compiler that compiles all the code to machine language before the program is first run.

To paraphrase, conventional compilers build the whole program as an EXE file BEFORE the first time you run it. For newer style programs, an assembly is generated with pseudocode (p-code). Only AFTER you execute the program on the OS (e.g., by double-clicking on its icon) will the (JIT) compiler kick in and generate machine code (m-code) that the Intel-based processor or whatever will understand.

JIT实现

demo

  1. 使用mmap在堆上分配可读,可写和可执行的内存块。
  2. 将实现add4函数的汇编/机器代码复制到此内存块中。
  3. 将该内存块首地址转换为函数指针,并通过调用这一函数指针来执行此内存块中的代码。

安全

内存块首先被分配了RW权限,因为我们需要将函数的机器代码写入该内存块。然后我们使用mprotect将块的权限从RW更改为RX,使其可执行但不再可写,所以最终效果是一样的,但是在我们的程序执行过程中,没有任何一个时间点,该内存块是同时可写的和可执行的。

本文介绍的这种技术几乎是真正的JIT引擎(例如LLVM和libjit)从内存中发出和运行可执行机器代码的方式,剩下的只是从其他东西合成机器代码的问题。LLVM有一个完整的编译器,所以它实际上可以在运行时将C和C ++代码(通过LLVM IR)转换为机器码,然后执行它。

JIT 的优势

  1. 编译耗时 有的需求是需要考虑程序的启动时间的
  2. 2.在运行期间收集一些数据可以更好的优化原本的代码(激进优化) 这也是JIT的优势之一
  3. 3.Java本身就支持一开始就本地编译…所以看你的选择

参考

JIT原理 - 简介

使用 Go 语言写一个即时编译器(JIT)

[TOC]

http mock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 注册 responder 函数
func (m *MockTransport) RegisterResponder(method, url string, responder Responder) {}


// MockTransport 实现 RoundTrip 接口
func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {}

// 使用 responder 返回 resp
func runCancelable(responder Responder, req *http.Request) (*http.Response, error) {
ctx := req.Context()
if req.Cancel == nil && ctx.Done() == nil { // nolint: staticcheck
resp, err := responder(req)
return resp, internal.CheckStackTracer(req, err)
}
}

概述

当用python3做爬虫的时候,一些网站为了防爬虫会设置一些检查机制,这时我们就需要添加请求头,伪装成浏览器正常访问。
header的内容在浏览器的开发者工具中便可看到,将这些信息添加到我们的爬虫代码中即可。
‘Accept-Encoding’:是浏览器发给服务器,声明浏览器支持的编码类型。一般有gzip,deflate,br 等等。
python3中的 requests包中response.text 和 response.content

  • response.content #字节方式的响应体,会自动为你解码 gzip 和 deflate 压缩 类型:bytes
  • reponse.text #字符串方式的响应体,会自动根据响应头部的字符编码进行解码。类型:str

但是这里是默认是不支持解码br的!!!!

Accept-Encoding: br

br 指的是 Brotli,是一种全新的数据格式,无损压缩,压缩比极高(比gzip高的).

现代的网页通常包含了由大量的HTML, CSS和JavaScript代码编写的图片、视频或其他大型文件数据,导致了网页打开的速度很慢。如果能有一种好的压缩算法将这些内容和数据进行压缩后传输,那么用户只需要等待很短时间就可以完全加载整个页面上的内容。

2015年9月谷歌发布了Brotli压缩算法,直到现在才开始被大多数的浏览器所兼容。Cloudflare公司的工程师们为了验证Brotli压缩算法比其他压缩算法更好,还特意做了实验来说明。还有http://Discouse.org的联合创始人Sam Saffron给各种压缩算法的文件压缩大小和压缩速度打分,事后证明Brotli不仅全面吊打其他压缩算法,还能支持HTTPS网络加密下的压缩

请求与相应

当我们向cdn缓存服务请求一个文本资源,如js,css,html资源时,请求头会携带如下信息

accept-encoding: gzip, deflate, br

这将表明 client 支持 gzip, deflate, br 等压缩算法

当服务端收到请求的时候,如果只支持gzip协议,那么响应头会有如下信息

content-encoding: gzip

很遗憾,服务端不支持 br

如果是这样

content-encoding: br

说明使用br压缩

python 中遇到的问题

直接出错

1
2
3
# 直接 resp.json(返回的数据就是json)会直接报错。resp.text 则完全是乱码, resp.content.decode() 也无法解码
print(resp.text)
msg = resp.json()

使用brotli解压缩(解决问题)

1
2
3
4
5
6
key = 'Content-Encoding'
if key in resp.headers and resp.headers[key] == 'br':
print(resp.encoding)
# data = brotli.decompress(resp.content)
# data1 = data.decode(resp.encoding)
# print(data1)

或者修改请求header,不接受 br 解码就可以了

1
2
3
4
5
requestHeaders = {
# "accept-encoding": "gzip, deflate, br",
'accept-encoding': 'gzip, deflate, utf-8',
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8"
}

参考

stackoverflow

python3爬虫中文乱码之请求头‘Accept-Encoding’:br 的问题

gzip与brotli压缩算法对比

[toc]

http 请求头部 header

http请求头部用来说明服务器要使用的附加信息,他的结构类型是key-value形式。key和 value 之间用“:”分隔,最后用 CRLF 换行表示字段结束。比如请求头中”host:127.0.0.1”,在这一行中”host”为key值,”127.0.0.1”为value值。

下面这张图展示了http请求报文所包含的信息,也会借此图详细说明每个header信息的作用及其含义。

2fpIs0.jpg

以这个图中的header头字段进行解释,比如Accept、Accept-Language、Connection、Host、Referer、User-Agent等等。

  • Host

Host表示将请求发送的目的地-服务端。告诉服务端由那些主机进行处理,当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用 Host 字段来选择,有点像是一个简单的“路由重定向”。

  • Connection

Connection 表示当client和server通信时对于长链接如何进行处理,取值范围为【”Keep-Alive”, “Close”】。

取值为【Keep-Alive】:若 client使用http1.1协议,但希望使用长链接,则需要在header中指明connection的值为Keep-Alive;如果server方 也想支持长链接,则在response中也需要明确说明connection的值为Keep-Alive。

取值为【Close】:若 client使用http1.1协议,但又不希望使用长链接,则需要在header中指明connection的值为close;如果server方 也不想支持长链接,则在response中也需要明确说明connection的值为close。

不论request还是response的header中包含了值为close的connection,都表明当前正在使用的tcp链接在当天请求处理完毕后会被断掉。以后client再进行新的请求时就必须创建新的tcp链接了。

  • User-Agent

User-Agent 用一个字符串表示发起请求的http客户端,服务器可以依据它来返回浏览器最适合的页面。

但由于历史的原因,User-Agent 非常混乱,每个浏览器都自称是“Mozilla”“Chrome”“Safari”,企图使用这个字段来互相“伪装”,导致 User-Agent 变得越来越长,最终变得毫无意义。

不过有的比较“诚实”的爬虫会在 User-Agent 里用“spider”标明自己是爬虫,所以可以利用这个字段实现简单的反爬虫策略。

  • Accept

Accept 标记客户端可以识别的MIME type,若支持多个类型的话,则用逗号进行分割;这样让服务端选择适合的类型进行返回。

例如 如下这个Accept 结构:

1
Accept: text/html,application/xml,image/webp,image/png

这表示: 告诉服务端,我(客户端)支持四种类型,HTML、XML 的文本,还有 webp 和 png 的图片,你需要按照我支持的类型进行返回,其他的类型我不认识。

  • Accept-Encoding

Accept-Encoding 表示客户端支持的压缩格式,例如gzip、deflate等,如果支持多个的话,也是以逗号进行分割。服务端就会按照客户端支持的压缩格式选择其中一种来进行压缩。

例如 如下Accept-Encoding 结构:

1
Accept-Encoding: gzip, deflate, br

这表示:告诉服务端,我(客户端)支持的压缩格式有三种,你需要按照我支持的压缩格式进行压缩返回。

  • Accept-Language

Accept-Language 表示客户端支持的自然语言,如果支持多个话,也是以逗号进行分割。

例如 如下Accept-Language 结构

1
Accept-Language: zh-CN, zh, en

这表示:告诉服务端,我(客户端)支持的自然语言有三种,但最后以zh-CN 格式给我,如果没有就用其他的汉语方言,如果还没有就给英文。

  • Content-Type

content-type 标记客户端中实体字段类型,指明body数据的类型,如果使用POST请求的话,需要添加上该字段属性。

例如 如下Content-Type 结构

1
Content-Type: application/json

这表示:告诉服务端,我(客户端)传输的数据类型为application/json格式,你需要按照此格式类型进行解析数据。

到这里,http请求报文的细节已经到达尾声了,接下来,一起看一下http响应报文那些事儿~~~

http 响应头部

  • Server

Server 响应字段,它告诉客户端正在提供web服务的软件名称和版本号,但这个字段不是非必须的,一般网站都不会暴露自己真实的服务名称,反正黑客进行攻击,一般会填写无关紧要的信息,比如 http://Nginx.com

比如GitHub网址,server 字段中看不出是使用了nginx还是Apache。server字段如下:

1
Server: GitHub.com
  • Date

Date 表示http报文创建的时间,目的让客户端使用该字段和其他字段配合起来决定缓存策略。

  • Content-Type

Content-Type 标记服务端返回给客户端的结构类型,和请求头部中的Content-Type使用方式一样。

例如 Content-Type格式如下

1
Content-Type: application/json

这表示:服务端返回给客户端的数据结构类型为application/json。

  • Content-Length

Content-Length 报文里body 的长度,也就是请求头或响应头空行后面数据的长度。服务器看到这个字段,就知道了后续有多少数据,可以直接接收。如果没有这个字段,那么 body 就是不定长的,需要使用 chunked 方式分段传输。

  • Content-Language

Content-Language 告诉客户端实体数据使用的实际语言类型

  • Content-Encoding

Content-Encoding 告诉客户端实体数据是使用那个压缩格式进行压缩的

到这里,http响应报文的细节已经到达尾声了,相信大家都有不一样的体会~~~

[toc]

CRLF

回车符(CR)和换行符(LF)是文本文件用于标记换行的控制字符(control characters)或字节码(bytecode)。

  • CR = Carriage Return,回车符号(\r,十六进制 ascii 码为0x0D,十进制 ascii 码为13),用于将鼠标移动到行首,并不前进至下一行。
  • LF = Line Feed,换行符号( \n, 十六进制 ascii 码为 0x0A,十进制 ascii 码为10)。

http报文结构

http的请求报文和http响应报文结构基本类似,都是由三部分组成:

  • 起始行(start line):描述请求或响应的基本信息

  • 头部字段集合(header):使用 key-value 形式更详细地说明报文

  • 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据

这其中前两部分起始行和头部字段经常又合称为“请求头”或“响应头”,消息正文又称为“

实体”,但与“header”对应,很多时候就直接称为“body”。

HTTP 协议规定报文必须有 header,但可以没有 body,而且在 header 之后必须要有一个“空行”,也就是“CRLF”,十六进制的“0D0A”。

所以,一个完整的 HTTP 报文就像是下图的这个样子,注意在 header 和 body 之间有一个“空行”。

2fS2g1.jpg

1: http 请求行 start line

聊完http的报文结构,那么接下里看一下http请求报文组成结构的第一组成部分:http起始行(http请求行)。

由图可知,http请求行有三部分组成:

  • 请求方法:如GET、POST请求方式,表示对资源的操作

  • 请求地址:表示请求方法要操作的资源

  • 版本号:表示报文所使用的http版本号

拿个实际的请求行来解释一下,加深影响

1
GET /ping HTTP/1.1

在这个请求行里,“GET”是请求方法,“/ping”是请求目标,“HTTP/1.1”是版本号,把这三部分连起来,意思就是“服务器你好,我想获取网站根目录下的默认文件,我用的协议版本号是 1.1,请不要用 1.0 或者 2.0 回复我。”

2: http 请求头 header

http请求头部用来说明服务器要使用的附加信息,他的结构类型是key-value形式。key和 value 之间用“:”分隔,最后用 CRLF 换行表示字段结束。比如请求头中”host:127.0.0.1”,在这一行中”host”为key值,”127.0.0.1”为value值。

  • Host

Host表示将请求发送的目的地-服务端。告诉服务端由那些主机进行处理,当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用 Host 字段来选择,有点像是一个简单的“路由重定向”。

其他不再展示

3: http 请求体 body

http响应

1: 响应行

举例说明一下响应行:

1
HTTP/1.1 200 OK

这表示:浏览器你好,我已经处理完了你的请求,这个报文使用的协议版本号是 1.1,状态码是 200,一切OK。”

响应行格式如下所示:

img

由图可知,响应行由三部分组成:

  • 版本号:http协议版本号。

  • 状态码:一个三位数,用代码的形式表示处理的结果,比如 200 是成功,500 是服务器错误。

  • 原因:作为数字状态码补充,是更详细的解释文字,帮助人理解原因。

2: 响应头

说完了响应行,那么接下来看一下http的响应头部。下面这张图展示了http响应报文所包含的信息,也会借此图详细说明每个header信息的作用及其含义。

img

3: 响应体

响应体通常是 json/html/js/css.

总结

今天我们聊了一下http报文那些事儿,在这儿做一个简单的小结。

  • http报文结构由三部分组成:起始行+头部字段集合+消息主体

  • http报文可以没有消息主体,但一定有起起始行+头部字段集合

  • 请求报文中请求行由三部分组成:请求方法+请求URI资源地址+http协议版本号

  • 响应报文中响应行由三部分组成:http协议版本号+状态码+原因

  • 头部字段是 key-value 的形式,用“:”分隔,不区分大小写,顺序任意,除了规定的标准头,也可以任意添加自定义字段,实现功能扩展

  • http请求头字段中常用的字段:Host、Accept、Accept-Encoding、Accept-Language、Content-Type、Connection

  • http响应字段中常用的字段:Server、Date、Content-Type、Content-Language、Content-Encoding、Content-Length

读到这里,相信大家也对http报文有了不一样的理解。好了,今天就到这里,我们下期再见~~~

参考

揭开http报文的神秘面纱

问题

  • 与人沟通存在抢话问题
    • 虽然你可能知道别人后面会说什么,但是还是需要耐心等待对方说完。
    • 其实我的沟通能力很强,能很快理解别人的意思,也能清晰表达自己的意思,但是就是爱抢话,这是不好的,要慢下来。
  • 做事比较急
    • 如果不是很紧急的事情,做事可以慢一点,思考清楚后再动手。这么多年的工作,我这样经验的人不应该只停留在能处理问题的层面,应该要能处理好问题。
  • 要去理解不同背景下的人的不同做事方式
    • 先去理解别人的做事方式,再来讨论

背景

服务 a 依赖 服务 b, 服务 b 依赖 服务 c

服务 c 做了如下的 replace

1
2
3
replace (
google.golang.org/grpc v1.35.0 => google.golang.org/grpc v1.29.1 // etcd-io/etcd#12124
)

为了引用服务 c,服务 b 做了如下的 replace (和c相同的)

1
2
3
replace (
google.golang.org/grpc v1.35.0 => google.golang.org/grpc v1.29.1 // etcd-io/etcd#12124
)

但是服务 a 做这样的replace 就会出错

1
2
3
4
5
➜  janus git:(ee) ✗ go mod tidy
go: found google.golang.org/grpc/naming in google.golang.org/grpc v1.35.0
go: found google.golang.org/grpc/examples/helloworld/helloworld in google.golang.org/grpc v1.35.0
go: found google.golang.org/grpc/naming in google.golang.org/grpc v1.35.0
go: found google.golang.org/grpc/examples/helloworld/helloworld in google.golang.org/grpc v1.35.0

服务 a 需要这样做

1
replace google.golang.org/grpc => google.golang.org/grpc v1.29.1 // etcd-io/etcd#12124