内核创建的用户进程printf不能输出一问的研究

本文探讨了Linux系统中fork()与execve()系统调用中文件描述符的继承关系,详细解析了父进程如何将文件描述符传递给子进程,并讨论了在execve()后文件描述符的变化。

fork()与execve()中stderr,stdio.stdout的继承关系

其实用继承这个词好像不太准确,要准确一点,可能复制更适合.
首先有二点:
1:父进程fork出子进程后,是共享所有文件描述符的(实际上也包括socket)
2:进程在execve后,除了用O_CLOEXEC标志打开的文件外,其它的文件描述符都是会复制到下个执行序列(注意这里不会产生一个新进程,只是将旧的进程替换了)
下面我们从代码中找依据来论证以上的两个观点.
对于第一点:
我们在分析进程创建的时候,已经说过,如果父过程在创建子进程的时候带了CLONE_FILES标志的时候,会和父进程共享task->files.如果没有定义,就会复制父进程的task->files.无论是哪种情况,父子进程的环境都是相同的.
代码如下:
static int copy_files(unsigned long clone_flags, struct task_struct * tsk)
{
     struct files_struct *oldf, *newf;
     int error = 0;
     oldf = current->files;
     if (!oldf)
         goto out;
 
     if (clone_flags & CLONE_FILES) {
         atomic_inc(&oldf->count);
         goto out;
     }
 
     tsk->files = NULL;
     newf = dup_fd(oldf, &error);
     if (!newf)
         goto out;
 
     tsk->files = newf;
     error = 0;
out:
     return error;
}
从上面的代码可以看出.如果带CLONE_FILES标志,只是会增加它的引用计数.否则,打开的文件描符述会全部复制.
 
对于二:
我们之前同样也分析过sys_execve().如果有不太熟悉的,到本站找到相关文章进行阅读.在这里不再详细说明整个流程.相关代码如下:
 
