免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 6887 | 回复: 8
打印 上一主题 下一主题

文件描述符在内核态下的一些小把戏 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2010-06-29 19:09 |只看该作者 |倒序浏览
本帖最后由 zyr-linux 于 2010-06-29 19:18 编辑

前面的话:
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‐\r
       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‐\r
       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‐\r
       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(),有兴趣的可以试试。

评分

参与人数 1可用积分 +30 收起 理由
Godbach + 30 多谢LZ分享

查看全部评分

论坛徽章:
0
2 [报告]
发表于 2010-06-29 19:21 |只看该作者
本帖最后由 zyr-linux 于 2010-06-29 19:47 编辑

以上算是背景介绍,这里是内核版,老是在用户态绕未免有点不成体统^0^

进入内核态,如何获得文件描述符相关信息?
1、内核函数fget(),根据fd号获得指向文件的struct file;
2、内核函数d_path(),根据struct file获取文件名及路径;
3、默认打开文件最大值(fd最大值):NR_OPEN_DEFAULT。

PS:如果要深究,这些信息存放在struct task_struct中:

struct task_struct
{
......
/* filesystem information */
        struct fs_struct *fs;
/* open file information */
        struct files_struct *files;
......
}


把以上东东写成一个函数:
  1. #include <linux/fs.h>           /*struct file*/
  2. #include <linux/file.h>         /*fget() fput()*/
  3. #include <linux/fdtable.h>      /*NR_OPEN_DEFAULT*/
  4. #include <linux/dcache.h>       /*d_path()*/

  5. void KernelFd_ShowFd(void)
  6. {
  7.         int i_Loop = 0;
  8.         char ac_Buf[64];
  9.         char * pc_FdName = NULL;
  10.         struct file * pst_File = NULL;

  11.         printk("\nshow Fd:\n");

  12.         //遍历FD       
  13.         for (i_Loop = 0; i_Loop < NR_OPEN_DEFAULT; i_Loop++)
  14.         {
  15.                 //取出FD对应struct file并检验
  16.                 pst_File = fget(i_Loop);

  17.                 if (NULL != pst_File)
  18.                 {
  19.                         //取出FD对应文件路径及文件名并检验
  20.                         pc_FdName = d_path(&(pst_File->f_path), ac_Buf, sizeof(ac_Buf));

  21.                         if (NULL != pc_FdName)
  22.                         {
  23.                                 printk("\tfd %02d is %s, addr is 0x%p\n", i_Loop, pc_FdName, pst_File);
  24.                         }
  25.                         else
  26.                         {
  27.                                 printk("\tfd %02d name is NULL, addr is 0x%p\n", i_Loop, pst_File);
  28.                         }
  29.                        
  30.                         //fget(),fput()成对
  31.                         fput(pst_File);

  32.                 }

  33.         }

  34.         printk("\n");

  35. }
复制代码
进行验证,编写内核模块,在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。

论坛徽章:
0
3 [报告]
发表于 2010-06-29 19:23 |只看该作者
小把戏之一:

既然fd(0、1、2)均指向当前终端,那么,操作一下?
有了fd,如何操作,很自然的就想到了sys_read(),sys_write();但是很不幸,unbutu10.4中sys_XXX没有导出。
那么研究一下sys_write():

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
                size_t, count)
{
        struct file *file;
        ssize_t ret = -EBADF;
        int fput_needed;

        file = fget_light(fd, &fput_needed);
        if (file) {
                loff_t pos = file_pos_read(file);
                ret = vfs_write(file, buf, count, &pos);
                file_pos_write(file, pos);
                fput_light(file, fput_needed);
        }

        return ret;
}


先抱怨一下,系统调用都用SYSCALL_DEFINE封装了,查找起来很麻烦。
file_pos_read()和file_pos_write()内容很简单;
fget_light()和fput_light()比较麻烦,不过好在有两个已经导出的内核函数fget()、fput()可以代替。

