kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

openssl常用命令,签名、非对称加解密

http://blog.csdn.net/modianwutong/article/details/43059435

https://www.cnblogs.com/gordon0918/p/5363466.html

https://stackoverflow.com/questions/5140425/openssl-command-line-to-verify-the-signature


查看网站证书

1
openssl s_client -showcerts -connect www.baidu.com:443

证书

证书是一个经证书授权中心签过名的包含公钥及公钥拥有者信息的文件。证书授权中心(CA)对证书签名的过程即为证书的颁发过程。证书里面的公钥只属于某一个实体(网站,个人等),它的作用是防止一个实体伪装成另外一个实体。

证书可以保证非对称加密算法的合理性,假设A和B的通话过程如下:

1
2
3
4
5
6
 A -------> Hello (plain text) ---------> B
 A <------- Hello (plain text) ---------- B
 A <------ B Send Certificate to A --- B
 A ------- cipher text ------------------> B
 A <------ cipher text ------------------- B
  … …

A在接受了B发过来的证书以后,A,B就可以使用密文进行通信了。

如果C想伪装成B,应该怎么做呢?我们想象下面的通话过程:

1
2
3
 A-------> Hello (plain text) ---------> C
 A <------ Hello (plain text) ----------- C
 A <------ C Send Certificate to A --- C

此时A没有怀疑C的身份,理所当然的接受了C的证书,并继续进行下面的通信

1
2
3
 A------- cipher text ------------------> C
 A <------ cipher text ------------------- C
  … …

这样的情况下A是没有办法发现C是假的,A的用户名,密码,甚至卡号等重要信息都有可能被C截获。如果A在通话过程中要求取得B的证书,并验证证书里面的名字,如果发现名字与B不符合,就可以发现对方不是B。验证B的名字通过后,再继续通信过程。

那么,如果证书是假的怎么办?或者证书被修改过怎么办?此时就要用到签名信息了。数字证书除了包含证书拥有者的名字和公钥外,还应包含颁发证书的机构名称,证书序列号和其它一些可选信息。最重要的是,它包含了证书颁发机构(Certification Authority,简称CA)的签名信息。

通过检查证书里面CA的签名信息,就知道这个证书的确是由该CA签发的,然后你就可以检查证书里面的证书拥有者的名字,检查通过后,就可以提取公钥,继续通信了。这样做的基础是,你信任该CA,认为该CA没有颁发错误的证书。

CA是第三方机构,被你信任,由它保证证书的确发给了应该得到证书的人。这里需要解释一下,CA也是一个实体,它也有自己的公钥和私钥,否则怎么做数字签名?它也有自己的证书,你可以去它的站点下载它的证书,来验证签名。

CA也是分级别的,最高级别的CA叫RootCA,低一级别的CA的证书由它来颁发和签名。这样我们信任RootCA,那些由RootCA签名过的证书的CA就可以来颁发证书给实体或其它CA了。那RootCA谁给签名呢?他们自己给自己签名,叫自签名。

现在常用的证书都是采用X.509格式的,这是一个国际标准的证书格式。任何遵循该标准的应用程序都可以读,写X509格式的证书。

下面是一个证书的例子:

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
# openssl x509 -text -in test.crt
Certificate:
   Data:
       Version: 3 (0x2)
       Serial Number: 17209717939030827578 (0xeed53348d899a23a)
   Signature Algorithm: sha1WithRSAEncryption                             // 签名算法
       Issuer: C=AU, ST=Some-State, O=Internet Widgits Pty Ltd            // 证书颁发者信息
       Validity
           Not Before: Jan 14 07:01:20 2015 GMT                           // 证书的有效期
           Not After : Feb 13 07:01:20 2015 GMT
       Subject: C=AU, ST=Some-State, O=Internet Widgits Pty Ltd           // 证书拥有者信息
       Subject Public Key Info:                                           //公钥信息
           Public Key Algorithm: rsaEncryption
                Public-Key: (512 bit)
                Modulus:
                   00:c5:63:8c:c8:32:b1:2e:15:58:a6:cd:22:f4:40:
                   ef:53:8e:e7:fa:4e:fd:d5:d9:fe:69:a2:c2:5a:fc:
                   20:4b:da:c9:17:49:35:4e:67:92:82:ec:4e:a7:a7:
                   1a:66:3a:c5:36:2e:74:77:30:7a:dd:65:5f:03:9a:
                   9b:2e:d0:b1:43
                Exponent: 65537 (0x10001)
       X509v3 extensions:                                                 //x509扩展信息
           X509v3 Subject Key Identifier:
               0E:DB:FE:E6:CF:FE:A6:C8:6D:38:06:A5:22:34:DA:82:A9:BE:42:B8
           X509v3 Authority Key Identifier:
               keyid:0E:DB:FE:E6:CF:FE:A6:C8:6D:38:06:A5:22:34:DA:82:A9:BE:42:B8
           X509v3 Basic Constraints:
               CA:TRUE
   Signature Algorithm: sha1WithRSAEncryption
        32:43:0c:e8:32:6f:30:10:c9:0f:a3:36:24:7c:a5:dc:da:8c:            // CA的签名
        c4:90:69:90:de:b1:1b:19:8e:b1:a5:35:ce:2e:7a:05:69:94:
        46:72:37:c2:2c:38:57:4a:0c:89:bc:90:95:03:af:f2:6f:a0:
        3f:13:5f:f0:90:a7:2c:bf:75:ee

算法

加密算法分为两种:对称加密算法和非对称加密算法;

对称加密算法:即信息的发送方和接收方使用同一个密钥去加密和解密数据。它的最大优势是加/解密速度快,适合于对大数据量进行加密,但密钥管理困难。AES,DES等都是常用的对称加密算法;
非对称加密算法:它需要使用不同的密钥来分别完成加密和解密操作,一个公开发布,即公开密钥,另一个由用户自己秘密保存,即私用密钥。信息发送者用公开密钥去加密,而信息接收者则用私用密钥去解密。公钥机制灵活,但加密和解密速度却比对称密钥加密慢得多。RSA,DSA等是常用的非对称加密算法;

所以在实际的应用中,人们通常将两者结合在一起使用,例如,对称密钥加密系统用于存储大量数据信息,而公开密钥加密系统则用于加密密钥。

另外还有一种我们需要知道的加密算法,叫做摘要算法,英文名是messagedigest,用来把任何长度的明文以一定规则变成固定长度的一串字符。那么我们在对文件做签名的时候,通常都是先使用摘要算法,获得固定长度的一串字符,然后对这串字符进行签名。

Base64不是加密算法,它是编码方式,用来在ASCII码和二进制码之间进行转换。

RSA

RSA是目前比较流行的非对称加密算法,所谓非对称,就是指该算法需要一对密钥,使用其中一个加密,则需要用另一个才能解密。下面简单介绍一下它的原理:

RSA的算法涉及三个参数,n、e1、e2。

其中,n是两个大质数p、q的积,n的二进制表示所占用的位数,就是所谓的密钥长度。

e1和e2是一对相关的值,e1可以任意取,但要求e1与(p-1)(q-1)互质;再选择e2,要求(e2e1) mod((p-1)*(q-1))=1。

(n,e1), (n,e2)就是密钥对。其中(n,e1)为公钥,(n,e2)为私钥。

RSA加解密的算法完全相同,设A为明文,B为密文,则:A=Be2 mod n;B=Ae1 mod n;(公钥加密体制中,一般用公钥加密,私钥解密)

e1和e2可以互换使用,即:

A=Be1mod n;B=Ae2 mod n;

关于RSA加密算法的详细介绍可以参考百度百科;

协议

SSL是Secure Sockets Layer(安全套接层协议)的缩写,可以在Internet上提供秘密性传输。Netscape公司在推出第一个Web浏览器的同时,提出了SSL协议标准。其目标是保证两个应用间通信的保密性和可靠性,可在服务器端和用户端同时实现支持。已经成为Internet上保密通讯的工业标准。

OpenSSL是一个强大的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及SSL协议,并提供丰富的应用程序供测试或其它目的使用。

OpenSSL整个软件包大概可以分成三个主要的功能部分:SSL协议库、应用程序以及密码算法库。OpenSSL的目录结构自然也是围绕这三个功能部分进行规划的。

Openssl目前最新的稳定版本是1.0.2,可以在openssl的官网下载openssl-1.0.2的源代码。

Android中已经集成了openssl,源码目录在:external/openssl

工具

RSA是目前最有影响力的公钥加密算法,它能抵抗到目前为止已知的绝大多数密码攻击。下面我们要介绍的工具将会主要涉及到秘钥的产生、管理,证书请求及证书的产生,数据加密、解密,算法签名及身份验证等;

genrsa

生成RSA私有密钥,用法如下:

1
2
3
4
5
6
7
8
9
openssl genrsa [-outfilename] [-passout arg] [-aes128] [-aes192] [-aes256] [-des] [-des3] [-idea][-f4] [-3]
[-rand file(s)] [-engine id] [numbits]

Options:
  -outfilename:将生成的私钥输出到指定文件,默认为标准输出;
  -passoutarg:如果对生成的密钥使用加密算法的话,可以使用”-passout”选项指定密码来源,这里的arg可以是”pass:password”,”file:pathname”,”stdin”等;
  -des|-des3|-idea:采用什么加密算法来加密生成的密钥,一般需要输入保护密码;
  -f4|3:生成密钥过程中使用的公共组件,65537或3,默认使用65537;
  numbits:密钥长度,必须是genrsa命令的最后一个参数,默认值是512;

注意:genrsa生成的私钥默认是PEM编码格式;

Example:

1)生成私钥:

1
openssl genrsa -out private.pem -3 -2048

rsa

RSA密钥管理工具,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
openssl rsa [-informPEM|NET|DER] [-outform PEM|NET|DER] [-in filename] [-passin arg] [-outfilename] [-passout arg] [-sgckey] [-aes128] [-aes192] [-aes256] [-des] [-des3][-idea] [-text] [-noout] [-modulus] [-check] [-pubin] [-pubout]

Options:
  -informPEM|NET|DER:指定输入格式,可以是PEM,NET或DER格式;
  -outformPEM|NET|DER:指定输出格式,可以是PEM,NET或DER格式;
  -infilename:指定输入文件,如果是加密过的密钥,会要求你输入密码;
  -outfilename:指定输出文件名称;
  -passinarg:输入密钥的密码来源;
  -passoutarg:输出密钥如果加密的话,该参数用于指定密码的来源;
  -text:以只读方式输出密钥及组件信息;
  -noout:不打印密钥文件;
  -modulus:显示密钥的模数;
  -pubin:默认从输入文件中读取私钥,如果该选项打开的话,就可以从输入文件中读取公钥;
  -pubout:默认输出私钥,使用该选项的话,将会输出公钥,如果pubin打开的话,pubout也会被自动打开;另外使用该参数的话,可以由私钥导出公钥;
  -check:检查RSA密钥是否被破坏;

Example:

1)去掉私钥的保护密码

1
openssl rsa -in private.pem -out private_out.pem

2)用DES3算法加密密钥,需要输入保护密码

1
openssl rsa -in private.pem -des3 -out private_out.pem

3)把私钥文件从PEM格式转换为DER格式

1
openssl rsa -in private.pem -outform DER -out private.der

4)打印私钥组件信息

1
openssl rsa -in private.pem -text -noout

5)导出公钥文件

1
openssl rsa -in private.pem -out public.pem -pubout

6)检查密钥文件的完整性

1
openssl rsa -in private.pem -check -noout

rsautil

