[置顶] Linux协议栈代码阅读笔记(一)

时间:2021-07-05 03:34:39

Linux协议栈代码阅读笔记(一)
(基于linux-2.6.21.7)

(一)用户态通过诸如下面的C库函数访问协议栈服务

int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr,  socklen_t addrlen);
int connect(int sockfd, const struct sockaddr *addr,  socklen_t addrlen);
……

(二)上述C库函数如何与内核交互
C库代码准备好相应的工作后(例如,设置系统调用号啦、参数构造啦、栈啦、寄存器设置啦),通过系统调用指令,进入内核态。从内核返回后,C库函数再做相应的善后工作,然后将结果返回给用户程序。

这部分代码,不同架构的处理器,有不同的实现。
可以参考Glibc的源码。
下面以X86为例,简要描述一下这个过程。
另外,后续的内容,如无特殊说明,均是针对X86架构。

对于X86架构,一般是通过“int  $0x80”指令进入内核,即触发128号中断。
内核中断向量表的定义如下(源码文件arch\i386\kernel\ Traps.c):

struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };

函数trap_init(源码文件arch\i386\kernel\ Traps.c)对此表进行了初始化。
其中,对128号中断的初始化方式为:
set_system_gate(SYSCALL_VECTOR,&system_call);
SYSCALL_VECTOR宏的值为0x80,即128。

因此,128号中断,即对应中断向量表的第128个条目,其中断服务程序为system_call这段代码。
system_call这段代码,是用汇编实现的。
其代码在arch\i386\kernel\entry.S中。
这个代码,主要是根据系统调用号,索引系统调用表中的一个条目进行执行。

(三)内核态如何处理用户的网络通讯请求
上一步,C库发起了系统调用,进入了内核128号中断,即系统调用软中断。
128号中断处理程序,根据系统调用号,进入系统调用表的相应表目。系统调用表如下,每个表目是一个函数指针。(源码文件:arch\i386\kernel\ syscall_table.S)

ENTRY(sys_call_table)
 .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
 .long sys_exit
 .long sys_fork
 .long sys_read
 .long sys_write
 .long sys_open  /* 5 */
 .long sys_close
 .long sys_waitpid
 .long sys_creat
 .long sys_link
 .long sys_unlink /* 10 */
 .long sys_ni_syscall /* old lock syscall holder */
 …
 .long sys_statfs
 .long sys_fstatfs /* 100 */
 .long sys_ioperm
.long sys_socketcall
 .long sys_syslog
 …
 .long sys_tee   /* 315 */
 .long sys_vmsplice
 .long sys_move_pages
 .long sys_getcpu
 .long sys_epoll_pwait

对于上述的几个socket库函数,全部对应同一个系统调用,即102号系统调用,即sys_socketcall函数。

sys_socketcall函数如何处理用户的socket请求
所有的socket相关的C库函数,如socket、bind、connect、listen、accept、send、recv、sendto、sendmsg等,全部都属于同一个系统调用(即102号系统调用),全部由这一个函数处理。
此函数的代大致如下(源码文件net\Socket.c)
long sys_socketcall(int call, unsigned long __user *args)
{
    ……
 switch (call) {
 case SYS_SOCKET:
  err = sys_socket(a0, a1, a[2]);
  break;
 case SYS_BIND:
  err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
  break;
 case SYS_CONNECT:
  err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
  break;
 case SYS_LISTEN:
  err = sys_listen(a0, a1);
  break;
 case SYS_ACCEPT:
  err =
      sys_accept(a0, (struct sockaddr __user *)a1,
          (int __user *)a[2]);
  break;
 case SYS_GETSOCKNAME:
  err =
      sys_getsockname(a0, (struct sockaddr __user *)a1,
        (int __user *)a[2]);
  break;
 case SYS_GETPEERNAME:
  err =
      sys_getpeername(a0, (struct sockaddr __user *)a1,
        (int __user *)a[2]);
  break;
 case SYS_SOCKETPAIR:
  err = sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
  break;
 case SYS_SEND:
  err = sys_send(a0, (void __user *)a1, a[2], a[3]);
  break;
 case SYS_SENDTO:
  err = sys_sendto(a0, (void __user *)a1, a[2], a[3],
     (struct sockaddr __user *)a[4], a[5]);
  break;
 case SYS_RECV:
  err = sys_recv(a0, (void __user *)a1, a[2], a[3]);
  break;
 case SYS_RECVFROM:
  err = sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
       (struct sockaddr __user *)a[4],
       (int __user *)a[5]);
  break;
 case SYS_SHUTDOWN:
  err = sys_shutdown(a0, a1);
  break;
 case SYS_SETSOCKOPT:
  err = sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]);
  break;
 case SYS_GETSOCKOPT:
  err =
      sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
       (int __user *)a[4]);
  break;
 case SYS_SENDMSG:
  err = sys_sendmsg(a0, (struct msghdr __user *)a1, a[2]);
  break;
 case SYS_RECVMSG:
  err = sys_recvmsg(a0, (struct msghdr __user *)a1, a[2]);
  break;
 default:
  err = -EINVAL;
  break;
 }
 return err;
}

