malloc()之后,内核发生了什么?

2012年9月2日 由 edsionte 6 条评论 »

考虑这样一种常见的情况:用户进程调用malloc()动态分配了一块内存空间,再对这块内存进行访问。这些用户空间发生的事会引发内核空间的那些反映?本文将简单为您解答。

1.brk系统调用服务例程

malloc()是一个API,这个函数在库中封装了系统调用brk。因此如果调用malloc,那么首先会引发brk系统调用执行的过程。brk()在内核中对应的系统调用服务例程为SYSCALL_DEFINE1(brk, unsigned long, brk),参数brk用来指定heap段新的结束地址,也就是重新指定mm_struct结构中的brk字段。

brk系统调用服务例程首先会确定heap段的起始地址min_brk,然后再检查资源的限制问题。接着,将新老heap地址分别按照页大小对齐,对齐后的地址分别存储与newbrk和okdbrk中。

brk()系统调用本身既可以缩小堆大小,又可以扩大堆大小。缩小堆这个功能是通过调用do_munmap()完成的。如果要扩大堆的大小,那么必须先通过find_vma_intersection()检查扩大以后的堆是否与已经存在的某个虚拟内存重合,如何重合则直接退出。否则,调用do_brk()进行接下来扩大堆的各种工作。

SYSCALL_DEFINE1(brk, unsigned long, brk)
{
        unsigned long rlim, retval;
        unsigned long newbrk, oldbrk;
        struct mm_struct *mm = current->mm;
        unsigned long min_brk;

        down_write(&mm->mmap_sem);

#ifdef CONFIG_COMPAT_BRK
        min_brk = mm->end_code;
#else
        min_brk = mm->start_brk;
#endif
        if (brk < min_brk)
                goto out;

        rlim = rlimit(RLIMIT_DATA);
        if (rlim < RLIM_INFINITY && (brk - mm->start_brk) +
                        (mm->end_data - mm->start_data) > rlim)

        newbrk = PAGE_ALIGN(brk);
        oldbrk = PAGE_ALIGN(mm->brk);
        if (oldbrk == newbrk)
                goto set_brk;

        if (brk brk) {
                if (!do_munmap(mm, newbrk, oldbrk-newbrk))
                        goto set_brk;
                goto out;
        }

        if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
                goto out;

        if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
                goto out;
set_brk:
        mm->brk = brk;
out:
        retval = mm->brk;
        up_write(&mm->mmap_sem);
        return retval;
}

brk系统调用服务例程最后将返回堆的新结束地址。

2.扩大堆

用户进程调用malloc()会使得内核调用brk系统调用服务例程,因为malloc总是动态的分配内存空间,因此该服务例程此时会进入第二条执行路径中,即扩大堆。do_brk()主要完成以下工作:

1.通过get_unmapped_area()在当前进程的地址空间中查找一个符合len大小的线性区间,并且该线性区间的必须在addr地址之后。如果找到了这个空闲的线性区间,则返回该区间的起始地址,否则返回错误代码-ENOMEM;

2.通过find_vma_prepare()在当前进程所有线性区组成的红黑树中依次遍历每个vma,以确定上一步找到的新区间之前的线性区对象的位置。如果addr位于某个现存的vma中,则调用do_munmap()删除这个线性区。如果删除成功则继续查找,否则返回错误代码。

3.目前已经找到了一个合适大小的空闲线性区,接下来通过vma_merge()去试着将当前的线性区与临近的线性区进行合并。如果合并成功,那么该函数将返回prev这个线性区的vm_area_struct结构指针,同时结束do_brk()。否则,继续分配新的线性区。

4.接下来通过kmem_cache_zalloc()在特定的slab高速缓存vm_area_cachep中为这个线性区分配vm_area_struct结构的描述符。

5.初始化vma结构中的各个字段。

6.更新mm_struct结构中的vm_total字段,它用来同级当前进程所拥有的vma数量。

7.如果当前vma设置了VM_LOCKED字段,那么通过mlock_vma_pages_range()立即为这个线性区分配物理页框。否则,do_brk()结束。

可以看到,do_brk()主要是为当前进程分配一个新的线性区,在没有设置VM_LOCKED标志的情况下,它不会立刻为该线性区分配物理页框,而是通过vma一直将分配物理内存的工作进行延迟,直至发生缺页异常。

3.缺页异常的处理过程

