C 中 關於printf 函數中度剖析

題外話 linux

      這篇博文主要圍繞printf函數分析的,主要講解printf 使用C的可變參數機制, printf是否可重入(是否線程安全),shell

printf函數的源碼實現.api

正文安全

1.C中可變參數機制多線程

咱們先舉個例子,假如如今有這樣一個需求 "須要一個不定參數整型求和函數".框架

具體實現代碼以下函數

// 須要一個不定參數整型求和函數
int 
sum_add(int len, ...)
{
    int sum = 0;
    va_list ap; 

    va_start(ap, len); // 初始化 將ap參數指向 len 下一個參數位置處
    while (len > 0) {
        int tmp = va_arg(ap, int); // 獲取當前參數,而且將ap指向 下一個參數位置處
        sum += tmp;
        --len;
    }
    va_end(ap); // 清除(銷燬)ap變量

    return sum;
}

 詳細一點的測試代碼以下oop

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

// 須要一個不定參數整型求和函數,len表示參數個數
int sum_add(int len, ...);



int main(int argc, char *argv[])
{    
    int sum;

    sum = sum_add(1, 2);
    printf("sum = %d\n",sum);

    sum = sum_add(4,1,2,3,4);
    printf("sum = %d\n", sum);

    sum = sum_add(10, 1, 2, 3, 4,5,6,7,8,9,10);
    printf("sum = %d\n", sum);

    system("pause");
    return 0;
}

這裏扯一點,對於system("pause"); 是調用系統shell 的pause命令,就是讓當前cmd關閉停留一下,輸出一段話等待一下. 效果圖以下源碼分析

這個功能在 Linux 有個 系統函數以下佈局

#include <unistd.h>

// 函數說明:pause()會令目前的進程暫停(進入睡眠狀態),直至信號(signal)所中斷。
// 返回值:只返回-1 int pause(void);

有的時候 須要在多個平臺,下 完成等待函數 ,就須要經過宏來判斷,這是很噁心的.也許是我的以爲,可移植程序內部都是噁心醜陋的 腐屍堆積體.

下面介紹一個 本身寫的一個通用函數  ,通用控制檯學習的等待函數.

#include <stdio.h>

//6.0 程序等待函數
extern void sh_pause(void);
//6.0 等待的宏 這裏 已經處理好了
#ifndef INIT_PAUSE
#define _STR_PAUSEMSG "請按任意鍵繼續. . ."
#define INIT_PAUSE() \
    atexit(sh_pause)
#endif/* !INIT_PAUSE */

//系統等待函數
void
sh_pause(void)
{
    rewind(stdin);
    printf(_STR_PAUSEMSG);
    getchar();
}

思路是先清空輸入流stdin ,再用getchar等待函數,等待用戶輸入回車結束此次控制檯學習.

1.1 可變參數機制介紹

首先看摘錄的源碼,這裏先分析Window 上源碼,Linux上也同樣.其實Linux源碼更容易看,由於它簡潔高效.都類似,重點看我的抉擇.

// stdarg.h
...
#define va_start __crt_va_start
#define va_arg   __crt_va_arg
#define va_end   __crt_va_end
#define va_copy(destination, source) ((destination) = (source))
...

//vadefs.h
...
typedef char* va_list;
...   
#define _ADDRESSOF(v) (&(v))
...
#elif defined _M_IX86

    #define _INTSIZEOF(n)          ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

    #define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
    #define __crt_va_arg(ap, t)     (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
    #define __crt_va_end(ap)        ((void)(ap = (va_list)0))

#elif defined _M_ARM
....

#define __crt_va_start(ap, x) __crt_va_start_a(ap, x)
...

在分析以前,摘了一個 表格,看一下也許會容易理解一點.以下

stdarg.h數據類型

類型名稱
描述
相容
va_list
用來保存宏va_arg與宏va_end所需信息
C89

 

 

 
stdarg.h宏
巨集名稱
描述
相容
va_start
使va_list指向起始的參數
C89
va_arg
檢索參數
C89
va_end
釋放va_list
C89
va_copy
拷貝va_list的內容
C99

 

 

 

 

 

 

