首頁 >系統教程 >Linux >Linux Netlink:一種高效靈活的核心與用戶空間通訊機制

Linux Netlink:一種高效靈活的核心與用戶空間通訊機制

WBOY
WBOY轉載
2024-02-10 09:50:17894瀏覽

Linux核心是一個複雜而強大的系統,它提供了許多功能和服務,如進程管理、記憶體分配、裝置驅動、網路協定等。但是,如何讓使用者空間的應用程式與核心互動呢?傳統的方法有系統呼叫、訊號、管道、套接字等,但它們都有一些限制和缺點。例如,系統呼叫只能執行預先定義的操作,訊號只能傳遞簡單的訊息,管道和套接字需要額外的緩衝區和拷貝。有沒有更有效率、靈活、可擴展的方式呢?答案是Linux Netlink。

1.什麼是 Netlink

什麼是Netlink? Netlink是linux提供的用於核心和用戶態進程之間的通訊方式。但注意雖然Netlink主要用於用戶空間和內核空間的通信,但也能用於用戶空間的兩個進程通信。只是進程間通訊有其他很多方式,一般不用Netlink。除非需要用到Netlink的廣播特性時。

那麼Netlink有什麼優勢呢?一般來說用戶空間和核心空間的通訊方式有三種:/proc、ioctl、Netlink。而前兩種都是單向的,但Netlink可以實現雙工通訊。

Netlink協定是基於BSDsocket和AF_NETLINK位址簇(addressfamily),使用32位元的連接埠號碼尋址(先前稱為PID),每個Netlink協定(或稱為總線,man手冊中則稱之為netlinkfamily),通常與一個或一組核心服務/元件相關聯,如NETLINK_ROUTE用於取得和設定路由與連結資訊、NETLINK_KOBJECT_UEVENT用於核心向使用者空間的udev行程發送通知等。 netlink具有以下特點:

①支援全雙工、非同步通訊(當然同步也支援)

②用戶空間可使用標準的BSDsocket介面(但netlink並沒有屏蔽掉協定包的建構與解析過程,建議使用libnl等第三方函式庫)

③在內核空間使用專用的核心API介面

④支援多播(因此支援「匯流排」式通信,可實現訊息訂閱)

⑤在核心端可用於進程上下文與中斷上下文

如何學習Netlink?我覺得最好的方式就是將Netlink和UDPsocket比較學習。因為他們真的很對地方相似。 AF_NETLINK和AF_INET對應,是一個協議族,而NETLINK_ROUTE、NETLINK_GENERIC這些是協議,對應於UDP。

那麼我們主要關注Netlink和UDPsocket之間的不同點,其中最重要的一點就是:使用UDPsocket發送資料包時,使用者無需建構UDP資料包的包頭,核心協定棧會根據原、目的位址(sockaddr_in )填充頭部資訊。但是Netlink需要我們自己建構一個包頭(這個包頭有什麼用,我們後面再說)。

一般我們使用Netlink都要指定一個協議,我們可以使用核心為我們預留的NETLINK_GENERIC(定義在linux/netlink.h中),也可以使用我們自訂的協議,其實就是定義一個核心還沒有所佔的數字。下面我們用NETLINK_TEST做來為我們定義的協定寫一個例子(注意:自訂協定不一定要加入到linux/netlink.h中,只要用戶態和核心態程式碼都能找到定義就行)。我們知道使用UDP發送訊息有兩種方式:sendto和sendmsg,同樣Netlink也支援這兩種方式。下面先看使用sendmsg的方式。

2.用戶態資料結構

#先來看看幾個重要的資料結構的關係:

Linux Netlink:一种高效灵活的内核与用户空间通信机制

2.1structmsghdr

#msghdr這個結構在socket變成中就會用到,並不算Netlink專有的,這裡不在太多說明。只說明一下如何更好地理解這個結構的功能。我們知道socket訊息的發送和接收函數一般有這幾對:recv/send、readv/writev、recvfrom/sendto。當然還有recvmsg/sendmsg,前面三對函數各有各的特色功能,而recvmsg/sendmsg就是要囊括前面三對的所有功能,當然還有自己特殊的用途。 msghdr的前兩個成員就是為了滿足recvfrom/sendto的功能,中間兩個成員msg_iov和msg_iovlen則是為了滿足readv/writev的功能,而最後的msg_flags則是為了滿足recv/send中flag的功能,剩下的msg_control和msg_controllen則是滿足recvmsg/sendmsg特有的功能。

2.2Structsockaddr_ln

#Structsockaddr_ln為Netlink的位址,和我們通常socket程式設計中的sockaddr_in作用一樣,他們的結構比較如下。