经过上面的过程,malloc()返回了线性地址,如果此时用户进程访问这个线性地址,那么就会发生缺页异常(Page Fault)。整个缺页异常的处理过程非常复杂,我们这里只关注与malloc()有关的那一条执行路径。

当CPU产生一个异常时,将会跳转到异常处理的整个处理流程中。对于缺页异常,CPU将跳转到page_fault异常处理程序中:

//linux-2.6.34/arch/x86/kernel/entry_32.S
ENTRY(page_fault)
        RING0_EC_FRAME
        pushl $do_page_fault
        CFI_ADJUST_CFA_OFFSET 4
        ALIGN
error_code:
        …………
        jmp ret_from_exception
        CFI_ENDPROC
END(page_fault)

该异常处理程序会调用do_page_fault()函数,该函数通过读取CR2寄存器获得引起缺页的线性地址,通过各种条件判断以便确定一个合适的方案来处理这个异常。

3.1.do_page_fault()

该函数通过各种条件来检测当前发生异常的情况,但至少do_page_fault()会区分出引发缺页的两种情况:由编程错误引发异常,以及由进程地址空间中还未分配物理内存的线性地址引发。对于后一种情况,通常还分为用户空间所引发的缺页异常和内核空间引发的缺页异常。

内核引发的异常是由vmalloc()产生的,它只用于内核空间内存的分配。显然,我们这里需要关注的是用户空间所引发的异常情况。这部分工作从do_page_fault()中的good_area标号处开始执行,主要通过handle_mm_fault()完成。

//linux-2.6.34/arch/x86/mm/fault.c
dotraplinkage void __kprobes
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
…… ……
good_area:
        write = error_code & PF_WRITE;

        if (unlikely(access_error(error_code, write, vma))) {
                bad_area_access_error(regs, error_code, address);
                return;
        }
        fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);
…… ……
}

3.2.handle_mm_fault()

该函数的主要功能是为引发缺页的进程分配一个物理页框,它先确定与引发缺页的线性地址对应的各级页目录项是否存在,如何不存在则分进行分配。具体如何分配这个页框是通过调用handle_pte_fault()完成的。

int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, unsigned int flags)
{
        pgd_t *pgd;
        pud_t *pud;
        pmd_t *pmd;
        pte_t *pte;
        …… ……
        pgd = pgd_offset(mm, address);
        pud = pud_alloc(mm, pgd, address);
        if (!pud)
                return VM_FAULT_OOM;
        pmd = pmd_alloc(mm, pud, address);
        if (!pmd)
                return VM_FAULT_OOM;
        pte = pte_alloc_map(mm, pmd, address);
        if (!pte)
                return VM_FAULT_OOM;
          return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}

3.3.handle_pte_fault()

该函数根据页表项pte所描述的物理页框是否在物理内存中,分为两大类:

请求调页:被访问的页框不再主存中,那么此时必须分配一个页框。

写时复制:被访问的页存在,但是该页是只读的,内核需要对该页进行写操作,此时内核将这个已存在的只读页中的数据复制到一个新的页框中。

用户进程访问由malloc()分配的内存空间属于第一种情况。对于请求调页,handle_pte_fault()仍然将其细分为三种情况:

static inline int handle_pte_fault(struct mm_struct *mm,
                struct vm_area_struct *vma, unsigned long address,
                pte_t *pte, pmd_t *pmd, unsigned int flags)
{
        …… ……
        if (!pte_present(entry)) {
                if (pte_none(entry)) {
                        if (vma->vm_ops) {
                                if (likely(vma->vm_ops->fault))
                                        return do_linear_fault(mm, vma, address,
                                                pte, pmd, flags, entry);
                        }
                        return do_anonymous_page(mm, vma, address,
                                                 pte, pmd, flags);
                }
                if (pte_file(entry))
                        return do_nonlinear_fault(mm, vma, address,
                                        pte, pmd, flags, entry);
                return do_swap_page(mm, vma, address,
                                        pte, pmd, flags, entry);
        }
…… ……
}

1.如果页表项确实为空(pte_none(entry)),那么必须分配页框。如果当前进程实现了vma操作函数集合中的fault钩子函数,那么这种情况属于基于文件的内存映射,它调用do_linear_fault()进行分配物理页框。否则,内核将调用针对匿名映射分配物理页框的函数do_anonymous_page()。

2.如果检测出该页表项为非线性映射(pte_file(entry)),则调用do_nonlinear_fault()分配物理页。