這裏再扯一點,目前用的C標準最可能是C89,流行編譯器例如gcc,VS2015基本上都支持,C89和C99.

其中gcc支持的比VS要好.畢竟VS主打的是CSharp和CPlusPlus.

還有一個編譯器Pelles C對C99支持的最好,對C11支持的還能夠.有機會你們能夠玩玩.作爲小白 還但願C11推廣開來.

由於C11標準對一些見解經常使用模塊例如多線程,數學複數,新的安全庫函數等等,缺點是太醜了.

下面繼續回到 可變參數的話題上. 其實理解 上面 代碼,主要是理解那幾個宏是什麼意思.

這裏說一下一個隱含條件 是 C編譯器對於可變參數函數 必須(默認) 是 __cdecl 修飾的,詳細的一點解釋以下:

__cdecl 是C Declaration的縮寫(declaration,聲明),

表示C語言默認的函數調用方法:全部參數從右到左依次入棧,這些參數由調用者清除,稱爲手動清棧。

被調用函數不會要求調用者傳遞多少參數,調用者傳遞過多或者過少的參數,甚至徹底不一樣的參數都不會產生編譯階段的錯誤。

二次解釋

參數從右向左入棧 => 最後一個參數先入棧,最後第一個參數在棧頂

調用者,被調用函數 => b() { a();} , a是被調用函數,b是調用者函數

調用者清除,稱爲手動清棧 => 在 b 彙編代碼中 會插入 清空a函數棧的彙編代碼

思考一下,只能這麼搞,才能知道函數的入口在哪裏,不然都找不見函數參數在那個位置. 這也是爲何可變參數須要第一個參數顯示聲明的緣由.

而那些宏就是爲了找到其它參數而設計的.核心是根據變量的內存佈局,指針來回指.依次剖析以下:

// 定義 char* 類型,這個類型指針偏移量值爲 1,
// 例如
// char *a = NULL ; 此時 a地址是 0x0
// ++a; => 此時 a地址爲 0x0 + 1*1 = 0x1位置處
        typedef char* va_list;


//
// 定義獲取變量地址的宏
//
#define _ADDRESSOF(v) (&(v))

再來分析 地址偏移宏

// 
// 這個宏是爲了編譯器字節對齊用的,用sizeof(int) 字節數進行對齊
// 
// 簡化一下 sizeof(int) - 1 假定爲 3,(當前2015年11月22日就是3) 
// _INTSIZEOF(n) => ((sizeof(n) + 3 ) & ~3 )
// 舉個例子
//   _INTSIZEOF(int) => 4
//   _INTSIZEOF(char) => 4
//  _INTSIZEOF(double) => 8
//  _INTSIZEOF(short) => 4
// 由於編譯器有內存位置調整,具體參見 struct 內存佈局,畢竟都是C基礎.編譯器這樣作以後,訪問速度回快一些,根據地址取值的次數會少一些.
 #define _INTSIZEOF(n)          ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

下面的宏就簡單了

   
// ap 是va_list 聲明變量,第一次時候調用
// v 表示 可變函數中第一個參數 
// 執行完畢後 ap指向 v 變量後面的下一個 函數參數
 #define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
   
// t 只能是類型,int char double ....
// 操做以後 ap又指向下一個函數參數,可是返回當前ap指向的位置處
// 講完了,關鍵看本身多寫,多讀源碼.有些大神都是不看註釋 直接經過源碼就入手框架了
 #define __crt_va_arg(ap, t)     (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))

// 清空ap變量,等同於簡單的清空野指針
    #define __crt_va_end(ap)        ((void)(ap = (va_list)0))

#define va_start __crt_va_start
#define va_arg   __crt_va_arg
#define va_end   __crt_va_end

// 地址賦值 , 直接等於 主要用於 ap_two = ap_one
// 具體 寫法就是 va_copy(ap_two,va_one) , 目前基本是冷板凳
#define va_copy(destination, source) ((destination) = (source))

到這裏C可變函數機制的源碼分析完畢.

1.2 經過一個例子將可變參數機制結尾

 

咱們的業務需求是這樣的, 須要一個機器掃描 輸入的字符串,輸入的字符串個數是不肯定的.