Linux Netlink:一种高效灵活的内核与用户空间通信机制

structsockaddr_nl{}的詳細定義與描述如下:

struct sockaddr_nl
{
   sa_family_t nl_family; /*该字段总是为AF_NETLINK */
   unsigned short nl_pad; /* 目前未用到,填充为0*/
   __u32 nl_pid; /* process pid */
   __u32 nl_groups; /* multicast groups mask */
};

(1)nl_pid:在Netlink規範裡,PID全名為Port-ID(32bits),其主要作用是用於唯一的標識一個基於netlink的socket通道。通常情況下nl_pid都會設定為目前進程的進程號。前面我們也說過,Netlink不僅可以實現使用者-核心空間的通訊還可使現實使用者空間兩個行程之間,或核心空間兩個行程之間的通訊。此屬性為0時一般指內核。

(2)nl_groups:如果使用者空間的程序希望加入某個多重播送群組,則必須執行bind()系統呼叫。此欄位指明了呼叫者希望加入的多播組號的遮罩(注意不是組號,後面我們會詳細講解這個欄位)。如果該欄位為0則表示呼叫者不希望加入任何多播群組。對於每個隸屬於Netlink協議域的協議,最多可支援32個多播組(因為nl_groups的長度為32位元),每個多播群組都以一個位元來表示。

2.3structnlmsghdr

Netlink的封包由訊息標頭和訊息體構成,structnlmsghdr即為訊息頭。訊息頭定義在檔案裡,由結構體nlmsghdr表示:

struct sockaddr_nl
{
   sa_family_t nl_family; /*该字段总是为AF_NETLINK */
   unsigned short nl_pad; /* 目前未用到,填充为0*/
   __u32 nl_pid; /* process pid */
   __u32 nl_groups; /* multicast groups mask */
};

訊息標頭中各成員屬性的解釋及說明:

(1)nlmsg_len:整個訊息的長度,以位元組計算。包括了Netlink訊息頭本身。

(2)nlmsg_type:訊息的類型,也就是資料還是控制訊息。目前(核心版本2.6.21)Netlink僅支援四種類型的控制訊息,如下:

a)NLMSG_NOOP-空訊息,什麼都不做;

b)NLMSG_ERROR-指明該訊息中包含一個錯誤;

c)NLMSG_DONE-如果核心透過Netlink佇列傳回了多個訊息,那麼佇列的最後一則訊息的型別為NLMSG_DONE,其餘所有訊息的nlmsg_flags屬性都被設定NLM_F_MULTI位元有效。

d)NLMSG_OVERRUN-暫時沒用到。

(3)nlmsg_flags:附加在訊息上的額外說明訊息,如上面提到的NLM_F_MULTI。

那訊息體怎麼設定呢?可以使用NLMSG_DATA,具體見後面範例。

3.用户态范例一

  • 客户端1
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include
 
#include 
#include 
#define MAX_PAYLOAD 1024 //
 maximum payload size
#define NETLINK_TEST 25 //自定义的协议
int main(int argc, char* argv[])
{
   int state;
   struct sockaddr_nl src_addr, dest_addr;
   struct nlmsghdr *nlh = NULL; //Netlink数据包头
   struct iovec iov;
   struct msghdr msg;
   int sock_fd, retval;
   int state_smg = 0;
   // Create a socket
   sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
   if(sock_fd == -1){
       printf("error getting socket: %s", strerror(errno));
       return -1;
   }
   // To prepare binding
   memset(&src_addr, 0, sizeof(src_addr));
   src_addr.nl_family = AF_NETLINK;
   src_addr.nl_pid = 100; //A:设置源端端口号
   src_addr.nl_groups = 0;
   //Bind
   retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
   if(retval printf("bind failed: %s", strerror(errno));
       close(sock_fd);
       return -1;
   }
   // To orepare create mssage
   nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
   if(!nlh){
       printf("malloc nlmsghdr error!\n");
       close(sock_fd);
       return -1;
}
   memset(&dest_addr,0,sizeof(dest_addr));
   dest_addr.nl_family = AF_NETLINK;
   dest_addr.nl_pid = 0; //B:设置目的端口号
   dest_addr.nl_groups = 0;
   nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
   nlh->nlmsg_pid = 100; //C:设置源端口
   nlh->nlmsg_flags = 0;
   strcpy(NLMSG_DATA(nlh),"Hello you!"); //设置消息体
   iov.iov_base = (void *)nlh;
   iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
   //Create mssage
   memset(&msg, 0, sizeof(msg));
   msg.msg_name = (void *)&dest_addr;
   msg.msg_namelen = sizeof(dest_addr);
   msg.msg_iov = &iov;
   msg.msg_iovlen = 1;
   //send message
   printf("state_smg\n");
   state_smg = sendmsg(sock_fd,&msg,0);
   if(state_smg == -1)
   {
       printf("get error sendmsg = %s\n",strerror(errno));
   }
   memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));
   //receive message
   printf("waiting received!\n");
   while(1){
       printf("In while recvmsg\n");
       state = recvmsg(sock_fd, &msg, 0);
       if(stateprintf("state);
       }
       printf("Received message: %s\n",(char *) NLMSG_DATA(nlh));
   }
   close(sock_fd);
   return 0;
}

