深入理解linux网络技术内幕读书笔记(三)--用户空间与内核的接口

时间:2023-01-22 22:08:38

概论

sysctl (/proc/sys目录)

此接口允许用户空间读取或修改内核变量的值。
两种方式访问sysctl的输出变量:

  • sysctl 系统调用
  • procfs

编程接口

/proc/sys/中的文件和目录都是依ctl_table结构定义的。ctl_table结构的注册和除名是通过在kernel/sysctl.c中定义的register_sysctl_table和unregister_sysctl_table函数完成。

  • ctl_table结构

     1:  struct ctl_table
    2: {
    3: const char *procname; /* proc/sys中所用的文件名 */
    4: void *data;
    5: int maxlen; /* 输出的内核变量的尺寸大小 */
    6: mode_t mode;
    7: struct ctl_table *child; /* 用于建立目录与文件之间的父子关系 */
    8: struct ctl_table *parent; /* Automatically set */
    9: proc_handler *proc_handler; /* 完成读取或者写入操作的函数 */
    10: void *extra1;
    11: void *extra2; /* 两个可选参数,通常用于定义变量的最小值和最大值ֵ */
    12: };

    一般来讲,/proc/sys下定义了以下几个主目录(kernel, vm, fs, debug, dev),其以及它的子目录定义如下:

     1:  static struct ctl_table root_table[] = {
    2: {
    3: .procname = "kernel",
    4: .mode = ,
    5: .child = kern_table,
    6: },
    7: {
    8: .procname = "vm",
    9: .mode = ,
    10: .child = vm_table,
    11: },
    12: {
    13: .procname = "fs",
    14: .mode = ,
    15: .child = fs_table,
    16: },
    17: {
    18: .procname = "debug",
    19: .mode = ,
    20: .child = debug_table,
    21: },
    22: {
    23: .procname = "dev",
    24: .mode = ,
    25: .child = dev_table,
    26: },
    27: /*
    28: * NOTE: do not add new entries to this table unless you have read
    29: * Documentation/sysctl/ctl_unnumbered.txt
    30: */
    31: { }
    32: };

sysfs (/sys 文件系统)

sysfs主要解决了procfs与sysctl滥用的问题而出现的。

ioctl 系统调用

一切均从系统调用开始,当用户调用ioctl函数时,会调用内核中的 SYSCALL_DEFINE3 函数,它是一个宏定义,如下:

1:  #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(, _##name, __VA_ARGS__)

【注意】宏定义中的第一个#代表替换, 则代表使用’-’强制连接。
SYSCALL_DEFINEx之后调用__SYSCALL_DEFINEx函数,而__SYSCALL_DEFINEx同样是一个宏定义,如下:

 1:  #define __SYSCALL_DEFINEx(x, name, ...)                 \
2: asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__)); \
3: static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__)); \
4: asmlinkage long SyS##name(__SC_LONG##x(__VA_ARGS__)) \
5: { \
6: __SC_TEST##x(__VA_ARGS__); \
7: return (long) SYSC##name(__SC_CAST##x(__VA_ARGS__)); \
8: } \
9: SYSCALL_ALIAS(sys##name, SyS##name); \
10: static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__))

中间会调用红色部分的宏定义, asmlinkage 会通知编译器仅从 栈 中提取该函数的参数,
上面只是阐述了系统调用的一般过程,值得注意的是,上面这种系统调用过程的应用于最新的内核代码中,老版本中的这些过程是在syscall函数中完成的。
我们就以在网络编程中ioctl系统调用为例介绍整个调用过程。当用户调用ioctl试图去从内核中获取某些值时,会触发调用:

 1:  SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
2: {
3: struct file *filp;
4: int error = -EBADF;
5: int fput_needed;
6:
7: filp = fget_light(fd, &fput_needed); //根据进程描述符获取对应的文件对象
8: if (!filp)
9: goto out;
10:
11: error = security_file_ioctl(filp, cmd, arg);
12: if (error)
13: goto out_fput;
14:
15: error = do_vfs_ioctl(filp, fd, cmd, arg);
16: out_fput:
17: fput_light(filp, fput_needed);
18: out:
19: return error;
20: }

