使用者空間與核心的介面

奔跑的路發表於2015-10-10

概論

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       = 0555,
     5:      .child      = kern_table,
     6:    },
     7:    {
     8:      .procname   = "vm",
     9:      .mode       = 0555,
    10:      .child      = vm_table,
    11:    },
    12:    {
    13:      .procname   = "fs",
    14:      .mode       = 0555,
    15:      .child      = fs_table,
    16:    },
    17:    {
    18:      .procname   = "debug",
    19:      .mode       = 0555,
    20:      .child      = debug_table,
    21:    },
    22:    {
    23:      .procname   = "dev",
    24:      .mode       = 0555,
    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(3, _##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 + 15)) {
 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 =   0,
 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 = 0;
 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網路技術內幕》):

http://images.cnitblog.com/blog/479389/201402/202305250767736.jpg

圖:ioctl命令的分派



netlink 套接字

netlink已經成為使用者空間與核心的IP網路配置之間的首選介面,同時它也可以作為核心內部與多個使用者空間程式之間的訊息傳輸系統.
在實現netlink用於核心空間與使用者空間之間的通訊時,使用者空間的建立方法和一般的套接字的建立使用類似,但核心的建立方法則有所不同,
下圖是netlink實現此類通訊時的建立過程:
http://images.cnitblog.com/blog/479389/201402/202305265973467.jpg
下面分別詳細介紹核心空間與使用者空間在實現此類通訊時的建立方法:
● 使用者空間:
建立流程大體如下:
① 建立socket套接字
② 呼叫bind函式完成地址的繫結,不過同通常意義下server端的繫結還是存在一定的差別的,server端通常繫結某個埠或者地址,而此處的繫結則是 將 socket 套介面與本程式的 pid 進行繫結
③ 通過sendto或者sendmsg函式傳送訊息;
④ 通過recvfrom或者rcvmsg函式接受訊息。


【說明】
◆ netlink對應的協議簇是AF_NETLINK,協議型別可以是自定義的型別,也可以是核心預定義的型別;


 1:  #define NETLINK_ROUTE       0   /* Routing/device hook              */
 2:  #define NETLINK_UNUSED      1   /* Unused number                */
 3:  #define NETLINK_USERSOCK    2   /* Reserved for user mode socket protocols  */
 4:  #define NETLINK_FIREWALL    3   /* Firewalling hook             */
 5:  #define NETLINK_INET_DIAG   4   /* INET socket monitoring           */
 6:  #define NETLINK_NFLOG       5   /* netfilter/iptables ULOG */
 7:  #define NETLINK_XFRM        6   /* ipsec */
 8:  #define NETLINK_SELINUX     7   /* SELinux event notifications */
 9:  #define NETLINK_ISCSI       8   /* Open-iSCSI */
10:  #define NETLINK_AUDIT       9   /* auditing */
11:  #define NETLINK_FIB_LOOKUP  10  
12:  #define NETLINK_CONNECTOR   11
13:  #define NETLINK_NETFILTER   12  /* netfilter subsystem */
14:  #define NETLINK_IP6_FW      13
15:  #define NETLINK_DNRTMSG     14  /* DECnet routing messages */
16:  #define NETLINK_KOBJECT_UEVENT  15  /* Kernel messages to userspace */
17:  #define NETLINK_GENERIC     16
18:  /* leave room for NETLINK_DM (DM Events) */
19:  #define NETLINK_SCSITRANSPORT   18  /* SCSI Transports */
20:  #define NETLINK_ECRYPTFS    19
21:  
22:  /* 如下這個型別是UTM組新增使用 */
23:  #define NETLINK_UTM_BLOCK   20  /* UTM block ip to userspace */
24:  #define NETLINK_AV_PROXY       21      /* av proxy */
25:  #define NETLINK_KURL        22  /*for commtouch*/
26:  
27:  #define MAX_LINKS 32        
上面是核心預定義的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() << 16 | 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 = 0; /*from kernel */
◆ netlink API函式sock_release可以用來釋放所建立的socket.

相關文章