无论是使用公钥加密还是私钥加密,RSA每次能够加密的数据长度不能超过RSA密钥长度,并且根据具体的补齐方式不同输入的加密数据最大长度也不一样,而输出长度则总是跟RSA密钥长度相等。RSA不同的补齐方法对应的输入输入长度如下表

1
2
3
4
5
数据补齐方式       输入数据长度      输出数据长度  参数字符串
PKCS#1 v1.5       少于(密钥长度-11)字节   同密钥长度 -pkcs
PKCS#1 OAEP       少于(密钥长度-11)字节   同密钥长度 -oaep
PKCS#1 for SSLv23 少于(密钥长度-11)字节   同密钥长度 -ssl
不使用补齐       同密钥长度     同密钥长度 -raw

RSA工具指令,包含RSA算法签名,身份验证,加密/解密数据等功能,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
openssl rsautl [-in file][-out file] [-inkey file] [-pubin] [-certin] [-sign] [-verify] [-encrypt][-decrypt] [-pkcs]
[-ssl] [-raw] [-hexdump] [-asn1parse]

Options:
  -infile:指定输入文件;
  -outfile:指定输出文件;
  -inkeyfile:加密/解密数据时使用的密钥文件,缺省使用RSA私钥;
  -pubin:如果设置该选项,则使用RSA公钥加密/解密数据;
  -certin:指定使用证书文件加密/解密,证书中包含RSA公钥;
  -sign:数据签名,需要指定RSA私钥;
  -verify:数据验证;
  -encrypt:数据加密;
  -decrypt:数据解密;
  -pkcs|-ssl|-raw:指定数据填充模式,分别代表:PKCS#1v1.5(缺省值),SSL v2填充模式或无填充。如果要签名,只能使用pkcs或raw选项;
  -hexdump:用十六进制输出数据;
  -asn1parse:按照ASN.1结构分析输出数据,详细可以查看asn1parse命令,和”-verify”选项一起使用威力强大;

Example:

1)公钥加密

1
openssl rsautl -encrypt -in test.txt -out test.enc -inkey public.pem -pubin

注意:如果使用证书加密的话,需要使用-certin选项,-inkey指定证书文件;

2)私钥解密

1
openssl rsautl -decrypt -in test.enc -out test.dec -inkey private.pem

3)私钥签名

1
openssl rsautl -sign -in test.txt -out test.sig -inkey private.pem

4)公钥验证

1
openssl rsautl -verify -in test.sig -out test.vfy -inkey public.pem -pubin

注意:如果使用证书验证签名,需要使用-certin选项,-inkey指定证书文件;

req

创建和处理证书请求的工具,它还能建立自签名的证书,做RootCA用。用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
openssl req [-inform PEM|DER][-outform PEM|DER] [-in filename] [-passin arg] [-out filename] [-passout arg][-text] [-pubkey] [-noout] [-verify] [-modulus] [-new] [-rand file(s)] [-newkeyrsa:bits] [-newkey alg:file] [-nodes] [-key filename] [-keyform PEM|DER][-keyout filename] [-keygen_engine id] [-[digest]] [-config filename] [-subjarg] [-multivalue-rdn] [-x509] [-days n] [-set_serial n] [-asn1-kludge][-no-asn1-kludge] [-newhdr] [-extensions section] [-reqexts section] [-utf8][-nameopt] [-reqopt] [-subject] [-subj arg] [-batch] [-verbose]


在创建证书请求(CSR)的过程中会要求用户输入一些必要的信息,包括位置、组织、邮箱等信息,可以把它们放在配置文件里,也可以放在命令行参数里;

Options:
  -text:以可读方式打印将CSR文件里的内容;
  -noout:不要打印CSR文件的编码信息;
  -pubkey:解析CSR文件里的公钥信息,并打印出来;
  -modulus:解析CSR文件里的公钥模数,并打印出来;
  -verify:检验请求文件的签名信息;
  -new:创建新的CSR,它会要求用户输入一些必要信息。如果没有指定-key参数,将会生成新的私钥;
  -newkeyarg:创建新的CSR和新的私钥,参数指示私钥算法等信息(类似rsa:1024);
  -keyfilename:指定私钥文件,该选项支持pkcs8格式的私钥;
  -keyoutfilename:如果产生了新的私钥,则把私钥保存到指定文件;
  -[digest]:指定证书的摘要算法,可以是sha1|md5等,默认使用哈希算法;
  -configfilename:指定config文件,配置文件可以包含创建CSR所需的各种信息;
  -x509:产生自签名的证书,而不是证书请求;
  -days:证书的有效期,缺省是30天;
  -subject:显示CSR的位置,组织等信息;
  -subjarg:设置CSR的位置,组织等信息,格式:/C=AU/ST=Some-State/type0=value0

关于配置文件的格式,可以参考openssl的在线帮助文档;

Example:

1)生成一个私钥,并使用该私钥产生证书请求

1
2
3
openssl genrsa -out private.pem 1024

openssl req -new -key private.pem -out req.pem

如果加上-sha256,则使用sha256摘要算法;

2)生成一个私钥,并使用该私钥产生证书请求,另一个方法

1
openssl req -newkey rsa:2048 -keyout private.pem -out req.pem

3)检测并验证证书请求

1
openssl req -verify -in req.pem -text -noout

4)制作自签名的根证书,使用已有私钥

1
openssl req -new -x509 -key private.pem -sha256 -out rootca.crt

5)制作自签名的根证书,创建新的私钥

1
openssl req -newkey rsa:2048 -keyout private.pem -x509 -sha256 -out rootca.crt

x509

证书管理工具,可以用来显示证书的内容,证书格式转换,为证书请求签名等。用法如下:

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
openssl x509 [-inform DER|PEM|NET][-outform DER|PEM|NET] [-keyform DER|PEM]

[-CAform DER|PEM] [-CAkeyform DER|PEM][-in filename] [-out filename] [-serial] [-hash] [-subject_hash] [-issuer_hash][-ocspid] [-subject] [-issuer] [-nameopt option] [-email] [-ocsp_uri][-startdate] [-enddate] [-purpose] [-dates] [-checkend num] [-modulus][-pubkey] [-fingerprint] [-alias] [-noout] [-trustout] [-clrtrust] [-clrreject][-addtrust arg] [-addreject arg] [-setalias arg] [-days arg] [-set_serial n][-signkey filename] [-passin arg] [-x509toreq] [-req] [-CA filename] [-CAkeyfilename] [-CAcreateserial] [-CAserial filename] [-force_pubkey key] [-text][-certopt option] [-C] [-md2|-md5|-sha1|-mdc2] [-clrext] [-extfile filename][-extensions section] [-engine id]

Options:

由于x509的选项比较多,下面我们来分类介绍:

1)输入/输出相关选项
  -inform|-outformDER|PEM|NET:指定输入/输出文件的编码格式;
  -in|-outfilename:指定输入/输出文件;
  -md2|-md5|-sha1|-mdc2:指定摘要算法,默认使用哈希算法;

2)显示选项
  -text:以可读方式打印证书内容;
  -pubkey:解析证书里面的公钥信息,并打印出来;
  -modulus:解析证书里面的公钥模数信息,并打印出来;
  -serial:显示证书的序列号;
  -subject:显示证书拥有者的信息;
  -issuer:显示证书颁发者的名字;
  -email:显示邮箱信息;
  -startdate|-enddate |-dates:显示证书有效期,起始和结束时间;
  -fingerprint:显示DER格式证书的DER版本信息;
  -C:以C语言源文件方式,输出证书;

3)信任相关的选项
  -purpose:显示证书用途;

4)签名相关选项
  -signkeyfilename:使用给定的私钥,对输入文件进行签名;如果输入文件是证书,那么它的颁发者就会被设置成其拥有者,其它项也会被设置成符合自签名特征的证书项;如果输入文件是证书请求,那么就会产生一个自签名的证书;
  -days:证书有效期;
  -x509toreq:把证书转换成证书请求,需要-signkey选项指定私钥;
  -req:默认输入文件是一个证书,使用该选项的话,指示输入文件是一个证书请求;
  -CAfilename:指定签名用的CA证书文件,该选项通常和-req一起用,对证书请求实施自签名;
  -CAkeyfilename:指定CA的私钥文件,对证书进行签名;

Examples:

1)显示证书内容

1
openssl x509 -in cert.pem –noout -text

2)显示证书序列号

1
openssl x509 -in cert.pem -noout -serial

3)显示证书的subject信息

1
openssl x509 -in cert.pem -noout -subject

4)显示证书的fingerprint信息

1
openssl x509 -in cert.pem -noout -fingerprint

5)证书格式转换

1
2
3
openssl x509 -in cert.pem -inform PEM -out cert.der -outform DER

openssl x509 -in cert.der -inform DER -out cert.pem -outform PEM

6)把证书转换为证书请求

1
openssl x509 -x509toreq -in cert.pem -out cert.csr -signkey private.pem

7)把证书请求转换为证书

1
openssl x509 -req -in careq.pem -extfile openssl.cnf -extensions v3_ca -signkey key.pem -out cacert.pem

8)从证书里面提取公钥

1
openssl x509 -pubkey -in cert.pem >pub.pem

9)以C语言源文件方式输出证书,PEM格式转换为DER格式

1
openssl x509 -in cert.pem -out cert.der -outform DER -C > cert.c

asn1parse

asn1parse是一个诊断工具,可以解析ASN.1结构的密钥,证书等,用法如下:

1
2
3
4
5
6
openssl asn1parse [-informPEM|DER] [-in filename] [-out filename] [-noout] [-offset number]
[-length number] [-i] [-oid filename][-dump] [-dlimit num] [-strparse offset] [-genstr string]
[-genconf file] [-strictpem]

Options:
  -i:以缩进方式显示ASN.1结构

Example:

1)解析文件:

1
openssl asn1parse -in file.pem

2)解析DER格式的文件

1
openssl asn1parse -inform file.der -infile.der

dgst

摘要算法,用法如下:

1
2
3
4
5
6
7
8
9
10
11
openssl dgst[-sha|-sha1|-mdc2|-ripemd160|-sha224|-sha256|-sha384|-sha512|-md2|-md4|-md5|-dss1][-c] [-d] [-hex] [-binary] [-r] [file...]

所谓摘要算法,就是把任何长度的明文以一定规则变成固定长度的一串字符。

Options:
  -sha|-sha1 |-sha256 |-md5:算法名称;
  -c:打印出哈希结果的时候用冒号来分隔开;
  -d:详细打印出调试信息;
  -hex:以十六进制输出哈希结果,默认值;
  -binary:以二进制输出哈希结果;
  file:要哈希的文件;

Example:

1)使用sha256摘要算法,并以二进制方式输出

1
openssl dgst -sha256 -binary file.txt

PEM 和 DER 格式

PEM和DER是两种不同的编码格式,PEM使用ASCII(base64)编码方式,PEM格式文件的第一行和最后一行指明文件内容,DER采用二进制编码格式;使用openssl工具生成的密钥或证书,默认使用PEM编码格式;

实例

Image签名

高通平台支持bootimg签名和校验功能,下面我们先来看看它的签名脚本:

1
2
3
4
5
6
7
8
9
10
define build-boot-image
 mv -f $(1) $(1).nonsecure
 openssl dgst -$(TARGET_SHA_TYPE) -binary $(1).nonsecure >$(1).$(TARGET_SHA_TYPE)
 openssl rsautl -sign -in $(1).$(TARGET_SHA_TYPE) -inkey$(PRODUCT_PRIVATE_KEY) -out $(1).sig
 dd if=/dev/zero of=$(1).sig.padded bs=$(BOARD_KERNEL_PAGESIZE) count=1
 dd if=$(1).sig of=$(1).sig.padded conv=notrunc
 cat $(1).nonsecure $(1).sig.padded > $(1).secure
 rm -rf $(1).$(TARGET_SHA_TYPE) $(1).sig $(1).sig.padded
 mv -f $(1).secure $(1)