之后依次经过 file_ioctl-—>vfs_ioctl 找到对应的与socket相对应的ioctl,即sock_ioctl.

 1:  static long vfs_ioctl(struct file *filp, unsigned int cmd,
2: unsigned long arg)
3: {
4: ........
5: if (filp->f_op->unlocked_ioctl) {
6: error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
7: if (error == -ENOIOCTLCMD)
8: error = -EINVAL;
9: goto out;
10: } else if (filp->f_op->ioctl) {
11: lock_kernel();
12: error = filp->f_op->ioctl(filp->f_path.dentry->d_inode,
13: filp, cmd, arg);
14: unlock_kernel();
15: }
16: .......
17: }

从上面代码片段中可以看出,根据对应的文件指针调用对应的ioctl,那么socket对应的文件指针的初始化是在哪完成的呢?可以参考socket.c文件下sock_alloc_file函数:

 1:  static int sock_alloc_file(struct socket *sock, struct file **f, int flags)
2: {
3: struct qstr name = { .name = "" };
4: struct path path;
5: struct file *file;
6: int fd;
7: ..............
8: file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
9: &socket_file_ops);
10: if (unlikely(!file)) {
11: /* drop dentry, keep inode */
12: atomic_inc(&path.dentry->d_inode->i_count);
13: path_put(&path);
14: put_unused_fd(fd);
15: return -ENFILE;
16: }
17: .............
18: }

alloc_file函数将socket_file_ops指针赋值给socket中的f_op.同时注意 file->private_data = sock 这条语句。

 1:  struct file *alloc_file(struct path *path, fmode_t mode,const struct file_operations *fop)
2: {
3: .........
4:
5: file->f_path = *path;
6: file->f_mapping = path->dentry->d_inode->i_mapping;
7: file->f_mode = mode;
8: file->f_op = fop;
9:
10: file->private_data = sock;
11: ........
12: }

而socket_file_ops是在socket.c文件中定义的一个静态结构体变量,它的定义如下:

 1:  static const struct file_operations socket_file_ops = {
2: .owner = THIS_MODULE,
3: .llseek = no_llseek,
4: .aio_read = sock_aio_read,
5: .aio_write = sock_aio_write,
6: .poll = sock_poll,
7: .unlocked_ioctl = sock_ioctl,
8: #ifdef CONFIG_COMPAT
9: .compat_ioctl = compat_sock_ioctl,
10: #endif
11: .mmap = sock_mmap,
12: .open = sock_no_open, /* special open code to disallow open via /proc */
13: .release = sock_close,
14: .fasync = sock_fasync,
15: .sendpage = sock_sendpage,
16: .splice_write = generic_splice_sendpage,
17: .splice_read = sock_splice_read,
18: };

从上面分析可以看出, filp-> f_op->unlocked_ioctl 实质调用的是sock_ioctl。
OK,再从sock_ioctl代码开始,如下:

 1:  static long sock_ioctl(struct file *file, unsigned cmd, unsigned long arg)
2: {
3: .......
4: sock = file->private_data;
5: sk = sock->sk;
6: net = sock_net(sk);
7: if (cmd >= SIOCDEVPRIVATE && cmd <= (SIOCDEVPRIVATE + )) {
8: err = dev_ioctl(net, cmd, argp);
9: } else
10: #ifdef CONFIG_WEXT_CORE
11: if (cmd >= SIOCIWFIRST && cmd <= SIOCIWLAST) {
12: err = dev_ioctl(net, cmd, argp);
13: } else
14: #endif
15: .......
16: default:
17: err = sock_do_ioctl(net, sock, cmd, arg);
18: break;
19: }
20: return err;
21: }

