main 函式解析(二)—— Linux-0.11 學習筆記(六)
main
函式解析(二)——Linux-0.11 學習筆記(六)
4.6 blk_dev_init
函式
void blk_dev_init(void)
{
int i;
for (i=0 ; i<NR_REQUEST ; i++) {
request[i].dev = -1; //表示空閒
request[i].next = NULL;
}
}
這裡的request
是一個全域性的陣列。
/*
* The request-struct contains all necessary data
* to load a nr of sectors into memory
*/
struct request request[NR_REQUEST]; //請求項陣列
#define NR_REQUEST 32
...
struct request {
int dev; //裝置號,若為-1,則表示空閒
int cmd; // 命令 READ or WRITE
int errors; //操作時產生的錯誤次數
unsigned long sector; // 起始扇區
unsigned long nr_sectors; // 讀/寫扇區數
char * buffer; //資料緩衝區
struct task_struct * waiting; // 任務等待操作執行完成的地方
struct buffer_head * bh; // 緩衝區頭指標
struct request * next; //指向下一個struct request,構成單向連結串列
};
以上的程式碼要想完全理解,就牽扯到塊裝置驅動程式了。
每個塊裝置的當前請求指標與請求項陣列中該裝置的請求項鍊表共同構成了該裝置的請求佇列。項與項之間利用欄位next指標形成連結串列。因此塊裝置項和相關的請求佇列形成如圖所示結構。請求項採用陣列加連結串列結構的主要原因是為了滿足兩個目的:一是利用請求項的陣列結構在搜尋空閒請求塊時可以進行迴圈操作,搜尋訪問時間複雜度為常數,因此程式可以編制得很簡潔;二是為滿足電梯演算法 (Elevator Algorithm) 插入請求項操作,因此也需要採用連結串列結構。圖中畫出了硬碟裝置當前具有4個請求項,軟盤裝置具有1個請求項,而虛擬盤裝置目前暫時沒有讀寫請求項。
對於一個當前空閒的塊裝置,當 ll_rw_block()函式為其建立第一個請求項時,會讓該裝置的當前請求項指標current_request直接指向剛建立的請求項,並且立刻呼叫對應裝置的請求項操作函式開始執行塊裝置讀寫操作。當一個塊裝置已經有幾個請求項組成的連結串列存在,ll_rw_block()就會利用電梯演算法,根據磁頭移動距離最小原則,把新建的請求項插入到連結串列適當的位置處。
以上內容摘自趙炯博士的《Linux核心完全剖析》。
雖然看完不甚理解,但至少明白blk_dev_init
函式了。其實很簡單,就是把陣列的每一項的裝置號設為 -1
,表示空閒,然後把 next 設為 NULL(因為空閒,所以沒有被插入連結串列)。
4.7 chr_dev_init
函式
此函式實現為空。
void chr_dev_init(void)
{
}
4.8 tty_init
函式
void tty_init(void)
{
rs_init(); // 初始化序列中斷程式和序列介面1、2
con_init(); // 初始化控制檯終端
}
4.8.1 rs_init
函式
void rs_init(void)
{
set_intr_gate(0x24,rs1_interrupt); // 設定序列口1的中斷門向量
set_intr_gate(0x23,rs2_interrupt); // 設定序列口2的中斷門向量
init(tty_table[1].read_q.data); // 初始化序列口1,引數是埠基地址
init(tty_table[2].read_q.data); // 初始化序列口2,引數是埠基地址
outb(inb_p(0x21)&0xE7,0x21); // 允許8259A主片響應IRQ3、IRQ4中斷請求, 0x21 對應 8259A 主片的中斷遮蔽暫存器
}
4.8.1.1 設定中斷門
#define set_intr_gate(n,addr) \
_set_gate(&idt[n],14,0,addr)
set_intr_gate(n,addr)
的巨集展開是_set_gate(&idt[n],14,0,addr)
。
上圖是中斷門描述符的格式,根據 [11:8] = 14,可以知道程式碼中的14表示中斷門。
巨集_set_gate(gate_addr,type,dpl,addr)
用於設定門描述符。具體的分析參見 main函式解析(一)——Linux-0.11 學習筆記(五)
這個巨集根據引數中的
- 門描述符型別
type
- 描述符特權級
dpl
- 中斷或異常處理過程的偏移地址地址
addr
設定位於地址 gate_addr
處的門描述符。
所以, set_intr_gate(n,addr)
表示在元素idt[n]
(idt[]
是中斷描述符表,其實是陣列,一共有 256 個表項,一個表項佔8位元組。)位置安裝一箇中斷處理過程的偏移地址地址為 addr
的、特權級為0的中斷門描述符。
Linux-0.11 系統
- 把主片的中斷號設定為
0x20~0x27
; - 把從片的中斷號設定為
0x28~0x2f
。
根據上圖可以知道,序列口1的中斷號是0x24,序列口2的中斷號是0x23;
順便提一下:使用中斷門(interrupt gate) 來構造中斷呼叫機制的,當 processor 進入 interrupt handler 執行前, 會將 eflags 的值壓入棧中儲存並且會清除 eflags.IF 標誌位,這意味著進入中斷後不再響應其他的可遮蔽中斷。
4.8.1.2 串列埠的初始化
static void init(int port) // port 是埠基地址
{
...
}
串列埠初始化,說白了就是配置一些暫存器,使串列埠可以收發資料。
說到這個串列埠,內容還挺多,不瞭解的一定要參考我的博文 PC 機 UART(NS8250)詳解
init(tty_table[1].read_q.data);
傳入的引數是tty_table[1].read_q.data
,如果沒有猜錯的話,tty_table[1].read_q.data
一定是埠的基地址。為了驗證猜測,我們找找和tty_table
有關的定義。
果然,在kernel\chr_drv\tty_io.c
中看到了以下程式碼:
struct tty_struct tty_table[] = {
{
...
},{
...
{0x3f8,0,0,0,""}, /* rs 1 */
{0x3f8,0,0,0,""},
{0,0,0,0,""}
},{
...
{0x2f8,0,0,0,""}, /* rs 2 */
{0x2f8,0,0,0,""},
{0,0,0,0,""}
}
};
0x3f8
和0x2f8
正是串列埠1和2的埠基地址。
static void init(int port) // port 是埠基地址
{
outb_p(0x80,port+3); /* set DLAB of line control reg */
outb_p(0x30,port); /* LS of divisor (48 -> 2400 bps */
outb_p(0x00,port+1); /* MS of divisor */
outb_p(0x03,port+3); /* reset DLAB */
outb_p(0x0b,port+4); /* set DTR,RTS, OUT_2 */
outb_p(0x0d,port+1); /* enable all intrs but writes */
(void)inb(port); /* read data port to reset things (?) */
}
目前遇到的讀寫埠的巨集有4個:
outb_p(value,port)
—— 把value
寫入埠port
.
inb_p(port)
——讀取埠port
的值。
outb(value,port)
—— 把value
寫入埠port
.
inb(port)
——讀取埠port
的值。
前2個帶延時,後2個不帶延時。
這4個巨集,具體的定義和分析可以參考我的博文 main函式解析(一)——Linux-0.11 學習筆記(五)
第3行:outb_p(0x80,port+3);
把0x80寫入埠(0x3f8+3=0x3fb,即 LCR 暫存器),也就是使 DLAB=1。
第4行:outb_p(0x30,port);
寫波特率因子的低位元組為0x30
第5行:outb_p(0x00,port+1);
寫波特率因子的高位元組為0x00
所以,波特率因子為 0x0030=48。
波特率和因子之間的關係是:
公式變形一下:
所以
第6行:outb_p(0x03,port+3);
無奇偶校驗位、8 位資料位和 1 位停止位,同時復位 DLAB;
第7行:outb_p(0x0b,port+4);
若要讓 UART 的中斷請求訊號能夠送到 8259A 中斷控制器,就需要把MODEM 控制暫存器 MCR 的位3(OUT2) 置位。因為在PC 機中,該位控制著 INTRPT 引腳到 8259A 的電路。MCR的位 1 和位 0 分別用於控制 MODEM , 當這兩位置位時,UART的資料終端就緒引腳(DTR)和請求傳送引腳(RTS)輸出有效。
第8行:outb_p(0x0d,port+1);
寫IER(Interrupt enable register,中斷允許暫存器 )
0x0d = 1101b
[3] = 1 允許modem狀態中斷;
[2] = 1 允許接收器線路狀態中斷;
[1] = 0 禁止傳送保持暫存器空中斷;
[0] = 1 允許接收到資料中斷;
第9行:(void)inb(port);
讀接收快取暫存器 ,以進行復位??其實這句話我也不清楚是否必要,先在這裡留個疑問。
4.8.2 con_init()
函式
con_init()
函式(在檔案linux/kernel/console.c
中)首先根據 setup.s
程式取得的系統硬體引數初始化幾個本函式專用的靜態全域性變數。然後根據顯示卡模式(單色還是彩色)和顯示卡型別(EGA/VGA、CGA、MDA 等) 分別設定顯示記憶體起始位置以及顯示索引暫存器和顯示數值暫存器的埠號。最後設定鍵盤中斷陷阱描述符並開啟對鍵盤中斷的遮蔽位。
顯示卡的歷史
磨刀不誤砍柴工,在理解這個函式之前,不妨先了解一下顯示卡的歷史。
顯示卡的前身
MDA
最早的顯示卡稱為顯示介面卡,在“黑底白字”的DOS年代,對顯示的要求是極低的。最早的顯示型別是 MDA(Monochrome Display Adapter),只能區別出黑白兩色。早期的8080、8088,一直到80286都是使用這種型別的顯示介面卡。它的功能極為簡單,一般整合 16KB 視訊記憶體,是不為人關注的電腦配件。
CGA
到了286時,PC上出現了一些和圖形相關的軟體,因此出現了一種四色介面卡,只能識別三原色和黑白。由於這是第一種彩色的顯示介面卡,所以稱為 CGA(Color Graphics Adaptor,彩色圖形介面卡)。CGA 時代對顯示卡的要求已經大幅度提高,但是當時的製作工藝仍然遠遠高於顯示卡晶片的需求,因此 CGA 顯示介面卡依舊被整合在主機板上,以一塊單晶片的方式來實現,所以“顯示卡”尚未誕生。
EGA
CGA 的解析度太低,於是又有了 EGA(Enhanced Graphics Adapter,增強圖形介面卡)。在顯示效能方面(顏色和解析度),EGA 介於 CGA 和 VGA 之間,可以在高達640x350的分辯率下達到16色。
顯示卡的誕生與換代
以上MDA、CGA、EGA 三種標準都是以 TTL 數字訊號輸出,而之後的 VGA 標準採用模擬訊號輸出,因而其彩色顯示能力大大加強,原則上可以顯示無窮多的顏色。VGA 最初代表解析度,在個人電腦的啟蒙時代,能夠輸出 VGA(640×480)這樣的解析度並不是一件易事,VGA 標準的出現對顯示輸出裝置首次提出了較高的要求,於是催生了 VGA Card,顯示卡正式誕生!
● 第一代顯示卡:VGA Card,支援256色顯示,1988年
● 第二代顯示卡:Graphics Card,支援Windows圖形加速,1991年
● 第三代顯示卡:Video Card,支援視訊加速,1994年
● 第四代顯示卡:3D Accelerator Card,支援3D加速,1994年
● 第五代顯示卡:GPU圖形處理器,支援硬體 T&L(Transform and Lighting,多邊形轉換與光源處理),1999年
● 現代和未來顯示卡:GPGPU(General-purpose computing on graphics processing units,簡稱 GPGPU 或 GP²U)通用計算圖形處理器,支援幾何著色、物理加速、高清解碼、科學計算……
和顯示有關的靜態全域性變數的初始化
void con_init(void)
{
register unsigned char a;
// 定義暫存器變數 a,該變數將被儲存在一個暫存器中,以便於高效訪問和操作。若想指定存放的暫存器(如 eax) ,則可寫成 register unsigned char a asm("ax");
char *display_desc = "????";
char *display_ptr;
video_num_columns = ORIG_VIDEO_COLS;
video_size_row = video_num_columns * 2;
video_num_lines = ORIG_VIDEO_LINES; //每屏25行
video_page = ORIG_VIDEO_PAGE; // 我覺得有點問題
video_erase_char = 0x0720; // 擦除字元(0x20是空格,0x07是屬性)
...
在檔案linux/kernel/console.c
中有巨集定義:
#define ORIG_X (*(unsigned char *)0x90000)
#define ORIG_Y (*(unsigned char *)0x90001)
#define ORIG_VIDEO_PAGE (*(unsigned short *)0x90004)
#define ORIG_VIDEO_MODE ((*(unsigned short *)0x90006) & 0xff)
#define ORIG_VIDEO_COLS (((*(unsigned short *)0x90006) & 0xff00) >> 8)
#define ORIG_VIDEO_LINES (25)
#define ORIG_VIDEO_EGA_AX (*(unsigned short *)0x90008)
#define ORIG_VIDEO_EGA_BX (*(unsigned short *)0x9000a)
#define ORIG_VIDEO_EGA_CX (*(unsigned short *)0x9000c)
右邊的地址,其實對應 setup.s
中取得的系統硬體引數的存放位置。如果忘了,可以參考我的博文 setup.s 分析—— Linux-0.11 學習筆記(二)
第9行:video_num_columns = (((*(unsigned short *)0x90006) & 0xff00) >> 8);
對比彙編程式碼,也就是把AH的值(字元列數)賦給全域性變數video_num_columns
;
`setup.s
中相關程式碼如下
! 獲取顯示卡當前的顯示模式
! 呼叫 BIOS 中斷 0x10,功能號 ah = 0x0f
! 返回: ah=字元列數; al=顯示模式;bh=當前顯示頁。
! ds = 0x9000
! 0x90004(l個字)存放當前頁;0x90006(1位元組)存放顯示模式;0x90007(1位元組)存放字元列數。
mov ah,#0x0f
int 0x10
mov [4],bx ! bh = 當前顯示頁
mov [6],ax ! al = 顯示模式, ah = 字元列數(視窗寬度)
第10行:video_size_row = video_num_columns * 2;
算出每行字元需使用的位元組數。
第12行:video_page = (*(unsigned short *)0x90004);
注意,video_page
是static unsigned char
型別。
根據彙編程式碼第8行,應該是0x90005處的一個位元組存放當前活動頁碼,所以我認為第12行應該改為:
video_page = (*(unsigned char *)0x90005);
在所有原始碼檔案中搜了一波發現video_page
這個變數沒有被用到,好吧,暫且不管,接著往下看。
顯示模式
#define ORIG_VIDEO_MODE ((*(unsigned short *)0x90006) & 0xff)
根據上面彙編程式碼的第9行,ORIG_VIDEO_MODE
中的值是顯示模式。
其取值對應的含義如下表。
AL | Type | Format | Cell | Colors | Adapter | Addr | Monitor |
---|---|---|---|---|---|---|---|
0 | text | 40x25 | 8x8* | 16/8 (shades) | CGA,EGA | b800 | Composite |
1 | text | 40x25 | 8x8* | 16/8 | CGA,EGA | b800 | Comp,RGB,Enh |
2 | text | 80x25 | 8x8* | 16/8 (shades) | CGA,EGA | b800 | Composite |
3 | text | 80x25 | 8x8* | 16/8 | CGA,EGA | b800 | Comp,RGB,Enh |
4 | graphic | 320x200 | 8x8 | 4 | CGA,EGA | b800 | Comp,RGB,Enh |
5 | graphic | 320x200 | 8x8 | 4 (shades) | CGA,EGA | b800 | Composite |
6 | graphic | 640x200 | 8x8 | 2 | CGA,EGA | b800 | Comp,RGB,Enh |
7 | text | 80x25 | 9x14* | 3 (b/w/bold) | MDA,EGA | b000 | TTL Mono |
8,9,0aH | PCjr modes | ||||||
0bH,0cH | (reserved; internal to EGA BIOS) | ||||||
0dH | graphic | 320x200 | 8x8 | 16 | EGA,VGA | a000 | Enh,Anlg |
0eH | graphic | 640x200 | 8x8 | 16 | EGA,VGA | a000 | Enh,Anlg |
0fH | graphic | 640x350 | 8x14 | 3 (b/w/bold) | EGA,VGA | a000 | Enh,Anlg,Mono |
10H | graphic | 640x350 | 8x14 | 4 or 16 | EGA,VGA | a000 | Enh,Anlg |
11H | graphic | 640x480 | 8x16 | 2 | VGA | a000 | Anlg |
12H | graphic | 640x480 | 8x16 | 16 | VGA | a000 | Anlg |
13H | graphic | 640x480 | 8x16 | 256 | VGA | a000 | Anlg |
Notes: With EGA, VGA, and PCjr you can add 80H to AL to initialize a video mode without clearing the screen.
*The character cell size for modes 0-3 and 7 varies, depending on the hardware. On modes 0-3: CGA=8x8, EGA=8x14, and VGA=9x16. For mode 7, MDPA and EGA=9x14, VGA=9x16, LCD=8x8.
if (ORIG_VIDEO_MODE == 7) // 等於7說明是單色
{
video_mem_start = 0xb0000; // 設定記憶體起始地址
video_port_reg = 0x3b4; // 設定索引暫存器埠
video_port_val = 0x3b5; // 設定資料暫存器埠
// 注意,這裡使用了 BL 在呼叫中斷 int 0x10 前後是否被改變的方法來判斷卡的型別。
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
{ // BL之前設定的值是0x10,呼叫中斷後發生改變,則說明是EGA
//#define VIDEO_TYPE_EGAM 0x20 /* EGA/VGA in Monochrome Mode*/
video_type = VIDEO_TYPE_EGAM; // 設定顯示型別
video_mem_end = 0xb8000; // 設定顯示記憶體末端地址
display_desc = "EGAm"; // 設定顯示描述字串
}
else // 沒有改變說明是MDA
{
video_type = VIDEO_TYPE_MDA;
video_mem_end = 0xb2000;
display_desc = "*MDA";
}
}
else // 說明是彩色
{
video_mem_start = 0xb8000; // 設定記憶體起始地址
video_port_reg = 0x3d4; // 設定索引暫存器埠
video_port_val = 0x3d5; // 設定資料暫存器埠
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
{
video_type = VIDEO_TYPE_EGAC; // 說明是EGA或者VGA顯示卡
video_mem_end = 0xbc000; // 設定顯示記憶體末端地址
display_desc = "EGAc"; // EGA彩色
}
else
{
video_type = VIDEO_TYPE_CGA; // 設定顯示型別為CGA
video_mem_end = 0xba000;
display_desc = "*CGA";
}
}
setup.s
中相關程式碼是:
! 檢查顯示方式(EGA/VGA)並獲取引數。
! 呼叫 BIOS 中斷 0x10,功能號: ah = 0xl2,子功能號: bl = 0xl0
! 返回:bh=顯示狀態。 0x00-彩色模式,I/O 埠=0x3dX
! 0x01-單色模式,I/O 埠=0x3bX
! bl = 安裝的顯示記憶體。0x00 - 64k
! 0x01 - 128k
! 0x02 - 192k
! 0x03 - 256k
! cx = 顯示卡特性引數。
!
mov ah,#0x12 ! 功能號
mov bl,#0x10 ! 子功能號
int 0x10
mov [8],ax ! 我也不知道這個是什麼(╯︵╰)
mov [10],bx ! bh=顯示狀態(單色模式/彩色模式),bl=已安裝的視訊記憶體大小
mov [12],cx ! ch=特性聯結器位元位資訊,cl=視訊開關設定資訊
注意,C程式碼使用了 BL 在呼叫中斷 int 0x10 前後是否被改變的方法來判斷卡的型別。BL在呼叫中斷之前被賦值為 0x10 ,在呼叫中斷後,其值可能不變(CGA 或 MDA),也可能變為0~3中的一個(EGA 或 VGA)。
在螢幕右上角顯示顯示卡型別
下面的程式碼作用是在螢幕右上角顯示描述字串。採用的方法是直接將字串寫到顯示記憶體的相應位置處。首先將顯示指標display_ptr
指到螢幕第1行最右端起第4個字元處(每個字元需 2 個位元組,因此減 8 ) ,然後迴圈複製字串的字元。
display_ptr = ((char *)video_mem_start) + video_size_row - 8;
while (*display_desc) // 迴圈能停止是因為字串末尾的'\0'
{
*display_ptr++ = *display_desc++; // 複製字元
display_ptr++; // 跳過屬性位元組
}
在我的實驗環境除錯,截圖如下:
和滾屏有關的變數
初始化用於滾屏的變數(主要用於EGA/VGA):
/* Initialize the variables used for scrolling (mostly EGA/VGA) */
origin = video_mem_start; // 滾屏起始視訊記憶體地址
scr_end = video_mem_start + video_num_lines * video_size_row;// 結束地址
top = 0; // 最頂端行號
bottom = video_num_lines; // 最底端行號
#define ORIG_X (*(unsigned char *)0x90000) // 列號
#define ORIG_Y (*(unsigned char *)0x90001) // 行號
...
// 初始化當前游標所在位置(x,y)和游標對應的視訊記憶體位置 pos
gotoxy(ORIG_X,ORIG_Y);
第1~2行對應的彙編程式碼(setup.s)是
mov ax,#INITSEG !INITSEG = 0x9000
mov ds,ax ! ds = 0x9000
mov ah,#0x03 ! 功能號=3,獲取游標的位置
xor bh,bh ! bh = 頁號 = 0(輸入)
int 0x10 ! 輸出: DH=行號,DL=列號
mov [0],dx ! 儲存游標的行號和列號到 0x90000,共佔2位元組.
gotoxy
函式(kernel\chr_drv\console.c)的實現如下:
/* NOTE! gotoxy thinks x==video_num_columns is ok */
static inline void gotoxy(unsigned int new_x,unsigned int new_y)
{
if (new_x > video_num_columns || new_y >= video_num_lines)
return;
x=new_x; // 記錄下當前游標所在的列
y=new_y; // 記錄下當前游標所在的行
pos=origin + y*video_size_row + (x<<1);// 記錄當前游標所在位置對應的視訊記憶體地址。我除錯時,video_size_row = 160
}
第4行:判斷引數是否合法。在我的實驗環境中,video_num_lines = 25
,即new_y
的取值是[0,24];video_num_columns = 80
,即new_x
的取值是[0,80],為什麼可以等於80呢?目前還不知道,我想作者這樣寫肯定有他的道理,後面多留個心。
和顯示有關的程式碼就暫時結束了。後面是關於鍵盤的。
允許鍵盤工作
set_trap_gate(0x21,&keyboard_interrupt); //安裝陷阱門,鍵盤的中斷號是0x21, 對應8259A主片的 IRQ1
// 已經反覆強調, Linux-0.11 系統把主片的中斷號設定為 `0x20~0x27`;
// 把從片的中斷號設定為 `0x28~0x2f`。
outb_p(inb_p(0x21)&0xfd,0x21); // 允許鍵盤中斷
a=inb_p(0x61); // 讀取鍵盤埠 0x61(8255A埠PB)到 a(之前定義的暫存器變數)
outb_p(a|0x80,0x61); // b7置位,禁止鍵盤工作
outb(a,0x61); // 再允許鍵盤工作,用以復位鍵盤
第4行:0x21是 8259A 主片命令字OCW1的埠地址,(注意:不是第1行的那個0x21)用於對其中斷遮蔽暫存器 IMR 進行讀/寫操作。
寫到這裡,儘管篇幅較長,可是才分析了 2 個函式。
blk_dev_init();
chr_dev_init(); //實現為空
tty_init();
數了數,距main
函式結束,還有10多個函式呢……好了,今天就到這裡,明日繼續精進。
參考資料
[0]《Linux核心完全剖析》(趙炯,機械工業出版社,2006)
[1] https://blog.csdn.net/mao0514/article/details/24730097
相關文章
- main函式解析(一)——Linux-0.11 學習筆記(五)AI函式Linux筆記
- python學習筆記(六)——函式Python筆記函式
- setup.s 分析—— Linux-0.11 學習筆記(二)Linux筆記
- C++學習筆記(二)——函式C++筆記函式
- JavaScript學習筆記(二)——函式和陣列JavaScript筆記函式陣列
- async函式學習筆記。函式筆記
- 生成函式 學習筆記函式筆記
- 函式學習六函式
- Python中的main函式解析PythonAI函式
- bootsect.s 分析—— Linux-0.11 學習筆記(一)bootLinux筆記
- iOS學習筆記42 Swift(二)函式和閉包iOS筆記Swift函式
- Golang學習筆記-1.6 函式Golang筆記函式
- JavaScript學習筆記 - 原生函式JavaScript筆記函式
- MYSQL學習筆記14: 函式MySql筆記函式
- TS學習筆記(四):函式筆記函式
- Oracle學習筆記(6)——函式Oracle筆記函式
- PHP 第六週函式學習記錄PHP函式
- 深度學習——loss函式的學習筆記深度學習函式筆記
- kernel_mktime() 詳解 —— Linux-0.11 學習筆記(四)Linux筆記
- 二項式反演學習筆記筆記
- MYSQL學習筆記7: 聚合函式MySql筆記函式
- OpenCV學習筆記(4)——mixChannels函式OpenCV筆記函式
- OpenCV學習筆記(5)——normalize函式OpenCV筆記ORM函式
- Flutter學習筆記(4)--Dart函式Flutter筆記Dart函式
- js純函式學習筆記(一)JS函式筆記
- c語言學習筆記===函式C語言筆記函式
- 《C++ Primer》學習筆記(六):C++模組設計——函式C++筆記函式
- 函式學習二函式
- 我的 golang 學習筆記系列二:golang的函式運用Golang筆記函式
- (C++通訊架構學習筆記):日誌列印、優化main函式呼叫順序C++架構筆記優化AI函式
- MYSQL學習筆記15: 數值函式MySql筆記函式
- PHP 第八週函式學習筆記PHP函式筆記
- 學習筆記:javascript中的Generator函式筆記JavaScript函式
- JavaScript學習筆記(七)—— 再說函式JavaScript筆記函式
- 工作學習筆記(三)to_char函式筆記函式
- pandas之常用基本函式學習筆記函式筆記
- 莫比烏斯函式 - 學習筆記函式筆記
- PHP 第二週函式學習記錄PHP函式