endef

这里定义了一个build-boot-image宏,使用方法如下:

1
2
3
4
$(INSTALLED_BOOTIMAGE_TARGET) :=$(PRODUCT_OUT)/boot.img
INSTALLED_SEC_BOOTIMAGE_TARGET :=$(PRODUCT_OUT)/boot.img.secure
$(INSTALLED_SEC_BOOTIMAGE_TARGET):$(INSTALLED_BOOTIMAGE_TARGET)
   $(hide) $(callbuild-boot-image,$(INSTALLED_BOOTIMAGE_TARGET))

上面就是对bootimg实施签名操作的脚本,build-boot-image宏做了什么事呢?

1)对bootimg实施摘要算法

1
openssl dgst -sha256 -binary boot.img.nonsecure > boot.img.sha256

2)制作签名

1
openssl rsautl -sign -inboot.img.sha256 -inkey qcom.key -out boot.img.sig

3)填充签名

1
2
dd if=/dev/zero of=boot.img.sig.paddedbs=2048 count=1
dd if=boot.img.sig of=boot.img.sig.paddedconv=notrunc

生成好的签名已经保存到boot.img.sig.padded,文件大小是2048字节,用0填充;

4)把签名打到image上,并删除中间文件

1
2
3
4
cat boot.img.nonsecure boot.img.sig.padded > boot.img.secure
rm -rf $boot.img.sha256 boot.img.sigboot.img.sig.padded

mv -f boot.img.secure boot.img

Image签名完成,签名信息保存在image的最后2048字节,不足的都用0来填充;

验证签名又是怎样的过程呢?前面用到了qcom.key,这个是高通提供的私钥,那么要验证签名,我们就需要用这个私钥制作的证书。验证过程是在LK里面完成的,所以我们还需要把证书转换为C源文件方式,方法很简单,用下面几条命令就可以了:

1
2
3
openssl req -new -x509 -key qcom.key -days11324 -sha256 -out qcom.crt

openssl x509 -inform PEM -in qcom.crt-outform DER -out qcom.der > cert.c

好了,cert.c就是以C源文件方式存储的证书,替换到LK里面相应的数组就可以了;

制作testkey

testkey是用来给apk签名的,保存在build/target/product/security目录下,testkey.pk8是私钥,

testkey.x509.pem是证书,那么这个key和证书是如何生成的呢?README写的很清楚:

1
2
development/tools/make_key testkey '/C=US/ST=California/L=MountainView/O=Android/OU=Android/
 CN=Android/emailAddress=android@android.com'

development/tools/make_key是制作testkey的脚本文件,我们看几条比较重要的命令吧:

1)生成私钥

1
openssl genrsa -f4 2048 | tee ${one}> ${two}

2)制作证书

1
openssl req -new -x509 ${hash} -key${two} -out $1.x509.pem -days 10000 -subj "$2"

3)制作pk8格式私钥

1
openssl pkcs8 -in ${one} -topk8-outform DER -out $1.pk8 –nocrypt

Help

OpenSSL提供了非常丰富的帮助信息,如果对哪个命令或参数不了解,可以很方便的使用帮助来查看命令的详细用法; 命令列表

使用下面命令查看openssl支持的命令和算法列表:

1
2
3
4
5
6
7
8
openssl [ list-standard-commands | list-message-digest-commands | list-cipher-commands | 
          list-cipher-algorithms | list-message-digest-algorithms | list-public-key-algorithms]
list-standard-commands:标准命令列表
list-message-digest-commands:摘要命令列表
list-cipher-commands:加密命令列表
list-cipher-algorithms:加密算法列表
list-message-digest-algorithms:摘要算法列表
list-public-key-algorithms:公钥加密算法列表

获取帮助

虽然openssl并不支持”-h”参数,使用”-h”参数,会出现类似” unknown option -h”的错误提示,但是openssl的帮助系统还是很nice的,在使用错误参数的情况下,会把命令的详细用法及参数解释列出来,因此,还是可以用”openssl command -h”的方式来获取帮助信息。

我们还可以使用类似”man genrsa”的方法,来查看openssl的帮助文档;

最后,我们还可以访问openssl的主页,里面有详细的帮助文档;

参考资料

openssl帮助文档:http://www.openssl.org/docs/

openssl命令详解.pdf

百度百科:openssl,RSA,X509;

使用openssl命令剖析RSA私钥文件格式:http://blog.csdn.net/lucien_cc/article/details/18407451

常用证书格式切换:http://blog.chinaunix.net/uid-20553497-id-2353867.html

内存屏障

https://www.cnblogs.com/icanth/archive/2012/06/10/2544300.html

内存屏障(Memory Barriers)

一方面,CPU由于采用指令流水线和超流水线技术,可能导致CPU虽然顺序取指令、但有可能会出现“乱序”执行的情况,当然,对于” a++;b = f(a);c = f”等存在依赖关系的指令,CPU则会在“b= f(a)”执行阶段之前被阻塞;另一方面,编译器也有可能将依赖关系很近“人为地”拉开距离以防止阻塞情况的发生,从而导致编译器乱序,如“a++ ;c = f;b = f(a)”。

一个CPU对指令顺序提供如下保证:

(1) On any given CPU, dependent memory accesses will be issued in order, with respect to itself.如Q = P; D = *Q;将保证其顺序执行

(2) Overlapping loads and stores within a particular CPU will appear to be ordered within that CPU.重叠的Load和Store操作将保证顺序执行(目标地址相同的Load、Store),如:a = X; X = b;

(3) It must_not be assumed that independent loads and stores will be issued in the order given.

(4) It must be assumed that overlapping memory accesses may be merged or discarded.如A = X; Y = A; => STORE A = X; Y = LOAD A; / or STORE *A = Y = X;

由此可见,无关的内存操作会被按随机顺序有效的得到执行,但是在CPU与CPU交互时或CPU与IO设备交互时, 这可能会成为问题. 我们需要一些手段来干预编译器和CPU, 使其限制指令顺序。内存屏障就是这样的干预手段. 他们能保证处于内存屏障两边的内存操作满足部分有序.(译注: 这里"部分有序"的意思是, 内存屏障之前的操作都会先于屏障之后的操作, 但是如果几个操作出现在屏障的同一边, 则不保证它们的顺序.)

(1) 写(STORE)内存屏障。在写屏障之前的STORE操作将先于所有在写屏障之后的STORE操作。

(2) 数据依赖屏障。两条Load指令,第二条Load指令依赖于第一条Load指令的结果,则数据依赖屏障保障第二条指令的目标地址将被更新。

(3) 读(LOAD)内存屏障。读屏障包含数据依赖屏障的功能, 并且保证所有出现在屏障之前的LOAD操作都将先于所有出现在屏障之后的LOAD操作被系统中的其他组件所感知.

(4) 通用内存屏障. 通用内存屏障保证所有出现在屏障之前的LOAD和STORE操作都将先于所有出现在屏障之后的LOAD和STORE操作被系统中的其他组件所感知.

(5) LOCK操作.它的作用相当于一个单向渗透屏障.它保证所有出现在LOCK之后的内存操作都将在LOCK操作被系统中的其他组件所感知之后才能发生. 出现在LOCK之前的内存操作可能在LOCK完成之后才发生.LOCK操作总是跟UNLOCK操作配对出现.

(6) UNLOCK操作。它保证所有出现在UNLOCK之前的内存操作都将在UNLOCK操作被系统中的其他组件所感知之前发生.

LINUX对于x86而言,在为UP体系统架构下,调用barrier()进行通用内存屏障。在SMP体系架构下,若为64位CPU或支持mfence、lfence、sfence指令的32位CPU,则smp_mb()、smp_rmb()、smp_smb()对应通用内存屏障、写屏障和读屏障;而不支持mfence、lfence、sfence指令的32位CPU则smp_mb()、smp_rmb()、smp_smb()对应LOCK操作。源码请参见《内存屏障源码分析》一节。

内存屏障源码分析

/include/asm-generic/system.h:

1
2
3
4
5
6
7
8
9
053 #ifdef CONFIG_SMP
054 #define smp_mb()        mb()
055 #define smp_rmb()       rmb()
056 #define smp_wmb()       wmb()
057 #else
058 #define smp_mb()        barrier()
059 #define smp_rmb()       barrier()
060 #define smp_wmb()       barrier()
061 #endif

在x86 UP体系架构中,smp_mb、smp_rmb、smp_wmb被翻译成barrier:

1
012 #define barrier() __asm__ __volatile__("": : :"memory")

__volatile告诉编译器此条语句不进行任何优化,"“: : :"memory” 内存单元已被修改、需要重新读入。

在x86 SMP体系架构中,smp_mb、smp_rmb、smp_wmb如下定义:

/arch/x86/include/asm/system.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
352 /*
353  * Force strict CPU ordering.
354  * And yes, this is required on UP too when we're talking
355  * to devices.
356  */
357 #ifdef CONFIG_X86_32
358 /*
359  * Some non-Intel clones support out of order store. wmb() ceases to be a
360  * nop for these.
361  */
362 #define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
363 #define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
364 #define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
365 #else
366 #define mb()    asm volatile("mfence":::"memory")
367 #define rmb()   asm volatile("lfence":::"memory")
368 #define wmb()   asm volatile("sfence" ::: "memory")
369 #endif

362~364行针对x86的32位CPU,366~368行针对x86的64位CPU。

在x86的64位CPU中,mb()宏实际为:

1
asm volatile("sfence" ::: "memory")。

volatile告诉编译器严禁在此处汇编语句与其它语句重组优化,memory强制编译器假设RAM所有内存单元均被汇编指令修改,"sfence" ::: 表示在此插入一条串行化汇编指令sfence。

mfence:串行化发生在mfence指令之前的读写操作

lfence:串行化发生在mfence指令之前的读操作、但不影响写操作

sfence:串行化发生在mfence指令之前的写操作、但不影响读操作

在x86的32位CPU中,mb()宏实际为:

1
mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)

由于x86的32位CPU有可能不提供mfence、lfence、sfence三条汇编指令的支持,故在不支持mfence的指令中使用:"lock; addl $0,0(%%esp)“, "mfence"。lock表示将“addl $0,0(%%esp)”语句作为内存屏障。

关于lock的实现:cpu上有一根pin #HLOCK连到北桥,lock前缀会在执行这条指令前先去拉这根pin,持续到这个指令结束时放开#HLOCK pin,在这期间,北桥会屏蔽掉一切外设以及AGP的内存操作。也就保证了这条指令的atomic。

参考资料

《memroy-barries.txt》,/Documentation/memory-barriers.txt

《LINUX内核内存屏障》,http://blog.csdn.net/ljl1603/article/details/6793982

SYN-ACK 重传

fastopen synack 重传