上面程序首先向内核发送一条消息;“Helloyou”,然后进入循环一直等待读取内核的回复,并将收到的回复打印出来。如果看上面程序感觉很吃力,那么应该首先复习一下UDP中使用sendmsg的用法,特别时structmsghdr的结构要清楚,这里再赘述。下面主要分析与UDP发送数据包的不同点:

1.socket地址结构不同,UDP为sockaddr_in,Netlink为structsockaddr_nl;

2.与UDP发送数据相比,Netlink多了一个消息头结构structnlmsghdr需要我们构造。

注意代码注释中的A、B、C三处分别设置了pid。首先解释一下什么是pid,网上很多文章把这个字段说成是进程的pid,其实这完全是望文生义。这里的pid和进程pid没有什么关系,仅仅相当于UDP的port。对于UDP来说port和ip标示一个地址,那对我们的NETLINK_TEST协议(注意Netlink本身不是一个协议)来说,pid就唯一标示了一个地址。所以你如果用进程pid做为标示当然也是可以的。当然同样的pid对于NETLINK_TEST协议和内核定义的其他使用Netlink的协议是不冲突的(就像TCP的80端口和UDP的80端口)。

下面分析这三处设置pid分别有什么作用,首先A和B位置的比较好理解,这是在地址(sockaddr_nl)上进行的设置,就是相当于设置源地址和目的地址(其实是端口),只是注意B处设置pid为0,0就代表是内核,可以理解为内核专用的pid,那么用户进程就不能用0做为自己的pid吗?这个只能说如果你非要用也是可以的,只是会产生一些问题,后面在分析。接下来看为什么C处的消息头仍然需要设置pid呢?这里首先要知道一个前提:内核不会像UDP一样根据我们设置的原、目的地址为我们构造消息头,所以我们不在包头写入我们自己的地址(pid),那内核怎么知道是谁发来的报文呢?当然如果内核只是处理消息不需要回复进程的话舍不设置这个消息头pid都可以。

所以每个pid的设置功能不同:A处的设置是要设置发送者的源地址,有人会说既然源地址又不会自动填充到报文中,我们为什么还要设置这个,因为你还可能要接收回复啊。就像寄信,你连“门牌号”都没有,即使你在写信时候写上你的地址是100号,对方回信目的地址也是100号,但是邮局发现根本没有这个地址怎么可能把信送到你手里呢?所以A的主要作用是注册源地址,保证可以收到回复,如果不需要回复当然可以简单将pid设置为0;B处自然就是收信人的地址,pid为0代表内核的地址,假如有一个进程在101号上注册了地址,并调用了recvmsg,如果你将B处的pid设置为101,那数据包就发给了另一个进程,这就实现了使用Netlink进行进程间通信;C相当于你在信封上写的源地址,通常情况下这个应该和你的真实地址(A)处注册的源地址相同,当然你要是不想收到回信,又想恶搞一下或者有特殊需求,你可以写成其他进程注册的pid(比如101)。这和我们现实中寄信是一样的,你给你朋友写封情书,把写信人写成你的另一个好基友,然后后果你懂得……

好了,有了这个例子我们就大概知道用户态怎么使用Netlink了,至于我们没有用到的nl_groups等其他信息后面讲到再说,下面看下内核是怎么处理Netlink的。

4.内核 Netlinkapi

4.1创建 netlinksocket

struct sock *netlink_kernel_create(struct net *net,
                             int unit,unsigned int groups,
                             void (*input)(struct sk_buff *skb),
                              struct mutex *cb_mutex,struct module *module);

参数说明:

(1)net:是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。默认情况下都是使用init_net这个全局变量。

(2)unit:表示netlink协议类型,如NETLINK_TEST、NETLINK_SELINUX。

(3)groups:多播地址。

(4)input:为内核模块定义的netlink消息处理函数,当有消息到达这个netlinksocket时,该input函数指针就会被引用,且只有此函数返回时,调用者的sendmsg才能返回。

(5)cb_mutex:为访问数据时的互斥信号量。

(6)module:一般为THIS_MODULE。

4.2发送单播消息 netlink_unicast

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

参数说明:

(1)ssk:为函数netlink_kernel_create()返回的socket。

(2)skb:存放消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,宏NETLINK_CB(skb)就用于方便设置该控制块。

(3)pid:为接收此消息进程的pid,即目标地址,如果目标为组或内核,它设置为0。

(4)nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回;而如果为0,该函数在没有接收缓存可利用定时睡眠。

4.3发送广播消息 netlink_broadcast

int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation)

前面的三个参数与netlink_unicast相同,参数group为接收消息的多播组,该参数的每一个位代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。

4.4释放 netlinksocket

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

5.内核态程序范例一

#include 
#include 
#include 
#include 

#include 
#include 
#include 
#define NETLINK_TEST 25
#define MAX_MSGSIZE 1024
int stringlength(char *s);
int err;
struct sock *nl_sk = NULL;
int flag = 0;
//向用户态进程回发消息
void sendnlmsg(char *message, int pid)
{
   struct sk_buff *skb_1;
   struct nlmsghdr *nlh;
   int len = NLMSG_SPACE(MAX_MSGSIZE);
   int slen = 0;
   if(!message || !nl_sk)
   {
       return ;
   }
   printk(KERN_ERR "pid:%d\n",pid);
   skb_1 = alloc_skb(len,GFP_KERNEL);
   if(!skb_1)
   {
       printk(KERN_ERR "my_net_link:alloc_skb error\n");
   }
   slen = stringlength(message);
   nlh = nlmsg_put(skb_1,0,0,0,MAX_MSGSIZE,0);
   NETLINK_CB(skb_1).pid = 0;
   NETLINK_CB(skb_1).dst_group = 0;
   message[slen]= '\0';
   memcpy(NLMSG_DATA(nlh),message,slen+1);
   printk("my_net_link:send message '%s'.\n",(char *)NLMSG_DATA(nlh));
   netlink_unicast(nl_sk,skb_1,pid,MSG_DONTWAIT);
}
int stringlength(char *s)
{
   int slen = 0;
   for(; *s; s++)
   {
       slen++;
   }
   return slen;
}
//接收用户态发来的消息
void nl_data_ready(struct sk_buff *__skb)
{
    struct sk_buff *skb;
    struct nlmsghdr *nlh;
    char str[100];
    struct completion cmpl;
    printk("begin data_ready\n");
    int i=10;
    int pid;
    skb = skb_get (__skb);
    if(skb->len >= NLMSG_SPACE(0))
    {
        nlh = nlmsg_hdr(skb);
        memcpy(str, NLMSG_DATA(nlh), sizeof(str));
        printk("Message received:%s\n",str) ;
        pid = nlh->nlmsg_pid;
        while(i--)
       {//我们使用completion做延时,每3秒钟向用户态回发一个消息
           init_completion(&cmpl);
           wait_for_completion_timeout(&cmpl,3 * HZ);
           sendnlmsg("I am from kernel!",pid);
       }
        flag = 1;
        kfree_skb(skb);
   }
}
// Initialize netlink
int netlink_init(void)
{
   nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 1,
                                nl_data_ready, NULL, THIS_MODULE);
   if(!nl_sk){
       printk(KERN_ERR "my_net_link: create netlink socket error.\n");
       return 1;
   }
   printk("my_net_link_4: create netlink socket ok.\n");
   return 0;
}
static void netlink_exit(void)
{
   if(nl_sk != NULL){
       sock_release(nl_sk->sk_socket);
   }
   printk("my_net_link: self module exited\n");
}
module_init(netlink_init);
module_exit(netlink_exit);
MODULE_AUTHOR("yilong");
MODULE_LICENSE("GPL");

附上内核代码的Makefile文件:

ifneq ($(KERNELRELEASE),)
obj-m :=netl.o
else
KERNELDIR ?=/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

6.程序结构分析

我们将内核模块insmod后,运行用户态程序,结果如下:

Linux Netlink:一种高效灵活的内核与用户空间通信机制

这个结果复合我们的预期,但是运行过程中打印出“state_smg”卡了好久才输出了后面的结果。这时候查看客户进程是处于D状态的(不了解D状态的同学可以google一下)。这是为什么呢?因为进程使用Netlink向内核发数据是同步,内核向进程发数据是异步。什么意思呢?也就是用户进程调用sendmsg发送消息后,内核会调用相应的接收函数,但是一定到这个接收函数执行完用户态的sendmsg才能够返回。我们在内核态的接收函数中调用了10次回发函数,每次都等待3秒钟,所以内核接收函数30秒后才返回,所以我们用户态程序的sendmsg也要等30秒后才返回。相反,内核回发的数据不用等待用户程序接收,这是因为内核所发的数据会暂时存放在一个队列中。

再来回到之前的一个问题,用户态程序的源地址(pid)可以用0吗?我把上面的用户程序的A和C处pid都改为了0,结果一运行就死机了。为什么呢?我们看一下内核代码的逻辑,收到用户消息后,根据消息中的pid发送回去,而pid为0,内核并不认为这是用户程序,认为是自身,所有又将回发的10个消息发给了自己(内核),这样就陷入了一个死循环,而用户态这时候进程一直处于D。

另外一个问题,如果同时启动两个用户进程会是什么情况?答案是再调用bind时出错:“Addressalreadyinuse”,这个同UDP一样,同一个地址同一个port如果没有设置SO_REUSEADDR两次bind就会出错,之后我用同样的方式再Netlink的socket上设置了SO_REUSEADDR,但是并没有什么效果。

7.用户态范例二

之前我们说过UDP可以使用sendmsg/recvmsg也可以使用sendto/recvfrom,那么Netlink同样也可以使用sendto/recvfrom。具体实现如下:

#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include
 
#include 
#include 
#define MAX_PAYLOAD 1024 //
 maximum payload size
#define NETLINK_TEST 25
int main(int argc, char* argv[])
{
   struct sockaddr_nl src_addr, dest_addr;
   struct nlmsghdr *nlh = NULL;
   int sock_fd, retval;
   int state,state_smg = 0;
   // Create a socket
   sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
   if(sock_fd == -1){
       printf("error getting socket: %s", strerror(errno));
       return -1;
   }
   // To prepare binding
   memset(&src_addr, 0, sizeof(src_addr));
   src_addr.nl_family = AF_NETLINK;
   src_addr.nl_pid = 100;
   src_addr.nl_groups = 0;
   //Bind
   retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
   if(retval printf("bind failed: %s", strerror(errno));
       close(sock_fd);
       return -1;
   }
   // To orepare create mssage head
   nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
   if(!nlh){
       printf("malloc nlmsghdr error!\n");
       close(sock_fd);
       return -1;
   }
   memset(&dest_addr,0,sizeof(dest_addr));
   dest_addr.nl_family = AF_NETLINK;
   dest_addr.nl_pid = 0;
   dest_addr.nl_groups = 0;
   nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
   nlh->nlmsg_pid = 100;
   nlh->nlmsg_flags = 0;
   strcpy(NLMSG_DATA(nlh),"Hello you!");
   //send message
   printf("state_smg\n");
   sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));
   if(state_smg == -1)
   {
       printf("get error sendmsg = %s\n",strerror(errno));
   }
   memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));
   //receive message
   printf("waiting received!\n");
while(1){
       printf("In while recvmsg\n");
state=recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,NULL,NULL);
       if(stateprintf("state);
       }
       printf("Received message: %s\n",(char *) NLMSG_DATA(nlh));
       memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));
   }
   close(sock_fd);
   return 0;
}

熟悉UDP编程的同学看到这个程序一定很熟悉,除了多了一个Netlink消息头的设置。但是我们发现程序中调用了bind函数,这个函数再UDP编程中的客户端不是必须的,因为我们不需要把UDPsocket与某个地址关联,同时再发送UDP数据包时内核会为我们分配一个随即的端口。但是对于Netlink必须要有这一步bind,因为Netlink内核可不会为我们分配一个pid。再强调一遍消息头(nlmsghdr)中的pid是告诉内核接收端要回复的地址,但是这个地址存不存在内核并不关心,这个地址只有用户端调用了bind后才存在。

再说一个体外话,我们看到这两个例子都是用户态首先发起的,那Netlink是否支持内核态主动发起的情况呢?当然是可以的,只是内核一般需要事件触发,这里,只要和用户态约定号一个地址(pid),内核直接调用netlink_unicast就可以了。

Linux Netlink是一种特殊的套接字类型,它允许内核与用户空间进行双向的异步消息传递。Netlink支持多种协议族,每个协议族负责处理不同的主题,如路由、防火墙、设备监控等。Netlink还提供了一些高级特性,如多播、分组、序列号、确认等。Netlink是一种非常强大和灵活的通信机制,它可以让用户空间的应用程序更方便地访问和控制内核的状态和行为。本文介绍了Netlink的基本概念和使用方法,希望对你有所帮助。

以上是Linux Netlink:一種高效靈活的核心與用戶空間通訊機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:lxlinux.net。如有侵權,請聯絡admin@php.cn刪除