並從中找出 長度 小於 5的 字符串,輸出 索引和當前串的內容.代碼以下

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>

//簡單的日誌宏 fmt必須是字面量字符串
#define ERRLOG(fmt,...) \
        fprintf(stderr,"[%s:%s:%d]" fmt "\r\n",__FILE__,__FUNCTION__,__LINE__,##__VA_ARGS__)

//簡單系統等待函數
#define _STR_PAUSE "請按任意鍵繼續. . ."
#define SPAUSE() \
    rewind(stdin),printf(_STR_PAUSE),getchar()

//
// 須要一個機器掃描 輸入的字符串,輸入的字符串個數是不肯定的.並從中找出 長度 小於 5的 字符串, 輸出 索引和當前串的內容
// 
#define _INT_FZ (5)
//
// 這裏 最後一個參數 必須是 NULL,同 linux中execl函數簇參數要求
// sstr : 開始的串
//
void with_stdin(const char *sstr, ...);

int main(int argc, char *argv[])
{
    with_stdin(NULL);
    with_stdin("1","1234331","adfada","ds",NULL);
    with_stdin("a","ad","adf","asdfg","asdsdfdf","123131",NULL);
    with_stdin("1","3353432", "1234331", "adfada", "ds","dasafadfa","dasdas", NULL);

    SPAUSE();//等待函數
    return 0;
}

void 
with_stdin(const char *sstr, ...)
{
    static int __id; // 第一聲明的時候賦值爲0,理解成單例
    va_list ap;
    const char *tmp;

    if (NULL == sstr) {
        ERRLOG("Warning check NULL == sstr.");
        return;
    }

    if (_INT_FZ > strlen(sstr))
        printf("%d %s\n",__id,sstr);
    ++__id;

    va_start(ap, sstr);
    while ((tmp = va_arg(ap, const char*)) != NULL) {
        if (_INT_FZ > strlen(tmp))
            printf("%d %s\n", __id, tmp);
        ++__id;
    }

    va_end(ap);
}

 

2.printf 函數可重入討論

首先咱們須要搭建一個pthread 開發環境在 Window上,若是你是用Linux,稍微新一點的系統,如今都是默認pthread線程庫.下面 我就講解 pthread 如何搭建.

第一步 去官網上下載源碼包

   http://sourceware.org/pthreads-win32/

 本身多點點點,下載最新版的目前是 2-9-1,很久沒更新了,在window上使用,還有點麻煩,須要簡單的修改源代碼.

第二步 建一個C控制檯

  用VS2015 建一個 空的控制檯.以下

第三步  在控制檯中添加 一些文件

  須要添加的文件以下:

 

須要添加到 剛纔項目 (右擊在文件夾下打開那個位置) 以下圖

最後是這樣的

這裏配置的是x86 開發環境文件多,配置x64文件就不多了. 這個學會了 之後 就特別簡單了.

第四步:修改頭文件 去掉衝突

 先添加那些頭文件 shift + alt + A,將 三個頭文件添加到項目裏來,以下:

將 pthread.h 下面 299行 改爲 下面這樣,直接在當前目錄下找頭文件

#include "sched.h"

在315行 回車一下 添加下面宏聲明,去掉重複結構定義

#define HAVE_STRUCT_TIMESPEC

第五步 添加一些文件包含

首先 添加 VS取消安全監測宏 _CRT_SECURE_NO_WARNINGS

在項目右擊選擇屬性,或者 鍵盤右擊鍵 + R

後面添加靜態庫

後面其它靜態庫,當找不見了本身添加. 固然若是 你想在 VS 經過代碼添加靜態庫 ,代碼 以下

// 添加 靜態庫 pthreadVC2.lib
// 放在 文件一開始位置處,通常放在頭文件中
#pragma comment(lib,"pthreadVC2.lib")

到這裏環境就配置好了. 下面 直接切入正題 . 

2.1 printf 函數測試

首先 測試 代碼以下 ,須要同窗本身敲一遍,關於pthread的代碼 仍是比較複雜,固然就算咱們開發庫用到的基本上是它中下難度部分api.

#include <stdio.h>
#include <stdlib.h>
#include "pthread.h"

//簡單的日誌宏 fmt必須是字面量字符串
#define ERRLOG(fmt,...) \
        fprintf(stderr,"[%s:%s:%d]" fmt "\r\n",__FILE__,__FUNCTION__,__LINE__,##__VA_ARGS__)

//簡單系統等待函數
#define _STR_PAUSE "請按任意鍵繼續. . ."
#define SPAUSE() \
    rewind(stdin),printf(_STR_PAUSE),getchar()

//每一個線程打印的條數
#define _INT_CUTS (1000)
//開啓的線程數
#define _INT_PTHS (4)
//線程一打印數據
#define _STR_ONES "1111111111111111111111111222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333334444444444444444444444444444444444444445555555555555555555555555555666666666666666666666666666677777777777777777777777777777777777777777777777777778888888888888888888888888888888883333333333333333333333332222222222222222222222211111111111111888888888888888888888888888899999999999999999999999999999999999999990000000000000000000000000000000"
//線程二打印數據
#define _STR_TWO "aaaaaaaaaaaaaaaaaaaaaaassssssssssssssssssssdddddddddddddddddddddddddddddddddddddddfffffffffffffffffffffffffffgggggggggggggggggggggggggggggggggghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkfffffffffffffffffffffffffffffffffffffffffoooooooooooooooooooooooppppppppppppppppppppppppppppvvvvvvvvvvvvvvvvbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbdddddddddddddds"
//線程三打印數據
#define _STR_THRE "AAAAAAAAAAAAAAAAAAAAQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOPPPPPPPPPPPPPPPPPPPPPPPBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBNNNNNNNNNNNNNNNNNNNNNNNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDDDDDDDDDDDDDDDDDDDDDDDDDDDSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSCCCCCCCCCCCCCCCCCCCCCCGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGSSSCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCFFFFFFFFFFFFFFFF"
//線程四打印數據
#define _STR_FIV "你好好的打打打假摔帝卡發的啥都就看見大大淡藍色空間對手卡就考慮到就阿里'省空間打算加快遞費的數量級匱乏綠豆沙聖誕快樂發送的房間打掃房間卡薩丁就卡機了速度快龍捲風撒嬌考慮到房間裏鄧麗君分手的距離見解就馬上發家裏睡覺了舒服你們啦的酸辣粉就看見了見解就李開復撒地方就拉近了見解就困啦風刀霜劍快樂付京東坑垃圾費便可復讀機啊健康路附近啊范德薩晶晶啊加合法的考慮加對方說對啦地方睡覺了啥打法來空間浪費你們來看范德薩龍捲風就阿里你好好的打打打假摔帝卡發的啥都就看見大大淡藍色空間對手卡就考慮到就阿里'省空間打算加快遞費的數量級匱乏綠豆沙聖誕快樂發送的房間打掃房間卡薩丁就卡機了速度快龍捲風撒嬌考慮到房間裏鄧麗君分手的距離見解就馬上發家裏睡覺了舒服你們啦的酸辣粉就看見了見解就李開復撒地方就拉近了見解就困啦風刀霜劍快樂付京東坑垃圾費便可復讀機啊健康路附近啊范德薩晶晶啊加合法的考慮加對方說對啦地方睡覺了啥打法來空間浪費你們來看范德薩龍捲風就阿里"

//全局測試
static FILE *__txt;
//寫入測試文件路徑
#define _STR_PATH "log.txt"

//線程啓動函數
void *start_printf(void *arg);

int main(int argc, char *argv[])
{
    pthread_t ths[_INT_PTHS];
    int i, j;
    int rt;

    puts("printf 線程是否安全測試開始");

    if ((__txt = fopen(_STR_PATH, "w")) == NULL) {
        ERRLOG(_STR_PATH "文件打開失敗");
        exit(-1);
    }

    for (i = 0; i<_INT_PTHS; ++i) {
        rt = pthread_create(ths + i, NULL, start_printf, (void*)i);
        if (0 != rt) {
            ERRLOG("pthread_create run error %d!", rt);
            goto __for_join;
        }
    }

__for_join:
    //等待線程結束
    for (j = 0; j<i; ++j)
        pthread_join(ths[j], NULL);//索引訪問錯誤

    puts("printf 線程是否安全測試結束");

    SPAUSE();//等待函數
    return 0;
}

//線程啓動函數
void *
start_printf(void *arg)
{
    int idx = (int)arg;
    int i;

    printf("線程%d已經啓動!\n", idx);
    for (i = 0; i<_INT_CUTS; ++i) {
        switch (idx) {
        case 0:
            fprintf(__txt, _STR_ONES);
            break;
        case 1:
            fprintf(__txt, _STR_TWO);
            break;
        case 2:
            fprintf(__txt, _STR_THRE);
            break;
        case 3:
            fprintf(__txt, _STR_FIV);
            break;
        default:
            printf("idx => %d 取你嗎的.\r\n", idx);
        }
    }

    printf("線程%d已經關閉!\n", idx);
    return (void*)idx;
}

這裏運行的結果以下:

固然還有生成的 log.txt 文件,

檢查結果是沒有出現亂序現象, 後面看 完<<posix 多線程程序設計>> 以後, 它那裏有這麼一句話,posix要求ANSI C 中標準輸入輸出函數式線程安全的.

因此這種老標準都安全,如今不用說了.

後來在 printf 源碼中找見了

  /* Lock stream.  */
  _IO_cleanup_region_start ((void (*) (void *)) &_IO_funlockfile, s);
  _IO_flockfile (s);

就是加鎖的意思.因此printf 是可重入的函數.說了這麼多,其實意思 之後 寫文件能夠直接拼一個大串直接printf 就能夠了.

這個細節會讓本身作的日誌庫輪子快一點.

3.printf函數的源碼實現

 這裏一樣我也以window 爲例 . 具體見下面代碼

int __cdecl printf (
        const char *format,
        ...
        )
/*
 * stdout 'PRINT', 'F'ormatted
 */
{
    va_list arglist;
    int buffing;
    int retval;

    _VALIDATE_RETURN( (format != NULL), EINVAL, -1);

    va_start(arglist, format);

    _lock_str2(1, stdout);
    __try {
        buffing = _stbuf(stdout);

        retval = _output_l(stdout,format,NULL,arglist);

        _ftbuf(buffing, stdout);

    }
    __finally {
        _unlock_str2(1, stdout);
    }

    return(retval);
}

是否是感受很簡單,先簡單檢測一下

後面獲取fmt以後的參數,而且加鎖 調用另外一個系統輸出函數_output_l

最後解鎖 返回結果.

哈哈,其實 printf函數 源碼 真的很簡單,只要理解了 可變參數機制讀上面代碼很容易.它的複雜見另外一個函數.

Linux上是vprintf函數,window上是_output_l函數,以vprintf爲例,難點在於 格式語法解析,

它完成的功能至關於一個簡單的 代碼解析器. 總共實現代碼2千多行. 看看以爲 Linux內核確實比較屌,單單這個vprintf.

實現就用了

C模板技術

狀態表機制

底層文件讀寫,CPU變量優化,宏,指針,共用體漫天飛.但這個函數 仍是能夠搞得.主要思路是圍繞 狀態表(能夠理解爲業務表)

完成相應的功能,在完成過程當中,對流進行控制,該保存的保存,該輸出輸入,改擴容的擴容,經過文件鎖鎖住 流輸入輸出.

其實有的時候 技術 仍是有點難的, 更多國同行喜歡不是技術,而是 可以提升 人命幣的 手段,順帶作一件其它事.

窮人沒有選擇,有的是生存和掙扎.長這麼大才明白初中生物老師說的,物競天擇適者生存,呵呵大合唱.

 

後記

  到這裏基本就結束,有點有始無終,可是printf 2千行代碼,要是解析起來,其實也就是說白話.熟悉了都是設計和業務.

確定有錯的,例如錯別字,技術錯誤等等,歡迎交流指正,下次右機會分享pthread 開發專題.最後後面分享幾個 本文參考的東西

1. C底層庫源碼 Window和Linux

2. posix 多線程程序設計

相關文章
相關標籤/搜索