重写的sys_write()如下:
  1. //功能实现所需头文件
  2. #include <linux/fs.h>           /*struct file
  3.                                                                     vfs_write()*/
  4.                                                                     
  5. #include <linux/file.h>         /*fget()
  6.                                                                     fput()*/

  7. #include <linux/uaccess.h>    /*get_fs()
  8.                                                                KERNEL_DS
  9.                                                                set_fs();*/
  10.                                                                

  11. long Kprintf_SysWrite(unsigned int Vui_Fd, char * Vstr_buf, unsigned int Vui_BufLen)
  12. {
  13.         long l_Ret = -EBADF;
  14.         struct file * pst_File = NULL;
  15.         mm_segment_t st_FsStatus;

  16.         //参数检查,Vui_Fd在fget()中检查, Vui_BufLen必为非负
  17.         if (NULL == Vstr_buf)
  18.         {
  19.                 printk(KERN_ALERT "write buffer is empty!\n");
  20.                 return l_Ret;
  21.         }

  22.         //设置文件系统接受内核态地址
  23.         st_FsStatus = get_fs();
  24.         set_fs(KERNEL_DS);

  25.         pst_File = fget(Vui_Fd);
  26.        
  27.         if (NULL != pst_File)
  28.         {
  29.                 loff_t Tst_Pos = pst_File->f_pos;
  30.                
  31.                 l_Ret = vfs_write(pst_File, Vstr_buf, Vui_BufLen, &Tst_Pos);

  32.                 pst_File->f_pos = Tst_Pos;

  33.                 fput(pst_File);
  34.         }

  35.         //恢复文件系统原状态
  36.         set_fs(st_FsStatus);

  37.         return l_Ret;
  38. }
复制代码
这里只实现了sys_write(),其他关于文件的系统调用根据此思路也可以实现,有兴趣的可以试试。
    调用它,即可将Vstr_buf中内容输出到终端,如果fd指向的不是终端文件呢?
    比如,一个socket,一个设备文件?
    或许可以通过在用户态打开,在内核态读、写、操作,避免用户态——内核态切换对性能造成的影响。
    当然,真要去实现还有很多后续工作。

论坛徽章:
0
4 [报告]
发表于 2010-06-29 19:26 |只看该作者
本帖最后由 zyr-linux 于 2010-06-29 19:27 编辑

我更感兴趣的是下面的东东。

小把戏之二:

更进一步,参考printk进行封装Kprintf_SysWrite():
  1. //kprintf一次最多打印1024个字符,1024参考printk()中设定
  2. #define KPRINTF_MAX 1024

  3. char Gac_KprintfBuf[KPRINTF_MAX] = {0, };

  4. int Kprintf(const char * Vstr_Fmt, ...)
  5. {
  6.         int i_Ret;
  7.         va_list st_Args;
  8.        
  9.         //参数检查
  10.         if (NULL == Vstr_Fmt)
  11.         {
  12.                 printk(KERN_ALERT "Vstr_Fmt is empty!\n");
  13.                 return -EBADF;
  14.         }

  15.         //清0
  16.         memset(Gac_KprintfBuf, sizeof(Gac_KprintfBuf), 0);

  17.         //组合字符串及其参数
  18.         va_start(st_Args, Vstr_Fmt);
  19.        
  20.         i_Ret = vsnprintf(Gac_KprintfBuf, KPRINTF_MAX, Vstr_Fmt, st_Args);

  21.         va_end(st_Args);

  22.         //检查组合后字符串长度
  23.         if (0 < i_Ret)
  24.         {
  25.                 //为正数才写入fd0
  26.                 i_Ret = (int)Kprintf_SysWrite(0, Gac_KprintfBuf, (unsigned int)i_Ret);
  27.         }
  28.         else
  29.         {
  30.                 printk("something is wrong with Vstr_Fmt or snprintf\n");
  31.         }
  32.        
  33.         return i_Ret;
  34. }
复制代码
这样,和printf的使用完全一样,内核态程序在可以在shell上显示信息,
再进一步,我们可以实现一条非常符合使用习惯的,内核与shell直接交互的通道。

论坛徽章:
0
5 [报告]
发表于 2010-06-29 19:30 |只看该作者
其他:


将printk中原始代码加入Kprintf()中,Kprintf就可以带有printk功能:
  1. int Kprintf_K(const char * Vstr_Fmt, ...)
  2. {
  3.         int i_Ret;
  4.         va_list st_Args;
  5.         va_list st_PrintkArgs;
  6.        
  7.         //参数检查
  8.         if (NULL == Vstr_Fmt)
  9.         {
  10.                 printk(KERN_ALERT "Vstr_Fmt is empty!\n");
  11.                 return -EBADF;
  12.         }

  13.         //清0
  14.         memset(Gac_KprintfBuf_K, sizeof(Gac_KprintfBuf_K), 0);

  15.         //组合字符串及其参数
  16.         va_start(st_Args, Vstr_Fmt);
  17.        
  18.         i_Ret = vsnprintf(Gac_KprintfBuf_K, KPRINTF_MAX, Vstr_Fmt, st_Args);

  19.         va_end(st_Args);

  20.         //检查组合后字符串长度
  21.         if (0 < i_Ret)
  22.         {
  23.                 //为正数才写入fd0
  24.                 i_Ret = (int)Kprintf_SysWrite(0, Gac_KprintfBuf_K, (unsigned int)i_Ret);
  25.         }
  26.         else
  27.         {
  28.                 printk("something is wrong with Vstr_Fmt or snprintf\n");
  29.         }

  30. [color=Red]        //原printk打印
  31.         va_start(st_PrintkArgs, Vstr_Fmt);
  32.        
  33.         vprintk(Vstr_Fmt, st_PrintkArgs);

  34.         va_end(st_PrintkArgs);[/color]

  35.         return i_Ret;
  36. }
