• 【计算机网络】Linux 内核网络概述


     文章目的

    • 了解 Linux 内核网络架构
    • 通过网络包过滤器或者防火墙获得使用的 IP 数据包(分组)管理技巧
    • 熟悉如何在 Linux 内核级别使用套接字

    概述

            网络应用程序的开发过去这些年按照指数级增长,这样增加了对系统网络子系统的速度要求和产品化要求。网络子系统不是 Linux 内核必须的组件(Linux 内核可以在没有网络支持的情况下编译通过)。然而非常少的计算系统(即便是嵌入式设备)很难没有网络支持,因为它们都需要联网。现代操作系统使用 TCP/IP 协议栈,协议栈实现了传输层以下的所有协议层,应用层协议通常在用户空间实现(HTTP、FTP、SSH等)。

    用户空间网络

            用户空间中,网络网络通信被抽象为套接字(socket),套接字抽象了通信通道,是基于内核 TCP/IP 协议栈交互接口。一个 IP 套接字和一个 IP 地址、传输层协议(TCP、UDP 等)、以及一个端口关联。使用套接字的普通函数调用包括:创建(socket)、初始化(bind)、连接(connect)、关闭套接字(close)。

            TCP 套接字通过 read/write 或者 recv/send 调用完成网络通信,而 UDP 使用 recvfrom/sento 接口调用完成网络调用。通信过程中的传输和接收操作对于应用程序来讲是透明的,即数据的封装以及网络传输由内核决定。然而,也可以通过原始套接字在用户空间实现 TCP/IP 协议栈(传教套接字时使用 PF_PACKET 选项),或者在内核实现应用层协议(比如 TUX web server)。

            更多关于用户空间使用套接字编程的信息,参考 Beej's Guide to Network Programming 

    Linux 网络通信

            Linux 内核提供了网络包工作的三个基本数据结构:struct socket、struct sock、struct sk_buff。

            前两个是套接字的抽象:

    • struct socket 和用户空间的抽象非常相似,也就是用来编写网络应用程序的 BSD 套接字
    • struct sock 或者 Linux 术语中的 INET 套接字是套接字的网络层表示。

            这两个结构是有关联的:struct socket 包含 INET 套接字字段,struct sock 有一个 BSD 套接字包含它。

            struct sk_buff 结构是网络包及其状态的表示。当内核接到一个一个数据包时,就会创建这个结构,数据包可以是从用户空间传来的也可以是从网络接口传来的。

    struct socket 结构

            struct socket 结构是 BSD 套接字在内核的表示,在它上面执行的操作和内核提供的操作非常类似(通过系统调用)。一些套接字的常见操作(创建、初始化/绑定、关闭等)会导致特定的系统调用,这些系统调用会使用 struct socket 结构。

            struct socket 操作在 net/socket.c 中实现,它是和具体协议类型无关的。因此,struct socket 结构是一个在各种网络操作实现上的一个通用接口。通常,这些操作以 sock_ 前缀开始。

    socket 结构上的操作

    creation

    创建操作和用户空间调用 socket() 函数类似,但是创建出的套接字被存到了 res 参数中:

    • int sock_create(int family, int typ, int protocol, struct socket **res) 在 socket() 系统调用后创建一个套接字;
    • Int sock_create_kern(struct net *net, int family, int type, int protocol, struct socket **ret) 创建一个内核套接字;
    • int sock_create_lite(int family, int type, int protocol, struct **res) 创建一个内核套接字,不进行 sanity 检查

    这些调用的如下:

    • net,是一个网络名字空间的引用,通常我们使用 init_net 初始化它
    • family 表示用于信息传输的协议家族,以 PF_  字符串开头,带上协议家族,这个常量表示协议家族,定义在 linux/socket.h 中,TCP/IP 通常使用 PF_INET
    • type 是套接字的类型,定义在 linux/net.h 中,通常面向连接的通信采用 SOCK_STREAM,而非连接通信采用 SOCK_DGRAM
    • protocol 表示采用的协议,和 type 相关,定义在 linux/in.h 中,TCP 使用 IPROTO_TCP,UDP 使用 IPROTO_UDP。

            在内核空间创建 TCP 套接字,必须调用:

    1. struct socket *sock;
    2. int err;
    3. err = sock_create_kern(&init_net, PF_INET, SOCK_STREAM, IPPROTO_TCP, &sock);
    4. if (err < 0) {
    5. /* handle error */
    6. }

            创建一个 UDP 套接字:

    1. struct socket *sock;
    2. int err;
    3. err = sock_create_kern(&init_net, PF_INET, SOCK_DGRAM, IPPROTO_UDP, &sock);
    4. if (err < 0) {
    5. /* handle error */
    6. }

             sys_socket() 系统调用处理函数(handler)中有相关使用举例:

    1. SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
    2. {
    3. int retval;
    4. struct socket *sock;
    5. int flags;
    6. /* Check the SOCK_* constants for consistency. */
    7. BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
    8. BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
    9. BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
    10. BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
    11. flags = type & ~SOCK_TYPE_MASK;
    12. if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
    13. return -EINVAL;
    14. type &= SOCK_TYPE_MASK;
    15. if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
    16. flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
    17. retval = sock_create(family, type, protocol, &sock);
    18. if (retval < 0)
    19. goto out;
    20. return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    21. }

    closing

            关闭连接(面向连接的套接字)并释放相关资源:

    • void sock_release(struct socket *sock) 调用套接字结构 ops 字段的  release  函数:
    1. void sock_release(struct socket *sock)
    2. {
    3. if (sock->ops) {
    4. struct module *owner = sock->ops->owner;
    5. sock->ops->release(sock);
    6. sock->ops = NULL;
    7. module_put(owner);
    8. }
    9. //...
    10. }

    sending/receiving 消息

            使用下面函数进行消息发送和接收:

    1. int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags);
    2. int kernel_recvmsg(struct socket *sock, struct msghdr *msg, struct kvec *vec, size_t num, size_t size, int flags);
    3. int sock_sendmsg(struct socket *sock, struct msghdr *msg);
    4. int kernel_sendmsg(struct socket *sock, struct msghdr *msg, struct kvec *vec, size_t num, size_t size);

            消息发送和接收函数会调用套接字 ops 字段的 sendmsg、recvmsg 函数,具有 kernel_ 前缀的函数是套接字在内核中使用的。

            参数如下:

    • msg,是一个 struct msghdr 结构,包含着需要发送接收的消息。这个结构中最重要的成员是 msg_name 和 msg_namelen,对于 UDP 套接字,必须填充为发送消息的目标地址(struct sockaddr_in)
    • vec,一个 struct kvec 结构,包含一个指向缓冲器(包含数据和大小)的指针,这个结构和 struct iovec 结构类似(struct iovec 结构用于用户空间,而 struct kvec 结构用于内核空间数据)

            sys_sendto() 系统调用处理函数里有一些使用例程:

    1. SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
    2. unsigned int, flags, struct sockaddr __user *, addr,
    3. int, addr_len)
    4. {
    5. struct socket *sock;
    6. struct sockaddr_storage address;
    7. int err;
    8. struct msghdr msg;
    9. struct iovec iov;
    10. int fput_needed;
    11. err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
    12. if (unlikely(err))
    13. return err;
    14. sock = sockfd_lookup_light(fd, &err, &fput_needed);
    15. if (!sock)
    16. goto out;
    17. msg.msg_name = NULL;
    18. msg.msg_control = NULL;
    19. msg.msg_controllen = 0;
    20. msg.msg_namelen = 0;
    21. if (addr) {
    22. err = move_addr_to_kernel(addr, addr_len, &address);
    23. if (err < 0)
    24. goto out_put;
    25. msg.msg_name = (struct sockaddr *)&address;
    26. msg.msg_namelen = addr_len;
    27. }
    28. if (sock->file->f_flags & O_NONBLOCK)
    29. flags |= MSG_DONTWAIT;
    30. msg.msg_flags = flags;
    31. err = sock_sendmsg(sock, &msg);
    32. out_put:
    33. fput_light(sock->file, fput_needed);
    34. out:
    35. return err;
    36. }

            struct socket 的字段:

    1. /**
    2. * struct socket - general BSD socket
    3. * @state: socket state (%SS_CONNECTED, etc)
    4. * @type: socket type (%SOCK_STREAM, etc)
    5. * @flags: socket flags (%SOCK_NOSPACE, etc)
    6. * @ops: protocol specific socket operations
    7. * @file: File back pointer for gc
    8. * @sk: internal networking protocol agnostic socket representation
    9. * @wq: wait queue for several uses
    10. */
    11. struct socket {
    12. socket_state state;
    13. short type;
    14. unsigned long flags;
    15. struct socket_wq __rcu *wq;
    16. struct file *file;
    17. struct sock *sk;
    18. const struct proto_ops *ops;
    19. };

            需要解释的字段有:

    • ops   -  这个结构存储一些协议相关的函数指针
    • sk     -  和套接字相关的 INET socket

    struct proto_ops 结构

            struct proto_ops 结构包含特定操作的实现(TCP/UDP 等),这些函数会被 struct socket(sock_release(), sock_sendmsg() 等) 普通函数调用。

            struct proto_ops 结构也就包含了一些指向这些协议实现的指针:

    1. struct proto_ops {
    2. int family;
    3. struct module *owner;
    4. int (*release) (struct socket *sock);
    5. int (*bind) (struct socket *sock,
    6. struct sockaddr *myaddr,
    7. int sockaddr_len);
    8. int (*connect) (struct socket *sock,
    9. struct sockaddr *vaddr,
    10. int sockaddr_len, int flags);
    11. int (*socketpair)(struct socket *sock1,
    12. struct socket *sock2);
    13. int (*accept) (struct socket *sock,
    14. struct socket *newsock, int flags, bool kern);
    15. int (*getname) (struct socket *sock,
    16. struct sockaddr *addr,
    17. int peer);
    18. //...
    19. }

            struct socket 的 ops 字段的初始化是通过 __sock_create() 函数实现的,通过调用 create() 函数,指定每个协议。一个等效的调用是 __sock_create() 函数的实现:

    1. //...
    2. err = pf->create(net, sock, protocol, kern);
    3. if (err < 0)
    4. goto out_module_put;
    5. //...

            这个会初始化这些函数指针为套接字指定协议类型的函数,sock_register() 和 sock_unregister() 调用用来填充 net_fanilies 向量。

            对于剩余的 socket 操作(除了创建、关闭、发送接收外),也会通过指针来调用,比如 bind 函数:

    1. #define MY_PORT 60000
    2. struct sockaddr_in addr = {
    3. .sin_family = AF_INET,
    4. .sin_port = htons (MY_PORT),
    5. .sin_addr = { htonl (INADDR_LOOPBACK) }
    6. };
    7. //...
    8. err = sock->ops->bind (sock, (struct sockaddr *) &addr, sizeof(addr));
    9. if (err < 0) {
    10. /* handle error */
    11. }
    12. //...

  • 相关阅读:
    19. 删除链表的倒数第 N 个结点 C++(快慢指针)
    Foxmail新版本迁移邮箱的数据文件教程
    【Spring进阶系列丨最终篇】一文详解Spring中的事务控制
    《MySQL 数据库 (一)》
    JDBC操作事务
    小谈设计模式(19)—备忘录模式
    【Leetcode】拿捏链表(三)——CM11 链表分割(牛客)、OR36 链表的回文结构(牛客)
    CSS基础教程3——字体和文字样式(上)
    ORACLE设置快照回滚点
    2.Android应用资源
  • 原文地址:https://blog.csdn.net/BillyThe/article/details/133430388