1
2
3
4
5
6
7
8
int tcp_conn_request(struct request_sock_ops *rsk_ops,
			const struct tcp_request_sock_ops *af_ops,
			struct sock *sk, struct sk_buff *skb)
{
	...
	fastopen = !want_cookie &&
			tcp_try_fastopen(sk, skb, req, &foc, dst);
	...
1
2
3
4
5
6
7
8
bool tcp_try_fastopen(struct sock *sk, struct sk_buff *skb,
			struct request_sock *req,
			struct tcp_fastopen_cookie *foc,
			struct dst_entry *dst)
{
	...
	if (tcp_fastopen_create_child(sk, skb, dst, req)) {
	...
1
2
3
4
5
6
7
8
9
10
11
12
static bool tcp_fastopen_create_child(struct sock *sk,
				struct sk_buff *skb,
				struct dst_entry *dst,
				struct request_sock *req)
{
	...
	child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
	...
	inet_csk_reset_xmit_timer(child, ICSK_TIME_RETRANS,
				TCP_TIMEOUT_INIT, TCP_RTO_MAX);
	...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void tcp_retransmit_timer(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);

	if (tp->fastopen_rsk) {
		WARN_ON_ONCE(sk->sk_state != TCP_SYN_RECV &&
			sk->sk_state != TCP_FIN_WAIT1);
		tcp_fastopen_synack_timer(sk);
		/* Before we receive ACK to our SYN-ACK don't retransmit
		 * anything else (e.g., data or FIN segments).
		 */
		return;
	}
	...
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
/*
 *      Timer for Fast Open socket to retransmit SYNACK. Note that the
 *      sk here is the child socket, not the parent (listener) socket.
 */
static void tcp_fastopen_synack_timer(struct sock *sk)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	int max_retries = icsk->icsk_syn_retries ? :
		sysctl_tcp_synack_retries + 1; /* add one more retry for fastopen */
	struct request_sock *req;

	req = tcp_sk(sk)->fastopen_rsk;
	req->rsk_ops->syn_ack_timeout(sk, req);

	if (req->num_timeout >= max_retries) {
		tcp_write_err(sk);
		return;
	}
	/* XXX (TFO) - Unlike regular SYN-ACK retransmit, we ignore error
	 * returned from rtx_syn_ack() to make it more persistent like
	 * regular retransmit because if the child socket has been accepted
	 * it's not good to give up too easily.
	 */
	inet_rtx_syn_ack(sk, req);
	req->num_timeout++;
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
		TCP_TIMEOUT_INIT << req->num_timeout, TCP_RTO_MAX);
}

http://blog.csdn.net/u011130578/article/details/44954891

1 Why

TCP服务器在收到SYN请求后发送SYN|ACK响应,然后等待对端的ACK到来以完成三次握手。如果没有收到ACK,TCP应该重传SYN|ACK,这个功能由SYN-ACK定时器完成。由于SYN|ACK发送后并没有放入发送队列中,故重传时必须重新构建SYN|ACK报文。

2 When

TCP在发送SYN|ACK响应后设置SYN-ACK定时器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1465 int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
1466 {
...
1598     skb_synack = tcp_make_synack(sk, dst, req,
1599         fastopen_cookie_present(&valid_foc) ? &valid_foc : NULL); //构建SYN|ACK
1600
1601     if (skb_synack) {
1602         __tcp_v4_send_check(skb_synack, ireq->loc_addr, ireq->rmt_addr);
1603         skb_set_queue_mapping(skb_synack, skb_get_queue_mapping(skb));
1604     } else
1605         goto drop_and_free;
1606
1607     if (likely(!do_fastopen)) {
1608         int err;
1609         err = ip_build_and_send_pkt(skb_synack, sk, ireq->loc_addr,
1610              ireq->rmt_addr, ireq->opt); //发送SYN|ACK
...
1617         /* Add the request_sock to the SYN table */
1618         inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT); //将requese sock加入到SYN表中,并设置SYN-ACK定时器
...

inet_csk_reqsk_queue_hash_add函数:

1
2
3
4
5
6
7
8
9
10
11
521 void inet_csk_reqsk_queue_hash_add(struct sock *sk, struct request_sock *req,
522                    unsigned long timeout)
523 {
524     struct inet_connection_sock *icsk = inet_csk(sk);
525     struct listen_sock *lopt = icsk->icsk_accept_queue.listen_opt;
526     const u32 h = inet_synq_hash(inet_rsk(req)->rmt_addr, inet_rsk(req)->rmt_port,
527                      lopt->hash_rnd, lopt->nr_table_entries);
528
529     reqsk_queue_hash_req(&icsk->icsk_accept_queue, h, req, timeout); //将request_sock放入syn_table中并记录超时时间
530     inet_csk_reqsk_queue_added(sk, timeout); //设置SYN-ACK定时器
531 }

reqsk_queue_hash_req函数会记录request_sock的超时时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
262 static inline void reqsk_queue_hash_req(struct request_sock_queue *queue,
263                     u32 hash, struct request_sock *req,
264                     unsigned long timeout)
265 {
266     struct listen_sock *lopt = queue->listen_opt;
267
268     req->expires = jiffies + timeout; //超时时间
269     req->num_retrans = 0;
270     req->num_timeout = 0;
271     req->sk = NULL;
272     req->dl_next = lopt->syn_table[hash];
273
274     write_lock(&queue->syn_wait_lock);
275     lopt->syn_table[hash] = req;
276     write_unlock(&queue->syn_wait_lock);
277 }

inet_csk_reqsk_queue_added函数为整个syn_table设置一个SYN-ACK定时器:

1
2
3
4
5
6
280 static inline void inet_csk_reqsk_queue_added(struct sock *sk,
281                           const unsigned long timeout)
282 {
283     if (reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue) == 0) //如果添加request sock之前syn_table为空
284         inet_csk_reset_keepalive_timer(sk, timeout);//设置SYN-ACK定时器
285 }

inet_csk_reset_keepalive_timer函数真正设置定时器:

1
2
3
4
404 void inet_csk_reset_keepalive_timer(struct sock *sk, unsigned long len)
405 {
406     sk_reset_timer(sk, &sk->sk_timer, jiffies + len);
407 }

SYN-ACK定时器的超时时间为TCP_TIMEOUT_INIT(1秒)。

3 What

SYN-ACK定时器的结构为sk->sk_timer,其超时函数为tcp_keepalive_timer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
558 static void tcp_keepalive_timer (unsigned long data)
559 {
560     struct sock *sk = (struct sock *) data;
561     struct inet_connection_sock *icsk = inet_csk(sk);
562     struct tcp_sock *tp = tcp_sk(sk);
563     u32 elapsed;
564
565     /* Only process if socket is not in use. */
566     bh_lock_sock(sk);
567     if (sock_owned_by_user(sk)) {
568         /* Try again later. */
569         inet_csk_reset_keepalive_timer (sk, HZ/20);
570         goto out;
571     }
572
573     if (sk->sk_state == TCP_LISTEN) { //如果是SYN-ACK定时器超时则判断为真
574         tcp_synack_timer(sk); //SYN-ACK定时器超时函数
575         goto out;
576     }
...

tcp_synack_timer函数:

1
2
3
4
5
534 static void tcp_synack_timer(struct sock *sk)
535 {
536     inet_csk_reqsk_queue_prune(sk, TCP_SYNQ_INTERVAL,
537                    TCP_TIMEOUT_INIT, TCP_RTO_MAX);
538 }

inet_csk_reqsk_queue_prune函数:

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
77
570 void inet_csk_reqsk_queue_prune(struct sock *parent,
571                 const unsigned long interval,
572                 const unsigned long timeout,
573                 const unsigned long max_rto)
574 {
575     struct inet_connection_sock *icsk = inet_csk(parent);
576     struct request_sock_queue *queue = &icsk->icsk_accept_queue;
577     struct listen_sock *lopt = queue->listen_opt;
578     int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries;
579     int thresh = max_retries;
580     unsigned long now = jiffies;
581     struct request_sock **reqp, *req;
582     int i, budget;
583
584     if (lopt == NULL || lopt->qlen == 0)
585         return;
...
604     if (lopt->qlen>>(lopt->max_qlen_log-1)) {
605         int young = (lopt->qlen_young<<1);
606
607         while (thresh > 2) {
608             if (lopt->qlen < young)
609                 break;
610             thresh--;
611             young <<= 1;
612         }
613     }
614
615     if (queue->rskq_defer_accept)    //需要等待数据到来再唤醒应用进程
616         max_retries = queue->rskq_defer_accept;
617
618     budget = 2 * (lopt->nr_table_entries / (timeout / interval));
619     i = lopt->clock_hand;
620
621     do {  //遍历SYN table
622         reqp=&lopt->syn_table[i];
623         while ((req = *reqp) != NULL) {
624             if (time_after_eq(now, req->expires)) { //超时
625                 int expire = 0, resend = 0;
626
627                 syn_ack_recalc(req, thresh, max_retries,
628                            queue->rskq_defer_accept,
629                            &expire, &resend);     //计算request sock是否过期以及是否需要重发SYN|ACK
630                 req->rsk_ops->syn_ack_timeout(parent, req); //调用tcp_syn_ack_timeout更新信息数据库
631                 if (!expire &&  //request socket没有超时
632                     (!resend ||
633                      !inet_rtx_syn_ack(parent, req) ||  //重传SYN-ACK
634                      inet_rsk(req)->acked)) {
635                     unsigned long timeo;
636
637                     if (req->num_timeout++ == 0)
638                         lopt->qlen_young--;
639                     timeo = min(timeout << req->num_timeout,
640                             max_rto);
641                     req->expires = now + timeo;  //更新request_sock超时时间
642                     reqp = &req->dl_next;
643                     continue;
644                 }
645
646                 /* Drop this request */
647                 inet_csk_reqsk_queue_unlink(parent, req, reqp); 
648                 reqsk_queue_removed(queue, req);
649                 reqsk_free(req);
650                 continue;
651             }
652             reqp = &req->dl_next;
653         }
654
655         i = (i + 1) & (lopt->nr_table_entries - 1);
656
657     } while (--budget > 0);
658
659     lopt->clock_hand = i;
660
661     if (lopt->qlen) //syn_table中还有成员
662         inet_csk_reset_keepalive_timer(parent, interval); //继续设置定时器,超时
663 }

604-611:当syn_table中剩余空间比较小时,需要减小最大重试次数,以便使旧的request_sock能够更快消亡,从而新的request_sock能够更多的被接受

647-649:将超时的request_sock移出syn_table并释放,即丢弃其对应的连接

631-642:全部满足下列条件就不删除request_sock而只是更新超时时间:
(1)request_sock没有超时
(2)下列3个条件之一成立
1)不需要重传SYN|ACK
2)重传SYN|ACK成功
3)应用进程使用TCP_DEFER_ACCEPT socket选项意图使数据到来时listen socket再唤醒进程,当ACK到来但没有数据时

syn_ack_recalc函数来确定request_sock是否超时以及是否需要重传SYN|ACK:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
539 static inline void syn_ack_recalc(struct request_sock *req, const int thresh,
540                   const int max_retries,
541                   const u8 rskq_defer_accept,
542                   int *expire, int *resend)
543 {
544     if (!rskq_defer_accept) {    //不需要等待数据到来再调用accept系统调用
545         *expire = req->num_timeout >= thresh;    //超时次数达到限制则超时
546         *resend = 1;    //重传SYN|ACK
547         return;
548     }
549     *expire = req->num_timeout >= thresh &&    //超时次数达到限制
550           (!inet_rsk(req)->acked || req->num_timeout >= max_retries);    //ACK没有到来或超时次数达到最高上限
551     /*
552      * Do not resend while waiting for data after ACK,
553      * start to resend on end of deferring period to give
554      * last chance for data or ACK to create established socket.
555      */
556     *resend = !inet_rsk(req)->acked || //ACK没有到来
557           req->num_timeout >= rskq_defer_accept - 1;    //超时次数超过或即将达到应用进程的限制,赶快重传SYN|ACK以便给对端最后一个机会建立连接
558 }