首先通过file变量的private_date成员将socket从sys_ioctl传递过来,最后通过执行sock_do_ioctl函数完成相应操作。

 1:  static long sock_do_ioctl(struct net *net, struct socket *sock,
2: unsigned int cmd, unsigned long arg)
3: {
4: ........
5: err = sock->ops->ioctl(sock, cmd, arg);
6: .........
7: }
8:
9: 那么此时的socket中的ops成员又是从哪来的呢?我们以IPV4为例,都知道在创建socket时,都会需要设置相应的协议类型,此处的ops也是socket在创建inet_create函数中遍历inetsw列表得到的。
10:
11: static int inet_create(struct net *net, struct socket *sock, int protocol,
12: int kern)
13: {
14: struct inet_protosw *answer;
15: ........
16: sock->ops = answer->ops;
17: answer_prot = answer->prot;
18: answer_no_check = answer->no_check;
19: answer_flags = answer->flags;
20: rcu_read_unlock();
21: .........
22: }

那么inetsw列表又是在何处生成的呢?那是在协议初始化函数inet_init中调用inet_register_protosw(将全局结构体数组inetsw_array数组初始化)来实现的,

 1:  static struct inet_protosw inetsw_array[] =
2: {
3: {
4: .type = SOCK_STREAM,
5: .protocol = IPPROTO_TCP,
6: .prot = &tcp_prot,
7: .ops = &inet_stream_ops,
8: .no_check = ,
9: .flags = INET_PROTOSW_PERMANENT |
10: INET_PROTOSW_ICSK,
11: },
12: {
13: .type = SOCK_DGRAM,
14: .protocol = IPPROTO_UDP,
15: .prot = &udp_prot,
16: .ops = &inet_dgram_ops,
17: .no_check = UDP_CSUM_DEFAULT,
18: .flags = INET_PROTOSW_PERMANENT,
19: },
20: {
21: .type = SOCK_RAW,
22: .protocol = IPPROTO_IP, /* wild card */
23: .prot = &raw_prot,
24: .ops = &inet_sockraw_ops,
25: .no_check = UDP_CSUM_DEFAULT,
26: .flags = INET_PROTOSW_REUSE,
27: }
28: };

很明显可以看到, sock-> ops->ioctl 需要根据具体的协议找到需要调用的ioctl函数。我们就以TCP协议为例,就需要调用 inet_stream_ops 中的ioctl函数——inet_ioctl,结构如下:

 1:  int inet_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
2: {
3: struct sock *sk = sock->sk;
4: int err = ;
5: struct net *net = sock_net(sk);
6:
7: switch (cmd) {
8: case SIOCGSTAMP:
9: err = sock_get_timestamp(sk, (struct timeval __user *)arg);
10: break;
11: case SIOCGSTAMPNS:
12: err = sock_get_timestampns(sk, (struct timespec __user *)arg);
13: break;
14: case SIOCADDRT: //增加路由
15: case SIOCDELRT: //删除路由
16: case SIOCRTMSG:
17: err = ip_rt_ioctl(net, cmd, (void __user *)arg); //IP路由配置
18: break;
19: case SIOCDARP: //删除ARP项
20: case SIOCGARP: //获取ARP项
21: case SIOCSARP: //创建或者修改ARP项
22: err = arp_ioctl(net, cmd, (void __user *)arg); //ARP配置
23: break;
24: case SIOCGIFADDR: //获取接口地址
25: case SIOCSIFADDR: //设置接口地址
26: case SIOCGIFBRDADDR: //获取广播地址
27: case SIOCSIFBRDADDR: //设置广播地址
28: case SIOCGIFNETMASK: //获取网络掩码
29: case SIOCSIFNETMASK: //设置网络掩码
30: case SIOCGIFDSTADDR: //获取某个接口的点对点地址
31: case SIOCSIFDSTADDR: //设置每个接口的点对点地址
32: case SIOCSIFPFLAGS:
33: case SIOCGIFPFLAGS:
34: case SIOCSIFFLAGS: //设置接口标志
35: err = devinet_ioctl(net, cmd, (void __user *)arg); //网络接口配置相关
36: break;
37: default:
38: if (sk->sk_prot->ioctl)
39: err = sk->sk_prot->ioctl(sk, cmd, arg);
40: else
41: err = -ENOIOCTLCMD;
42: break;
43: }
44: return err;
45: }

到此,基本上找到了socket所对应的ioctl处理代码片段。整个流程大致可以用下面图进行概括(来自《深入理解Linux网络技术内幕》):

深入理解linux网络技术内幕读书笔记(三)--用户空间与内核的接口

图:ioctl命令的分派

