文件描述符在内核态下的一些小把戏
http://bbs.chinaunix.net/thread-1928306-1-1.html
前面的话:
linux环境:虚拟机VMware Server上安装的ubuntu10.4,通过putty登录shell。
抄书:
文件描述符(file descriptor:fd)是个简单的整数,用以标明每一个被进程所打开的文件。
可以通过查看/proc/pid/fd/目录查看该进程的fd。
先从用户态开始:
编写一个helloworld,运行后通过proc可以看到进程helloworld有三个fd(0,1,2),指向3个设备文件,均为/dev/pts/0。
然后在helloworld中打开一个文件,查看会发现0、1、2没有变化,另多了一个fd(3)指向打开的文件。
继续抄书,这次是Linux Programmer's Manual:
DESCRIPTION
Under normal circumstances every Unix program has three streams opened for it when it starts up, one for input, one for output, and one for print‐
ing diagnostic or error messages. These are typically attached to the user's terminal (see tty(4) but might instead refer to files or other
devices, depending on what the parent process chose to set up. (See also the "Redirection" section of sh(1).)
The input stream is referred to as "standard input"; the output stream is referred to as "standard output"; and the error stream is referred to as
"standard error". These terms are abbreviated to form the symbols used to refer to these files, namely stdin, stdout, and stderr.
Each of these symbols is a stdio(3) macro of type pointer to FILE, and can be used with functions like fprintf(3) or fread(3).
Since FILEs are a buffering wrapper around Unix file descriptors, the same underlying files may also be accessed using the raw Unix file inter‐
face, that is, the functions like read(2) and lseek(2).
On program startup, the integer file descriptors associated with the streams stdin, stdout, and stderr are 0, 1, and 2, respectively. The pre‐
processor symbols STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO are defined with these values in <unistd.h>. (Applying freopen(3) to one of
these streams can change the file descriptor number associated with the stream.)
Note that mixing use of FILEs and raw file descriptors can produce unexpected results and should generally be avoided. (For the masochistic among
you: POSIX.1, section 8.2.3, describes in detail how this interaction is supposed to work.) A general rule is that file descriptors are handled
in the kernel, while stdio is just a library. This means for example, that after an exec(3), the child inherits all open file descriptors, but
all old streams have become inaccessible.
Since the symbols stdin, stdout, and stderr are specified to be macros, assigning to them is non-portable. The standard streams can be made to
refer to different files with help of the library function freopen(3), specially introduced to make it possible to reassign stdin, stdout, and
stderr. The standard streams are closed by a call to exit(3) and by normal program termination.
fd(0,1,2)就是常说的stdin、stdout、stderr;用户态程序运行时默认建立,/dev/pts/0则是运行程序时的终端。
(纯粹的内核进程则不同,后面会提到)
fd在用户态下可以通过函数dup2()进行重定向,而内核态下也有系统调用sys_dup2(),有兴趣的可以试试。
以上算是背景介绍,这里是内核版,老是在用户态绕未免有点不成体统^0^
进入内核态,如何获得文件描述符相关信息?
1、内核函数fget(),根据fd号获得指向文件的struct file;
2、内核函数d_path(),根据struct file获取文件名及路径;
3、默认打开文件最大值(fd最大值):NR_OPEN_DEFAULT。
PS:如果要深究,这些信息存放在struct task_struct中:
把以上东东写成一个函数:[code]#include <linux/fs.h> /*struct file*/
#include <linux/file.h> /*fget() fput()*/
#include <linux/fdtable.h> /*NR_OPEN_DEFAULT*/
#include <linux/dcache.h> /*d_path()*/
void KernelFd_ShowFd(void)
{
int i_Loop = 0;
char ac_Buf[64];
char * pc_FdName = NULL;
struct file * pst_File = NULL;
printk("\nshow Fd:\n");
//遍历FD
for (i_Loop = 0; i_Loop < NR_OPEN_DEFAULT; i_Loop++)
{
//取出FD对应struct file并检验
pst_File = fget(i_Loop);
if (NULL != pst_File)
{
//取出FD对应文件路径及文件名并检验
pc_FdName = d_path(&(pst_File->f_path), ac_Buf, sizeof(ac_Buf));
if (NULL != pc_FdName)
{
printk("\tfd %02d is %s, addr is 0x%p\n", i_Loop, pc_FdName, pst_File);
}
else
{
printk("\tfd %02d name is NULL, addr is 0x%p\n", i_Loop, pst_File);
}
//fget(),fput()成对
fput(pst_File);
}
}
printk("\n");
}[/code]进行验证,编写内核模块,在init函数中加入延时,insmod后通过proc查看fd,和KernelFd_ShowFd()结论一致。
看看不同情况下fd指向谁:
1、通过putty登录,加载模块,fd(0、1、2)均指向/dev/pts/0;
2、再开启一个putty,加载模块,fd(0、1、2)均指向/dev/pts/1;
3、在ubuntu桌面进入shell,加载模块,fd(0、1、2)均指向/dev/tty1;
4、使用kthread_create建立内核态thread,通过proc查看,fd0指向./,fd1指向../。
insmod加载模块时,实际是先启动了进程:insmod XXX.ko;结果和helloworld一致。
此时fd(0、1、2)为stdin、stdout、stderr,而stdin、stdout、stderr都指向当前终端,可见通过不同的终端加载模块,fd指向的设备文件不同。
而纯粹的kernel thread,其fd既没有三个,也没有指向任何设备文件,因为对它而言,没有stdin、stdout、stderr。
小把戏之一:
既然fd(0、1、2)均指向当前终端,那么,操作一下?
有了fd,如何操作,很自然的就想到了sys_read(),sys_write();但是很不幸,unbutu10.4中sys_XXX没有导出。
那么研究一下sys_write():
先抱怨一下,系统调用都用SYSCALL_DEFINE封装了,查找起来很麻烦。
file_pos_read()和file_pos_write()内容很简单;
fget_light()和fput_light()比较麻烦,不过好在有两个已经导出的内核函数fget()、fput()可以代替。
重写的sys_write()如下:[code]//功能实现所需头文件
#include <linux/fs.h> /*struct file
vfs_write()*/
#include <linux/file.h> /*fget()
fput()*/
#include <linux/uaccess.h> /*get_fs()
KERNEL_DS
set_fs();*/
long Kprintf_SysWrite(unsigned int Vui_Fd, char * Vstr_buf, unsigned int Vui_BufLen)
{
long l_Ret = -EBADF;
struct file * pst_File = NULL;
mm_segment_t st_FsStatus;
//参数检查,Vui_Fd在fget()中检查, Vui_BufLen必为非负
if (NULL == Vstr_buf)
{
printk(KERN_ALERT "write buffer is empty!\n");
return l_Ret;
}
//设置文件系统接受内核态地址
st_FsStatus = get_fs();
set_fs(KERNEL_DS);
pst_File = fget(Vui_Fd);
if (NULL != pst_File)
{
loff_t Tst_Pos = pst_File->f_pos;
l_Ret = vfs_write(pst_File, Vstr_buf, Vui_BufLen, &Tst_Pos);
pst_File->f_pos = Tst_Pos;
fput(pst_File);
}
//恢复文件系统原状态
set_fs(st_FsStatus);
return l_Ret;
}[/code]这里只实现了sys_write(),其他关于文件的系统调用根据此思路也可以实现,有兴趣的可以试试。
调用它,即可将Vstr_buf中内容输出到终端,如果fd指向的不是终端文件呢?
比如,一个socket,一个设备文件?
或许可以通过在用户态打开,在内核态读、写、操作,避免用户态——内核态切换对性能造成的影响。
当然,真要去实现还有很多后续工作。
我更感兴趣的是下面的东东。
小把戏之二:
更进一步,参考printk进行封装Kprintf_SysWrite():[code]//kprintf一次最多打印1024个字符,1024参考printk()中设定
#define KPRINTF_MAX 1024
char Gac_KprintfBuf[KPRINTF_MAX] = {0, };
int Kprintf(const char * Vstr_Fmt, ...)
{
int i_Ret;
va_list st_Args;
//参数检查
if (NULL == Vstr_Fmt)
{
printk(KERN_ALERT "Vstr_Fmt is empty!\n");
return -EBADF;
}
//清0
memset(Gac_KprintfBuf, sizeof(Gac_KprintfBuf), 0);
//组合字符串及其参数
va_start(st_Args, Vstr_Fmt);
i_Ret = vsnprintf(Gac_KprintfBuf, KPRINTF_MAX, Vstr_Fmt, st_Args);
va_end(st_Args);
//检查组合后字符串长度
if (0 < i_Ret)
{
//为正数才写入fd0
i_Ret = (int)Kprintf_SysWrite(0, Gac_KprintfBuf, (unsigned int)i_Ret);
}
else
{
printk("something is wrong with Vstr_Fmt or snprintf\n");
}
return i_Ret;
}[/code]这样,和printf的使用完全一样,内核态程序在可以在shell上显示信息,
再进一步,我们可以实现一条非常符合使用习惯的,内核与shell直接交互的通道。
其他:
将printk中原始代码加入Kprintf()中,Kprintf就可以带有printk功能:[code]int Kprintf_K(const char * Vstr_Fmt, ...)
{
int i_Ret;
va_list st_Args;
va_list st_PrintkArgs;
//参数检查
if (NULL == Vstr_Fmt)
{
printk(KERN_ALERT "Vstr_Fmt is empty!\n");
return -EBADF;
}
//清0
memset(Gac_KprintfBuf_K, sizeof(Gac_KprintfBuf_K), 0);
//组合字符串及其参数
va_start(st_Args, Vstr_Fmt);
i_Ret = vsnprintf(Gac_KprintfBuf_K, KPRINTF_MAX, Vstr_Fmt, st_Args);
va_end(st_Args);
//检查组合后字符串长度
if (0 < i_Ret)
{
//为正数才写入fd0
i_Ret = (int)Kprintf_SysWrite(0, Gac_KprintfBuf_K, (unsigned int)i_Ret);
}
else
{
printk("something is wrong with Vstr_Fmt or snprintf\n");
}
//原printk打印
va_start(st_PrintkArgs, Vstr_Fmt);
vprintk(Vstr_Fmt, st_PrintkArgs);
va_end(st_PrintkArgs);
return i_Ret;
}[/code]内核版曾有帖子提到内核态下如何操作文件,走的是file->f_op->write,按该帖思路实现对各个fd操作代码如下:
(仅验证用,没有写成与Kprintf_SysWrite一致格式)[code]void KernelFd_WriteFd(void)
{
int i_Loop = 0;
long l_Ret;
char * str_WriteString0 = "Test write fd in kernel module\n";
//char * str_WriteString1 = "Test write fd in kernel module";
char ac_Buf[64];
char * pc_FdName = NULL;
struct file * pst_File = NULL;
printk("\nfile->f_op->write:\n");
//遍历FD
for (i_Loop = 0; i_Loop < NR_OPEN_DEFAULT; i_Loop++)
{
//取出FD对应struct file并检验
pst_File = fget(i_Loop);
if (NULL != pst_File)
{
//取出FD对应文件路径及文件名并检验
pc_FdName = d_path(&(pst_File->f_path), ac_Buf, sizeof(ac_Buf));
if (NULL != pc_FdName)
{
printk("\tfd %2d is %s, addr is 0x%p\n", i_Loop, pc_FdName, pst_File);
}
else
{
printk("\tfd %2d name is NULL, addr is 0x%p\n", i_Loop, pst_File);
}
//调用file->f_op->write进行操作
printk("\t\twrite '%s' to %s\n", str_WriteString0, pc_FdName);
if ((NULL != pst_File->f_op) && (NULL != pst_File->f_op->write))
{
mm_segment_t Tst_FsStatus;
Tst_FsStatus = get_fs();
set_fs(KERNEL_DS);
l_Ret = pst_File->f_op->write(pst_File, str_WriteString0, strlen(str_WriteString0), &(pst_File->f_pos));
set_fs(Tst_FsStatus);
printk(KERN_ALERT "\t\twrite fd %2d return %ld!\n", i_Loop, l_Ret);
}
//fget(),fput()成对
fput(pst_File);
printk("\n");
}
}
printk("\n");
}[/code]
最后附上调用kprintk和kprintf_k的图:[code]static __init int Kprintf_init(void)
{
printk(KERN_ALERT "Hello world!\n");
Kprintf("Hello everybody, I am kprintf!\n");
Kprintf("Test: %d, %ld, 0x%x, %c, %s!\n", 1024, 1024UL, 1024, 'c', "string");
Kprintf_K("Hello everybody, I am Kprintf_K!\n");
Kprintf_K("Kprintf_K Test: %d, %ld, 0x%x, %c, %s!\n", 1024, 1024UL, 1024, 'c', "string");
return 0;
}[/code]