linux內核IOCTL網絡控制框架實現分析
linux內核IOCTL網絡控制框架實現分析
(轉載請註明出處)
目錄
從ioctl這個名稱上看,它是設備驅動程序中對設備的I/O通道進行管理的函數。所謂對I/O通道進行管理,就是對設備的一些特性進行控制,例如串口的傳輸波特率、馬達的轉速等等, 但實際上ioctl所處理的對象並不限制是真正的I/O設備,還可以是其它任何一個內核設備.ioctl以系統調用的形式提供了一條用戶與內核交互的便捷途徑。當前一些寬帶計費網關、防火牆系統均利用Ioctl與內核良好的通信互動特點支持用戶對基於內核模塊的軟件系統的控制.本文針對i386平臺下的ioctl內核網絡源代碼控制框架進行剖析解釋,在文章最後列舉一個實例,通過編程實踐展示如何通過ioctl控制函數實現自定義的功能的控制,使讀者可以對ioctl實現原理有一個全面的認識,本文只對ioctl實現流程框架做一定的敘述,並不會深入到具體的控制函數。爲了更好的閱讀本文,要求讀者對
Linux 下的網絡編程有一定的瞭解。
本文約定:
1、以下內容如果沒有特殊說明,均參照linux內核2.4.0版本
2、“->”箭頭符表示函數調用關係,如sys_socket->sock_map_fd表示sys_socket函數調用的sock_map_fd函數。
3、第五節的實踐是在redhat9上實現,基於2.4.20內核,但本文所述在2.4內核下都適用。
二、用戶空間ioctl控制函數調用形式
通過man 2 ioctl命令查看ioctl函數的調用形式類似如下:
#include <sys/ioctl.h>
int ioctl(int d, int request, ...);
其中d就是用戶程序打開設備時使用open函數返回的文件描述符,request就是用戶程序對設備的控制命令,至於後面的省略號,則是一些補充參數,一般最多一個,有或沒有是和request的意義相關的,詳情請參考man 2 ioctl_list以瞭解更多。ioctl函數是文件結構中的一個屬性分量,就是說如果驅動程序提供了對ioctl的支持,用戶就可以在用戶程序中使用ioctl函數控制設備的I/O通道或其它一些自己想要控制且設備支持的功能。
內核實現ioctl()函數的是sys_ioctl(),在內核中主要調用框架圖如下,它清晰地給我們展示ioctl的控制傳遞框架,我們接下來的內容將根據此圖向大家做詳細的解釋:
四、IOCTL框架源代碼分析
根據前面的圖示,我們從入口函數sys_ioctl開始分析:
4.1、入口函數:sys_ioctl
以下源碼在fs/ioctl.c中,其中刪除了部分與網絡控制關係不大的代碼:
asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg)
{
…//根據fd獲取文件結構(struct file)
lock_kernel();
switch (cmd) {
case FIOCLEX://對文件設置專用標誌,通知內核自動關閉打開的文件
…
case FIONCLEX://與FIOCLEX標誌相反,清除專用標誌
…
case FIONBIO://將文件操作設置成阻塞/非阻塞
…
case FIOASYNC:// 將文件操作設置成同步/異步IO
… //以上省略的代碼是關於具體的磁盤文件系統的控制處理,
//關於socket的阻塞或非阻塞等設置很簡單,有興趣的讀者直接閱讀源碼吧
default: //文件其它部分的處理被放在了default部分
error = -ENOTTY;
if (S_ISREG(filp->f_dentry->d_inode->i_mode)) //普通文件
error = file_ioctl(filp, cmd, arg); //
else if (filp->f_op && filp->f_op->ioctl) //socket控制在此處理
error = filp->f_op->ioctl(filp->f_dentry->d_inode, filp, cmd, arg);
}
unlock_kernel();
fput(filp);
out:
return error;
}
注意上面藍色字體部分,即爲調用網絡部分的代碼入口。大家注意在default情況下,有個S_ISREG宏對文件類型作判斷,其定義在include/linux/stat.h中:
#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) //符號連接文件
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) //普通文件
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) //目錄文件
#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) //字符設備文件
#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK) //塊設備文件
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) //管道文件
#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK) //socket套接字文件
因爲linux內核把socket套接字當作文件來處理,內核在創建socket套接字時,爲套接字分配文件id以及生成與id對應的文件節點,節點的i_mode域是代表文件類型的位域標誌字段,所以內核定義了上述宏來簡化判斷操作。由於套接字文件不屬於普通文件之列,所以程序直接執行藍色字體部分。
4.2、入口函數跳轉
我們來看一下filp->f_op->ioctl函數指針指向了什麼函數,可以參考net/socket.c文件中的sys_socket->sock_map_fd函數中的一行代碼(藍色部分代碼):
static int sock_map_fd(struct socket *sock)
{
…
sock->file = file;
file->f_op = sock->inode->i_fop = &socket_file_ops;
file->f_mode = 3;
file->f_flags = O_RDWR;
file->f_pos = 0;
…
}
內核在用戶創建socket套接字時就將此套接字的文件操作函數指針初始化了。從上面的代碼我們可以看到,filp->f_op以及文件對應的socket節點的i_fop指針都被賦值爲指向socket_file_ops結構,所以我們來看看內核是如何實現這個控制過程的轉移的。還是在內核的net/socket.c文件中,定義了socket_file_ops結構如下:
static struct file_operations socket_file_ops = {
llseek: sock_lseek,
read: sock_read,
write: sock_write,
poll: sock_poll,
ioctl: sock_ioctl,
mmap: sock_mmap,
open: sock_no_open, /* special open code to disallow open via /proc */
release: sock_close,
fasync: sock_fasync,
readv: sock_readv,
writev: sock_writev
};
從上面的代碼來看,這個結構定義了socket描述字的文件操作函數,如對描述字調用read函數讀數據時最終將訪問sock_read函數,對描述字調用write函數讀數據時最終將訪問sock_write函數,等等。而對ioctl的訪問最終將轉化爲調用sock_ioctl函數,看到此處我們明白了,filp->f_op->ioctl(filp->f_dentry->d_inode, filp, cmd, arg)調用實質上轉化爲對sock_ioctl函數的調用。
4.3、sock_ioctl函數
sock_ioctl函數依然在net/socket.c文件中,列出如下:
int sock_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
struct socket *sock;
int err;
unlock_kernel();
sock = socki_lookup(inode);
err = sock->ops->ioctl(sock, cmd, arg);
lock_kernel();
return err;
}
此處函數引入inode參數實質是通過節點找到套接字對應的socket結構,通過socket的struct proto_ops類型的字段ops執行具體的控制操作(即sock->ops->ioctl(sock, cmd, arg)),函數socki_lookup也在文件net/socket.c中,列出如下:
extern __inline__ struct socket *socki_lookup(struct inode *inode)
{
return &inode->u.socket_i;
}
寫到這大家可能要問爲什麼不直接在filp->f_op->ioctl函數指針指向的函數裏面執行ioctl控制操作而要做兩次跳轉呢?其實這與linux良好的設計規範和業務支持的實際情況都有關係,第一次跳轉是轉入套接字單獨處理,因爲內核中網絡部分是非常重要的,可以與文件系統相提並論,將網絡部分獨立出來處理在設計思路上更清晰;另外,linux內核支持不同層次、類型的套接字,如ipv4、ipv6套接字以及sock_raw原始套接字,對於這些套接字的處理有一定的相似性,又有其不同的地方。所以引入第二次跳轉的目的也即在此,以支持對不同的協議類型的套接字進行不同控制,詳情見下面小節的介紹。
4.4、二次跳轉
閒話少說,步入正題。接下來我們看看sock->ops->ioctl函數指針調用了什麼函數,首先看看 sock變量的結構類型struct socket,大家要多注意這個結構,在後面我們也列出了相關結構相互引用圖中涉及到的這個結構的幾個字段,以加深大家的印象.結構的源代碼在include/linux/Net.h文件中:
struct socket
{
socket_state state;
unsigned long flags;
struct proto_ops *ops;
struct inode *inode;
struct fasync_struct *fasync_list; /* Asynchronous wake up list */
struct file *file; /* File back pointer for gc */
struct sock *sk;
wait_queue_head_t wait;
…
};
套接字就是通過結構中ops指針來執行具體的ioctl控制函數的。struct proto_ops定義在同樣的頭文件中:
struct proto_ops {
int family;
int (*release) (struct socket *sock);
int (*bind) (struct socket *sock, struct sockaddr *umyaddr, int sockaddr_len);
int (*connect) (struct socket *sock, struct sockaddr *uservaddr, int sockaddr_len, int flags);
int (*socketpair) (struct socket *sock1, struct socket *sock2);
int (*accept) (struct socket *sock, struct socket *newsock, int flags);
int (*getname) (struct socket *sock, struct sockaddr *uaddr, int *usockaddr_len, int peer);
unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait);
int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg);
int (*listen) (struct socket *sock, int len);
int (*shutdown) (struct socket *sock, int flags);
int (*setsockopt) (struct socket *sock, int level, int optname, char *optval, int optlen);
int (*getsockopt) (struct socket *sock, int level, int optname, char *optval, int *optlen);
int (*sendmsg) (struct socket *sock, struct msghdr *m, int total_len, struct scm_cookie *scm);
int (*recvmsg) (struct socket *sock, struct msghdr *m, int total_len, int flags, struct scm_cookie *scm);
int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma);
};
補充一下基礎知識,一個套接字接口在邏輯上有三個要素:網域,類型和規程(協議).
網域:表明套接字接口用於哪一中網絡或這說哪一族網絡規程.就是我們通常說的地址族(family),常見的有AF_UNIX/AF_INET/AF_X25/AF_IPX等待.
類型:表明通訊中所遵循的模式,主要有兩種模式:”有連接”和”無連接”,對應到以太網就是SOCK_STREAM和SOCK_DGRAM兩種.
規程:具體的網絡協議.通常,網域和類型基本就能夠確定使用的規程了.
這裏的proto_ops結構就是通過不同的實例來支持具體的網域的不同類型、規程所使用的通信函數,每個網域都有多種類型、多種規程,所以也有多個proto_ops實例,給這個實例賦值具體規程的處理函數,如ipv4的有連接和無連接實例所指定的控制函數都是inet_ioctl(如果處理不同也可以指向不同的控制函數),這樣可以使具體的控制操作轉向具體的處理,細節實現我們下一小節介紹.
構造內核時,內核會初始化網絡地址族,即初始化net_families[NRPORO]全局量,這是一個靜態指針數組。每個網域地址族的初始化函數都由其中一個元素來表徵,例如,“INET”和它的初始程序地址分別是PF_INET(等同於AF_INET)和inet_create。當套接口啓動時被初始化時,要調用每一網域初始化程序,爲具體的類型指定處理函數,內核初始化網域地址族後net_families[NRPORO]變量的相關字段取值狀態示意圖如下:
對IPV4地址族來說,這個初始化函數就是inet_create,其代碼在net/ipv4/af_inet.c中:
static int inet_create(struct socket *sock, int protocol)
{
…
switch (sock->type) {
case SOCK_STREAM:
if (protocol && protocol != IPPROTO_TCP) //類型與規程檢測
goto free_and_noproto;
protocol = IPPROTO_TCP;
prot = &tcp_prot;
sock->ops = &inet_stream_ops; //此處指定函數跳轉表
break;
case SOCK_SEQPACKET:
goto free_and_badtype;
case SOCK_DGRAM:
if (protocol && protocol != IPPROTO_UDP)
goto free_and_noproto;
protocol = IPPROTO_UDP;
sk->no_check = UDP_CSUM_DEFAULT;
prot=&udp_prot;
sock->ops = &inet_dgram_ops; //此處指定函數跳轉表
break;
case SOCK_RAW:
if (!capable(CAP_NET_RAW)) //檢驗是否有創建原始套接字的權限
…
sock->ops = &inet_dgram_ops;//
if (protocol == IPPROTO_RAW)
sk->protinfo.af_inet.hdrincl = 1;
break;
default:
goto free_and_badtype;
}
…
}
從上面的代碼可以看出:已註冊的網域的類型所對應的操作被存在socket結構的ops 指針中,它就是指向具體的proto_ops數據結構實例,如inet_stream_ops、inet_dgram_ops等。proto_ops結構由地址族類型和一系列指向與特定地址族對應的socket操作函數的指針組成。ops 字段通過地址族標識符來索引,接下來我們看看proto_ops結構。
4.5、struct
proto_ops結構實例
前面說過,具體的ioctl執行過程時通過兩次跳轉而來,其中第二次就是針對各個不同層次、類型的套接字。我們來看看內核中所定義的各個具體的proto_ops結構實例以分析不同的控制執行流程. 內核中爲每個規程定義了一個proto_ops結構實例,常見的如下:
1、在net/ipv4/Af_inet.c文件中:
struct proto_ops inet_stream_ops = {
…
poll: tcp_poll,
ioctl: inet_ioctl,
listen: inet_listen,
…
};
struct proto_ops inet_dgram_ops = {
…
poll: datagram_poll,
ioctl: inet_ioctl,
listen: sock_no_listen,
…
};
可見這兩個實例有相當多的處理函數都是一樣的,並且最終調用相同的控制函數inet_ioctl.
2、在net/ipv6/Af_inet6.c文件中提供了inet6_stream_ops和inet6_dgram_ops,其地址族及ioctl處理函數分別爲PF_INET6和inet6_ioctl:
struct proto_ops inet6_stream_ops = {
family: PF_INET6,
…
ioctl: inet6_ioctl, /* must change */
…
};
struct proto_ops inet6_dgram_ops = {
family: PF_INET6,
…
ioctl: inet6_ioctl, /* must change */
…
};
3、在net/packet/Af_ packet 6.c文件中提供了packet_ops_spkt和packet_ops,其地址族及ioctl處理函數分別爲PF_PACKET和packet_ioctl:
struct proto_ops packet_ops = {
family: PF_PACKET,
…
ioctl: packet_ioctl,
…
};
還有x25和ipx、netlink、unix域等等地址族所對應的文件提供了各自的協議規程操作函數指針以支持不同的ioctl處理函數,大家有興趣可以參考內核相關源碼.
可見,通過二次跳轉表,內核可以支持不同協議規程做不同的操作,包括控制處理。本文把重點放在ipv4的ioctl控制函數,引導大家深入到其處理源碼.
4.6、inet_ioctl函數
由於inet_ioctl函數內容分支很多,但功能、處理不難理解,所以我把一些不常見的內容都省去,挑簡單重要的說,完全在於拋磚引玉:
static int inet_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
{
…
switch(cmd)
{
case FIOSETOWN://設置屬主
case SIOCSPGRP://設置進程組
err = get_user(pid, (int *) arg);
if (err)
return err;
if (current->pid != pid && current->pgrp != -pid &&
!capable(CAP_NET_ADMIN))
return -EPERM;
sk->proc = pid;
return(0);
case FIOGETOWN://獲取屬主
case SIOCGPGRP://獲取進程組
return put_user(sk->proc, (int *)arg);
case SIOCGSTAMP://
if(sk->stamp.tv_sec==0)
return -ENOENT;
err = copy_to_user((void *)arg,&sk->stamp,sizeof(struct timeval));
if (err)
err = -EFAULT;
return err;
case SIOCADDRT://增加路由
case SIOCDELRT://刪除路由
case SIOCRTMSG:
return(ip_rt_ioctl(cmd,(void *) arg));//IP路由配置
case SIOCDARP://刪除arp項
case SIOCGARP://獲取arp項
case SIOCSARP://創建/修改arp項
return(arp_ioctl(cmd,(void *) arg));//arp配置
case SIOCGIFADDR://獲取接口地址
case SIOCSIFADDR://設置接口地址
case SIOCGIFBRDADDR://獲取廣播地址
case SIOCSIFBRDADDR://設置廣播地址
case SIOCGIFNETMASK://獲取網絡掩碼
case SIOCSIFNETMASK://設置網絡掩碼
case SIOCGIFDSTADDR://獲取p2p地址
case SIOCSIFDSTADDR://設置p2p地址
case SIOCSIFPFLAGS: //
case SIOCGIFPFLAGS:
case SIOCSIFFLAGS://設置接口標誌
return(devinet_ioctl(cmd,(void *) arg));//網絡接口相關配置,linux內核自帶的ifconfig
//的很多處理都是通過這裏實現的
case SIOCGIFBR:
case SIOCSIFBR://網橋設置,稍後的實例就是介紹如何截獲網橋控制鉤子
#if defined(CONFIG_BRIDGE) || defined(CONFIG_BRIDGE_MODULE) //如果內核支持網橋功能
#ifdef CONFIG_KMOD//若支持內核模塊動態加載
if (br_ioctl_hook == NULL)//網橋鉤子爲空則動態請求模塊
request_module("bridge");//加載網橋模塊
#endif
if (br_ioctl_hook != NULL)
return br_ioctl_hook(arg);//通過鉤子函數處理命令參數
#endif
case SIOCGIFDIVERT://
case SIOCSIFDIVERT:
#ifdef CONFIG_NET_DIVERT
return(divert_ioctl(cmd, (struct divert_cf *) arg));
#else
return -ENOPKG;
#endif /* CONFIG_NET_DIVERT */
return -ENOPKG;
case SIOCADDDLCI://
case SIOCDELDLCI:// 數據鏈路連接標識控制
#ifdef CONFIG_DLCI
lock_kernel();
err = dlci_ioctl(cmd, (void *) arg);//控制函數
unlock_kernel();
return err;
#endif
#ifdef CONFIG_DLCI_MODULE
#ifdef CONFIG_KMOD
if (dlci_ioctl_hook == NULL)//如果鉤子函數爲空,則加載模塊
request_module("dlci");
#endif
if (dlci_ioctl_hook) {//鉤子函數指針不空
lock_kernel();
err = (*dlci_ioctl_hook)(cmd, (void *) arg);//調用鉤子函數
unlock_kernel();
return err;
}
#endif
return -ENOPKG;
default:
…
return err;
}
/*NOTREACHED*/
return(0);
}
從上面的函數代碼來看,同套接字有關的控制請求主要有如下幾類:
1、文件操作
2、套接字操作
3、路由選項操作
4、接口操作
5、ARP高速緩存操作
6、網橋控制
7、數據鏈路連接標識控制
結合代碼中的註釋,讀者不難理解具體的控制分支。具體的控制處理就轉到具體的函數裏面去處理了,例如關於內核自帶的命令工具ifconfig對ip地址的配置處理,基本都在devinet_ioctl函數中;關於arp命令的處理都在arp_ioctl中處理;關於路由配置都在ip_rt_ioctl中處理。其中參數arg是用戶空間傳來的自定義的數據,可以是結構,可以是聯合或其它一些更復雜的類型,由具體的業務模塊來解釋處理。在隨後的實踐中,我們就是通過arg的不同解釋來做不同的處理。
4.7、網絡主要結構相關字段相互引用圖
通過上面的分析,大家應該大致明白了linux內核網絡ioctl控制框架的實現了。下面是在內核網絡組件初始化後,ipv4相關的結構字段之間相互引用圖,供大家閱讀是參考:
結合前面主要函數調用關係圖與源碼分析,讀者可以很清晰的順着上圖所示的箭頭,從ioctl入口函數開始,方便地找到具體的處理模塊.其中,文件操作對象socket_file_ops調用sock_ioctl()時,通過inode節點的socket_i字段最終找到inet_ioctl()函數.
此處介紹通過自己編寫控制程序,在用戶空間調用ioctl函數控制內核顯示一行信息的例子供大家參考:
1.編寫運行於用戶空間的控制程序
(1)一般先定義自己的結構參數類型,如下:
typedef struct stMyIoctlArg {
unsigned int cmd;//其實就是第一個參數,當作自己的命令參數
unsigned int arg1; //用於提供給具體的命令參數
unsigned int arg2;
…//如果有更多參數直接加在後面
} IOCTL_ARG,P IOCTL_ARG;
(2)然後在main中賦值並調用ioctl函數:
#define FIOCSMYSHOW 0x1234
int main( int argc, char **argv )
{
int fd;
IOCTL_ARG arg; //需要組織傳遞到內核的參數
arg.cmd= FIOCSMYSHOW //自定義命令
…//其它參數賦值
int fd = socket( AF_INET, SOCK_STREAM, 0 );//創建控制socket
if ( fd < 0 )
{
perror( "socket failed" );
return 0;
}
if ( ioctl( fd, SIOCSIFBR, &arg) < 0 ) //通過網橋請求參數來控制內核作相關操作
{
perror( "ioctl( SIOCSIFBR ) failed" );
close( fd );
return 0;
}
…
close(fd);
…
}
例子源代碼:
2.內核功能支持
2.1、修改內核相關代碼:
(1)在內核include/linux/sockios.h的尾部加入前面定義的公共的結構與常量:
typedef struct stMyIoctlArg {
unsigned int cmd;//其實就是第一個參數,當作自己的命令參數
unsigned int arg1;
unsigned int arg2;
…//如果有更多參數直接加在後面
} IOCTL_ARG,P IOCTL_ARG;
#define FIOCSMYSHOW 0x1234
(2)在inet_ioctl函數網橋處理分支處增加如下藍色字體內容:
IOCTL_ARG myarg;//在inet_ioctl函數開始時加入此變量定義
…
#if defined(CONFIG_BRIDGE) || defined(CONFIG_BRIDGE_MODULE)
if ( copy_from_user( & myarg, (void *) arg, sizeof(IOCTL_ARG ) ) ) //拷貝用戶空間參數
return -EFAULT;
switch (myarg.cmd ){
case FIOCSMYSHOW ://解析自己的命令
printk(KERN_INFO “get ioctl hook./n”); //可以增加對arg1/arg2等參數的解析處理
return 0; //直接返回
break;
…
default:
break;
}
#ifdef CONFIG_KMOD
if (br_ioctl_hook == NULL)
request_module("bridge");
#endif
if (br_ioctl_hook != NULL)
return br_ioctl_hook(arg);
#endif
內核修改文件: 。注意在修改內核代碼後,用README中的命令編譯一下修改的文件,沒有錯誤才編譯內核,避免走彎路重新編譯。
2.2、編譯內核
具體編譯過程請參照網絡上的文章,我所用到的重要的命令有:
make mrproper
make oldconfig
make xconfig //在network options中選擇802.1 ethernet bridge選項支持網橋功能
make dep
make bzImage
make modules
make modules_install
depmod -a
cp System.map /boot/System.map-2.4.20-8custom
cp arch/i386/boot/bzImage /boot/vmlinuz-2.4.20-8custom
new-kernel-pkg --install --mkinitrd --depmod 2.4.20-8custom
3.運行控制程序
內核編譯前運行顯示:
內核編譯後運行顯示:
4.查看結果
可以通過dmesg | grep hook命令查看結果,顯示:
這正是我們在內核中要打印的字符,說明我們的控制命令已經通知給內核了。
ioctl系統調用是最常用的用戶與內核空間交互的手段之一,linux系統自帶的相當多的命令工具尤其是網絡控制工具都是採用ioctl控制框架實現了用戶和內核通信的橋樑,在當前一些基於內核模塊技術的軟件系統中也有重要的用途,如某些寬帶計費網關、防火牆軟件、網絡交換機等。瞭解ioctl控制框架,無疑會提高我們對linux內核通信機制的認識,也可以指導我們的實踐工作。
1 linux內核源代碼情景分析
2 linux內核2.4.0源碼
3 ioctl man手冊
4 ifconfig工具源碼
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.