实验环境
不同版本的 golang/linux 内核, 对应的内核调用(细节)可能会不一行
环境以及使用的工具:
-
objdump 2.30-73
-
strace
-
nm
-
nc
还有shell 工具, 这里我用的是 xshell
顺手验证一下, 编译器是怎么实现c++的函数重载
1 #include<iostream>
2
3 using std::cout;
4 using std::cin;
5 using std::endl;
6
7 double sum(double a, double b) {
8 return a + b;
9 }
10
11 float sum(float a, float b) {
12 return a + b;
13 }
14
15 int sum(int a, int b) {
16 return a + b;
17 }
18
19 char sum(char a, char b) {
20 return a + b;
21 }
22
23 long sum(long a, long b) {
24 return a + b;
25 }
26
27 using ll = long long;
28
29 ll sum(ll a, ll b) {
30 return a + b;
31 }
32
33 int main(void) {
34 // todo
35 return 0;
36 }
37
重载的函数名字叫做 sum, 使用 nm 分析可执行文件, 分析符号表,
[root@192 src]# nm a.out | grep -C 0 sum
0000000000400785 t _GLOBAL__sub_I__Z3sumdd
--
00000000004006ee T _Z3sumcc
00000000004006a6 T _Z3sumdd
00000000004006c0 T _Z3sumff
00000000004006da T _Z3sumii
000000000040070a T _Z3sumll
0000000000400723 T _Z3sumxx
可以得到, 对于不同的参数类型, 函数名称也发生了对应的变化, 比如 两个参数都是 char 的, 符号是 _Z3sumcc ...., 其实重载在编译器层面是通过传入参数的不同类型来作为(整个函数的)标识符的(通过nm 工具分析得到的);之前感觉都被骗了, 分析了之后才知道, 原来如此 😳 (有被骗到的请给我点个👍好么)
实现简单的 http ping/pong 服务器
package main
import (
"bytes"
"fmt"
"net"
"time"
)
func main() {
listener, err := net.Listen("tcp", ":8080") // 绑定 8080 端口号, 通知系统把和 8080 端口相关的请求转发给这个进程
if err != nil {
panic(err)
}
// 系统调用, 向标准输出写字符串
fmt.Println("simple... ping pong")
defer listener.Close()
for {
// 阻塞监听事件
c, err := listener.Accept()
if err != nil {
fmt.Println("error ", err)
continue
}
go consumer(c)
}
}
// 消费客户端的请求
func consumer(conn net.Conn) {
// HTTP 信息
// 1. 请求行
// 2. 请求头
// 3. 请求头体(可能有)
start := time.Now()
var buff = bytes.Buffer{}
body := fmt.Sprintf(`{"ping":"pong","time":"%s"}`, time.Now().String()) // 响应体
buff.WriteString("HTTP/1.1 200 OK\r\n") // 响应行,
buff.WriteString("Content-Type:application/json\r\n") // 响应头
buff.WriteString(fmt.Sprintf("Content-Length:%d\r\n", len(body)))
buff.WriteString("\r\n") // 空一行
buff.WriteString(body)
conn.Write(buff.Bytes())
fmt.Println("--- new client ----", conn.RemoteAddr().Network(), conn.RemoteAddr().String(), " ", string("data"), time.Now().Sub(start))
conn.Close() // 关闭文件(关闭链接)
}
[root@192 src]# ll -h
total 2.9M
-rw-r--r--. 1 root root 409 Nov 5 15:38 hello.c
-rwxr-xr-x. 1 root root 2.9M Nov 5 17:02 main
-rw-r--r--. 1 root root 1.5K Nov 5 17:02 main.go
[root@192 src]# ./main
精简版本的 http 服务器... ping pong
--- new client ---- tcp [::1]:58298 data 108.825µs
可以看到, 自带运行时的程序还是挺大的, 居然有 2.9mb, 不过, 这个并不是我们关心的重点信息
使用 netcat 请求一下, 验证程序是否可以正常工作
[root@192 src]# nc localhost 8080
HTTP/1.1 200 OK
Content-Type:application/json
Content-Length:79
{"ping":"pong","time":"2020-11-05 17:05:40.415831503 +0800 CST m=+7.840433991"}
完美, 看到完整的 http 响应信息
服务程序完成网络请求, 调用了哪些系统函数呢?
- 使用 strace 来追踪系统的调用记录
strace -ff -o output ./main
-ff: 表示 follow forks with output into separate files
-o: 表示 send trace output to FILE instead of stderr (原来默认是标准错误输出流的数据, 😳)
output: 表示输出文件名的前缀信息
./main: 程序的路径
- 然后使用 netstat 命令过滤出进程的 id
这里监听的端口是 8080, 进程 id 是 14082
[root@192 src]# netstat -antp | grep 8080
tcp6 0 0 :::8080 :::* LISTEN 14082/./main
- 分析 strace 输出的日志文件
这里是 output.13688 文件里面的内容
文件里面的第一行是 execve("./main", ["./main"], 0x7ffc1ee43cb8 /* 31 vars */) = 0 可以看到是调用 execve 函数把程序运行起来的
- net.Listen 做了啥?
listener, err := net.Listen("tcp", ":8080")
对应的 linux 内核调用如下(省略了一大半的内容):
// ....
epoll_ctl(5, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=3692773144, u64=140049691393816}}) = 0
getsockname(3, {sa_family=AF_INET6, sin6_port=htons(8080), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28]) = 0
其中主要就是:
bind(3, {sa_family=AF_INET6, sin6_port=htons(8080), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0 把 8080 端口和 3 绑定起来了, 8080 是程序监听的端口信息, 3 是 sock 的文件描述信息
bind 的函数原型如下 (使用 man 2 bind 可以获取帮助文档):
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
struct sockaddr 的对象信息如下, golang 里面的语句信息 net.Listen("tcp", ":8080"):
{sa_family=AF_INET6, sin6_port=htons(8080), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}
fmt.Println("simple... ping pong") 对应的系统调用是啥?
write(1, "simple... ping pong\n", 20) = 20
等到程序执行的之后, 实际上会调用内核函数向文件描述为 3 的文件写入 "simple... ping pong\n", 写入的长度为 20,
还有重要的信息
for {
// 阻塞监听事件
c, err := listener.Accept()
if err != nil {
fmt.Println("error ", err)
continue
}
go consumer(c)
}
代码中的 listener.Accept() 这里是阻塞型的,
listener.Accept() 这里的阻塞型监听中, 获取 sock_fd 的信息, 有可能获取不到
accept4(3, {sa_family=AF_INET6, sin6_port=htons(62425), inet_pton(AF_INET6, "::ffff:192.168.145.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28], SOCK_CLOEXEC|SOCK_NONBLOCK) = 4
accept4 函数的返回值:
RETURN VALUE On success, these system calls return a nonnegative integer that is a file descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately.
func consumer(conn net.Conn) {
// HTTP 信息
// 1. 请求行
// 2. 请求头
// 3. 请求头体(可能有)
start := time.Now()
var buff = bytes.Buffer{}
body := fmt.Sprintf(`{"ping":"pong","time":"%s"}`, time.Now().String()) // 响应体
buff.WriteString("HTTP/1.1 200 OK\r\n") // 响应行,
buff.WriteString("Content-Type:application/json\r\n") // 响应头
buff.WriteString(fmt.Sprintf("Content-Length:%d\r\n", len(body)))
buff.WriteString("\r\n") // 空一行
buff.WriteString(body)
conn.Write(buff.Bytes())
fmt.Println("--- new client ----", conn.RemoteAddr().Network(), conn.RemoteAddr().String(), " ", string("data"), time.Now().Sub(start))
conn.Close() // 关闭文件(关闭链接)
}
consumer 函数的系统调用有, 如下图所示的内容
整个服务程序的逻辑如下:
总结
哦, 对了, golang 里面的 time.Now() 函数, 会调用系统函数openat(AT_FDCWD, "/etc/localtime", O_RDONLY) 🐻
以上内容都是在 linux 上分析的, 其中还有很多的没有分析, 不是搞不动了, 而是内容太多了