netlink 套接字

netlink已经成为用户空间与内核的IP网络配置之间的首选接口,同时它也可以作为内核内部与多个用户空间进程之间的消息传输系统.
在实现netlink用于内核空间与用户空间之间的通信时,用户空间的创建方法和一般的套接字的创建使用类似,但内核的创建方法则有所不同,
下图是netlink实现此类通信时的创建过程:
深入理解linux网络技术内幕读书笔记(三)--用户空间与内核的接口
下面分别详细介绍内核空间与用户空间在实现此类通信时的创建方法:
● 用户空间:
创建流程大体如下:
① 创建socket套接字
② 调用bind函数完成地址的绑定,不过同通常意义下server端的绑定还是存在一定的差别的,server端通常绑定某个端口或者地址,而此处的绑定则是 将 socket 套接口与本进程的 pid 进行绑定
③ 通过sendto或者sendmsg函数发送消息;
④ 通过recvfrom或者rcvmsg函数接受消息。

【说明】
◆ netlink对应的协议簇是AF_NETLINK,协议类型可以是自定义的类型,也可以是内核预定义的类型;

 1:  #define NETLINK_ROUTE          /* Routing/device hook              */
2: #define NETLINK_UNUSED /* Unused number */
3: #define NETLINK_USERSOCK /* Reserved for user mode socket protocols */
4: #define NETLINK_FIREWALL /* Firewalling hook */
5: #define NETLINK_INET_DIAG /* INET socket monitoring */
6: #define NETLINK_NFLOG /* netfilter/iptables ULOG */
7: #define NETLINK_XFRM /* ipsec */
8: #define NETLINK_SELINUX /* SELinux event notifications */
9: #define NETLINK_ISCSI /* Open-iSCSI */
10: #define NETLINK_AUDIT /* auditing */
11: #define NETLINK_FIB_LOOKUP
12: #define NETLINK_CONNECTOR
13: #define NETLINK_NETFILTER /* netfilter subsystem */
14: #define NETLINK_IP6_FW
15: #define NETLINK_DNRTMSG /* DECnet routing messages */
16: #define NETLINK_KOBJECT_UEVENT /* Kernel messages to userspace */
17: #define NETLINK_GENERIC
18: /* leave room for NETLINK_DM (DM Events) */
19: #define NETLINK_SCSITRANSPORT /* SCSI Transports */
20: #define NETLINK_ECRYPTFS
21:
22: /* 如下这个类型是UTM组新增使用 */
23: #define NETLINK_UTM_BLOCK /* UTM block ip to userspace */
24: #define NETLINK_AV_PROXY /* av proxy */
25: #define NETLINK_KURL /*for commtouch*/
26:
27: #define MAX_LINKS

上面是内核预定义的20种类型,当然也可以自定义一些。

◆ 前面说过,netlink处的绑定有着自己的特殊性,其需要绑定的协议地址可用以下结构来描述:

1:  struct sockaddr_nl {
2: sa_family_t nl_family; /* AF_NETLINK */
3: unsigned short nl_pad; /* zero */
4: __u32 nl_pid; /* port ID */
5: __u32 nl_groups; /* multicast groups mask */
6: };

其中成员nl_family为AF_NETLINK,nl_pad当前未使用,需设置为0,成员nl_pid为 接收或发送消息的进程的 ID ,如果希望内核处理消息或多播消息,
就把该字段设置为 0,否则设置为处理消息的进程 ID.,不过在此特别需要说明的是,此处是以进程为单位,倘若进程存在多个线程,那在与netlink通信的过程中如何准确找到对方线程呢?
此时nl_pid可以这样表示:

1:  pthread_self() <<  | getpid()

pthread_self函数是用来获取线程ID,总之能够区分各自线程目的即可。 成员 nl_groups 用于指定多播组 ,bind 函数用于把调用进程加入到该字段指定的多播组, 如果设置为 0 ,表示调用者不加入任何多播组
◆ 通过netlink发送的消息结构:

1:  struct nlmsghdr {
2: __u32 nlmsg_len; /* Length of message including header */
3: __u16 nlmsg_type; /* Message content */
4: __u16 nlmsg_flags; /* Additional flags */
5: __u32 nlmsg_seq; /* Sequence number */
6: __u32 nlmsg_pid; /* Sending process port ID */
7: };

其中nlmsg_len指的是消息长度,nlmsg_type指的是消息类型,用户可以自己定义。字段nlmsg_flags 用于设置消息标志,对于一般的使用,用户把它设置为0 就可以,
只是一些高级应用(如netfilter 和路由daemon 需要它进行一些复杂的操作), 字段 nlmsg_seq 和 nlmsg_pid 用于应用追踪消息,前者表示顺序号,后者为消息来源进程 ID

● 内核空间:
内核空间主要完成以下三方面的工作:
① 创建netlinksocket,并注册回调函数,注册函数将会在有消息到达netlinksocket时会执行;
② 根据用户请求类型,处理用户空间的数据;
③ 将数据发送回用户。
【说明】
◆ netlink中利用netlink_kernel_create函数创建一个netlink socket.

1:  extern struct sock *netlink_kernel_create(struct net *net,
2: int unit,unsigned int groups,
3: void (*input)(struct sk_buff *skb),
4: struct mutex *cb_mutex,
5: struct module *module);

net字段指的是网络的命名空间,一般用&init_net替代; unit 字段实质是 netlink 协议类型,值得注意的是,此值一定要与用户空间创建 socket 时的第三个参数值保持一致 ;
groups字段指的是socket的组名,一般置为0即可;input字段是回调函数,当netlink收到消息时会被触发;cb_mutex一般置为NULL;module一般置为THIS_MODULE宏。

◆ netlink是通过调用API函数netlink_unicast或者netlink_broadcast将数据返回给用户的.

1:  int netlink_unicast(struct sock *ssk, struct sk_buff *skb,u32 pid, int nonblock)

ssk字段正是由netlink_kernel_create函数所返回的socket;参数skb指向的是socket缓存, 它的 data 字段用来指向要发送的 netlink 消息结构 ; 参数 pid 为接收消息进程的 pid ,
参数 nonblock 表示该函数是否为非阻塞,如果为 1 ,该函数将在没有接收缓存可利用时立即返回,而如果为 0 ,该函数在没有接收缓存可利用时睡眠 。

【引申】内核发送的netlink消息是通过struct sk_buffer结构来管理的,即socket缓存,linux/netlink.h中定义了

1:  #define NETLINK_CB(skb)     (*(struct netlink_skb_parms*)&((skb)->cb))

来方便消息的地址设置。

1:  struct netlink_skb_parms {
2: struct ucred creds; /* Skb credentials */
3: __u32 pid;
4: __u32 dst_group;
5: kernel_cap_t eff_cap;
6: __u32 loginuid; /* Login (audit) uid */
7: __u32 sessionid; /* Session id (audit) */
8: __u32 sid; /* SELinux security id */
9: };

其中pid指的是发送者的进程ID,如:

1:  NETLINK_CB(skb).pid = ; /*from kernel */

◆ netlink API函数sock_release可以用来释放所创建的socket.