static void flush_old_files(struct files_struct * files)
{
     long j = -1;
     struct fdtable *fdt;
 
     spin_lock(&files->file_lock);
     for (;;) {
         unsigned long set, 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():
int get_unused_fd_flags(int flags)
{
     ……
     …….
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结构如下所示:
#define INIT_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中的,定义如下:
static struct files_struct init_files = INIT_FILES;
#define INIT_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()中代码片段如下:
static void noinline __init_refok rest_init(void)
     __releases(kernel_lock)
{
     int pid;
 
     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();
 
     /*
      * The boot idle thread must execute schedule()
      * at least once to get things moving:
      */
     init_idle_bootup_task(current);
     preempt_enable_no_resched();
     schedule();
     preempt_disable();
 
     /* Call into cpu_idle with preempt disabled */
     cpu_idle();
}
该函数创建了两个进程,然后本进程将做为idle进程在轮转.
在创建kernel_init进程的时候,带的参数是CLONE_FS | CLONE_SIGHAND.它没有携带CLONE_FILES标志.也就是说,kernel_init中的文件描述符信息是从内核第一进程中复制过去的.并不和它共享.以后,kernel_init进程中,任何关于files的打开,都不会影响到父进程.
 
然后在kernel_init() à init_post()中有:
static int noinline init_post(void)
{
     ……
     ……
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
         printk(KERN_WARNING "Warning: unable to open an initial console./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()
{
         int ret;
        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@163.com" );
 
 
 
 
static int exeuser_init()
{
     int ret;
 
     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);
 
     return 0;
}
 
static int exeuser_exit()
{
     printk("exeuser_exit .../n");
     return 0;
}
 
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>
 
 
int main(int argc,char *argv[],char *env[])
{
     int i;
     int fd;
     int size;
     char *tty;
     FILE *confd;
     char printfmt[4012];
 
     system("echo i am coming > /var/console");
     for(i=0; env[i]!=NULL;i++){
         sprintf(printfmt,"echo env[%d]:%s. >>/var/console",i,env[i]);
         system(printfmt);
     }
 
     for(i=0; i<argc ;i++){
         sprintf(printfmt,"echo arg[%d]:%s. >>/var/console",i,argv[i]);
         system(printfmt);     
 
     }
 
 
 
     tty = ttyname(0);
     if(tty == NULL)
         system("echo tty0 is NULL >> /var/console");
     else{
         sprintf(printfmt,"echo ttyname0 %s. >>/var/console",tty);
         system(printfmt); 
     }
 
     tty = ttyname(1);
     if(tty == NULL)
         system("echo tty1 is NULL >> /var/console");
     else{
         sprintf(printfmt,"echo ttyname1 %s. >>/var/console",tty);
         system(printfmt); 
     }
 
     tty = ttyname(2);
     if(tty == NULL)
         system("echo tty2 is NULL >> /var/console");
     else{
         sprintf(printfmt,"echo ttyname2 %s. >>/var/console",tty);
         system(printfmt); 
     }
 
     tty = ttyname(fd);
     if(tty == NULL)
         system("echo fd is NULL >> /var/console");
     else{
         sprintf(printfmt,"echo fd %s. >>/var/console",tty);
         system(printfmt); 
     }
 
    
 
     return 0;
}
插入模块过后,调用用户空间的程序,然后这个程序将进程环境输出到/var/console中,完了可以看到.这个进程输出的0,1,2描述符信息全部NULL.
千万要注意,在测试的用户空间程序,不能打开文件.这样会破坏该进程的原始文件描述符环境(因为这个问题.狠调了一个晚上,汗颜…).
这样.用户空间的printf当然就不能打印出东西了.
<think> 我们有两个目标: 1. 将内核日志重定向到当前终端(通过TIOCCONS) 2. 同时将用户进程的标准输出(以及内核日志,因为内核日志也被重定向到当前终端)保存到文件中 但是注意:TIOCCONS是将内核的console输出重定向到当前终端,而用户进程的标准输出默认也是输出到当前终端。因此,在重定向后,当前终端会同时显示内核日志和用户进程输出。 然而,我们想要将这些内容(包括内核日志和用户进程的标准输出)都保存到个文件中,同时还能在终端上看到。 思路: 我们可以使用重定向(dup2)将当前进程的标准输出重定向到个文件,这样用户进程的标准输出就会写入文件。但是,内核日志是通过TIOCCONS重定向到当前终端的,它不会经过我们进程的标准输出。 所以,我们需要另外的方法来捕获终端上的所有输出(包括内核日志和用户进程输出)。种常见的方法是使用伪终端(pty)或者使用tee命令的原理。 但是,考虑到我们已经在代码中使用了TIOCCONS,我们可以这样设计: 步骤1:执行TIOCCONS将内核日志重定向到当前终端(tty1)。 步骤2:将当前进程的标准输出重定向到个文件(这样printf等就会写入文件)。 步骤3:但是,这样内核日志仍然只输出到终端(tty1),而不会写入文件。因此,我们需要同时捕获终端上显示的内容。 如何同时捕获终端上显示的内容? 我们可以打开当前终端设备(tty1)的另个文件描述符,然后从该描述符读取数据并写入文件。但是,这需要非阻塞读取和额外的线程,因为读取终端设备通常是阻塞的。 另种方法是使用伪终端(pty)来创建个主从设备,将当前进程的标准输出内核日志都重定向到伪终端的从设备,然后在主设备上读取数据,这样就能捕获所有写入从设备的数据(包括内核日志和用户进程输出),然后将这些数据同时写入日志文件和当前终端。 但是,这种方法比较复杂,而且需要改变当前进程的终端。 因此,我们采用折中方案:使用tee命令的原理,即创建个管道,将标准输出重定向到管道,然后创建个子进程,从管道读取数据并同时写入文件和终端。同时,我们还需要捕获内核日志(它直接写入终端)。但是,内核日志直接写入终端,我们无法通过管道捕获。 所以,我们需要将内核日志也重定向到某个我们可以控制的管道?然而,TIOCCONS只能重定向到终端设备,不能重定向到管道。 重新思考: 另种思路:我们可以将当前终端(tty1)的所有输出(包括内核日志和用户进程输出)通过个中间程序(比如tee)来传递。但是,这需要我们在启动程序之前就设置好重定向。 鉴于我们已经在内核日志重定向的代码中,我们可以这样做: 1. 首先,我们记录下当前终端的设备名(tty_name)。 2. 然后,我们创建个管道(pipe1)和个子进程。 3. 在子进程中,我们打开个日志文件,然后不断地从管道读取数据,将读取到的数据同时写入日志文件和原来的终端(通过之前记录的tty_name打开的文件描述符)。 4. 在父进程中,我们将标准输出重定向到管道(这样用户进程的标准输出就会进入管道)。 5. 同时,我们使用TIOCCONS将内核日志重定向到当前终端(注意:当前终端已经被我们通过子进程在写,但内核日志是直接写入终端设备的,我们无法通过管道捕获,所以我们需要将内核日志也重定向到管道?不行,因为TIOCCONS只能重定向到终端设备,不能是管道。 题:如何同时捕获内核日志和标准输出? 实际上,在TIOCCONS之后,内核日志被重定向到当前终端,而当前终端是个设备文件。我们可以打开这个设备文件(以读方式)来读取终端上显示的内容。但是,终端设备文件只能被个读取者读取(通常只有进程可以成为控制终端的前台进程组,并读取输入),而且输出内容我们通常不能读取(除非我们记录终端的所有输出,这通常需要终端驱动支持)。 因此,我们可能需要使用个伪终端来接管当前终端的工作: 步骤: 1. 创建个伪终端(pty),获得主设备(master)和从设备(slave)的文件描述符。 2. 将当前进程的标准输出重定向到从设备。 3. 使用TIOCCONS将内核日志重定向到从设备(注意:TIOCCONS要求设备是终端,而伪终端的从设备就是个终端)。 4. 然后,从主设备读取数据,这样就能得到所有写入从设备的数据(包括内核日志和用户进程的标准输出)。 5. 将从主设备读取到的数据同时写入日志文件和当前真正的终端(用户原来的终端)。 但是,这样会改变当前进程的终端,可能会影响交互(比如输入)。 考虑到我们可能不需要交互(输入),我们可以这样做。 具体步骤: 1. 创建伪终端(使用posix_openpt, grantpt, unlockpt等函数)。 2. 获取从设备名(ptsname)。 3. 将标准输出和标准错误输出重定向到从设备。 4. 使用TIOCCONS将内核日志重定向到从设备(因为从设备是个终端,所以TIOCCONS允许)。 5. 然后,从主设备读取数据,并同时写入日志文件和原来的终端。 但是,这有题:TIOCCONS要求重定向的目标是个终端,而且我们原来的终端(tty1)会被伪终端取代吗?实际上,我们并不改变原来的终端,我们只是将输出重定向到伪终端,然后通过伪终端再输出到原来的终端。 注意:原来的终端(tty1)仍然存在,我们只是将内核日志和标准输出都重定向到伪终端的从设备,然后通过主设备读取并转发到原来的终端(tty1)。 这样,我们在伪终端的主设备上读取的数据就是内核日志和标准输出的混合。 代码结构: 1. 创建伪终端,并设置。 2. 保存原始的标准输出文件描述符(用于恢复)。 3. 重定向标准输出到伪终端的从设备。 4. 打开原始终端(tty1)的文件描述符(用于写入,以便将数据输出到原终端)。 5. 执行TIOCCONS,将内核日志重定向到伪终端的从设备(因为从设备是终端)。 6. 创建进程,负责从伪终端的主设备读取数据,然后同时写入日志文件和原始终端。 7. 父进程继续执行,并输出内容(标准输出内核日志都会进入伪终端的从设备,然后被子进程读取并写入文件和原终端)。 但是,这样会导致所有输出(包括内核日志和标准输出)都经过伪终端,并且通过子进程写入原终端。同时,我们还可以写入日志文件。 注意:这个方案比较复杂,而且需要处理伪终端的设置和数据的转发。 由于代码较长,我给出个简化的框架: ```c #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <pty.h> // for openpty int main() { int master, slave; char tty_name_buf[256]; char *orig_tty_name = ttyname(STDOUT_FILENO); if (!orig_tty_name) { perror("ttyname"); exit(1); } // 1. 创建伪终端 if (openpty(&master, &slave, tty_name_buf, NULL, NULL) < 0) { perror("openpty"); exit(1); } // 2. 保存原始标准输出 int orig_stdout = dup(STDOUT_FILENO); // 3. 重定向标准输出到伪终端的slave dup2(slave, STDOUT_FILENO); dup2(slave, STDERR_FILENO); // 可选,重定向标准错误 // 4. 打开原始终端(用于写入) int orig_tty_fd = open(orig_tty_name, O_WRONLY); if (orig_tty_fd < 0) { perror("open orig tty"); exit(1); } // 5. 将内核日志重定向到伪终端的slave(因为slave是终端) if (ioctl(slave, TIOCCONS) < 0) { perror("ioctl TIOCCONS"); } // 6. 创建进程来读取master并写入文件和原始终端 pid_t pid = fork(); if (pid < 0) { perror("fork"); exit(1); } else if (pid == 0) { // 子进程 close(slave); // 子进程不需要slave close(orig_stdout); // 打开日志文件 int log_fd = open("output.log", O_WRONLY|O_CREAT|O_TRUNC, 0644); if (log_fd < 0) { perror("open logfile"); exit(1); } char buf[4096]; ssize_t n; while ((n = read(master, buf, sizeof(buf))) > 0) { // 同时写入日志文件和原始终端 write(log_fd, buf, n); write(orig_tty_fd, buf, n); } close(log_fd); close(orig_tty_fd); close(master); exit(0); } else { // 父进程 close(slave); close(master); close(orig_tty_fd); // 父进程不需要orig_tty_fd,因为子进程负责写 // 现在,所有标准输出内核日志都将写入伪终端slave,然后被子进程读取并写入文件和原终端 // 示例:输出些内容 printf("This is a test from user process.\n"); fflush(stdout); // 生成内核日志(例如通过syslog,或者这里我们可以触发内核消息?) // 注意:普通用户可能无法触发内核日志,我们可以用下面这个(需要root) system("echo 'Hello from kernel' > /dev/kmsg"); // 等待子进程结束?或者我们让子进程直运行 // 但是注意,我们这里只是示例,实际中可能需要直运行 // 这里等待5秒,让子进程有时间读取 sleep(5); // 然后关闭子进程?这里只是示例,实际应用可能需要更复杂的退出处理 // 恢复原始标准输出 dup2(orig_stdout, STDOUT_FILENO); close(orig_stdout); // 恢复内核日志重定向(可选) int console_fd = open("/dev/console", O_WRONLY); if (console_fd >= 0) { ioctl(console_fd, TIOCCONS); close(console_fd); } // 等待子进程 int status; waitpid(pid, &status, 0); } return 0; } ``` 注意: 1. 上述代码需要root权限,因为TIOCCONS和写入/dev/kmsg通常需要root。 2. 我们使用openpty来简化伪终端的创建。 3. 在父进程中,我们关闭了不需要的文件描述符(slave, master, orig_tty_fd),因为子进程负责读取和写入。 4. 在子进程中,我们读取主设备,并将数据写入日志文件和原始终端。 5. 我们重定向了标准错误输出(STDERR_FILENO),以便将错误信息也捕获。 6. 恢复:在父进程结束前,我们恢复标准输出,并将内核日志重定向回/dev/console(可选)。 但是,这个程序有题:当父进程退出时,子进程可能还在阻塞读取(因为没有数据),所以我们需要个机制让子进程退出。我们可以设置超时,或者在父进程退出前关闭主
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值