复制代码
内核版曾有帖子提到内核态下如何操作文件,走的是file->f_op->write,按该帖思路实现对各个fd操作代码如下:
(仅验证用,没有写成与Kprintf_SysWrite一致格式)
  1. void KernelFd_WriteFd(void)
  2. {
  3.         int i_Loop = 0;
  4.         long l_Ret;
  5.         char * str_WriteString0 = "Test write fd in kernel module\n";
  6.         //char * str_WriteString1 = "Test write fd in kernel module";
  7.         char ac_Buf[64];
  8.         char * pc_FdName = NULL;
  9.         struct file * pst_File = NULL;

  10.         printk("\nfile->f_op->write:\n");

  11.         //遍历FD       
  12.         for (i_Loop = 0; i_Loop < NR_OPEN_DEFAULT; i_Loop++)
  13.         {
  14.                 //取出FD对应struct file并检验
  15.                 pst_File = fget(i_Loop);               

  16.                 if (NULL != pst_File)
  17.                 {
  18.                         //取出FD对应文件路径及文件名并检验
  19.                         pc_FdName = d_path(&(pst_File->f_path), ac_Buf, sizeof(ac_Buf));

  20.                         if (NULL != pc_FdName)
  21.                         {
  22.                                 printk("\tfd %2d is %s, addr is 0x%p\n", i_Loop, pc_FdName, pst_File);
  23.                         }
  24.                         else
  25.                         {
  26.                                 printk("\tfd %2d name is NULL, addr is 0x%p\n", i_Loop, pst_File);
  27.                         }

  28.                         //调用file->f_op->write进行操作
  29.                         printk("\t\twrite '%s' to %s\n", str_WriteString0, pc_FdName);

  30.                         if ((NULL != pst_File->f_op) && (NULL != pst_File->f_op->write))
  31.                         {
  32.                                 mm_segment_t Tst_FsStatus;
  33.                                 Tst_FsStatus = get_fs();
  34.                                 set_fs(KERNEL_DS);
  35.                                 l_Ret = pst_File->f_op->write(pst_File, str_WriteString0, strlen(str_WriteString0), &(pst_File->f_pos));
  36.                                 set_fs(Tst_FsStatus);
  37.                                 printk(KERN_ALERT "\t\twrite fd %2d return %ld!\n", i_Loop, l_Ret);
  38.                         }


  39.                         //fget(),fput()成对
  40.                         fput(pst_File);

  41.                         printk("\n");
  42.                 }

  43.         }

  44.         printk("\n");

  45. }
复制代码

论坛徽章:
0
6 [报告]
发表于 2010-06-29 19:37 |只看该作者
本帖最后由 zyr-linux 于 2010-06-30 09:49 编辑

最后附上调用kprintk和kprintf_k的图:
  1. static __init int Kprintf_init(void)
  2. {
  3.     printk(KERN_ALERT "Hello world!\n");

  4.         Kprintf("Hello everybody, I am kprintf!\n");

  5.         Kprintf("Test: %d, %ld, 0x%x, %c, %s!\n", 1024, 1024UL, 1024, 'c', "string");


  6.         Kprintf_K("Hello everybody, I am Kprintf_K!\n");

  7.         Kprintf_K("Kprintf_K Test: %d, %ld, 0x%x, %c, %s!\n", 1024, 1024UL, 1024, 'c', "string");

  8.     return 0;
  9. }
复制代码

论坛徽章:
0
7 [报告]
发表于 2010-06-29 22:26 |只看该作者
有意思
不错的思路,收藏了!

论坛徽章:
1
2015年辞旧岁徽章
日期:2015-03-03 16:54:15
8 [报告]
发表于 2010-06-30 08:57 |只看该作者
好东西,收藏!

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
9 [报告]
发表于 2010-06-30 11:36 |只看该作者
多谢LZ分享。有时间仔细拜读一下。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP