1:实验环境选择
我选择的是实验楼平台,在 LinuxKernel 目录已经构建好了基于 3.18.6 的内核环境,可以使用实验楼的虚拟机打开 Xfce 终端(Terminal), 运行 MenuOS 系统。
2:启动内核
打开终端键入以下命令:
$ cd ~/LinuxKernel/
$ qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
内核启动:
使用跟踪分析 ~/Linux 内核的启动过程的 -s 和 -S 选项启动 MenuOS 系统。
$ qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
关于 -s
和 -S
选项的说明:
-S
freeze CPU at startup (use ’c’ to start execution)-s
shorthand for -gdb tcp::1234 若不想使用 1234 端口,则可以使用 -gdb tcp:xxxx 来取代 -s 选项
3:执行gdb
打开一个 Xfce 终端(Terminal),执行 gdb
# 打开 GDB 调试器 $ gdb # 在 GDB 中输入以下命令: # 在 gdb 界面中 targe remote 之前加载符号表 (gdb)file linux-3.18.6/vmlinux # 建立 gdb 和 gdbserver 之间的连接 (gdb)target remote:1234 # 断点的设置可以在target remote之前,也可以在之后 (gdb)break start_kernel # 按 c 让qemu上的Linux继续运行 (gdb)c
gdc的继续执行命令会执行到设置的断点
4:将网络通信程序的服务端集成到 MenuOS 系统中
接下来我们需要将 C/S 方式的网络通信程序的服务端集成到 MenuOS 系统中,成为 MenuOS 系统的命令 replyhi。
我们 git clone 克隆一个 linuxnet.git,进入 lab2 目录执行 make 可以将我们集成好的代码 copy 到 menu 项目中。
然后进入 menu,我们写了一个脚本 rootfs,运行 make rootfs,脚本就可以帮助我们自动编译、自动生成根文件系统,还会帮我们运行起来 MenuOS 系统。
详细命令如下:
$ cd ~/LinuxKernel $ git clone https://github.com/mengning/linuxnet.git $ cd linuxnet/lab2 $ make $ cd ../../menu/ $ make rootfs
尝试输入replyhi命令
可以正常通信
这里简单介绍一下socket接口函数的内核处理函数sys_socket
摘录其中的关键代码如下:
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { int retval; struct socket *sock; retval = sock_create(family, type, protocol, &sock); }
socket接口函数主要作用是建立socket套接字描述符,Unix-like系统非常成功的设计是将一切都抽象为文件,socket套接字也是一种特殊的文件,sock_create内部就是使用文件系统中的数据结构inode为socket套接字分配了文件描述符。socket套接字与普通的文件在内部存储结构上是一致的,甚至文件描述符和套接字描述符是通用的,但是套接字和文件还是特殊之处,因此定义了结构体struct socket,struct socket的结构体定义见/linux-3.18.6/include/linux/net.h#105,具体代码摘录如下:
struct socket { socket_state state; kmemcheck_bitfield_begin(type); short type; kmemcheck_bitfield_end(type); unsigned long flags; struct socket_wq __rcu *wq; struct file *file; struct sock *sk; const struct proto_ops *ops; };
bind()函数
正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数的三个参数分别为:
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
- addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };
struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ }; struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ };
#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
- addrlen:对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
listen()和connect()函数
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
int listen(int sockfd, int backlog); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
5:gdb调试
接下里可以使用gdb对内核进行调试,如list命令:
参考资料:https://github.com/mengning/net/blob/master/doc/socketSourceCode.md