1. 程式人生 > >Linux 下 stdin stdout stderr 的由來

Linux 下 stdin stdout stderr 的由來

現在就從linux kernel的原始碼的角度來分析該問題

fork()與execve()中stderr,stdio.stdout的繼承關係

其實用繼承這個詞好像不太準確,要準確一點,可能複製更適合.
首先有2點:
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();
    
     init_idle_bootup_task(current);
     preempt_enable_no_resched();
     schedule();
     preempt_disable();
    
     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:[email protected]" );
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當然就不能打印出東西了.
ps:這位老兄的帖子解了我的一些疑惑。大致瞭解了printf怎麼會在螢幕上顯示的原理。其實我是不明白在嵌入式系統中,如何通過串列埠列印一些訊息的。我想應該是在啟動過程中將/dev/console改成我們的串列埠裝置檔案,比如/dev/tq2440seris0,這樣,在printf時就會從串列埠0列印。

相關推薦

Linux stdin stdout stderr由來

現在就從linux kernel的原始碼的角度來分析該問題 fork()與execve()中stderr,stdio.stdout的繼承關係 其實用繼承這個詞好像不太準確,要準確一點,可能複製更適合. 首先有2點: 1:父程序fork出子程序後,是共享所有檔案描述符的(實

linux的stdin,stdout,stderr詳解

轉自 : http://blog.csdn.net/yinjiabin/article/details/7419895 stdout, stdin, stderr的中文名字分別是標準輸出,標準輸入和標準錯誤。 在Linux下,當一個使用者程序被建立的時候,系統會自

Python 標準輸入輸出stdin stdout stderr 對照c解讀 為理解Linux的檔案描述符fileno做鋪墊

參考連結 詳解stdin,stdout,stderr 作者: 茶鹽耙 How to print to stderr in Python 作者:stack overflow 標準輸入流:鍵盤、掃碼槍等輸入裝置輸入的東西。 標準輸出流:螢幕、檔案等接受的東西 緩衝區:記憶體? 檔

php的三種CLI常量:STDIN,STDOUT,STDERR

 PHP CLI(command line interface)中,有三個系統常量,分別是STDIN、STDOUT、STDERR,代表檔案控制代碼。   常量 描述     &n

C語言 fflush和stdin stdout stderr之間的聯絡

寫在前面: 閱讀這篇文章前,讀者應該大致瞭解C語言“緩衝區”的概念,在前面的文章中有詳細介紹。 **Stdin:** 是標準輸入,一般指鍵盤輸入到緩衝區裡的東西。 **Stdout:** C語言中的 stdout 是一個定義在<stdio.h>的巨集(macro),它

stdin stdout stderr的定義

//C:\Program Files\Microsoft Visual Studio\VC98\CRT\SRC\STDIO.H ... struct _iobuf { char *_ptr; int _cnt; char

stdin,stdout,stderr

 *標準輸入輸出流 #include <stdio.h> extern FILE *stdin; extern FILE *stdout; extern FILE *stderr; //man 1.stdin *0;     //標準檔案描述符; *標準輸入流;

linuxstdin,stdoutstderr

參考自 在linux中經常會看到stdin,stdout和stderr,這3個可以稱為終端(Terminal)的標準輸入(standard input),標準輸出(standard out)和標準錯誤輸出(standard error)。 通過man stdin檢視手冊,可以看到它們都是

fflush(stdin)函式在linux無效

今天在程式設計的時候發現一個有關fflush重新整理緩衝區問題: #include<stdio.h> int main(int argc, char const *argv[]) { char c; int number; scanf("%d",&number)

linux 關於標準輸入輸出錯誤 STDIN STDOUT STDEER

入門: stdout, stdin, stderr的中文名字分別是標準輸出,標準輸入和標準錯誤。 在Linux下,當一個使用者程序被建立的時候,系統會自動為該程序建立三個資料流,也就是題目中所提到的這三個。那麼什麼是資料流呢(stream)?我們知道,一個程式要執行,

linux標準輸入流,標準輸出流 stdin stdout大概是什麼個概念

在linux中把所有東西都看作是檔案來處理,比如印表機,比如網絡卡,比如音效卡等等 stdin也是一個檔案 stdout 也是一個檔案 每當啟動一個程序的時候,都會有一個預設的stdin和stdout生成, 預設情況下,stdin就是鍵盤 預設情況下,

linux安裝JDK

down 安裝目錄 port libraries .com hit java_home sse ava 方法一:手動安裝jdk的安裝包,配置環境變量 1.在/usr/目錄下創建java目錄 [[email protected]/* */ ~]# mkdi

Linuxnagios的搭建及相關配置

linux下nagios的搭建及相關配置一、LAMP環境部署1、安裝php 1.安裝yum源 rpm -Uvh http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm rpm -Uvh http://rpm

Linuxsvn的安裝與部署

myeclipse span .com 服務 點擊 svn服務器 。。 hook idt     最近工作碰到一個問題,我和一個同夥負責開發一個管理系統,基於原來的代碼上進行修改,每當他修改之後,我要再修改都要和他確定是不是最新的文件,才能進行修改。非常影響工作的效率,所以

Linux掛載u盤

成功 掛載 消失 linux下 spa 一個 fat 執行 class 1.先進入/mnt/目錄新建一個usb目錄   cd /mnt/   mkidr usb 2.先fdisk -l,然後插上U盤,fdisk -l   查看是否有新的硬盤添加上來了 3.執行掛載   mo

Linux常用命令之sed學習總結

linux sed sed命令 正則表達式 sed總結 Sed功能說明:Sed是linux下一個強大的文本文件處理工具,通過對文件增加、刪除、查找、查詢操作,配合正則表達式以實現工作中的各種需求。同時也是一名運維人員必須掌握的核心技能。---------------------------

linux安裝DB2的詳細步驟

justify linux style 第一步:檢查程序包及其版本 compat-libstdc++-7.3-2.96.118.i386.rpm 在linux的安裝盤上,找到後使用rpm -i compat-libstdc++-7.3-2.96.118.i386.rpm 安裝即可

Linux的DHCP配置

linux檢測是否已經按照DHCP:rpm -q dhcp 安裝dhcp:yum install dhcp centos 7 的安裝方法:vim /etc/dhcp/dhcpd.conf ddns-update-style interim; /*dhcp支持的dns動態更新方式*/i

Linux安裝PHP的lua擴展庫

directory http ash make try 不安裝 .net ges 執行 一、安裝Lua 5.3.4 下載 http://www.lua.org/ftp/lua-5.3.4.tar.gz tar xvf lua-5.3.4.tar.gz cd lua

LinuxC結構體初始化

直觀 tro 擴展性 方式 建議 struct 初始化方式 www 寫到 原文地址在這裏: http://www.cnblogs.com/Anker/p/3545146.html 我 只把裏面的主要介紹和代碼寫到這裏了. 順序初始化   教科書上講C語言結構體初始化