综上,SYN|ACK定时器超时时重传SYN|ACK的条件是下列条件全部成立:
(1)request_sock超时
(2)request_sock的超时次数达到限制
(3)下列条件之一成立:
1)应用进程没有使用TCP_DEFER_ACCEPT socket选项来延迟accept request_sock的时间
2)应用进程使用TCP_DEFER_ACCEPT socket选项设置了超时次数限制,但ACK没有到来或,超时次数达到最高限制且超时次数超过或即将达到应用进程的限制

SYN|ACK的重传是由inet_rtx_syn_ack函数完成的:

1
2
3
4
5
6
7
8
560 int inet_rtx_syn_ack(struct sock *parent, struct request_sock *req)
561 {
562     int err = req->rsk_ops->rtx_syn_ack(parent, req);    //指向tcp_v4_rtx_synack或tcp_v6_rtx_synack
563
564     if (!err)
565         req->num_retrans++;
566     return err;
567 }

tcp_v4_rtx_synack函数:

1
2
3
4
5
6
7
8
870 static int tcp_v4_rtx_synack(struct sock *sk, struct request_sock *req)
871 {
872     int res = tcp_v4_send_synack(sk, NULL, req, 0, false); //构建并发送SYN-ACK
873
874     if (!res)
875         TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_RETRANSSEGS);
876     return res;
877 }

TCP Fast Open(TFO), tcp_fastopen

https://www.2cto.com/kf/201701/586043.html

http://blog.csdn.net/u011130578/article/details/44515165

http://blog.sina.com.cn/s/blog_583f42f101011veh.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define TFO_CLIENT_ENABLE       1
#define TFO_SERVER_ENABLE       2
#define TFO_CLIENT_NO_COOKIE    4       /* Data in SYN w/o cookie option */

/* Process SYN data but skip cookie validation */
#define TFO_SERVER_COOKIE_NOT_CHKED     0x100 // 收到cookie也不检查
/* Accept SYN data w/o any cookie option */
#define TFO_SERVER_COOKIE_NOT_REQD      0x200 // 不需要cookie需要data就能创建fastopen child,默认情况下syn的data会被忽略

/* Force enable TFO on all listeners, i.e., not requiring the
 * TCP_FASTOPEN socket option. SOCKOPT1/2 determine how to set max_qlen.
 */
#define TFO_SERVER_WO_SOCKOPT1  0x400     // 调listen后不需要再调setsockopt就开启fastopen
#define TFO_SERVER_WO_SOCKOPT2  0x800     // 调listen后不需要再调setsockopt就开启fastopen,backlog=TFO_SERVER_WO_SOCKOPT2>>16
/* Always create TFO child sockets on a TFO listener even when
 * cookie/data not present. (For testing purpose!)
 */
#define TFO_SERVER_ALWAYS       0x1000        // 不需要cookie也不需要data就创建fastopen child, 容易被攻击,不开启

测试

开启

1
2
3
4
echo 3 > /proc/sys/net/ipv4/tcp_fastopen   # 1 开启客户端,2 开启服务端,3 都开启

tc qdisc add dev lo root netem delay 300ms # 设置延迟才能看出效果
ifconfig lo mtu 1500

client

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
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include <netinet/tcp.h>

#ifndef MSG_FASTOPEN
#define MSG_FASTOPEN   0x20000000
#endif

int main(int argc, char *argv[])
{
	int sockfd, n;
	struct sockaddr_in servaddr;
	char buf[50000] = "aaabbbccc";
	int ret = 0, tot;

	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		printf ("create socket error: %s(errno: %d)\n", strerror (errno), errno);
		return -1;
	}

	memset (&servaddr, 0, sizeof (servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons (1935);
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

#define FASTOPEN_TEST
#ifndef FASTOPEN_TEST
	if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) {
		printf("connect error\n");
		return -2;
	}
	ret = send(sockfd, buf, 1005, 0);
#else
	ret = sendto(sockfd, buf, 1005, MSG_FASTOPEN, (struct sockaddr *)&servaddr, sizeof(servaddr));
#endif
	if (ret < 0) {
		printf ("send msg error: %s(errno: %d)\n", strerror (errno), errno);
		// 如果是连接失败会打印:Connection refused(errno: 111)
		return -2;
	}
	printf("client fastopen sendto len=%d\n", ret);
	if ((ret = send(sockfd, buf, 20000, 0)) < 0) {
		printf("send error ret = %d\n", ret);
	}
	printf("client send len = %d\n", ret);
	shutdown(sockfd, 1);

	tot = 0;
	while ((n = recv(sockfd, buf, 1024, 0)) > 0)
		tot += n;
	printf("client recv len = %d\n", tot);
	close (sockfd);
	return 0;
}

server

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
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdio.h>

int main()
{
	int serverSock, clientSock;
	struct sockaddr_in addr, clientAddr;
	int addrLen;

	char buf[10240];
	int n, tot;

	serverSock = socket(AF_INET, SOCK_STREAM, 0);
	if (serverSock == -1) {
		printf("socket failed!\n");
		return -1;
	}

	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(1935);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	if (bind(serverSock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
		printf("bind failed!\n");
		return -2;
	}

	int qlen = 5;
	setsockopt(serverSock, SOL_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));

	if (listen(serverSock, 511) < 0) {
		printf("listen failed!\n");
		return -3;
	}

	while (1) {
		addrLen = sizeof(clientAddr);
		clientSock = accept(serverSock, (struct sockaddr*)&clientAddr, &addrLen);
		if (clientSock < 0) {
			printf("accept failed!\n");
			return -4;
		}

		if ((n = send(clientSock, buf, 10000, 0)) < 0) {
			printf("send error ret = %d\n", n);
			return -5;
		}
		printf("server send len = %d\n", n);
		shutdown(clientSock, 1);

		sleep(1);

		tot = 0;
		while ((n = recv(clientSock, buf, 1024, 0)) > 0)
			tot += n;
		printf("server recv len = %d\n", tot);
		close(clientSock);
	}

	return 0;
}

原理

1.客户端发送一个SYN包到服务器,这个包中携带了Fast Open Cookie Request;

2.服务器生成一个cookie,这个cookie是加密客户端的IP地址生成的。服务器给客户端发送SYN+ACK响应,在响应包的选项中包含了这个cookie;

3.客户端存储这个cookie以便将来再次与这个服务器的IP建立TFO连接时使用;

也就是说,第一次TCP连接只是交换cookie信息,无法在SYN包中携带数据。在第一次交换之后,接下来的TCP连接就可以在SYN中携带数据了。流程如下:

4.客户端发送一个SYN包,这个包比较特殊,因为它携带应用数据和cookie;

5.服务器验证这个cookie,如果合法,服务器发送一个SYN+ACK,这个ACK同时确认SYN和数据。然后数据被传递到应用进程;

如果不合法,服务器丢弃数据,发送一个SYN+ACK,这个ACK只确认SYN,接下来走三次握手的普通流程;

6.如果验证合法(接收了SYN包中的数据),服务器在接收到客户端的第一个ACK前可以发送其它响应数据;

7.如果验证不合法(客户端在SYN中带的数据没被确认),客户端发送ACK确认服务器的SYN;并且,数据会在ACK包中重传;

8.下面的流程与普通的TCP交互流程无异。

源码分析

TFO功能在Linux 2.6.34内核中开始集成。

下面通过分析内核代码来了解TFO的运行机制。开启TFO功能后,server端进程在调用listen系统调用时会初始化TFO队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int inet_listen(struct socket *sock, int backlog)
{
	struct sock *sk = sock->sk;
	unsigned char old_state;
	int err;
	...
	if (old_state != TCP_LISTEN) {
	...
		if ((sysctl_tcp_fastopen & TFO_SERVER_ENABLE) != 0 &&
			inet_csk(sk)->icsk_accept_queue.fastopenq == NULL) {
			if ((sysctl_tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) != 0)
				err = fastopen_init_queue(sk, backlog);
			else if ((sysctl_tcp_fastopen &
				  TFO_SERVER_WO_SOCKOPT2) != 0)
				err = fastopen_init_queue(sk,
					((uint)sysctl_tcp_fastopen) >> 16);
			else
				err = 0;
			if (err)
				goto out;
		}
		err = inet_csk_listen_start(sk, backlog);
...

fastopen_init_queue函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static inline int fastopen_init_queue(struct sock *sk, int backlog)
{
	struct request_sock_queue *queue =
		&inet_csk(sk)->icsk_accept_queue;

	if (queue->fastopenq == NULL) {
		queue->fastopenq = kzalloc(
			sizeof(struct fastopen_queue),
			sk->sk_allocation);
		if (queue->fastopenq == NULL)
			return -ENOMEM;

		sk->sk_destruct = tcp_sock_destruct;
		spin_lock_init(&queue->fastopenq->lock);
	}
	queue->fastopenq->max_qlen = backlog;
	return 0;
}

如果net.ipv4.tcp_fastopen && (TFO_SERVER_WO_SOCKOPT1|TFO_SERVER_WO_SOCKOPT2)为假,则TFO队列不会被初始化。但setsockopt函数也可以初始化TFO队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int do_tcp_setsockopt(struct sock *sk, int level,
		int optname, char __user *optval, unsigned int optlen)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	int val;
	int err = 0;
	...
	case TCP_FASTOPEN:
		if (val >= 0 && ((1 << sk->sk_state) & (TCPF_CLOSE |
			TCPF_LISTEN)))
			err = fastopen_init_queue(sk, val);
		else
			err = -EINVAL;
		break;
	...

如果inet_csk(sk)->icsk_accept_queue.fastopenq为NULL的话意味着TFO功能未开启。

轮到client端出场了!client端的sendto系统调用在内核中对应的TCP函数是tcp_sendmsg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t size)
{
	struct iovec *iov;
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *skb;
	int iovlen, flags, err, copied = 0;
	int mss_now = 0, size_goal, copied_syn = 0, offset = 0;
	bool sg;
	long timeo;

	lock_sock(sk);

	flags = msg->msg_flags;
	if (flags & MSG_FASTOPEN) {//要使用TFO功能
		err = tcp_sendmsg_fastopen(sk, msg, &copied_syn);//发送TFO数据
		if (err == -EINPROGRESS && copied_syn > 0)
			goto out;
		else if (err)
			goto out_err;
		offset = copied_syn;
	}

tcp_sendmsg_fastopen函数用于发送带TFO请求的SYN或携带数据的SYN:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int tcp_sendmsg_fastopen(struct sock *sk, struct msghdr *msg, int *size)
{
	struct tcp_sock *tp = tcp_sk(sk);
	int err, flags;

	if (!(sysctl_tcp_fastopen & TFO_CLIENT_ENABLE))
		return -EOPNOTSUPP;
	if (tp->fastopen_req != NULL)
		return -EALREADY; /* Another Fast Open is in progress */

	tp->fastopen_req = kzalloc(sizeof(struct tcp_fastopen_request),
				   sk->sk_allocation);
	if (unlikely(tp->fastopen_req == NULL))
		return -ENOBUFS;
	tp->fastopen_req->data = msg;

	flags = (msg->msg_flags & MSG_DONTWAIT) ? O_NONBLOCK : 0;
	err = __inet_stream_connect(sk->sk_socket, msg->msg_name,
					msg->msg_namelen, flags);      //发送连接请求
	*size = tp->fastopen_req->copied; //记录发送了多少数据,如果发送的是TFO请求则*size为0
	tcp_free_fastopen_req(tp);
	return err;
}

  __inet_stream_connect函数会调用tcp_connect函数发送SYN:

1
2
3
4
5
6
7
8
9
int tcp_connect(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *buff;
	int err;
	...
	/* Send off SYN; include data in Fast Open. */
	err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
		  tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); //如果使用TFO,则会调用tcp_send_syn_data发送SYN

tcp_send_syn_data函数:

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
77
78
79
80
81
static int tcp_send_syn_data(struct sock *sk, struct sk_buff *syn)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct tcp_fastopen_request *fo = tp->fastopen_req;
	int syn_loss = 0, space, i, err = 0, iovlen = fo->data->msg_iovlen;
	struct sk_buff *syn_data = NULL, *data;
	unsigned long last_syn_loss = 0;

	tp->rx_opt.mss_clamp = tp->advmss;  /* If MSS is not cached */
	tcp_fastopen_cache_get(sk, &tp->rx_opt.mss_clamp, &fo->cookie,
				   &syn_loss, &last_syn_loss);//查询缓存的TFO cookie信息
	/* Recurring FO SYN losses: revert to regular handshake temporarily */
	if (syn_loss > 1 &&
		time_before(jiffies, last_syn_loss + (60*HZ << syn_loss))) {
		fo->cookie.len = -1;
		goto fallback;
	}

	if (sysctl_tcp_fastopen & TFO_CLIENT_NO_COOKIE)//无论有没有cookie,都发送携带数据的SYN
		fo->cookie.len = -1;
	else if (fo->cookie.len <= 0)      //没有cookie,发送携带TFO请求选项的SYN
		goto fallback;

	/* MSS for SYN-data is based on cached MSS and bounded by PMTU and
	 * user-MSS. Reserve maximum option space for middleboxes that add
	 * private TCP options. The cost is reduced data space in SYN :(
	 */
	if (tp->rx_opt.user_mss && tp->rx_opt.user_mss < tp->rx_opt.mss_clamp)
		tp->rx_opt.mss_clamp = tp->rx_opt.user_mss;
	space = __tcp_mtu_to_mss(sk, inet_csk(sk)->icsk_pmtu_cookie) -
		MAX_TCP_OPTION_SPACE;//计算SYN包中的能够携带的数据的最大大小

	syn_data = skb_copy_expand(syn, skb_headroom(syn), space,
				   sk->sk_allocation);//复制SYN包中的内容,并扩展SKB中的空间
	if (syn_data == NULL)
		goto fallback;

	for (i = 0; i < iovlen && syn_data->len < space; ++i) {//将用户态中缓存的数据copy到内核
		struct iovec *iov = &fo->data->msg_iov[i];
		unsigned char __user *from = iov->iov_base;
		int len = iov->iov_len;

		if (syn_data->len + len > space)//数据总长度大于SKB中空间的总大小
			len = space - syn_data->len;
		else if (i + 1 == iovlen)
			/* No more data pending in inet_wait_for_connect() */
			fo->data = NULL;//数据全部发送完毕,不需要在inet_wait_for_connect中等待时发送

		if (skb_add_data(syn_data, from, len))//将用户数据copy到SKB中
			goto fallback;
	}

	/* Queue a data-only packet after the regular SYN for retransmission */
	data = pskb_copy(syn_data, sk->sk_allocation);
	if (data == NULL)
		goto fallback;
	TCP_SKB_CB(data)->seq++;
	TCP_SKB_CB(data)->tcp_flags &= ~TCPHDR_SYN;
	TCP_SKB_CB(data)->tcp_flags = (TCPHDR_ACK|TCPHDR_PSH);
	tcp_connect_queue_skb(sk, data);
	fo->copied = data->len;

	if (tcp_transmit_skb(sk, syn_data, 0, sk->sk_allocation) == 0) {//发送携带数据的SYN
		tp->syn_data = (fo->copied > 0);
		NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFASTOPENACTIVE);
		goto done;
	}
	syn_data = NULL;

fallback:
	/* Send a regular SYN with Fast Open cookie request option */
	if (fo->cookie.len > 0)
		fo->cookie.len = 0;
	err = tcp_transmit_skb(sk, syn, 1, sk->sk_allocation);
	if (err)
		tp->syn_fastopen = 0;
	kfree_skb(syn_data);
done:
	fo->cookie.len = -1;  /* Exclude Fast Open option for SYN retries */
	return err;
}

