引言
有的人真的是天命所歸

延安時期炸彈 投到他院子都 沒炸. 有些事無法改變 是命!
我們也快'老'了, 常回家看看.
前言
扯淡結束了,今天分享的可能有點多,都很簡單,但是糅合在一起就是有點復雜. 我會具體講解一些開發中坑.
主要圍繞如何在Linux和Window 上搭建C基礎開發框架, 並且寫一個支持多用戶分級的日志庫. sclog.
需要材料
1.Linux 用的code linux_sc_console
2.window 用的 項目 代碼 sc_console_start
下載上面源碼.其實源碼都一樣,只是放在不同平台下運行測試,一切正常. 這裡回答一個問題,為什麼C程序員那麼喜歡造輪子.
因為C自由,自由就以為著自己開心就好. 如果性能還可以那就更好了. 說白了開心就好.(當然,C中沒有一同天下的框架,導致群雄割據,小明東奔西跑.)
歡迎交流提高.
正文
1.先從Linux 環境說起來
那我們剛起
1.1 首先看下面結構

從上面 結構中我們可以看出 這個 sc_console 項目在 Linux中文件結構,簡單介紹一下
/* Makefile => 編譯文件 main => 存放 主 main.c 的目錄 main.c => 主業務,主要測試代碼 module/schead/ => 都是結構目錄 include => schead模塊中保存頭文件目錄 // main 放主業務, module存放主模塊,每個模塊單獨一個文件夾 scatom.h => 原子操作頭文件 schead.h => C中一些跨平台幫助操作宏,頭文件 schead.c => 對schead.h一些特定接口實現,例如大小端判斷 sclog.h => 分級多用戶日志庫 頭文件 sclog.c => 多線程日志 實現 */
這裡 簡單說明了一下,文件主要意義. 後面會直接貼代碼, 有些東西不好說, 因為不自己琢磨看開源代碼, 很難簡單說明白. 後面
會對一些細節和不注意的坑說明一下. 這個框架 實戰意義值得學習, 當然因具體業務可以再優化.
下面看看 Makefile 文件內容,來了解 編譯的具體細節.
CC=gcc
DEBUG=-g -Wall -D_DEBUG
#指定pthread線程庫
PTHREAD=-lpthread
#指定一些目錄
DIR=-I./module/schead/include
#具體運行函數
RUN=$(CC) $(DEBUG) -o $@ $^ $(PTHREAD) $(DIR)
RUNO=$(CC) $(DEBUG) -c -o $@ $^ $(DIR)
# 主要生成的產品
sc_console.out:main.o schead.o sclog.o
$(RUN)
main.o:./main/main.c
$(RUNO)
schead.o:./module/schead/schead.c
$(RUNO)
sclog.o:./module/schead/sclog.c
$(RUNO)
#刪除命令
clean:
rm -rf *.i *.s *.o *.out __* log ; ls -hl
這裡我再細細說來,畢竟簡單我也喜歡說
-g -Wall 表示 讓 gcc開啟強警告和插入調試代碼
-I./module/schead/include 表示gcc 編譯的時候包含這個文件,文件路徑采用的相對路徑.
-c 生成編譯後的機器碼.
後面意思是 需要 sc_console.out 但是依賴 main.o 和 schead.o 和 sclog.o
而main.o 依賴 main.c 等等
後面
clean是第二條命令不會執行.
但是可以通過 make clean 來執行這條命令,
後面 刪除 log 和 __*是刪除生成的日志和持久數據文件. 大家可以試試效果很好.
到這裡 Linux上編譯已經通過了. 下面直接上代碼 . 一個個的來
2.2 首先看原子操作類 scatom.h
#ifndef _SC_ATOM
#define _SC_ATOM
/*
* 作者 : wz
*
* 描述 : 簡單的原子操作,目前只考慮 VS(CL) 小端機 和 gcc
* 推薦用 posix 線程庫
*/
// 如果 是 VS 編譯器
#if defined(_MSC_VER)
#include <Windows.h>
//忽略 warning C4047: “==”:“void *”與“LONG”的間接級別不同
#pragma warning(disable:4047)
// v 和 a 多 long 這樣數據
#define ATOM_FETCH_ADD(v, a) \
InterlockedExchangeAdd((LONG*)&(v), (LONG)(a))
#define ATOM_ADD_FETCH(v, a) \
InterlockedAdd((LONG*)&(v), (LONG)(a))
#define ATOM_SET(v, a) \
InterlockedExchange((LONG*)&(v), (LONG)(a))
#define ATOM_CMP(v, c, a) \
(c == InterlockedCompareExchange((LONG*)&(v), (LONG)(a), (LONG)c))
/*
對於 InterlockedCompareExchange(v, c, a) 等價於下面
long tmp = v ; v == a ? v = c : ; return tmp;
咱麼的 ATOM_FETCH_CMP(v, c, a) 等價於下面
long tmp = v ; v == c ? v = a : ; return tmp;
*/
#define ATOM_FETCH_CMP(v, c, a) \
InterlockedCompareExchange((LONG*)&(v), (LONG)(a), (LONG)c)
#define ATOM_LOCK(v) \
while(ATOM_SET(v, 1)) \
Sleep(0)
#define ATOM_UNLOCK(v) \
ATOM_SET(v, 0)
//否則 如果是 gcc 編譯器
#elif defined(__GNUC__)
#include <unistd.h>
/*
type tmp = v ; v += a ; return tmp ;
type 可以是 8,16,32,84 的 int/uint
*/
#define ATOM_FETCH_ADD(v, a) \
__sync_fetch_add_add(&(v), (a))
/*
v += a ; return v;
*/
#define ATOM_ADD_FETCH(v, a) \
__sync_add_and_fetch(&(v), (a))
/*
type tmp = v ; v = a; return tmp;
*/
#define ATOM_SET(v, a) \
__sync_lock_test_and_set(&(v), (a))
/*
bool b = v == c; b ? v=a : ; return b;
*/
#define ATOM_CMP(v, c, a) \
__sync_bool_compare_and_swap(&(v), (c), (a))
/*
type tmp = v ; v == c ? v = a : ; return v;
*/
#define ATOM_FETCH_CMP(v, c, a) \
__sync_val_compare_and_swap(&(v), (c), (a))
/*
加鎖等待,知道 ATOM_SET 返回合適的值
_INT_USLEEP 是操作系統等待納秒數,可以優化,看具體操作系統
使用方式
int lock;
ATOM_LOCK(lock);
//to do think ...
ATOM_UNLOCK(lock);
*/
#define _INT_USLEEP (2)
#define ATOM_LOCK(v) \
while(ATOM_SET(v, 1)) \
usleep(_INT_USLEEP)
/*
對ATOM_LOCK 解鎖, 當然 直接調用相當於 v = 0;
*/
#define ATOM_UNLOCK(v) \
__sync_lock_release(&(v))
#endif /*!_MSC_VER && !__GNUC__ */
#endif /*!_SC_ATOM*/
這些原子操作,在我前面講解 雲風的字符串詳細提過,這裡簡單說一下 為什麼 會有 LONG*
這是這兩種原子操作機制不一樣. Linux上 __sync 是 在編譯器層次實現的, 而 window的 Interlock 是在 函數庫層實現的.
差距很大,這裡強轉LONG* 是一種偽裝操作.
2.3 再看 schead.h
#ifndef _H_CHEAD
#define _H_CHEAD
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <stdint.h>
#include <stddef.h>
/*
* 1.0 錯誤定義宏 用於判斷返回值狀態的狀態碼 _RF表示返回標志
* 使用舉例 :
int flag = scconf_get("pursue");
if(flag != _RT_OK){
sclog_error("get config %s error! flag = %d.", "pursue", flag);
exit(EXIT_FAILURE);
}
* 這裡是內部 使用的通用返回值 標志
*/
#define _RT_OK (0) //結果正確的返回宏
#define _RT_EB (-1) //錯誤基類型,所有錯誤都可用它,在不清楚的情況下
#define _RT_EP (-2) //參數錯誤
#define _RT_EM (-3) //內存分配錯誤
#define _RT_EC (-4) //文件已經讀取完畢或表示鏈接關閉
#define _RT_EF (-5) //文件打開失敗
/*
* 2.0 如果定義了 __GNUC__ 就假定是 使用gcc 編譯器,為Linux平台
* 否則 認為是 Window 平台,不可否認宏是丑陋的
*/
#if defined(__GNUC__)
//下面是依賴 Linux 實現,等待毫秒數
#include <unistd.h>
#include <sys/time.h>
#define SLEEPMS(m) \
usleep(m * 1000)
#else
// 這裡創建等待函數 以毫秒為單位 , 需要依賴操作系統實現
#include <Windows.h>
#include <direct.h> // 加載多余的頭文件在 編譯階段會去掉
#define inline __inline //附加一個內聯函數宏
#define rmdir _rmdir
/**
* Linux sys/time.h 中獲取時間函數在Windows上一種移植實現
**tv : 返回結果包含秒數和微秒數
**tz : 包含的時區,在window上這個變量沒有用不返回
** : 默認返回0
**/
extern int gettimeofday(struct timeval* tv, void* tz);
//為了解決 不通用功能
#define localtime_r(t, tm) localtime_s(tm, t)
#define SLEEPMS(m) \
Sleep(m)
#endif /*__GNUC__ 跨平台的代碼都很丑陋 */
//3.0 浮點數據判斷宏幫助, __開頭表示不希望你使用的宏
#define __DIFF(x, y) ((x)-(y)) //兩個表達式做差宏
#define __IF_X(x, z) ((x)<z&&(x)>-z) //判斷宏,z必須是宏常量
#define EQ(x, y, c) EQ_ZERO(__DIFF(x,y), c) //判斷x和y是否在誤差范圍內相等
//3.1 float判斷定義的宏
#define _FLOAT_ZERO (0.000001f) //float 0的誤差判斷值
#define EQ_FLOAT_ZERO(x) __IF_X(x,_FLOAT_ZERO) //float 判斷x是否為零是返回true
#define EQ_FLOAT(x, y) EQ(x, y, _FLOAT_ZERO) //判斷表達式x與y是否相等
//3.2 double判斷定義的宏
#define _DOUBLE_ZERO (0.000000000001) //double 0誤差判斷值
#define EQ_DOUBLE_ZERO(x) __IF_X(x,_DOUBLE_ZERO) //double 判斷x是否為零是返回true
#define EQ_DOUBLE(x,y) EQ(x, y, _DOUBLE_ZERO) //判斷表達式x與y是否相等
//4.0 控制台打印錯誤信息, fmt必須是雙引號括起來的宏
#ifndef CERR
#define CERR(fmt, ...) \
fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
__FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)
#endif/* !CERR */
//4.1 控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量
#ifndef CERR_EXIT
#define CERR_EXIT(fmt,...) \
CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
#endif/* !ERR */
#ifndef IF_CERR
/*
*4.2 if 的 代碼檢測
*
* 舉例:
* IF_CERR(fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP), "socket create error!");
* 遇到問題打印日志直接退出,可以認為是一種簡單模板
* code : 要檢測的代碼
* fmt : 必須是""括起來的字符串宏
* ... : 後面的參數,參照printf
*/
#define IF_CERR(code, fmt, ...) \
if((code) < 0) \
CERR_EXIT(fmt, ##__VA_ARGS__)
#endif //!IF_CERR
//5.0 獲取數組長度,只能是數組類型或""字符串常量,後者包含'\0'
#ifndef LEN
#define LEN(arr) \
(sizeof(arr)/sizeof(*(arr)))
#endif/* !ARRLEN */
//6.0 程序清空屏幕函數
#ifndef CONSOLE_CLEAR
#ifndef _WIN32
#define CONSOLE_CLEAR() \
system("printf '\ec'")
#else
#define CONSOLE_CLEAR() \
system("cls")
#endif/* _WIN32 */
#endif /*!CONSOLE_CLEAR*/
//7.0 置空操作
#ifndef BZERO
//v必須是個變量
#define BZERO(v) \
memset(&v,0,sizeof(v))
#endif/* !BZERO */
//9.0 scanf 健壯的
#ifndef SAFETY_SCANF
#define SAFETY_SCANF(scanf_code,...) \
while(printf(__VA_ARGS__),scanf_code){\
while(getchar()!='\n');\
puts("輸入出錯,請按照提示重新操作!");\
}\
while(getchar()!='\n')
#endif /*!SAFETY_SCANF*/
//10.0 簡單的time幫助宏
#ifndef TIME_PRINT
#define TIME_PRINT(code) {\
clock_t __st,__et;\
__st=clock();\
code\
__et=clock();\
printf("當前代碼塊運行時間是:%lf秒\n",(0.0+__et-__st)/CLOCKS_PER_SEC);\
}
#endif /*!TIME_PRINT*/
//11.0 等待的宏 這裡 已經處理好了
#define _STR_PAUSEMSG "請按任意鍵繼續. . ."
extern void sh_pause(void);
#ifndef INIT_PAUSE
# ifdef _DEBUG
# define INIT_PAUSE() atexit(sh_pause)
# else
# define INIT_PAUSE() (void)316 /* 別說了,都重新開始吧 */
# endif
#endif/* !INIT_PAUSE */
//12.0 判斷是大端序還是小端序,大端序返回true
extern bool sh_isbig(void);
/**
* sh_free - 簡單的釋放內存函數,對free再封裝了一下
**可以避免野指針
**pobj:指向待釋放內存的指針(void*)
**/
extern void sh_free(void** pobj);
/**
* 獲取 當前時間串,並塞入tstr中長度並返回
** 使用舉例
char tstr[64];
puts(gettimes(tstr, LEN(tstr)));
**tstr : 保存最後生成的最後串
**len : tstr數組的長度
** : 返回tstr首地址
**/
extern int sh_times(char tstr[], int len);
#endif/* ! _H_CHEAD */
這裡需要說明的一下是
/** * Linux sys/time.h 中獲取時間函數在Windows上一種移植實現 **tv : 返回結果包含秒數和微秒數 **tz : 包含的時區,在window上這個變量沒有用不返回 ** : 默認返回0 **/ extern int gettimeofday(struct timeval* tv, void* tz); //為了解決 不通用功能 #define localtime_r(t, tm) localtime_s(tm, t)
這兩個函數都是為了在window上模擬 Linux 行為. 首先 gettimeofday 在window 沒有這個功能,獲取當前時間.
對於 安全的localtime 對於 不同平台實現不一樣吧,這裡覺得window設計的好.上面對於實現了大小端代碼也特別巧妙.
2.4 schead.c 具體實現, 這些還是有一點看頭,以後可能只關注Linux,window太羅嗦了
#include <schead.h>
//簡單通用的等待函數
void
sh_pause(void)
{
rewind(stdin);
printf(_STR_PAUSEMSG);
getchar();
}
//12.0 判斷是大端序還是小端序,大端序返回true
bool
sh_isbig(void)
{
static union {
unsigned short _s;
unsigned char _cs[sizeof(unsigned short)];
} __ut = { 1 };
return __ut._cs[0] == 0;
}
/**
* sh_free - 簡單的釋放內存函數,對free再封裝了一下
**可以避免野指針
**@pobj:指向待釋放內存的指針(void*)
**/
void
sh_free(void** pobj)
{
if (pobj == NULL || *pobj == NULL)
return;
free(*pobj);
*pobj = NULL;
}
#if defined(_MSC_VER)
/**
* Linux sys/time.h 中獲取時間函數在Windows上一種移植實現
**tv : 返回結果包含秒數和微秒數
**tz : 包含的時區,在window上這個變量沒有用不返回
** : 默認返回0
**/
int
gettimeofday(struct timeval* tv, void* tz)
{
time_t clock;
struct tm tm;
SYSTEMTIME wtm;
GetLocalTime(&wtm);
tm.tm_year = wtm.wYear - 1900;
tm.tm_mon = wtm.wMonth - 1; //window的計數更好寫
tm.tm_mday = wtm.wDay;
tm.tm_hour = wtm.wHour;
tm.tm_min = wtm.wMinute;
tm.tm_sec = wtm.wSecond;
tm.tm_isdst = -1; //不考慮夏令時
clock = mktime(&tm);
tv->tv_sec = (long)clock; //32位使用,接口已經老了
tv->tv_usec = wtm.wMilliseconds * 1000;
return _RT_OK;
}
#endif
/**
* 獲取 當前時間串,並塞入tstr中C長度並返回
** 使用舉例
char tstr[64];
puts(gettimes(tstr, LEN(tstr)));
**tstr : 保存最後生成的最後串
**len : tstr數組的長度
** : 返回tstr首地址
**/
int
sh_times(char tstr[], int len)
{
struct tm st;
time_t t = time(NULL);
localtime_r(&t, &st);
return (int)strftime(tstr, len, "%F %X", &st);
}
上面函數基本都是線程安全的, 實現也都比較簡單. 大家可以自行練習.
2.5 sclog.h 關於C日志庫的接口設計 多用戶安全跨平台的日志庫
#ifndef _H_SCLOG
#define _H_SCLOG
//-------------------------------------------------------------------------------------------|
// 第一部分 共用的參數宏
//-------------------------------------------------------------------------------------------|
//
//關於日志切分,需要用第三方插件例如crontab , 或者下次我自己寫一個監測程序.
#define _INT_LITTLE (64) //保存時間或IP長度
#define _INT_LOG (1024<<3) //最多8k日志
#define _STR_SCLOG_PATH "log" //日志相對路徑目錄,如果不需要需要配置成""
#define _STR_SCLOG_LOG "sc.log" //普通log日志 DEBUG,INFO,NOTICE,WARNING,FATAL都會輸出
#define _STR_SCLOG_WFLOG "sc.log.wf" //級別比較高的日志輸出 FATAL和WARNING
/**
* fstr : 為標識串 例如 _STR_SCLOG_FATAL, 必須是雙引號括起來的串
**
** 拼接一個 printf 輸出格式串
**/
#define SCLOG_PUTS(fstr) \
"%s][" fstr "][%s:%d:%s][logid:%u][reqip:%s][mod:%s]"
#define _STR_SCLOG_FATAL "FATAL" //錯誤,後端使用
#define _STR_SCLOG_WARNING "WARNING" //警告,前端使用錯誤,用這個
#define _STR_SCLOG_NOTICE "NOTICE" //系統使用,一般標記一條請求完成,使用這個日志
#define _STR_SCLOG_INFO "INFO" //普通的日志打印
#define _STR_SCLOG_TRACE "TRACE"
#define _STR_SCLOG_DEBUG "DEBUG" //測試用的日志打印,在發布版這些日志會被清除掉
/**
* fstr : 只能是 _STR_SCLOG_* 開頭的宏
** fmt : 必須是""括起來的宏.單獨輸出的格式宏
** ... : 對映fmt參數集
**
** 拼接這裡使用的宏,為sl_printf 打造一個模板,這裡存在一個坑,在Window \n表示 CRLF, Unix就是LF
**/
#define SCLOG_PRINTF(fstr, fmt, ...) \
sl_printf(SCLOG_PUTS(fstr) fmt "\n", sl_get_times(), __FILE__, __LINE__, __func__, \
sl_get_logid(), sl_get_reqip(), sl_get_mod(), ##__VA_ARGS__)
/**
* FATAL... 日志打印宏
** fmt : 輸出的格式串,需要""包裹起來
** ... : 後面的參數,服務於fmt
**/
#define SL_FATAL(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_FATAL, fmt, ##__VA_ARGS__)
#define SL_WARNING(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_WARNING, fmt, ##__VA_ARGS__)
#define SL_NOTICE(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_NOTICE, fmt, ##__VA_ARGS__)
#define SL_INFO(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_INFO, fmt, ##__VA_ARGS__)
// 發布狀態下,關閉SL_DEBUG 宏,需要重新編譯,沒有改成運行時的判斷,這個框架主要圍繞單機部分多服務器
#if defined(_DEBUG)
# define SL_TRACE(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_TRACE, fmt, ##__VA_ARGS__)
# define SL_DEBUG(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_DEBUG, fmt, ##__VA_ARGS__)
#else
# define SL_TRACE(fmt, ...) (void)0x123 /* 人生難道就是123*/
# define SL_DEBUG(fmt, ...) (void)0xa91 /* 愛過哎 */
#endif
//-------------------------------------------------------------------------------------------|
// 第二部分 對日志信息體操作的get和set,這裡隱藏了信息體的實現
//-------------------------------------------------------------------------------------------|
/**
* 線程的私有數據初始化
**
** mod : 當前線程名稱
** reqip : 請求的ip
** return : _RT_OK 表示正常,_RF_EM內存分配錯誤
**/
extern int sl_pecific_init(const char* mod, const char* reqip);
/**
* 重新設置線程計時時間
** 正常返回 _RT_OK, _RT_EM表示內存沒有分配
**/
int sl_set_timev(void);
/**
* 獲取日志信息體的唯一的logid
**/
unsigned sl_get_logid(void);
/**
* 獲取日志信息體的請求ip串,返回NULL表示沒有初始化
**/
const char* sl_get_reqip(void);
/**
* 獲取日志信息體的時間串,返回NULL表示沒有初始化
**/
const char* sl_get_times(void);
/**
* 獲取日志信息體的名稱,返回NULL表示沒有初始化
**/
const char* sl_get_mod(void);
//-------------------------------------------------------------------------------------------|
// 第三部分 對日志系統具體的輸出輸入接口部分
//-------------------------------------------------------------------------------------------|
/**
* 日志系統首次使用初始化,找對對映日志文件路徑,創建指定路徑
**返回值具體見 schead.h 中定義的錯誤類型
**/
extern int sl_start(void);
/**
* 這個函數不希望你使用,是一個內部限定死的日志輸出內容.推薦使用相應的宏
**打印相應級別的日志到對映的文件中.
**
** format : 必須是""號括起來的宏,開頭必須是 [FALTAL:%s]後端錯誤
** [WARNING:%s]前端錯誤, [NOTICE:%s]系統使用, [INFO:%s]普通信息,
** [DEBUG:%s] 開發測試用
**
** return : 返回輸出內容長度
**/
int sl_printf(const char* format, ...);
#endif // !_H_SCLOG
關於這個宏
/**
* fstr : 為標識串 例如 _STR_SCLOG_FATAL, 必須是雙引號括起來的串
**
** 拼接一個 printf 輸出格式串
**/
#define SCLOG_PUTS(fstr) \
"%s][" fstr "][%s:%d:%s][logid:%u][reqip:%s][mod:%s]"
主要為了 下面這種宏拼接字符串用的
#define _STR_SCLOG_FATAL "FATAL" //錯誤,後端使用
第一個%s輸出 運行時間量用的.
這個日志庫的使用流程是先初始化,後就可以用了,初始化調用 int sl_pecific_init(const char* mod, const char* reqip); 添加模塊名稱和請求ip.
2.6 關於 sclog.c 的具體實現
#include <sclog.h>
#include <schead.h>
#include <scatom.h>
#include <pthread.h>
#include <stdarg.h>
//-------------------------------------------------------------------------------------------|
// 第二部分 對日志信息體操作的get和set,這裡隱藏了信息體的實現
//-------------------------------------------------------------------------------------------|
//線程私有數據 __lkey, __lonce為了__lkey能夠正常初始化
static pthread_key_t __lkey;
static pthread_once_t __lonce = PTHREAD_ONCE_INIT;
static unsigned __logid = 0; //默認的全局logid,唯一標識
//內部簡單的釋放函數,服務於pthread_key_create 防止線程資源洩露
static void __slinfo_destroy(void* slinfo)
{
//printf("pthread 0x%p:0x%p destroy!\n", pthread_self().p, slinfo);
free(slinfo);
}
static void __gkey(void)
{
pthread_key_create(&__lkey, __slinfo_destroy);
}
struct slinfo {
unsigned logid; //請求的logid,唯一id
char reqip[_INT_LITTLE]; //請求方ip
char times[_INT_LITTLE]; //當前時間串
struct timeval timev; //處理時間,保存值,統一用毫秒
char mod[_INT_LITTLE]; //當前線程的模塊名稱,不能超過_INT_LITTLE - 1
};
/**
* 線程的私有數據初始化
**
** mod : 當前線程名稱
** reqip : 請求的ip
** return : _RT_OK 表示正常,_RF_EM內存分配錯誤
**/
int
sl_pecific_init(const char* mod, const char* reqip)
{
struct slinfo* pl;
//保證 __gkey只被執行一次
pthread_once(&__lonce, __gkey);
if((pl = pthread_getspecific(__lkey)) == NULL){
//重新構建
if ((pl = malloc(sizeof(struct slinfo))) == NULL)
return _RT_EM;
//printf("pthread 0x%p:0x%p create!\n", pthread_self().p,pl);
}
gettimeofday(&pl->timev, NULL);
pl->logid = ATOM_ADD_FETCH(__logid, 1); //原子自增
strcpy(pl->mod, mod); //復制一些數據
strcpy(pl->reqip, reqip);
//設置私有變量
pthread_setspecific(__lkey, pl);
return _RT_OK;
}
/**
* 重新設置線程計時時間
** 正常返回 _RT_OK, _RT_EM表示內存沒有分配
**/
int
sl_set_timev(void)
{
struct slinfo* pl = pthread_getspecific(__lkey);
if (NULL == pl)
return _RT_EM;
gettimeofday(&pl->timev, NULL);
return _RT_OK;
}
/**
* 獲取日志信息體的唯一的logid
**/
unsigned
sl_get_logid(void)
{
struct slinfo* pl = pthread_getspecific(__lkey);
if (NULL == pl) //返回0表示沒有找見
return 0u;
return pl->logid;
}
/**
* 獲取日志信息體的請求ip串,返回NULL表示沒有初始化
**/
const char*
sl_get_reqip(void)
{
struct slinfo* pl = pthread_getspecific(__lkey);
if (NULL == pl) //返回NULL表示沒有找見
return NULL;
return pl->reqip;
}
/**
* 獲取日志信息體的時間串,返回NULL表示沒有初始化
**/
const char*
sl_get_times(void)
{
struct timeval et; //記錄時間
unsigned td;
struct slinfo* pl = pthread_getspecific(__lkey);
if (NULL == pl) //返回NULL表示沒有找見
return NULL;
gettimeofday(&et, NULL);
//同一用微秒記
td = 1000000 * (et.tv_sec - pl->timev.tv_sec) + et.tv_usec - pl->timev.tv_usec;
snprintf(pl->times, LEN(pl->times), "%u", td);
return pl->times;
}
/**
* 獲取日志信息體的名稱,返回NULL表示沒有初始化
**/
const char*
sl_get_mod(void)
{
struct slinfo* pl = pthread_getspecific(__lkey);
if (NULL == pl) //返回NULL表示沒有找見
return NULL;
return pl->mod;
}
//-------------------------------------------------------------------------------------------|
// 第三部分 對日志系統具體的輸出輸入接口部分
//-------------------------------------------------------------------------------------------|
//錯誤重定向宏 具體應用 於 "mkdir -p \"" _STR_SCLOG_PATH "\" >" _STR_TOOUT " 2>" _STR_TOERR
#define _STR_TOOUT "__out__"
#define _STR_TOERR "__err__"
#define _STR_LOGID "__lid__" //保存logid,持久化
static struct { //內部用的私有變量
FILE* log;
FILE* wf;
bool isdir; //標志是否創建了目錄
} __slmain;
/**
* 日志關閉時候執行,這個接口,關閉打開的文件句柄
**/
static void __sl_end(void)
{
FILE* lid;
void* pl;
// 在簡單地方多做安全操作值得,在核心地方用算法優化的才能穩固
if (!__slmain.isdir)
return;
//重置當前系統打開文件結構體
fclose(__slmain.log);
fclose(__slmain.wf);
BZERO(__slmain);
//寫入文件
lid = fopen(_STR_LOGID, "w");
if (NULL != lid) {
fprintf(lid, "%u", __logid);
fclose(lid);
}
//主動釋放私有變量,其實主進程 相當於一個線程是不合理的!還是不同的生存周期的
pl = pthread_getspecific(__lkey);
__slinfo_destroy(pl);
pthread_setspecific(__lkey, NULL);
}
/**
* 日志系統首次使用初始化,找對對映日志文件路徑,創建指定路徑
**返回值具體見 schead.h 中定義的錯誤類型
**/
int
sl_start(void)
{
FILE *lid;
//單例只執行一次
if (!__slmain.isdir) {
__slmain.isdir = true;
//先多級創建目錄,簡易不借助宏實現跨平台,system返回值是很復雜,默認成功!
system("mkdir -p \"" _STR_SCLOG_PATH "\" >" _STR_TOOUT " 2>" _STR_TOERR);
rmdir("-p");
remove(_STR_TOOUT);
remove(_STR_TOERR);
}
if (NULL == __slmain.log) {
__slmain.log = fopen(_STR_SCLOG_PATH "/" _STR_SCLOG_LOG, "a+");
if (NULL == __slmain.log)
CERR_EXIT("__slmain.log fopen %s error!", _STR_SCLOG_LOG);
}
//繼續打開 wf 文件
if (NULL == __slmain.wf) {
__slmain.wf = fopen(_STR_SCLOG_PATH "/" _STR_SCLOG_WFLOG, "a+");
if (NULL == __slmain.wf) {
fclose(__slmain.log); //其實這都沒有必要,圖個心安
CERR_EXIT("__slmain.log fopen %s error!", _STR_SCLOG_WFLOG);
}
}
//讀取文件內容
if ((lid = fopen(_STR_LOGID, "r")) != NULL) { //讀取文件內容,持久化
fscanf(lid, "%u", &__logid);
}
//這裡可以單獨開啟一個線程或進程,處理日志整理但是 這個模塊可以讓運維做,按照規則搞
sl_pecific_init("main thread","0.0.0.0");
//注冊退出操作
atexit(__sl_end);
return _RT_OK;
}
int
sl_printf(const char* format, ...)
{
char tstr[_INT_LITTLE];// [%s] => [2016-01-08 23:59:59]
int len;
va_list ap;
char logs[_INT_LOG]; //這個不是一個好的設計,最新c 中支持 int a[n];
if (!__slmain.isdir) {
CERR("%s fopen %s | %s error!",_STR_SCLOG_PATH, _STR_SCLOG_LOG, _STR_SCLOG_WFLOG);
return _RT_EF;
}
//初始化參數
sh_times(tstr, _INT_LITTLE - 1);
len = snprintf(logs, LEN(logs), "[%s ", tstr);
va_start(ap, format);
vsnprintf(logs + len, LEN(logs) - len, format, ap);
va_end(ap);
// 寫普通文件 log
fputs(logs, __slmain.log); //把鎖機制去掉了,fputs就是線程安全的
// 寫警告文件 wf
if (format[4] == 'F' || format[4] == 'W') { //當為FATAL或WARNING需要些寫入到警告文件中
fputs(logs, __slmain.wf);
}
return _RT_OK;
}
我們對 __sl_end 函數解析一下 主要做有兩部分工作比較特殊,第一部分
//寫入文件
lid = fopen(_STR_LOGID, "w");
if (NULL != lid) {
fprintf(lid, "%u", __logid);
fclose(lid);
}
將 __logid 變量持久化.保存在一個文件中,算作一個唯一標識吧.
第二部分是為了解決 sl_start中使用了線程私有數據,在退出時候釋放, 對於線程私有數據設置為NULL,表示處理釋放的時候跳過釋放的函數操作.
//主動釋放私有變量,其實主進程 相當於一個線程是不合理的!還是不同的生存周期的
pl = pthread_getspecific(__lkey);
__slinfo_destroy(pl);
pthread_setspecific(__lkey, NULL);
還有在sl_start 中有一段創建目錄的代碼
//單例只執行一次
if (!__slmain.isdir) {
__slmain.isdir = true;
//先多級創建目錄,簡易不借助宏實現跨平台,system返回值是很復雜,默認成功!
system("mkdir -p \"" _STR_SCLOG_PATH "\" >" _STR_TOOUT " 2>" _STR_TOERR);
rmdir("-p");
remove(_STR_TOOUT);
remove(_STR_TOERR);
}
也是偷懶的寫法,不同平台創建多層文件接口不一樣,寫起來麻煩,自己用shell 苟合了一個. 啟動的時候用,還湊合著吧.
2.7 最後測試文件 main.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
//簡單測試 pthread線程庫
static void* __run(void* arg)
{
puts("你好!");
return NULL;
}
int main_pthread_test(int argc, char* argv[])
{
pthread_t tid;
//開始跑起來
pthread_create(&tid, NULL, __run, NULL);
//等待結束
pthread_join(tid, NULL);
system("pause");
return 0;
}
// -------------------------下面測試 sclog.h 接口功能
#include <schead.h>
#include <sclog.h>
static void* test_one(void* arg)
{
sl_pecific_init("test_one", "8.8.8.8");
SL_TRACE("test_one log test start!");
for (int i = 0; i < 100; ++i) {
SL_FATAL("pthread test one fatal is at %d, It's %s.",i, "OK");
SL_WARNING("pthread test one warning is at %d, It's %s.", i, "OK");
SL_INFO("pthread test one info is at %d, It's %s.", i, "OK");
SL_DEBUG("pthread test one debug is at %d, It's %s.", i, "OK");
SLEEPMS(1); //等待1s
}
SL_TRACE("test_one log test end!");
return NULL;
}
// 線程二測試函數
static void* test_two(void* arg)
{
//線程分離,自回收
pthread_detach(pthread_self());
sl_pecific_init("test_two", "8.8.8.8");
SL_TRACE("test_two log test start!");
for (int i = 0; i < 3; ++i) {
SL_FATAL("pthread test two fatal is at %d, It's %s.", i, "OK");
SL_WARNING("pthread test two warning is at %d, It's %s.", i, "OK");
SL_INFO("pthread test two info is at %d, It's %s.", i, "OK");
SL_DEBUG("pthread test two debug is at %d, It's %s.", i, "OK");
SLEEPMS(2); //等待1s
}
SL_TRACE("test_two SL_TRACE test end!");
return NULL;
}
int main(int argc, char* argv[])
{
pthread_t tone, ttwo;
//注冊等待函數
INIT_PAUSE();
sl_start();
SL_NOTICE("main log test start!");
pthread_create(&tone, NULL, test_one, NULL);
pthread_create(&ttwo, NULL, test_two, NULL);
pthread_join(tone, NULL);
SL_NOTICE("main log test end!");
return 0;
}
上面第一部分是 window上測試 posix線程框架的跳過,後面是測試當前整個框架的一切正常.
3 查看運行結果
首先編譯

查看緩沖文件 __lid__ 保存logid

後面具體生成日志文件如下

同樣我們看看window上結果如下

結果都相似,後面將著重介紹如何在window上搭建開發環境
4.在window 上搭建 sc_console 開發項目,自己生成模板
首先看目錄結構

外部正式文件結果如下

首先我們看 pthread 配置 前面已經說過了,現在有兩方便要注意
第一方面關於 ptread.h 源碼修改 刪除 一個 關於 time結構體沖突.
後面添加一個庫引用處理代碼

後面在對應生成的執行文件中導入相應的pthread動態庫例如如下

到這裡基礎的就能運行了, 現在是挨個配置 具體操作 這裡有個文件 要求總結如下
help.txt
/*
wz 這裡關於這個系統使用的一些注意事項主要是對於 VS的操作的,
對於GCC還需要單搞,這些代碼都具備跨平台的能力,但是需要配置,需要你熟悉!
了解下面操作的原因,熟悉它,為了項目管理C開發大型項目約束太多了,都需要從頭來!
1.設置 VS的 項目右鍵屬性 -> VC++ 目錄
1.1. 添加 包含目錄
$(ProjectDir)main
$(ProjectDir)module
$(ProjectDir)module/pthread
$(ProjectDir)module/pthread/inlcude
$(ProjectDir)module/schead
$(ProjectDir)module/schead/inlcude
2. lib 庫添加
2.1 添加 pthread 模塊lib 引用, 看 引用目錄
$(ProjectDir)/pthread/lib/x86
2.2 對於 x64 那就添加為
$(ProjectDir)/pthread/lib/x64
3. dll 庫的添加
3.1 添加 dll 目前這個需要手工操作,目前不智能,VS 對C++支持的好緩慢, M$確實很坑
找到相應的 生成的exe目錄下添加 對應的 dll,
x86 => $(ProjectDir)/pthread/dll/x86
x64 => $(ProjectDir)/pthread/dll/x64
4. 添加部分宏 C/C++ -> 預處理器 -> 預處理器定義
_CRT_SECURE_NO_WARNINGS
*/
按照上面配置 具體 截圖看下面



按照這個操作將

四種組合都配置一遍,基本都ok了這個框架就搭建好了.
到這裡 擴展一下再 Release 發布模塊下怎麼調試, 請按照下面做
設置在Release模式下調試的方法:
1.工程項目上右鍵 -> 屬性
2.c++ -> 常規 -〉調試信息格式 選 程序數據庫(/Zi)或(/ZI), 注意:如果是庫的話,只能(Zi)
3.c++ -> 優化 -〉優化 選 禁止(/Od)
4.連接器 -〉調試 -〉生成調試信息 選 是 (/DEBUG)
5.在優化裡 關閉全程序優化

到這裡基本就結束了,歡迎喜歡C的同學試試.
後記
有錯誤是難免的,以後准備逐步放棄跨平台操作, 簡單的window來,復雜的Linux來. 對於跨平台冗余代碼比較多,而且強扭的瓜不甜.
而且別說跨平台了,跨編譯器都很惡心. 而且像雲風那種老鳥都不寫跨平台代碼,自己這種菜鳥更不敢寫了. 歡迎大家試試. 上面只是這個sc_console.
中最基礎的後面會假如 配置讀取,json引擎,csv引擎代碼,還有一些特定平台的代碼功能等等. 有錯誤會立馬改正.
有時覺得寫代碼還是很有意思的.