3.如果页框事先被分配,但是此刻已经由主存换出到了外存,则调用do_swap_page()完成页框分配。

由malloc分配的内存将会调用do_anonymous_page()分配物理页框。

3.4.do_anonymous_page()

此时,缺页异常处理程序终于要为当前进程分配物理页框了。它通过alloc_zeroed_user_highpage_movable()来完成这个过程。我们层层拨开这个函数的外衣,发现它最终调用了alloc_pages()。

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pte_t *page_table, pmd_t *pmd,
                unsigned int flags)
{
…… ……
        if (unlikely(anon_vma_prepare(vma)))
                goto oom;
        page = alloc_zeroed_user_highpage_movable(vma, address);
        if (!page)
                goto oom;
…… ……
}

经过这样一个复杂的过程,用户进程所访问的线性地址终于对应到了一块物理内存。

参考:

1.《深入理解LINUX内核》

2.《深入LINUX内核架构》

RPM包的管理

2012年9月1日 由 edsionte 没有评论 »

1.软件包管理器

软件包管理器在Linux各发行版中占据重要地位,为用户提供了便利。软件包管理器提供给用户已经编译好的二进制软件包,它免去了通过源码包安装软件时遇到的各种问题,比如检测操作系统安装环境、编译以及设置参数等,这样使得用户在安装、升级和卸载软件包时变得非常简单。

RPM是Linux界两大主流的软件包管理器之一,RPM全称Red Hat Package Manager,它是Red Hat公司所开发的软件包管理器,它在Redhat,Fedora,CentOS和SUSE等Linux发行版中被广泛使用。另一款软件包管理器为Debian公司开发的dpkg,它在Debian,Ubuntu和Linux Mint等发行版中被使用。

1.1 RPM和SRPM

RPM软件包最大的特点是将你要安装的软件提前编译好,并且打包成为符合RPM标准的软件安装包。在安装时,RPM管理器会将该安装包中的二进制文件释放到配置文件所指定的路径中。

这种方法对用户来说简单方便,但是同时也缺少一定的灵活性。因为RPM包中的数据是已经编译完成的二进制文件,因此安装该软件包的计算机环境必须与编译该软件包时所在计算机环境相同。

SRPM可以解决上述问题,它的全称是Source RPM,也就是说这种RPM包中包含有源代码。与普通的源码包不同的是,SRPM包中除了包含源代码,还包含该软件所需要的依赖性软件说明。在通过SRPM安装软件时,它会自动检查该软件所依赖的软件是否已经安装。

1.2 YUM在线升级机制

在安装某个RPM包之前,必须确保该RPM包所依赖的软件包都已经安装。否则,强制安装该RPM包会导致该软件不能正常使用。yum在线升级机制将解决软件包之间的依赖问题,它在安装指定软件包时,将该软件包的所有依赖软件同时安装。

使用yum机制的Linux发行版将发布好的所有软件放在yum服务器中,并且将所有软件之间的依赖关系记录下来。当客户端发送了安装软件的需求时,客户端主机将yum服务器上关于该软件的依赖清单与本地机中的软件对比,依次安装缺失的依赖软件。通过这话总方法可以轻松的解决软件包的依赖问题。

yum是redhat系列发行版中的在线升级机制,apt-get是Debian系列发行版中的在线升级机制。

2.RPM打包方法

RPM包的通用制作过程为:下载源码包,再编写spec文件,最后通过rpmbuild构建RPM包。其中,打包最主要的工作是对spec文件的编写,该文件用于对即将构建的软件包进行描述,它包含了软件包的诸多信息,如软件包的名字、版本、类别、简要说明、创建时要执行的命令、安装时要执行的命令等。

对于spec文件的来源,可按照以下的优先级进行选取:

1.首先,如果源码包上有spec文件,则使用该文件。

2.否则,如果社区上有同样的包,但是版本不符合,可以针对具体情况对这些spec文件进行修改。

3.否则,自己编写spec文件。

参考:1.《鸟哥的Linux私房菜》

Netlink编程-使用NETLINK_INET_DIAG协议

2012年8月25日 由 edsionte 没有评论 »

Netlink可以使得内核和用户进程进行双向通信,前文已经介绍过用户进程主动发起会话请求的例子。在那个示例程序中,必须同时编写用户态程序和内核模块,因为他们之间通信的协议是我们自己设定的,并没有使用netlink已有的通信协议。如果使用netlink已有的通信协议,那么我们无需编写内核模块,只需编写用户态程序即可。