深入理解linux网络技术内幕读书笔记(三)--用户空间与内核的接口的更多相关文章

  1. 深入理解linux网络技术内幕读书笔记&lpar;七&rpar;--组件初始化的内核基础架构

    Table of Contents 1 引导期间的内核选项 2 注册关键字 3 模块初始化代码 引导期间的内核选项 linux运行用户把内核配置选项传给引导记录,然后引导记录再把选项传给内核. 在引导 ...

  2. 深入理解linux网络技术内幕读书笔记&lpar;十&rpar;--帧的接收

    Table of Contents 1 概述 1.1 帧接收的中断处理 2 设备的开启与关闭 3 队列 4 通知内核帧已接收:NAPI和netif_rx 4.1 NAPI简介 4.1.1 NAPI优点 ...

  3. 深入理解linux网络技术内幕读书笔记&lpar;四&rpar;--通知链

    Table of Contents 1 概述 2 定义链 3 链注册 4 链上的通知事件 5 网络子系统的通知链 5.1 包裹函数 5.2 范例 6 测试实例 概述 [注意] 通知链只在内核子系统之间 ...

  4. 深入理解linux网络技术内幕读书笔记&lpar;八&rpar;--设备注册与初始化

    Table of Contents 1 设备注册之时 2 设备除名之时 3 分配net_device结构 4 NIC注册和除名架构 4.1 注册 4.2 除名 5 设备初始化 6 设备类型初始化: x ...

  5. 深入理解linux网络技术内幕读书笔记&lpar;五&rpar;--网络设备初始化

    Table of Contents 1 简介 2 系统初始化概论 2.1 引导期间选项 2.2 中断和定时器 2.3 初始化函数 3 设备注册和初始化 3.1 硬件初始化 3.2 软件初始化 3.3 ...

  6. 深入理解linux网络技术内幕读书笔记&lpar;九&rpar;--中断与网络驱动程序

    Table of Contents 1 接收到帧时通知驱动程序 1.1 轮询 1.2 中断 2 中断处理程序 3 抢占功能 4 下半部函数 4.1 内核2.4版本以后的下半部函数: 引入软IRQ 5 ...

  7. 深入理解linux网络技术内幕读书笔记&lpar;二&rpar;--关键数据结构

    Table of Contents 1 套接字缓冲区: sk_buff结构 1.1 网络选项及内核结构 1.2 结构说明及操作函数 2 net_device结构 2.1 MTU 2.2 结构说明及操作 ...

  8. 深入理解linux网络技术内幕读书笔记&lpar;六&rpar;--PCI层与网络接口卡

    Table of Contents 1 本章涉及的数据结构 1.1 pci_device_id结构 1.2 pci_dev结构 1.3 pci_driver结构 2 PCI NIC设备驱动程序的注册 ...

  9. 深入理解linux网络技术内幕读书笔记&lpar;一&rpar;--简介

    Table of Contents 1 基本术语 1.1 本书常用的缩写 2 引用计数 2.1 引用计数函数 3 垃圾回收 3.1 异步 3.2 同步 4 函数指针 4.1 缺点 5 goto语句 5 ...

随机推荐

  1. linux sort&comma;uniq&comma;cut&comma;wc&period;

    文章转自 http://www.cnblogs.com/ggjucheng/archive/2013/01/13/2858385.html sort sort 命令对 File 参数指定的文件中的行排 ...

  2. 分享&quot&semi;狼用&quot&semi;API一個

    API People that are interested in using our service for automated caching of their newly created .to ...

  3. mysql qps tps

    (1)QPS(每秒Query量) QPS = Questions(or Queries) / seconds mysql > show global status like 'Question% ...

  4. lesson9&colon;分布式定时任务

    在实际的开发过程中,我们一定会遇到服务自有的定时任务,又因为现在的服务都是分布式的,但是对于定时任务,很多的业务场景下,还是只希望单台服务来执行,网上有很多分布式定时任务的框架,各位如感兴趣,可以自行 ...

  5. vmstat

    vmstat(virtual memory statitics)命令是最常见的Linux/Unix监控工具,可以展现给定时间间隔的服务器的状态值,包括服务器的CPU使用率,内存使用,虚拟内存交换情况, ...

  6. Oracle如何禁止并行

    PURPOSE -------   To explain how to disable Parallel Execution on Session/System level     SCOPE &am ...

  7. Java框架之Hibernate(三)

    本文主要讲解: 1 级联 cascade 关键字 2 级联删除 3 inverse 关键字 4 懒加载 5 缓存的模拟 6 Hibernate 的一级缓存 7 Hibernate 的二级缓存 一.级联 ...

  8. Abd学习笔记

    Abd学习笔记 V快捷键:转正坐标 Tab快捷键:切换xyz或是长度角度 空格键快捷键:切换长度或弧度 Enter快捷键:确定方向x或y O快捷键:做辅助线 E:切换平面,分别有t,f,s Ra:创建 ...

  9. MyEclipse部署WebLogic

    ====================================================================================

  10. java笔试总结

    1. Java的IO操作中有面向字节(Byte)和面向字符(Character)两种方式.面向字节的操作为以8位为单位对二进制的数据进行操作,对数据不进行转换,这些类都是InputStream和Out ...