如果client是发送TFO请求,则tcp_send_syn_data函数会发送一个不带数据的SYN包,数据部分则会由tcp_sendmsg函数放入发送队列中,等待三次握手完成后再发送。

tcp_transmit_skb函数会调用tcp_syn_options函数构建选项信息,tcp_options_write函数负责将选项写入TCP报头中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
				struct tcp_out_options *opts,
				struct tcp_md5sig_key **md5)
{
	struct tcp_sock *tp = tcp_sk(sk);
	unsigned int remaining = MAX_TCP_OPTION_SPACE;
	struct tcp_fastopen_request *fastopen = tp->fastopen_req;
	...
	if (fastopen && fastopen->cookie.len >= 0) {
		u32 need = TCPOLEN_EXP_FASTOPEN_BASE + fastopen->cookie.len;
		need = (need + 3) & ~3U;  /* Align to 32 bits */
		if (remaining >= need) {
			opts->options |= OPTION_FAST_OPEN_COOKIE;
			opts->fastopen_cookie = &fastopen->cookie;
			remaining -= need;
			tp->syn_fastopen = 1;
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void tcp_options_write(__be32 *ptr, struct tcp_sock *tp,
				  struct tcp_out_options *opts)
{
	u16 options = opts->options;    /* mungable copy */
	...
	if (unlikely(OPTION_FAST_OPEN_COOKIE & options)) {
		struct tcp_fastopen_cookie *foc = opts->fastopen_cookie;

		*ptr++ = htonl((TCPOPT_EXP << 24) |
				   ((TCPOLEN_EXP_FASTOPEN_BASE + foc->len) << 16) |
				   TCPOPT_FASTOPEN_MAGIC);

		memcpy(ptr, foc->val, foc->len);  //如果找到了TFO cookie,则写入;没有RFO cookie则仅仅是一个TFO请求
		if ((foc->len & 3) == 2) {
			u8 *align = ((u8 *)ptr) + foc->len;
			align[0] = align[1] = TCPOPT_NOP;
		}
		ptr += (foc->len + 3) >> 2;
	}

client端在每次使用TFO功能时都会在TCP的选项中添加一个TFO选项,与server端进行第一次TFO交互时TFO选项只有4字节长,其值是一个“MAGIC”,这种TFO被称为“TFO请求”;后续的TFO选项长度会增加一个从服务器端获得的TFO cookie的长度值,并且在这个SYN中会携带数据。

server收到SYN后,会在tcp_v4_conn_request中进行处理:

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
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
	struct tcp_options_received tmp_opt;
	struct request_sock *req;
	struct inet_request_sock *ireq;
	struct tcp_sock *tp = tcp_sk(sk);
	struct dst_entry *dst = NULL;
	__be32 saddr = ip_hdr(skb)->saddr;
	__be32 daddr = ip_hdr(skb)->daddr;
	__u32 isn = TCP_SKB_CB(skb)->when;
	bool want_cookie = false;
	struct flowi4 fl4;
	struct tcp_fastopen_cookie foc = { .len = -1 };
	struct tcp_fastopen_cookie valid_foc = { .len = -1 };
	struct sk_buff *skb_synack;
	int do_fastopen;
	...
	tcp_parse_options(skb, &tmp_opt, 0, want_cookie ? NULL : &foc);//解析TFO选项
	...
	do_fastopen = tcp_fastopen_check(sk, skb, req, &foc, &valid_foc);//检查TFO选项的合法性
	...
	skb_synack = tcp_make_synack(sk, dst, req,
		fastopen_cookie_present(&valid_foc) ? &valid_foc : NULL);//如果客户端发送的是TFO请求则发送TFO cookie,否则不发送
	...
	if (likely(!do_fastopen)) {
	...
	} else if (tcp_v4_conn_req_fastopen(sk, skb, skb_synack, req))//创建子sock,将SYN中的数据放入socekt中的接收队列中
		goto drop_and_free;

	return 0;

tcp_fastopen_check函数用于检查SYN中TFO请求的合法性以及生成TFO cookie:

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
static bool tcp_fastopen_check(struct sock *sk, struct sk_buff *skb,
				   struct request_sock *req,
				   struct tcp_fastopen_cookie *foc,
				   struct tcp_fastopen_cookie *valid_foc)
{
	bool skip_cookie = false;
	struct fastopen_queue *fastopenq;

	if (likely(!fastopen_cookie_present(foc))) {//SYN中没有携带TFO选项
		/* See include/net/tcp.h for the meaning of these knobs */
		if ((sysctl_tcp_fastopen & TFO_SERVER_ALWAYS) ||
			((sysctl_tcp_fastopen & TFO_SERVER_COOKIE_NOT_REQD) &&
			(TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq + 1)))
			skip_cookie = true; /* no cookie to validate */  //无需校验cookie,直接允许SYN中携带数据
		else
			return false;
	}
	fastopenq = inet_csk(sk)->icsk_accept_queue.fastopenq;
	...
	if ((sysctl_tcp_fastopen & TFO_SERVER_ENABLE) == 0 ||
		fastopenq == NULL || fastopenq->max_qlen == 0)//未开启Server端TFO功能
		return false;

	if (fastopenq->qlen >= fastopenq->max_qlen) {//TFO队列已满
		struct request_sock *req1;
		spin_lock(&fastopenq->lock);
		req1 = fastopenq->rskq_rst_head;
		if ((req1 == NULL) || time_after(req1->expires, jiffies)) {
			spin_unlock(&fastopenq->lock);
			NET_INC_STATS_BH(sock_net(sk),
				LINUX_MIB_TCPFASTOPENLISTENOVERFLOW);
			/* Avoid bumping LINUX_MIB_TCPFASTOPENPASSIVEFAIL*/
			foc->len = -1;
			return false;
		}
		fastopenq->rskq_rst_head = req1->dl_next;//替换队列中最老的一个
		fastopenq->qlen--;
		spin_unlock(&fastopenq->lock);
		reqsk_free(req1);
	}
	if (skip_cookie) {//不使用cookie,直接接收数据
		tcp_rsk(req)->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
		return true;
	}
	if (foc->len == TCP_FASTOPEN_COOKIE_SIZE) {//SYN中携带了TFO cookie
		if ((sysctl_tcp_fastopen & TFO_SERVER_COOKIE_NOT_CHKED) == 0) {
			tcp_fastopen_cookie_gen(ip_hdr(skb)->saddr, valid_foc);//生成TFO cookie
			if ((valid_foc->len != TCP_FASTOPEN_COOKIE_SIZE) || //TFO初始化不成功
				memcmp(&foc->val[0], &valid_foc->val[0], //TFO cookie不合法
				TCP_FASTOPEN_COOKIE_SIZE) != 0)
				return false;
			valid_foc->len = -1;
		}
		/* Acknowledge the data received from the peer. */
		tcp_rsk(req)->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
		return true;
	} else if (foc->len == 0) { /* Client requesting a cookie */
		tcp_fastopen_cookie_gen(ip_hdr(skb)->saddr, valid_foc);//生成一个TFO cookie保存在valid_foc中
		NET_INC_STATS_BH(sock_net(sk),
			LINUX_MIB_TCPFASTOPENCOOKIEREQD);
	} else {
		/* Client sent a cookie with wrong size. Treat it
		 * the same as invalid and return a valid one.
		 */
		tcp_fastopen_cookie_gen(ip_hdr(skb)->saddr, valid_foc);
	}
	return false;
}

1327:rskq_rst_head为NULL的场景为有很多带TFO的SYN到来但SYN|ACK发送后并没有收到RST包,这意味着之前收到的那些带数据的TFO SYN可能是合法的;如果不为NULL但对立中最老的一个仍然没有超时的话,也不能将其替换

1344-1351:如果clienet端的TFO不是请求,而是cookie,则不设置valid_foc;另外如果server端被设置为不检查cookie的合法性,则生成一个cookie再检查SYN中的TFO cookie的合法性,如果不合法则不使用TFO功能。

tcp_make_synack函数会将tcp_fastopen_check中生成的TFO cookie写入TCP首部中,tcp_synack_options函数用来构建SYN|ACK报文的选项信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static unsigned int tcp_synack_options(struct sock *sk,
				   struct request_sock *req,
				   unsigned int mss, struct sk_buff *skb,
				   struct tcp_out_options *opts,
				   struct tcp_md5sig_key **md5,
				   struct tcp_fastopen_cookie *foc)
{
	...
	if (foc != NULL) {
		u32 need = TCPOLEN_EXP_FASTOPEN_BASE + foc->len;
		need = (need + 3) & ~3U;  /* Align to 32 bits */
		if (remaining >= need) {
			opts->options |= OPTION_FAST_OPEN_COOKIE;
			opts->fastopen_cookie = foc;
			remaining -= need;
		}
	}
	...

将选项信息写入SYN|ACK的方法与client发送SYN时一样,都是调用tcp_options_write函数。可以看出,TCP server端会返回给发送TFO请求的client端一个TFO cookie。client发送的下一个带数据的SYN必须携带这个cookie,而TCP server对这样的SYN回复的SYN|ACK中不会携带TFO选项。

在SYN携带TFO cookie的情况下TCP server会在收到SYN时就创建sock,这个功能由cp_v4_conn_req_fastopen函数完成:

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
77
78
79
80
81
82
static int tcp_v4_conn_req_fastopen(struct sock *sk,
					struct sk_buff *skb,
					struct sk_buff *skb_synack,
					struct request_sock *req)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
	const struct inet_request_sock *ireq = inet_rsk(req);
	struct sock *child;
	...

	child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);//生成子socket,其状态为TCP_SYN_RECV
	...
	err = ip_build_and_send_pkt(skb_synack, sk, ireq->loc_addr,
					ireq->rmt_addr, ireq->opt);//构建SYN|ACK的IP头并将其发送出去
	err = net_xmit_eval(err);
	if (!err)
		tcp_rsk(req)->snt_synack = tcp_time_stamp;
	/* XXX (TFO) - is it ok to ignore error and continue? */

	spin_lock(&queue->fastopenq->lock);
	queue->fastopenq->qlen++;//将这个连接计入TFO queue
	spin_unlock(&queue->fastopenq->lock);
	...
	tp = tcp_sk(child);

	tp->fastopen_rsk = req;
	/* Do a hold on the listner sk so that if the listener is being
	 * closed, the child that has been accepted can live on and still
	 * access listen_lock.
	 */
	sock_hold(sk);
	tcp_rsk(req)->listener = sk;

	/* RFC1323: The window in SYN & SYN/ACK segments is never
	 * scaled. So correct it appropriately.
	 */
	tp->snd_wnd = ntohs(tcp_hdr(skb)->window);

	/* Activate the retrans timer so that SYNACK can be retransmitted.
	 * The request socket is not added to the SYN table of the parent
	 * because it's been added to the accept queue directly.
	 */
	inet_csk_reset_xmit_timer(child, ICSK_TIME_RETRANS,
		TCP_TIMEOUT_INIT, TCP_RTO_MAX);

	/* Add the child socket directly into the accept queue */
	inet_csk_reqsk_queue_add(sk, req, child);

	/* Now finish processing the fastopen child socket. */
	inet_csk(child)->icsk_af_ops->rebuild_header(child);
	tcp_init_congestion_control(child);
	tcp_mtup_init(child);
	tcp_init_buffer_space(child);
	tcp_init_metrics(child);

	/* Queue the data carried in the SYN packet. We need to first
	 * bump skb's refcnt because the caller will attempt to free it.
	 *
	 * XXX (TFO) - we honor a zero-payload TFO request for now.
	 * (Any reason not to?)
	 */
	if (TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq + 1) {//SYN包中没有数据
		/* Don't queue the skb if there is no payload in SYN.
		 * XXX (TFO) - How about SYN+FIN?
		 */
		tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
	} else {
		skb = skb_get(skb);
		skb_dst_drop(skb);
		__skb_pull(skb, tcp_hdr(skb)->doff * 4);
		skb_set_owner_r(skb, child);
		__skb_queue_tail(&child->sk_receive_queue, skb);//将数据放入child的接收队列中
		tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
		tp->syn_data_acked = 1;
	}
	sk->sk_data_ready(sk, 0);//通知持有listening socket的进程调用accept系统调用创建新连接
	bh_unlock_sock(child);
	sock_put(child);
	WARN_ON(req->sk == NULL);
	return 0;
}

应用进程收到listening socket的可读通告后,使用accept系统调用建立socket,就可以立即从这个新的socket中读到数据,并开始与客户端进行数据交互。

如果client的TFO是cookie,则SYN|ACK的处理过程与不使用TFO的情况是一样的;如果client发送的TFO是请求,则在收到SYN|ACK时需要将包中的TFO cookie保存下来:

1
2
3
4
5
6
7
8
9
10
11
12
13
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
					 const struct tcphdr *th, unsigned int len)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	struct tcp_fastopen_cookie foc = { .len = -1 };
	int saved_clamp = tp->rx_opt.mss_clamp;

	tcp_parse_options(skb, &tp->rx_opt, 0, &foc);//解析TFO选项
	...
		if ((tp->syn_fastopen || tp->syn_data) && //如果发送过TFO选项或在SYN中发送过数据
			tcp_rcv_fastopen_synack(sk, skb, &foc))//记录SYN|ACK中的FTO cookie
			return -1;

tcp_rcv_fastopen_synack函数检查并保存server端发送的TFO cookie:

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
static bool tcp_rcv_fastopen_synack(struct sock *sk, struct sk_buff *synack,
					struct tcp_fastopen_cookie *cookie)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *data = tp->syn_data ? tcp_write_queue_head(sk) : NULL;
	u16 mss = tp->rx_opt.mss_clamp;
	bool syn_drop;

	if (mss == tp->rx_opt.user_mss) {
		struct tcp_options_received opt;

		/* Get original SYNACK MSS value if user MSS sets mss_clamp */
		tcp_clear_options(&opt);
		opt.user_mss = opt.mss_clamp = 0;
		tcp_parse_options(synack, &opt, 0, NULL);
		mss = opt.mss_clamp;
	}

	if (!tp->syn_fastopen)  /* Ignore an unsolicited cookie */
		cookie->len = -1;//如果客户端没有发送TFO请求但服务器给出了TFO cookie,忽略之

	/* The SYN-ACK neither has cookie nor acknowledges the data. Presumably
	 * the remote receives only the retransmitted (regular) SYNs: either
	 * the original SYN-data or the corresponding SYN-ACK is lost.
	 */
	syn_drop = (cookie->len <= 0 && data && tp->total_retrans); //客户端认为发生了SYN丢失事件

	tcp_fastopen_cache_set(sk, mss, cookie, syn_drop);//存储SYN|ACK包中的TFO cookie,并记录发现SYN丢失事件的时间

	if (data) { /* Retransmit unacked data in SYN */
		tcp_for_write_queue_from(data, sk) {
			if (data == tcp_send_head(sk) ||
				__tcp_retransmit_skb(sk, data))
				break;
		}
		tcp_rearm_rto(sk);
		return true;
	}
	tp->syn_data_acked = tp->syn_data;
	return false;
}

在保存了TFO cookie后,client在向相同IP地址的server发送SYN时都可以携带数据(这时必须发送TFO cookie)。client在收到SYN|ACK后需要回复ACK报文,服务器端在接收ACK时对TFO的处理如下:

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
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
			  const struct tcphdr *th, unsigned int len)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct request_sock *req;
	...
	req = tp->fastopen_rsk;//找到在SYN请求到来后创建子socket时使用的request sock
	if (req != NULL) {
		WARN_ON_ONCE(sk->sk_state != TCP_SYN_RECV &&
			sk->sk_state != TCP_FIN_WAIT1);

		if (tcp_check_req(sk, skb, req, NULL, true) == NULL)//检查包的合法性
			goto discard;
	}
	...
		switch (sk->sk_state) {
		case TCP_SYN_RECV:
			if (acceptable) {
				/* Once we leave TCP_SYN_RECV, we no longer
				 * need req so release it.
				 */
				if (req) {//使用了TFO cookie
					tcp_synack_rtt_meas(sk, req);
					tp->total_retrans = req->num_retrans;

					reqsk_fastopen_remove(sk, req, false);//将request sock从TFO queue中删除,TFO流程全部结束
				} else {
	...

综上,TFO在收到SYN的时候就创建socket并将数据提交给应用进程,这样就比普通模式节省了SYN|ACK与ACK的交互时间,减小了通信延迟。

Linux 内核线程及普通进程总结

http://cuckootan.me/2016/04/27/Linux/Linux%20%E5%86%85%E6%A0%B8%E7%BA%BF%E7%A8%8B%E5%8F%8A%E6%99%AE%E9%80%9A%E8%BF%9B%E7%A8%8B%E6%80%BB%E7%BB%93/

1 Linux 中的进程与线程

对于 Linux 来讲,所有的线程都当作进程来实现,因为没有单独为线程定义特定的调度算法,也没有单独为线程定义特定的数据结构(所有的线程或进程的核心数据结构都是 task_struct)。

对于一个进程,相当于是它含有一个线程,就是它自身。对于多线程来说,原本的进程称为主线程,它们在一起组成一个线程组。

进程拥有自己的地址空间,所以每个进程都有自己的页表。而线程却没有,只能和其它线程共享某一个地址空间和同一份页表。

这个区别的 根本原因 是,在进程/线程创建时,因是否拷贝当前进程的地址空间还是共享当前进程的地址空间,而使得指定的参数不同而导致的。

具体地说,进程和线程的创建都是执行 clone 系统调用进行的。而 clone 系统调用会执行 do_fork 内核函数,而它则又会调用 copy_process 内核函数来完成。主要包括如下操作:

在调用 copy_process 的过程中,会创建并拷贝当前进程的 task_stuct,同时还会创建属于子进程的 thread_info 结构以及内核栈。
此后,会为创建好的 task_stuct 指定一个新的 pid(在 task_struct 结构体中)。
然后根据传递给 clone 的参数标志,来选择拷贝还是共享打开的文件,文件系统信息,信号处理函数,进程地址空间等。这就是进程和线程不一样地方的本质所在。

2 三个数据结构

每个进程或线程都有三个数据结构,分别是 struct thread_info, struct task_struct 和 内核栈。

注意,虽然线程与主线程共享地址空间,但是线程也是有自己独立的内核栈的。

thread_info 对象中存放的进程/线程的基本信息,它和这个进程/线程的内核栈存放在内核空间里的一段 2 倍页长的空间中。其中 thread_info 结构存放在低地址段的末尾,其余空间用作内核栈。内核使用 伙伴系统 为每个进程/线程分配这块空间。

thread_info 结构体中有一个 struct task_struct *task,task 指向的就是这个进程或线程相关的 task_struct 对象(也在内核空间中),这个对象叫做进程描述符(叫做任务描述符更为贴切,因为每个线程也都有自己的 task_struct)。内核使用 slab 分配器为每个进程/线程分配这块空间。

如下图所示:

3 task_struct 结构体

每个进程或线程都有只属于自己的 task_struct 对象,是它们各自最为核心的数据结构。

3.1 task_struct 结构体中的主要元素

1
2
3
4
5
6
struct thread_info *thread_info。thread_info 指向该进程/线程的基本信息。
struct mm_struct *mm。mm_struct 对象用来管理该进程/线程的页表以及虚拟内存区。
struct mm_struct *active_mm。主要用于内核线程访问主内核页全局目录。
struct fs_struct *fs。fs_struct 是关于文件系统的对象。
struct files_struct *files。files_struct 是关于打开的文件的对象。
struct signal_struct *signal。signal_struct 是关于信号的对象。

3.2 task_struct 结构体中的三个 ID 与一个指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pid
每个 task_struct 都会有一个不同的 ID,就是这个 PID。
tid
线程 ID,用来标识每个线程的。

tgid
线程组领头线程的 PID,事实上就是主线程的 PID。
当创建一个子进程时,它的 tgid 与 pid 相等;
当创建一个线程时,它的 tgid 等于主线程的 pid。

    getpid() 函数事实上返回的是当前进程或线程的 tgid。

pgid
进程组领头进程的 PID。
sid
会话领头进程的 PID。
group_leader
是一个 task_struct 类型的指针,指向的是进程组的组长对应的 task_struct 对象。

4 虚拟内存地址空间

4.1 内存管理

内存是由内核来管理的。

内存被分为 n 个页框,然后进一步组织为多个区。而装入页框中的内容称为页。

当内核函数申请内存时,内核总是立即满足(因为内核完全信任它们,所以优先级最高)。在分配适当内存空间后,将其映射到内核地址空间中(3-4GB 中的某部分空间),然后将地址映射写入页表。

申请内存空间的内核函数有 vmalloc, kmalloc, alloc_pages, __get_free_pages 等。

4.2 内核常驻内存

就是说,内核地址空间(3-4GB)中的页面所映射的页框始终在物理内存中存在,不会被换出。即使是 vmalloc 动态申请的页面也会一直在物理内存中,直至通过相关内核函数释放掉。

其原因在于,一方面内核文件不是太大,完全可以一次性装入物理内存;另一方面在于即使是动态申请内存空间,也能立即得到满足。

因此,处于内核态的普通进程或内核线程(后面会提到)不会因为页面没有在内存中而产生缺页异常(不过处于内核态的普通进程会因为页表项没有同步的原因而产生缺页异常)。

4.3 为什么要有虚拟地址空间

普通进程在申请内存空间时会被内核认为是不紧要的,优先级较低。因而总是延迟处理,在之后的某个时候才会真正为其分配物理内存空间。

比如,普通进程中的 malloc 函数在申请物理内存空间时,内核不会直接为其分配页框。

另一方面,普通进程对应的可执行程序文件较大,不能够立即装入内存,而是采取运行时按需装入。

要实现这种延迟分配策略,就需要引入一种新的地址空间,即 虚拟地址空间。可执行文件在装入时或者进程在执行 malloc 时,内核只会为其分配适当大小的虚拟地址空间。

虚拟地址空间并不单纯地指线性地址空间。准确地说,指的是页面不能因为立即装入物理内存而采取折衷处理后拥有的线性地址空间。 因此,虽然普通进程的虚拟地址空间为 4GB,但是从内核的角度来说,内核地址空间(也是线性空间)不能称为虚拟地址空间,内核线程不拥有也不需要虚拟地址空间。 因此,虚拟地址空间只针对普通进程。

当然,这样的话就会产生所要访问的页面不在物理内存中而发生缺页异常。

4.4 虚拟地址空间的划分

每一个普通进程都拥有 4GB 的虚拟地址空间(对于 32 位的 CPU 来说,即 232 B)。

主要分为两部分,一部分是用户空间(0-3GB),一部分是内核空间(3-4GB)。每个普通进程都有自己的用户空间,但是内核空间被所有普通进程所共享。

如下图所示:

之所以能够使用 3-4GB 的虚拟地址空间(对于普通进程来说),是因为每个进程的页全局目录(后面会提到)中的后面部分存放的是内核页全局目录的所有表项。当通过系统调用或者发生异常而陷入内核时,不会切换进程的页表。此时,处于内核态的普通进程将会直接使用进程页表中前面的页表项即可。这也是为什么在执行系统调用或者处理异常时没有发生进程的上下文切换的真实原因。 同样,正因为每个进程的也全局目录中的后面部分存放的是内核页全局目录中的所有表项,所以所有普通进程共享内核空间。

另外,

用户态下的普通进程只能访问 0-3GB 的用户空间; 内核态下的普通进程既能访问 0-3GB 的用户空间,也能访问 3-4GB 的内核空间(内核态下的普通进程有时也会需要访问用户空间)。

4.5 普通线程的用户堆栈与寄存器

对于多线程环境,虽然所有线程都共享同一片虚拟地址空间,但是每个线程都有自己的用户栈空间和寄存器,而用户堆仍然是所有线程共享的。

栈空间的使用是有明确限制的,栈中相邻的任意两条数据在地址上都是连续的。试想,假设多个普通线程函数都在执行递归操作。如果多个线程共有用户栈空间,由于线程是异步执行的,那么某个线程从栈中取出数据时,这条数据就很有可能是其它线程之前压入的,这就导致了冲突。所以,每个线程都应该有自己的用户栈空间。

寄存器也是如此,如果共用寄存器,很可能出现使用混乱的现象。

而堆空间的使用则并没有这样明确的限制,某个线程在申请堆空间时,内核只要从堆空间中分配一块大小合适的空间给线程就行了。所以,多个线程同时执行时不会出现向栈那样产生冲突的情况,因而线程组中的所有线程共享用户堆。

那么在创建线程时,内核是怎样为每个线程分配栈空间的呢?

由之前所讲解可知,进程/线程的创建主要是由 clone 系统调用完成的。而 clone 系统调用的参数中有一个 void *child_stack,它就是用来指向所创建的进程/线程的堆栈指针。

而在该进程/线程在用户态下是通过调用 pthread_create 库函数而陷入内核的。对于 pthread_create 函数,它则会调用一个名为 pthread_allocate_stack 的函数,专门用来为所创建的线程分配的栈空间(通过 mmap 系统调用)。然后再将这个栈空间的地址传递给 clone 系统调用。这也是为什么线程组中的每个线程都有自己的栈空间。

4.6 普通进程的页表

有两种页表,一种是内核页表(会在后面说明),另一种是进程页表。

普通进程使用的则是进程页表,而且每个普通进程都有自己的进程页表。如果是多线程,则这些线程共享的是主线程的进程页表。

4.6.1 四级页表

现在的 Linux 内核中采用四级页表,分别为:

1
2
3
4
页全局目录 (Page Global Directory, pgd);
页上级目录 (Page Upper Directory, pud);
页中间目录 (Page Middle Directory, pmd);
页表 (Page Table, pt)。

task_struct 中的 mm_struct 对象用于管理该进程(或者线程共享的)页表。准确地说,mm_struct 中的 pgd 指针指向着该进程的页全局目录。

4.6.2 普通进程的页全局目录

普通进程的页全局目录中,第一部分表项映射的线性地址为 0-3GB 部分,剩余部分存放的是主内核页全局目录(后面会提到)中的所有表项。

5 内核线程

内核线程是一种只运行在内核地址空间的线程。所有的内核线程共享内核地址空间(对于 32 位系统来说,就是 3-4GB 的虚拟地址空间),所以也共享同一份内核页表。这也是为什么叫内核线程,而不叫内核进程的原因。

由于内核线程只运行在内核地址空间中,只会访问 3-4GB 的内核地址空间,不存在虚拟地址空间,因此每个内核线程的 task_struct 对象中的 mm 为 NULL。

普通线程虽然也是同主线程共享地址空间,但是它的 task_struct 对象中的 mm 不为空,指向的是主线程的 mm_struct 对象。

普通进程与内核线程有如下区别:

内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态;
内核线程只使用 3-4GB (假设为 32 位系统) 的内核地址空间(共享的),但普通进程由于既可以运行在用户态,又可以运行在内核态,因此可以使用 4GB 的虚拟地址空间。

系统在正式启动内核时,会执行 start_kernel 函数。在这个函数中,会自动创建一个进程,名为 init_task。其 PID 为 0,运行在内核态中。然后开始执行一系列初始化。

5.1 init 内核线程

init_task 在执行 rest_init 函数时,会执行 kernel_thread 创建 init 内核线程。它的 PID 为 1,用来完成内核空间初始化。

在内核空间完成初始化后,会调用 exceve 执行 init 可执行程序 (/sbin/init)。之后,init 内核线程变成了一个普通的进程,运行在用户空间中。

init 内核线程没有地址空间,且它的 task_struct 对象中的 mm 为 NULL。因此,执行 exceve 会使这个 mm 指向一个 mm_struct,而不会影响到 init_task 进程的地址空间。 也正因为此,init 在转变为进程后,其 PID 没变,仍为 1。

创建完 init 内核线程后,init_task 进程演变为 idle 进程(PID 仍为 0)。

之后,init 进程再根据再启动其它系统进程 (/etc/init.d 目录下的各个可执行文件)。

5.2 kthreadd 内核线程

init_task 进程演变为 idle 进程后,idle 进程会执行 kernel_thread 来创建 kthreadd 内核线程(仍然在 rest_init 函数中)。它的 PID 为 2,用来创建并管理其它内核线程(用 kthread_create, kthread_run, kthread_stop 等内核函数)。

系统中有很多内核守护进程 (线程),可以通过:

1
ps -efj

进行查看,其中带有 [] 号的就属于内核守护进程。它们的祖先都是这个 kthreadd 内核线程。

5.3 主内核页全局目录

内核维持着一组自己使用的页表,也即主内核页全局目录。当内核在初始化完成后,其存放在 swapper_pg_dir 中,而且所有的普通进程和内核线程就不再使用它了。

5.4 内核线程如何访问页表

5.4.1 active_mm

对于内核线程,虽然它的 task_struct 中的 mm 为 NULL,但是它仍然需要访问内核空间,因此需要知道关于内核空间映射到物理内存的页表。然而不再使用 swapper_pg_dir,因此只能另外想法解决。

由于所有的普通进程的页全局目录中的后面部分为主内核页全局目录,因此内核线程只需要使用某个普通进程的页全局目录就可以了。

在 Linux 中,task_struct 中还有一个很重要的元素为 active_mm,它主要就是用于内核线程访问主内核页全局目录。

对于普通进程来说,task_struct 中的 mm 和 active_mm 指向的是同一片区域; 然而对内核线程来说,task_struct 中的 mm 为 NULL,active_mm 指向的是前一个普通进程的 mm_struct 对象。

5.4.2 mm_users 和 mm_count

但是这样还是不行,因为如果因为前一个普通进程退出了而导致它的 mm_struct 对象也被释放了,则内核线程就访问不到了。

为此,mm_struct 对象维护了一个计数器 mm_count,专门用来对引用这个 mm_struct 对象的自身及内核线程进行计数。初始时为 1,表示普通进程本身引用了它自己的 mm_struct 对象。只有当这个引用计数为 0 时,才会真正释放这个 mm_struct 对象。

另外,mm_struct 中还定义了一个 mm_users 计数器,它主要是用来对共享地址空间的线程计数。事实上,就是这个主线程所在线程组中线程的总个数。初始时为 1。

注意,两者在实质上都是针对引用 mm_struct 对象而设置的计数器。 不同的是,mm_count 是专门针对自身及内核线程或引用 mm_struct 而进行计数;而 mm_users 是专门针对该普通线程所在线程组的所有普通线程而进行计数。 另外,只有当 mm_count 为 0 时,才会释放 mm_struct 对象,并不会因为 mm_users 为 0 就进行释放。

Reference