本文将说明如何在用户态使用NETLINK_INET_DIAG协议。

1.创建netlink套接字

Netlink的使用方法与普通套接字并无太大差异,前文已经说明参数的差异,这里不再赘述。

struct sk_req {
	struct nlmsghdr nlh;
	struct inet_diag_req r;
};

int main(int argc, char **argv)
{
	int fd;
	struct sk_req req;
	struct sockaddr_nl dest_addr;
	struct msghdr msg;
	char buf[8192];
	char src_ip[20];
	char dest_ip[20];
	struct iovec iov;

	if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_INET_DIAG)) < 0) {
		eprint(__LINE__, errno, "socket");
		return -1;
	}

2.发送消息到内核

用户进程通过msghdr结构将消息发送到内核中,因此必须首先初始化msghdr类型的变量msg。该数据结构与iovec类型的变量iov和sockaddr_nl类型的变量dest_addr关联,iov指向数据缓冲区,dest_addr用于描述目的套接字地址。

这里需要将nlmsghdr结构中的nlmsg_type指定为TCPDIAG_GETSOCK,说明获取的是TCP套接字。同时需要将nlmsg_flags字段指定为NLM_F_REQUEST |  NLM_F_ROOT,NLM_F_REQUEST是所有向内核发出消息请求的用户进程所必须所设置的,NLM_F_ROOT则指明返回所有的套接字。

	req.nlh.nlmsg_len = sizeof(req);
	req.nlh.nlmsg_type = TCPDIAG_GETSOCK;
	req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ROOT;
	req.nlh.nlmsg_pid = 0;

	memset(&req.r, 0, sizeof(req.r));
	req.r.idiag_family = AF_INET;
	req.r.idiag_states = ((1 << TCP_CLOSING + 1) - 1);

	iov.iov_base = &req;
	iov.iov_len = sizeof(req);

	memset(&dest_addr, 0, sizeof(dest_addr));
	dest_addr.nl_family = AF_NETLINK;
	dest_addr.nl_pid = 0;
	dest_addr.nl_groups = 0;

	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;

数据缓冲区通过req结构来表示,它封装了两个数据结构nlmsghdr和inet_diag_req。前者用来表示netlink消息头,它是必须封装的数据结构。后者是NETLINK_INET_DIAG协议所特有的请求会话的数据结构,具体结构如下:

struct inet_diag_req {
        __u8    idiag_family;           /* Family of addresses. */
        __u8    idiag_src_len;
        __u8    idiag_dst_len;
        __u8    idiag_ext;              /* Query extended information */

        struct inet_diag_sockid id;

        __u32   idiag_states;           /* States to dump */
        __u32   idiag_dbs;              /* Tables to dump (NI) */
};

这里需要特别注意的是inet_diag_req结构中的idiag_states字段,它用来表示内核将要反馈哪些状态的套接字到用户空间。用户空间通过一个枚举类型来表示套接字状态:

enum
{
  TCP_ESTABLISHED = 1,
  TCP_SYN_SENT,
  TCP_SYN_RECV,
  TCP_FIN_WAIT1,
  TCP_FIN_WAIT2,
  TCP_TIME_WAIT,
  TCP_CLOSE,
  TCP_CLOSE_WAIT,
  TCP_LAST_ACK,
  TCP_LISTEN,
  TCP_CLOSING
};

idiag_states字段的每一位表示一个状态,因此通过位偏移可以将具体某个状态位置1。上述的实例程序中,将表示所有状态的位都置1,因此内核将向用户进程反馈所有状态的套接字。

	if (sendmsg(fd, &msg, 0) < 0) {
		eprint(__LINE__, errno, "sendmsg");
		return -1;
	}

初始化相关的数据结构之后,接下来用户进程通过sendmsg函数发送消息到内核中。

3.用户进程接收消息

用户进程通过两层循环来接受并处理内核发送的消息。外层循环通过recvmsg函数不断接收内核发送的数据,在接收数据之前还要将新的数据缓冲区buf与iov进行绑定。内层循环将内核通过一次系统调用所发送的数据进行分批处理。对于本文所描述的NETLINK_INET_DIAG协议,内核每次向用户进程发送的消息通过inet_diag_msg结构描述:

struct inet_diag_msg {
        __u8    idiag_family;
        __u8    idiag_state;
        __u8    idiag_timer;
        __u8    idiag_retrans;

        struct inet_diag_sockid id;

        __u32   idiag_expires;
        __u32   idiag_rqueue;
        __u32   idiag_wqueue;
        __u32   idiag_uid;
        __u32   idiag_inode;
};

每一次外层循环将接收到的数据存放在buf缓冲区中,该缓冲区中存放了多条消息,结构如下:

struct nlhdrmsg struct inet_idiag_msg || struct nlhdrmsg struct inet_idiag_msg || ……

按照这样的数据存储方式,内层循环要做的就是依次获取这些数据结构。由于每条数据报都至少封装了nlmsghdr结构,因此具体的处理方法通过NLMSG_XXX宏即可完成。

	memset(buf, 0 ,sizeof(buf));
	iov.iov_base = buf;
	iov.iov_len = sizeof(buf);

	while (1) {
		int status;
		struct nlmsghdr *h;

		msg = (struct msghdr) {
			(void *)&dest_addr, sizeof(dest_addr),
				&iov, 1, NULL, 0, 0
		};
		status = recvmsg(fd, &msg, 0);
		if (status < 0) {
			if (errno == EINTR)
				continue;
			eprint(__LINE__, errno, "recvmsg");
			continue;
		}

		if (status == 0) {
			printf("EOF on netlink\n");
			close(fd);
			return 0;
		}

		h = (struct nlmsghdr *)buf;

		while (NLMSG_OK(h, status)) {
			struct inet_diag_msg *pkg = NULL;

			if (h->nlmsg_type == NLMSG_DONE) {
				close(fd);
				printf("NLMSG_DONE\n");
				return 0;
			}

			if (h->nlmsg_type == NLMSG_ERROR) {
				struct nlmsgerr *err;
				err = (struct nlmsgerr*)NLMSG_DATA(h);
				fprintf(stderr, "%d Error %d:%s\n", __LINE__, -(err->error), strerror(-(err->error)));
				close(fd);
				printf("NLMSG_ERROR\n");
				return 0;
			}

			pkg = (struct inet_diag_msg *)NLMSG_DATA(h);
			print_skinfo(pkg);
			get_tcp_state(pkg->idiag_state);
			h = NLMSG_NEXT(h, status);
		}//while
	}//while
	close(fd);
	return 0;

NLMSG_OK宏每次判断buf中的数据是否读取完毕,NLMSG_DATA取当前netlink消息头结构紧邻的inet_diag_msg结构,NLMSG_NEXT则取下一个netlink消息头结构。

pkg指向当前获取到的消息,接下来具体需求处理各个字段即可。上述程序中,print_skinfo函数打印pkg中的各个字段,get_tcp_state则是打印每个套接字连接的状态。

Netlink编程-用户主动发起会话

2012年8月19日 由 edsionte 没有评论 »

Netlink是一种在内核态和用户态可以进行双向通信的机制,也就是说,用户进程既可以作为服务器端又可以作为客户端,内核也是如此。用户进程和内核谁是服务器端谁是客户端,这个问题与谁先主动发起数据交互会话有关。

用户进程主动向内核发起会话在Linux内核中很常见,比如系统调用、对/proc的操作等。本文通过详解一个简单的实例程序来说明用户进程通过netlink机制如何主动向内核发起会话。在该程序中,用户进程向内核发送一段字符串,内核接收到后再将该字符串后再重新发给用户进程。

用户态程序

netlink是一种特殊的套接字,在用户态除了一些参数的传递对其使用的方法与一般套接字无较大差异,。

1.宏与数据结构的定义

在使用netlink进行用户进程和内核的数据交互时,最重要的是定义好通信协议。协议一词直白的说就是用户进程和内核应该以什么样的形式发送数据,以什么样的形式接收数据。而这个“形式”通常对应程序中的一个特定数据结构。

本文所演示的程序并没有使用netlink已有的通信协议,因此我们自定义一种协议类型NETLINK_TEST。

#define NETLINK_TEST 18
#define MAX_PAYLOAD 1024

struct req {
	struct nlmsghdr nlh;
	char buf[MAX_PAYLOAD];
};

除此之外,我们应该再自定义一个数据报类型req,该结构包含了netlink数据包头结构的变量nlh和一个MAX_PAYLOAD大小的缓冲区。这里我们为了演示简单,并没有像上文中描述的那样将一个特定数据结构与nlmsghdr封装起来。

