内核创建的用户进程printf不能输出一问的研究.docx
- 文档编号:11750767
- 上传时间:2023-06-02
- 格式:DOCX
- 页数:14
- 大小:19.22KB
内核创建的用户进程printf不能输出一问的研究.docx
《内核创建的用户进程printf不能输出一问的研究.docx》由会员分享,可在线阅读,更多相关《内核创建的用户进程printf不能输出一问的研究.docx(14页珍藏版)》请在冰点文库上搜索。
内核创建的用户进程printf不能输出一问的研究
内核创建的用户进程printf不能输出一问的研究
一:
前言
上个星期同事无意间说起,在用核中创建的用户空间进程中,使用printf不能显示的问题.这个问题我当时一时半会没有解释清楚.现在就从linuxkernel的源代码的角度来分析该问题的原因所在.
二:
fork()与execve()中stderr,stdio.stdout的继承关系
其实用继承这个词好像不太准确,要准确一点,可能复制更适合.
首先有二点:
1:
父进程fork出子进程后,是共享所有文件描述符的(实际上也包括socket)
2:
进程在execve后,除了用O_CLOEXEC标志打开的文件外,其它的文件描述符都是会复制到下个执行序列(注意这里不会产生一个新进程,只是将旧的进程替换了)
下面我们从代码中找依据来论证以上的两个观点.
对于第一点:
我们在分析进程创建的时候,已经说过,如果父过程在创建子进程的时候带了CLONE_FILES标志的时候,会和父进程共享task->files.如果没有定义,就会复制父进程的task->files.无论是哪种情况,父子进程的环境都是相同的.
代码如下:
staticintcopy_files(unsignedlongclone_flags,structtask_struct*tsk)
{
structfiles_struct*oldf,*newf;
interror=0;
oldf=current->files;
if(!
oldf)
gotoout;
if(clone_flags&CLONE_FILES){
atomic_inc(&oldf->count);
gotoout;
}
tsk->files=NULL;
newf=dup_fd(oldf,&error);
if(!
newf)
gotoout;
tsk->files=newf;
error=0;
out:
returnerror;
}
从上面的代码可以看出.如果带CLONE_FILES标志,只是会增加它的引用计数.否则,打开的文件描符述会全部复制.
对于二:
我们之前同样也分析过sys_execve().如果有不太熟悉的,到本站找到相关文章进行阅读.在这里不再详细说明整个流程.相关代码如下:
staticvoidflush_old_files(structfiles_struct*files)
{
longj=-1;
structfdtable*fdt;
spin_lock(&files->file_lock);
for(;;){
unsignedlongset,i;
j++;
i=j*__NFDBITS;
fdt=files_fdtable(files);
if(i>=fdt->max_fds)
break;
set=fdt->close_on_exec->fds_bits[j];
if(!
set)
continue;
fdt->close_on_exec->fds_bits[j]=0;
spin_unlock(&files->file_lock);
for(;set;i++,set>>=1){
if(set&1){
sys_close(i);
}
}
spin_lock(&files->file_lock);
}
spin_unlock(&files->file_lock);
}
该函数会将刷新旧环境的文件描述符信息.如果该文件描述符在fdt->close_on_exec被置位,就将其关闭.
然后,我们来跟踪一下,在什么样的情况下,才会将fdt->close_on_exec的相关位置1.
在sys_open()àget_unused_fd_flags():
intget_unused_fd_flags(intflags)
{
……
…….
if(flags&O_CLOEXEC)
FD_SET(fd,fdt->close_on_exec);
else
FD_CLR(fd,fdt->close_on_exec);
……
}
只有在带O_CLOEXEC打开的文件描述符,才会在execve()中被关闭.
三:
用户空间的stderr,stdio.stdout初始化
论证完上面的二个观点之后,后面的就很容易分析了.我们先来分析一下,在用户空间中,printf是可以使用的.哪它的stderr,stdio.stdout到底是从哪点来的呢?
我们知道,用户空间的所有进程都是从init进程fork出来的.因此,它都是继承了init进程的相关文件描述符.
因此,问题都落在,init进程的stderr,stdio.stdout是在何时被设置的?
首先,我们来看一下内核中的第一个进程.它所代码的task_struct结构如下所示:
#defineINIT_TASK(tsk)
{
.state=0,
.stack=&init_thread_info,
.usage=ATOMIC_INIT
(2),
.flags=0,
.lock_depth=-1,
.prio=MAX_PRIO-20,
.static_prio=MAX_PRIO-20,
.normal_prio=MAX_PRIO-20,
.policy=SCHED_NORMAL,
.cpus_allowed=CPU_MASK_ALL,
…….
.files=&init_files,
……
}
它所有的文件描述符信息都是在init_files中的,定义如下:
staticstructfiles_structinit_files=INIT_FILES;
#defineINIT_FILES
{
.count=ATOMIC_INIT
(1),
.fdt=&init_files.fdtab,
.fdtab=INIT_FDTABLE,
.file_lock=__SPIN_LOCK_UNLOCKED(init_task.file_lock),
.next_fd=0,
.close_on_exec_init={{0,}},
.open_fds_init={{0,}},
.fd_array={NULL,}
}
我们从这里可以看到,内核的第一进程是没有带打开文件信息的.
我们来看一下用户空间的init进程的创建过程:
Start_kernel()-àrest_init()中代码片段如下:
staticvoidnoinline__init_refokrest_init(void)
__releases(kernel_lock)
{
intpid;
kernel_thread(kernel_init,NULL,CLONE_FS|CLONE_SIGHAND);
numa_default_policy();
pid=kernel_thread(kthreadd,NULL,CLONE_FS|CLONE_FILES);
kthreadd_task=find_task_by_pid(pid);
unlock_kernel();
/*
*Thebootidlethreadmustexecuteschedule()
*atleastoncetogetthingsmoving:
*/
init_idle_bootup_task(current);
preempt_enable_no_resched();
schedule();
preempt_disable();
/*Callintocpu_idlewithpreemptdisabled*/
cpu_idle();
}
该函数创建了两个进程,然后本进程将做为idle进程在轮转.
在创建kernel_init进程的时候,带的参数是CLONE_FS|
CLONE_SIGHAND.它没有携带CLONE_FILES标志.也就是说,kernel_init中的文件描述符信息是从内核第一进程中复制过去
的.并不和它共享.以后,kernel_init进程中,任何关于files的打开,都不会影响到父进程.
然后在kernel_init()àinit_post()中有:
staticintnoinlineinit_post(void)
{
……
……
if(sys_open((constchar__user*)"/dev/console",O_RDWR,0)<0)
printk(KERN_WARNING"Warning:
unabletoopenaninitialconsole.\n");
(void)sys_dup(0);
(void)sys_dup(0);
……
……
run_init_process(XXXX);
}
从上面的代码中可以看到,它先open了/dev/console.在open的时候,会去找进程没使用的最小文件序号.而,当前进程没有打开任
何文件,所以sys_open()的时候肯定会找到0.然后,两次调用sys_dup(0)来复制文件描述符0.复制后的文件找述符肯定是1.2.这样,
0.1.2就建立起来了.
然后这个进程调用run_init_process()à
kernel_execve()将当前进程替换成了用户空间的一个进程,这也就是用户空间init进程的由来.此后,用户空间的进程全是它的子孙进程.也
就共享了这个0.1.2的文件描述符了.这也就是我们所说的stderr.stdio,stdout.
从用户空间写个程序测试一下:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
main()
{
intret;
char*ttyname0,*ttyname1,*ttyname2;
ttyname0=ttyname(0);
ttyname1=ttyname
(1);
ttyname2=ttyname
(2);
printf(“file0:
%s\n”,ttyname0);
printf(“file1:
%s\n”,ttyname1);
printf(“file2:
%s\n”,ttyname2);
return;
}
运行这个程序,我们会看到,0,1,2描述符的信息全为/dev/consle.
四:
内核创建用户空间进程的过程
在内核中创建用户空间进程的相应接口为call_usermodehelper().
实现上,它将要创建的进程信息链入一个工作队列中,然后由工作队列处理函数调用kernel_thread()创建一个子进程,然后在这个进程里调用kernel_execve()来创建用户空间进程.
在这里要注意工作队列和下半部机制的差别.工作队列是利用一个内核进程来完成工作的,它和下半部无关.也就是说,它并不在一个中断环境中.
那就是说,这样创建出来的进程,其实就是内核环境,它没有打开0,1.2的文件描述符.
可能也有人会这么说:
那我就不在内核环境下创建用户进程不就行了?
例如,我在init_module的时候,创建一个内核线程,然后在这个内核线程里,kernel_execve()一个用户空间进程不就可以了吗?
的确,在这样的情况下,创建的进程不是一个内核环境,因为在调用init_module()的时候,已经通过系统调用进入kernel,这时的环
境是对应用户进程环境.但是别忘了.在系统调用环境下,再进行系统调用是不会成功的(kernel_execve也对应一个系统调用.)
举例印证如下:
Mdoule代码:
#include<linux/ioport.h>
#include<linux/interrupt.h>
#include<asm/io.h>
#include<linux/serial_core.h>
#include<linux/kmod.h>
#include<linux/file.h>
#include<linux/unistd.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ericxiao:
xgr178@");
staticintexeuser_init()
{
intret;
char*argv[]=
{
"/mnt/hgfs/vm_share/user_test/main",
NULL,
};
char*env[]=
{
"HOME=/",
"PATH=/sbin:
/bin:
/usr/sbin:
/usr/bin",
NULL,
};
printk("exeuser_init...\n");
ret=call_usermodehelper(argv[0],argv,env,UMH_WAIT_EXEC);
return0;
}
staticintexeuser_exit()
{
printk("exeuser_exit...\n");
return0;
}
module_init(exeuser_init);
module_exit(exeuser_exit);
用户空间程序代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
intmain(intargc,char*argv[],char*env[])
{
inti;
intfd;
intsize;
char*tty;
FILE*confd;
charprintfmt[4012];
system("echoiamcoming>/var/console");
for(i=0;env[i]!
=NULL;i++){
sprintf(printfmt,"echoenv[%d]:
%s.>>/var/console",i,env[i]);
system(printfmt);
}
for(i=0;i<argc;i++){
sprintf(printfmt,"echoarg[%d]:
%s.>>/var/console",i,argv[i]);
system(printfmt);
}
tty=ttyname(0);
if(tty==NULL)
system("echotty0isNULL>>/var/console");
else{
sprintf(printfmt,"echottyname0%s.>>/var/console",tty);
system(printfmt);
}
tty=ttyname
(1);
if(tty==NULL)
system("echotty1isNULL>>/var/console");
else{
sprintf(printfmt,"echottyname1%s.>>/var/console",tty);
system(printfmt);
}
tty=ttyname
(2);
if(tty==NULL)
system("echotty2isNULL>>/var/console");
else{
sprintf(printfmt,"echottyname2%s.>>/var/console",tty);
system(printfmt);
}
tty=ttyname(fd);
if(tty==NULL)
system("echofdisNULL>>/var/console");
else{
sprintf(printfmt,"echofd%s.>>/var/console",tty);
system(printfmt);
}
return0;
}
插入模块过后,调用用户空间的程序,然后这个程序将进程环境输出到/var/console中,完了可以看到.这个进程输出的0,1,2描述符信息全部NULL.
千万要注意,在测试的用户空间程序,不能打开文件.这样会破坏该进程的原始文件描述符环境(因为这个问题.狠调了一个晚上,汗颜…).
这样.用户空间的printf当然就不能打印出东西了.
四:
小结
一个小问题.却能引申这么多东西,看来以后不能放过工作和学习中的任何一个问题.刨根到底,总会有收获的.
一:
前言
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 内核 创建 用户 进程 printf 不能 输出 研究