【计算机网络】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 套接字,必须调用:

复制代码
struct socket *sock;
int err;

err = sock_create_kern(&init_net, PF_INET, SOCK_STREAM, IPPROTO_TCP, &sock);
if (err < 0) {
        /* handle error */
}

创建一个 UDP 套接字:

复制代码
struct socket *sock;
int err;

err = sock_create_kern(&init_net, PF_INET, SOCK_DGRAM, IPPROTO_UDP, &sock);
if (err < 0) {
        /* handle error */
}

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

复制代码
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
      int retval;
      struct socket *sock;
      int flags;

      /* Check the SOCK_* constants for consistency.  */
      BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
      BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
      BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
      BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

      flags = type & ~SOCK_TYPE_MASK;
      if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
              return -EINVAL;
      type &= SOCK_TYPE_MASK;

      if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
              flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

      retval = sock_create(family, type, protocol, &sock);
      if (retval < 0)
              goto out;

      return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

closing

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

  • void sock_release(struct socket *sock) 调用套接字结构 ops 字段的 release 函数:

    void sock_release(struct socket *sock)
    {
    if (sock->ops) {
    struct module *owner = sock->ops->owner;

    复制代码
                sock->ops->release(sock);
                sock->ops = NULL;
                module_put(owner);
        }
        //...

    }

sending/receiving 消息

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

复制代码
int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags);
int kernel_recvmsg(struct socket *sock, struct msghdr *msg, struct kvec *vec, size_t num, size_t size, int flags);
int sock_sendmsg(struct socket *sock, struct msghdr *msg);
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() 系统调用处理函数里有一些使用例程:

复制代码
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
              unsigned int, flags, struct sockaddr __user *, addr,
              int, addr_len)
{
      struct socket *sock;
      struct sockaddr_storage address;
      int err;
      struct msghdr msg;
      struct iovec iov;
      int fput_needed;

      err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
      if (unlikely(err))
              return err;
      sock = sockfd_lookup_light(fd, &err, &fput_needed);
      if (!sock)
              goto out;

      msg.msg_name = NULL;
      msg.msg_control = NULL;
      msg.msg_controllen = 0;
      msg.msg_namelen = 0;
      if (addr) {
              err = move_addr_to_kernel(addr, addr_len, &address);
              if (err < 0)
                      goto out_put;
              msg.msg_name = (struct sockaddr *)&address;
              msg.msg_namelen = addr_len;
      }
      if (sock->file->f_flags & O_NONBLOCK)
              flags |= MSG_DONTWAIT;
      msg.msg_flags = flags;
      err = sock_sendmsg(sock, &msg);

out_put:
      fput_light(sock->file, fput_needed);
out:
      return err;
}

struct socket 的字段:

复制代码
/**
 *  struct socket - general BSD socket
 *  @state: socket state (%SS_CONNECTED, etc)
 *  @type: socket type (%SOCK_STREAM, etc)
 *  @flags: socket flags (%SOCK_NOSPACE, etc)
 *  @ops: protocol specific socket operations
 *  @file: File back pointer for gc
 *  @sk: internal networking protocol agnostic socket representation
 *  @wq: wait queue for several uses
 */
struct socket {
      socket_state            state;

      short                   type;

      unsigned long           flags;

      struct socket_wq __rcu  *wq;

      struct file             *file;
      struct sock             *sk;
      const struct proto_ops  *ops;
};

需要解释的字段有:

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

struct proto_ops 结构

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

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

复制代码
struct proto_ops {
      int             family;
      struct module   *owner;
      int             (*release)   (struct socket *sock);
      int             (*bind)      (struct socket *sock,
                                    struct sockaddr *myaddr,
                                    int sockaddr_len);
      int             (*connect)   (struct socket *sock,
                                    struct sockaddr *vaddr,
                                    int sockaddr_len, int flags);
      int             (*socketpair)(struct socket *sock1,
                                    struct socket *sock2);
      int             (*accept)    (struct socket *sock,
                                    struct socket *newsock, int flags, bool kern);
      int             (*getname)   (struct socket *sock,
                                    struct sockaddr *addr,
                                    int peer);
      //...
}

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

复制代码
//...
      err = pf->create(net, sock, protocol, kern);
      if (err < 0)
              goto out_module_put;
//...

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

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

复制代码
#define MY_PORT 60000

struct sockaddr_in addr = {
      .sin_family = AF_INET,
      .sin_port = htons (MY_PORT),
      .sin_addr = { htonl (INADDR_LOOPBACK) }
};

//...
      err = sock->ops->bind (sock, (struct sockaddr *) &addr, sizeof(addr));
      if (err < 0) {
              /* handle error */
      }
//...
相关推荐
A小辣椒20 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言