2.创建netlink套接字

要使用netlink,必须先创建一个netlink套接字。创建方法同样采用socket(),只是这里需要注意传递的参数:

	int sock_fd;
	sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
	if (sock_fd < 0) {
		eprint(errno, "socket", __LINE__);
		return errno;
	}

第一个参数必须指定为PF_NETLINK或AF_NETLINK。第二个参数必须指定为SOCK_RAW或SOCK_DGRAM,因为netlink提供的是一种无连接的数据报服务。第三个参数则指定具体的协议类型,我们这里使用自定义的协议类型NETLINK_TEST。

另外,eprint()是一个自定义的出错处理函数,实现如下:

void eprint(int err_no, char *str, int line)
{
	printf("Error %d in line %d:%s() with %s\n", err_no, line, str, strerror(errno));
}

3.将本地套接字与源地址绑定

将本地的套接字与源地址进行绑定通过bind()完成。在绑定之前,需要将源地址进行初始化,nl_pid字段指明发送消息一方的pid,nl_groups表示多播组的掩码,这里我们并没有涉及多播,因此默认为0。

	struct sockaddr_nl src_addr;
	memset(&src_addr, 0, sizeof(src_addr));
	src_addr.nl_family = AF_NETLINK;
	src_addr.nl_pid = getpid();
	src_addr.nl_groups = 0;

	if (bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) < 0) {
		eprint(errno, "bind", __LINE__);
		return errno;
	}

4.初始化msghdr结构

用户进程最终发送的是msghdr结构的消息,因此必须对这个结构进行初始化。而此结构又与sockaddr_nl,iovec和nlmsghdr三个结构相关,因此必须依次对这些数据结构进行初始化。

首先初始化目的套接字的地址结构,该结构与源套接字地址结构初始化的方法稍有不同,即nl_pid必须为0,表示接收方为内核。

	struct sockaddr_nl dest_addr;
	memset(&dest_addr, 0, sizeof(dest_addr));
	dest_addr.nl_family = AF_NETLINK;
	dest_addr.nl_pid = 0;
	dest_addr.nl_groups = 0;

接下来对req类型的数据报进行初始化,即依次对其封装的两个数据结构初始化:

	struct req r;
	r.nlh.nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
	r.nlh.nlmsg_pid = getpid();
	r.nlh.nlmsg_flags = 0;
	memset(r.buf, 0, MAX_PAYLOAD);
	strcpy(NLMSG_DATA(&(r.nlh)), "hello, I am edsionte!");

这里的nlmsg_len为为sizeof(struct nlmsghdr)+MAX_PAYLOAD的总和。宏NLMSG_SPACE会自动将两者的长度相加。接下来对缓冲区向量iov进行初始化,让iov_base字段指向数据报结构,而iov_len为数据报长度。

	struct iovec iov;
	iov.iov_base = (void *)&r;
	iov.iov_len = sizeof(r);

一切就绪后,将目的套接字地址与当前要发送的消息msg绑定,即将目的套接字地址复制给msg_name。再将要发送的数据iov与msg_iov绑定,如果一次性要发送多个数据包,则创建一个iovec类型的数组。

	struct msghdr msg;
	msg.msg_name = (void *)&dest_addr;
	msg.msg_namelen = sizeof(dest_addr);
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

5.向内核发送消息

发送消息则很简单,通过sendmsg函数即可完成,前提是正确的创建netlink套接字和要发送的消息。

	if (sendmsg(sock_fd, &msg, 0) < 0) {
		eprint(errno, "sendmsg", __LINE__);
		return errno;
	}

6.接受内核发来的消息

如果用户进程需要接收内核发送的消息,则需要通过recvmsg完成,只不过在接收之前需要将数据报r重新初始化,因为发送和接收时传递的数据结构可能是不同的。