(四)socket的创建
使用上述C库函数,第一步当然是使用socket库函数创建一个socket。后续的操作,则都是针对这一步创建出的socket而进行了。因此,我们先来看看socket的创建。
创建socket,主要就是分配一个struct socket结构变量,并适当的初始化。
这个工作由sys_socket 通过如下形式调用sock_create完成。其中的参数family、type、protocol都是用户调用socket库函数时传入的。

sock_create(family, type, protocol, &sock);

sock_create成功返回后,就创建了一个struct socket结构变量。
后续的数据收发,状态维护,差不多都基于这个结构变量了。

struct socket结构如下(源码文件:net\Socket.c)
struct socket {
 socket_state  state;
 unsigned long  flags;
 const struct proto_ops *ops;
 struct fasync_struct *fasync_list;
 struct file  *file;
 struct sock  *sk;
 wait_queue_head_t wait;
 short   type;
};

这个结构的初始化,主要依赖于family, type, protocol这三项信息,查找到相应的协议栈模块,填充struct socket结构中相应的成员。
这个初始化过程的层次比较深,涉及较多细节。在下也没有深入阅读理解。

不过,我们可以简单看看大的数据结构。
内核中的协议栈,也是按family, type, protocol这三项信息进行了组织。

a) 固定的协议信息(net\ipv4\ Af_inet.c)
Socket的创建,需要根据family, type, protocol确定一个协议。
内核中固定的协议信息,都在inetsw_array中进行了登记。

Static  struct  inet_protosw  inetsw_array[] =
{
 {
  .type =       SOCK_STREAM,
  .protocol =   IPPROTO_TCP,
  .prot =       &tcp_prot,
  .ops =        &inet_stream_ops,
  .capability = -1,
  .no_check =   0,
  .flags =      INET_PROTOSW_PERMANENT |
         INET_PROTOSW_ICSK,
 },

{
  .type =       SOCK_DGRAM,
  .protocol =   IPPROTO_UDP,
  .prot =       &udp_prot,
  .ops =        &inet_dgram_ops,
  .capability = -1,
  .no_check =   UDP_CSUM_DEFAULT,
  .flags =      INET_PROTOSW_PERMANENT,
       },

{
        .type =       SOCK_RAW,
        .protocol =   IPPROTO_IP, /* wild card */
        .prot =       &raw_prot,
        .ops =        &inet_sockraw_ops,
        .capability = CAP_NET_RAW,
        .no_check =   UDP_CSUM_DEFAULT,
        .flags =      INET_PROTOSW_REUSE,
       }
};

数组中的每个元素,对应一个协议。
其中,每个元素的prot成员,指向相应的协议(如TCP、UDP等)提供的proto结构变量,其中含有大量的函数指针,指向相应的函数,这些函数用于实现各种协议的交互、收发、控制等。这样一来,每一个协议,在这个数组中都能查到了,如何操作使用他们,也都有了相应的信息。
每个协议的ops成员,也指向一个proto_ops结构变量,其中也包含大量函数指针。这些操作,可以认为是包装后的,更抽象的操作。是更接近用户的socket操作函数。例如,这些操作函数包括:bind、connect、listen等。
具体包含哪些操作,是由family, type决定的(例如,family=PF_INET,type=SOCK_DGRAM时,则使用sendmsg接收数据)。对于多个协议,即使实现不同,但是如果他们的family, type相同,那么对于用户来说,操作都是一样的。
初始化完成后,最终的情况是:

a)    socket.sk.__sk_common.skc_prot指向具体的协议提供的proto结构变量。内含大量函数,实现具体的协议操作。
b)    socket. ops 指向相应的proto_ops结构变量,实现各种socket操作。
c)   最终的流程是:socket. ops包装了socket操作,但是socket. ops中的函数是利用socket.sk.__sk_common.skc_prot中的函数完成最终的操作。

最后,inetsw_array中的元素(协议),不是遍历查找的。他们被按照type分类组织到中inetsw了。Inetsw包含了PF_INET协议族中的全部协议。
Inetsw是个链表数组,定义如下(源码文件net\ipv4\ Af_inet.c)。
static  struct  list_head  inetsw[SOCK_MAX];

(五)使用socket进行收发
上一步已经完成了相关的初始化工作。
后续的建链、收发、断链等操作,也还都是由socketcall这一个函数完成的。
有兴趣的朋友可以自己研习研习相关的代码了。
在下对这方面也没有深入阅读理解:)