为了简单演示netlink的用法,本文所述的用户进程发送的是一段字符串,这一点从数据报结构req的定义可以看出。而内核向用户进程发送的也是一段字符串,具体情况下面将会具体说明。

        memset(&r.nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
        if (recvmsg(sock_fd, &msg, 0) < 0) {
                eprint(errno, "recvmsg", __LINE__);
                return errno;
        }

        printf("Received message payload:%s\n", (char *)NLMSG_DATA(&r.nlh));
        close(sock_fd);

接收完毕后,通过专门的宏NLMSG_DATA对数据报进行操作。 netlink对数据报的的访问和操作都是通过一系列标准的宏NLMSG_XXX来完成的,具体的说明可以通过man netlink查看。这里的NLMSG_DATA传递进去的是nlh,但它获取的是紧邻nlh的真正数据。本程序中传递的是字符串,所以取数据时候用char *强制类型转换,如果传递的是其他数据结构,则相应转换数据类型即可。

内核模块

netlink既然是一种用户态和内核态之间的双向通信机制,那么除了编写用户程序还要编写内核模块,也就是说用户进程和内核模块之间对数据的处理要彼此对应起来。

1.内核模块加载和卸载函数

内核模块加载函数主要通过netlink_kernel_create函数申请服务器端的套接字nl_sk,内核中对套接字表示为sock结构。另外,在创建套接字时还需要传递和用户进程相同的netlink协议类型NETLINK_TEST。创建套接字函数的第一个参数默认为init_net,第三个参数为多播时使用,我们这里不使用多播因此默认值为0。nl_data_handler是一个钩子函数,每当内核接收到一个消息时,这个钩子函数就被回调对用户数据进行处理。

#define NETLINK_TEST 17
struct sock *nl_sk = NULL;
static int __init hello_init(void)
{
	printk("hello_init is starting..\n");
	nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 0, nl_data_ready, NULL, THIS_MODULE);
	if (nl_sk == 0)
	{
		printk("can not create netlink socket.\n");
		return -1;
	}
	return 0;
}

内核模块卸载函数所做的工作与加载函数相反,通过sock_release函数释放一开始申请的套接字。

static void __exit hello_exit(void)
{
	sock_release(nl_sk->sk_socket);
	printk("hello_exit is leaving..\n");
}

2.钩子函数的实现

在内核创建netlink套接字时,必须绑定一个钩子函数,该钩子函数原型为:

 void (*input)(struct sk_buff *skb);

钩子函数的实现主要是先接收用户进程发送的消息,接收以后内核再发送一条消息到用户进程。

在钩子函数中,先通过skb_get函数对套接字缓冲区增加一次引用值,再通过nlmsg_hdr函数获取netlink消息头指针nlh。接着使用NLMSG_DATA宏获取用户进程发送过来的数据str。除此之外,再打印发送者的pid。

void nl_data_handler(struct sk_buff *__skb)
{
	struct sk_buff *skb;
	struct nlmsghdr *nlh;
	u32 pid;
	int rc;
	char str[100];
	int len = NLMSG_SPACE(MAX_PAYLOAD);

	printk("read data..\n");
	skb = skb_get(__skb);

	if (skb->len >= NLMSG_SPACE(0)) {
		nlh = nlmsg_hdr(skb);
		printk("Recv: %s\n", (char *)NLMSG_DATA(nlh));
		memcpy(str, NLMSG_DATA(nlh), sizeof(str));
		pid = nlh->nlmsg_pid;
		printk("pid is %d\n", pid);
		kfree_skb(skb);

接下来重新申请一个套接字缓冲区,为内核发送消息到用户进程做准备,nlmsg_put函数将填充netlink数据报头。接下来将用户进程发送的字符串复制到nlh紧邻的数据缓冲区中,等待内核发送。netlink_unicast函数将以非阻塞的方式发送数据包到用户进程,pid具体指明了接收消息的进程。

		skb = alloc_skb(len, GFP_ATOMIC);
		if (!skb){
			printk(KERN_ERR "net_link: allocate failed.\n");
			return;
		}
		nlh = nlmsg_put(skb, 0, 0, 0, MAX_PAYLOAD, 0);
		NETLINK_CB(skb).pid = 0;

		memcpy(NLMSG_DATA(nlh), str, sizeof(str));
		printk("net_link: going to send.\n");
		rc = netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
		if (rc < 0) {
			printk(KERN_ERR "net_link: can not unicast skb (%d)\n", rc);
		}
		printk("net_link: send is ok.\n");
	}
}

这样就完成了内核模块的编写,它与用户进程通信共同完成数据交互。

Netlink编程-数据结构

2012年8月18日 由 edsionte 没有评论 »

内核态与用户态进行数据交互的方法很多,分为用户程序主动发起的消息交交互和内核主动发起的信息交互。通常我们所熟知的系统调用、对/proc进行读写操作、mmap和编写驱动程序等都属于前者,而由内核主动发起的信息交互方法比较少,最典型的方法为内核发送信号给当前进程。上述这些方法都是单向通信的,也就是说要么是用户态进程主动发起数据交互会话,要么是内核主动发起会话。有没有一种可以在用户态和内核态进行双向数据交互的方法呢?

Netlink是一种在内核态和用户态可以进行双向数据交互的通信机制。在用户态,我们可以将netlink看作是一种特殊的socket,因此通过socket接口来就可以直接使用netlink;在内核态,则需要通过一组特殊的内核接口编写内核模块。

如果初次编写netlink程序,可能会对它所涉及的数据结构感到困惑,本文将简单介绍一下netlink编程中遇到的基本数据结构。

struct msghdr

如果使用sendmsg()发送数据,那么必须使用msghdr结构,该结构内部封装了本次发送数据的一些参数:

           struct msghdr {
               void         *msg_name;       /* optional address */
               socklen_t     msg_namelen;    /* size of address */
               struct iovec *msg_iov;        /* scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* ancillary data, see below */
               size_t        msg_controllen; /* ancillary data buffer len */
               int           msg_flags;      /* flags on received message */
           };

netlink提供的是一种基于数据报的通信服务,因此与UDP通信协议类似,必须通过msg_name指明目的套接字地址结构,而msg_namelen则指明该地址结构的长度。msg_iov指定数据缓冲区数组,而msg_iovlen指明了该数组的元素个数。

struct iovec

iovec结构表示一个向量元素,它定义了一个标准的数据缓冲区格式。其包含两个字段:指向数据的指针和数据长度。

struct iovec
{
    void *iov_base;     /* Pointer to data.  */
    size_t iov_len;     /* Length of data.  */
};

当使用iovec结构类型的数组传递数据时,可以将多个消息通过一次系统调用进行发送。

struct nlmsghdr

如果使用sendmsg函数来发送netlink数据报,那么iovec结构中iov_base字段应该根据具体协议指向一个自定义的数据结构。一般这个自定义的数据结构如下所示:

struct req {
        struct nlmsghdr *nlh;
        struct special_struct *r;
};

其中nlmsghdr结构为必须所有的,它用来描述netlink消息头。

           struct nlmsghdr {
               __u32 nlmsg_len;    /* Length of message including header. */
               __u16 nlmsg_type;   /* Type of message content. */
               __u16 nlmsg_flags;  /* Additional flags. */
               __u32 nlmsg_seq;    /* Sequence number. */
               __u32 nlmsg_pid;    /* PID of the sending process. */
           };

对于special_struct结构则要根据具体需求而定。如果使用netlink现有的协议,比如NETLINK_INET_DIAG,那么special_struct结构在用户程序向内核发送消息时应该为inet_diag_req结构,如果当内核发送消息给用户进程那么special_struct结构又必须定义为inet_diag_msg。这些结构体都是事先定义好的,我们直接拿来用,按需获取这些结构中的某些字段的值即可。

如果是自己定义的通信协议,那么则根据具体需求自定义数据结构即可。比如用户进程想通过pid在内核中获取该进程亲属的pid,那么用户进程在发送消息和接受消息时的special_struct分别可以按照如下定义:

struct get_pid_req {
        int pid;
};

struct get_pid_msg {
        int parent_pid;
        int sibling_pid;
        int child_pid;
}

为netlink消息确定好了结构以后,则将该结构赋值给iovec结构的iov_base字段。

struct sockaddr_nl

netlink是一种特殊的无连接套接字,因此必须在msghdr结构的msg_name中指明目的netlink套接字的地址。套接字地址的通用结构为struct sockaddr,但对于具体类型的套接字则有不同的地址结构,比如TCP/IP协议族为sockaddr_in,对于netlink则为sockaddr_nl结构:

           struct sockaddr_nl {
               sa_family_t     nl_family;  /* AF_NETLINK */
               unsigned short  nl_pad;     /* Zero. */
               pid_t           nl_pid;     /* Process ID. */
               __u32           nl_groups;  /* Multicast groups mask. */
           };

其中该结构中的nl_family字段填充为AF_NETLINK。

以上这些结构在netlink编程中都会涉及到,但是通过上面描述的关系来看,它们最终都是与msghdr结构有关的。而msghdr结构作为参数通过sendmsg函数就可以netlink数据包发送至内核。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key