[置顶] Linux协议栈代码阅读笔记(一)的更多相关文章

  1. [置顶] Linux协议栈代码阅读笔记(二)网络接口的配置

    Linux协议栈代码阅读笔记(二)网络接口的配置 (基于linux-2.6.11) (一)用户态通过C库函数ioctl进行网络接口的配置 例如,知名的ifconfig程序,就是通过C库函数sys_io ...

  2. Linux协议栈代码阅读笔记(二)网络接口的配置

    Linux协议栈代码阅读笔记(二)网络接口的配置 (基于linux-2.6.11) (一)用户态通过C库函数ioctl进行网络接口的配置 例如,知名的ifconfig程序,就是通过C库函数sys_io ...

  3. Linux-3.0.8 input subsystem代码阅读笔记

    先乱序记录一下阅读Linux input subsystem代码的笔记. 在input device driver的入口代码部分,需要分配并初始化input device结构,内核提供的API是inp ...

  4. [置顶] Linux信号相关笔记

    最近又温习了一遍Linux中的信号知识,发现有很多东西以前没有注意到,就通过这篇博客记录一下,巩固一下知识点. 一,信号基础: 信号是什么?为了回答这个问题,首先要从异常说起,这里的异常不是指c++/ ...

  5. [置顶] linux常用命令大全

    SSH 密令控制台 user/pwd 一:停止tomcat 1,cd .. 进入根目录 2,cd home/ 3,ll 4,cd bin/ 进入tomcat bin目录 5,ll 6,ps -ef | ...

  6. [置顶]
 Linux学习总结(20)——Linux 文件夹结构和作用

     /bin 二进制可执行命令 /dev 设备特殊文件 /etc 系统管理和配置文件 /etc/rc.d 启动的配置文件和脚本 /home 用户主目录的基点,比如用户user的主目录就是/home/us ...

  7. [置顶]
 Linux 常用命令集锦

    出处:http://www.vaikan.com/what-are-the-most-useful-swiss-army-knife-one-liners-on-unix/ Linux命令行里的&qu ...

  8. [置顶] linux中fork()函数详解(原创!!实例讲解)

    分类: 计算机系统 linux2010-06-01 23:35 60721人阅读 评论(105) 收藏 举报 linux2010存储  一.fork入门知识 一个进程,包括代码.数据和分配给进程的资源 ...

  9. linux源码阅读笔记 fork函数

    在阅读源码的过程中,发现找不到fork函数的定义.后来在linux/init/main.c中找到了这样一条语句 static inline _syscall0(int,fork) 原来这里就是fork ...

随机推荐

  1. [译]使用JMH进行微基准测试:不要猜,要测试!

    英文原文:Micro Benchmarking with JMH: Measure, don't guess!翻译地址:使用JMH进行微基准测试:不要猜,要测试!原文作者:Antonio翻译作者:Ho ...

  2. CAlayer层的属性

    iOS开发UI篇—CAlayer层的属性 一.position和anchorPoint 1.简单介绍 CALayer有2个非常重要的属性:position和anchorPoint @property ...

  3. Linux下安装setup tools小工具

    1, 最小化的linux系统(centos\redhat)默认都是没有安装setup图形小工具的,你输入setup命令会提示 command not found . 如果要使用这个命令安装方法 1.安 ...

  4. python sorted用法

    python列表排序 python字典排序 sorted List的元素可以是各种东西,字符串,字典,自己定义的类等. sorted函数用法如下: sorted(data, cmp=None, key ...

  5. Sql Sever 字符串截取汉字

    最近需要在SQL的字符串中截取汉字,利用unicode函数判断字符的unicode编码,根据编码范围过滤掉非汉字字符. 写成了一个function /*@str 需要获取汉字的字符串*/ create ...

  6. 在CheckBox中,仅仅允许选择一项

    作用相当于RadioButonList <html xmlns="http://www.w3.org/1999/xhtml"> <head runat=&quot ...

  7. 使用hive客户端java api读写hive集群上的信息

    上文介绍了hdfs集群信息的读取方式,本文说hive 1.先解决依赖 <properties> <hive.version>1.2.1</hive.version> ...

  8. Postgres的tuple的组装

    1.相关的数据类型 我们先看相关的数据类型: HeapTupleData(src/include/access/htup.h) typedef struct HeapTupleData { uint3 ...

  9. linux系统下安装配置解压版的MySQL数据库

    一.解压文件到当前目录 命令:tar -zxvf mysql....tar.gz 二.移动解压完成的文件夹到目标目录并更名mysql 命令:mv mysql-版本号 /usr/local/mysql ...

  10. Python数据分析&lowbar;Pandas&lowbar;窗函数

    窗函数(window function)经常用在频域信号分析中.我其实不咋个懂,大概是从无限长的信号中截一段出来,然后把这一段做延拓变成一个虚拟的无限长的信号.用来截取的函数就叫窗函数,窗函数又分很多 ...