從小白到進階:解鎖linux與c語言高級編程知識點嵌入式開發的任督二脈(2)

【硬核揭秘】Linux與C高級編程:從入門到精通,你的全棧之路!

第三部分:Shell腳本編程——自動化你的Linux世界,讓效率飛起來!

嘿,各位C語言的“卷王”們!

在Linux的世界里,命令行是你的雙手,讓你能夠直接與系統交互。但如果你的工作總是重復著“復制粘貼”、“修改配置”、“編譯部署”這些繁瑣的步驟,你有沒有想過,能不能讓電腦自己來完成這些?

答案是:能! 這就是我們今天要深入學習的——Shell腳本編程

Shell腳本,就像給你的Linux系統施加了“自動化魔法”。它允許你將一系列命令組合成一個可執行的文件,然后一鍵運行,讓那些重復、枯燥的任務瞬間變得高效、精準!對于嵌入式開發者來說,掌握Shell腳本,就如同擁有了一把“效率神器”,無論是自動化構建系統、批量處理數據,還是部署測試環境,都能讓你事半功倍!

本篇是“Linux與C高級編程”系列的第三部分,我們將帶你:

  • 揭秘Shell腳本: 它的本質是什么?為什么它如此重要?

  • 變量的藝術: 如何在腳本中存儲和操作數據?

  • 流程控制的魔法: 讓你的腳本能夠“思考”和“重復”!

  • 函數的奧秘: 編寫模塊化、可復用的腳本代碼。

最重要的是,我們還會用一個超級硬核的C語言模擬器,帶你一探Shell腳本在底層是如何被解析、如何管理變量、如何執行控制流的!讓你不僅會寫腳本,更懂腳本!

準備好了嗎?咱們這就開始,讓你的Linux效率,徹底“飛”起來!

3.1 Shell腳本編程:自動化你的Linux世界

3.1.1 什么是Shell腳本?

簡單來說,Shell腳本就是包含一系列Shell命令的文本文件。這些命令按照從上到下的順序執行,就好像你在終端中一行一行地輸入它們一樣。

  • Shell: 是一個命令行解釋器,它接收用戶輸入的命令并將其傳遞給操作系統內核執行。常見的Shell有Bash (Bourne-Again SHell)、Zsh、Ksh等。在Linux中,Bash是最常用的默認Shell。

  • 腳本: 意味著它是一系列指令的集合,可以被解釋器自動執行。

3.1.2 為什么要使用Shell腳本?
  • 自動化重復任務: 這是最主要的原因!例如,每天備份文件、定時清理日志、批量處理圖片等。

  • 簡化復雜操作: 將一系列復雜的命令行操作封裝成一個簡單的腳本,方便執行和分享。

  • 系統管理與維護: 自動化服務器部署、軟件安裝、系統監控、故障排查等。

  • 交叉編譯與部署: 在嵌入式開發中,經常需要編寫腳本來自動化交叉編譯過程、打包固件、遠程部署到目標板等。

  • 提高效率與減少錯誤: 自動化執行比手動操作更快、更準確,大大減少人為錯誤。

3.1.3 Shell腳本的基本結構

一個最簡單的Shell腳本通常包含以下部分:

  1. Shebang (Hashbang) 行:#!/bin/bash

    • 這是腳本的第一行,必須以#!開頭。

    • 它告訴操作系統應該使用哪個解釋器來執行這個腳本。#!/bin/bash表示使用/bin/bash這個程序來解釋執行后續的命令。

    • 重要性: 如果沒有這一行,系統可能會嘗試使用默認的Shell來執行,可能導致兼容性問題。

  2. 注釋:#

    • #開頭的行是注釋,Shell會忽略它們。

    • 用于解釋代碼的功能、邏輯,提高腳本的可讀性。

  3. Shell命令:

    • 腳本的主體,一行一個命令,或者多個命令用;分隔。

示例:第一個Shell腳本 hello.sh

#!/bin/bash
# 這是一個簡單的Shell腳本,用于打印“Hello, Shell Script!”echo "Hello, Shell Script!"
echo "當前日期和時間是: $(date)"

執行腳本的兩種方式:

  1. 作為可執行文件運行(推薦):

    • 首先,給腳本添加執行權限:chmod +x hello.sh

    • 然后,直接運行:./hello.sh ( ./ 表示當前目錄)

  2. 通過解釋器執行:

    • bash hello.sh (明確指定使用bash解釋器)

    • 這種方式不需要給腳本添加執行權限。

思維導圖:Shell腳本基礎

graph TDA[Shell腳本基礎] --> B[什么是Shell腳本?]B --> B1[包含Shell命令的文本文件]B --> B2[由Shell解釋器執行]A --> C[為什么要用Shell腳本?]C --> C1[自動化重復任務]C --> C2[簡化復雜操作]C --> C3[系統管理與維護]C --> C4[嵌入式開發自動化]C --> C5[提高效率, 減少錯誤]A --> D[Shell腳本基本結構]D --> D1[Shebang: #!/bin/bash]D --> D2[注釋: #]D --> D3[Shell命令]A --> E[如何執行腳本?]E --> E1[添加執行權限: chmod +x script.sh]E --> E2[直接運行: ./script.sh]E --> E3[通過解釋器運行: bash script.sh]

3.2 變量:腳本的“記憶”和“計算器”

變量是Shell腳本中存儲數據的“容器”。它們允許你存儲字符串、數字等信息,并在腳本中進行操作。

3.2.1 定義和使用變量
  • 定義: 變量名=值

    • 注意: 等號兩邊不能有空格!

    • 變量名通常約定為大寫字母,但不是強制要求。

  • 使用: $變量名${變量名}

    • 推薦使用${變量名},尤其是在變量名與周圍字符容易混淆時。

示例:定義和使用變量

#!/bin/bash# 定義字符串變量
NAME="張三"
GREETING="你好"# 定義數字變量
AGE=30
SCORE=95echo "$GREETING, $NAME!"
echo "你的年齡是: $AGE"
echo "你的分數是: ${SCORE}分" # 使用{}避免與后面的“分”混淆# 變量的重新賦值
NAME="李四"
echo "現在是: $NAME"# 變量的刪除
unset AGE
echo "刪除AGE后: $AGE" # AGE變量將為空
3.2.2 特殊變量:Shell自帶的“情報員”

Shell提供了一些特殊的內置變量,它們存儲了腳本運行時的重要信息。

變量名

含義

示例

$0

腳本本身的名稱

echo "腳本名稱: $0"

$n

傳遞給腳本的第n個參數 (n=1, 2, ...)

echo "第一個參數: $1"

$#

傳遞給腳本的參數個數

echo "參數個數: $#"

$*

傳遞給腳本的所有參數,作為一個字符串

echo "所有參數: $*"

$@

傳遞給腳本的所有參數,每個參數是獨立的字符串

for arg in "$@"; do echo $arg; done

$?

上一個命令的退出狀態 (0表示成功,非0表示失敗)

ls no_such_file; echo "退出狀態: $?"

$$

當前Shell進程的PID

echo "當前進程PID: $$"

$!

上一個后臺運行命令的PID

sleep 5 & echo "后臺進程PID: $!"

$-

當前Shell的選項

echo "Shell選項: $-"

$_

上一個命令的最后一個參數

ls /tmp; echo "最后一個參數: $_"

示例:特殊變量的使用

#!/bin/bash
# 文件名: special_vars.shecho "--- 特殊變量演示 ---"
echo "腳本名稱: $0"
echo "腳本的PID: $$"echo "參數個數: $#"
echo "所有參數 (\$*): $*"
echo "所有參數 (\$@): $@"# 遍歷所有參數 (推薦使用"$@",因為它能正確處理包含空格的參數)
echo "--- 遍歷參數 ---"
for arg in "$@"; doecho "  - $arg"
done# 演示退出狀態
ls /no_such_directory > /dev/null 2>&1 # 嘗試一個會失敗的命令,并重定向輸出
echo "上一個命令的退出狀態: $?"sleep 2 & # 讓sleep命令在后臺運行
echo "后臺sleep進程的PID: $!"

執行: ./special_vars.sh arg1 "arg two" arg3

3.2.3 算術運算:Shell的“計算能力”

Shell腳本默認將所有變量視為字符串。要進行算術運算,需要使用特定的語法。

  1. expr 命令:

    • 用于執行整數運算,每個操作數和運算符之間必須有空格。

    • 乘法符號*需要轉義,因為*在Shell中有特殊含義(通配符)。

    #!/bin/bash
    num1=10
    num2=5
    result=$(expr $num1 + $num2)
    echo "10 + 5 = $result" # 輸出 15result=$(expr $num1 \* $num2) # 注意乘號需要轉義
    echo "10 * 5 = $result" # 輸出 50
  2. $(( )) 語法(推薦):

    • Bash內置的算術擴展,支持更復雜的整數運算,無需轉義。

    #!/bin/bash
    num1=10
    num2=5
    result=$(( num1 + num2 ))
    echo "10 + 5 = $result" # 輸出 15result=$(( num1 * num2 ))
    echo "10 * 5 = $result" # 輸出 50result=$(( (num1 + num2) * 2 / 5 )) # 支持括號和更復雜的表達式
    echo "(10 + 5) * 2 / 5 = $result" # 輸出 6
  3. bc 命令(浮點運算):

    • Shell本身不支持浮點運算,可以使用bc(Basic Calculator)命令。

    #!/bin/bash
    float1=10.5
    float2=2.5
    result=$(echo "$float1 + $float2" | bc)
    echo "10.5 + 2.5 = $result" # 輸出 13.0result=$(echo "scale=2; $float1 / $float2" | bc) # scale=2表示保留兩位小數
    echo "10.5 / 2.5 = $result" # 輸出 4.20
3.2.4 字符串操作:文本的“魔術師”

Shell腳本提供了豐富的字符串操作功能。

操作類型

語法

示例

結果

備注

字符串長度

${#string}

str="Hello"; echo ${#str}

5

子串提取

${string:pos:len}

str="Hello World"; echo ${str:6:5}

World

從pos開始提取len個字符

子串替換

${string/old/new}

str="Hello World"; echo ${str/World/Bash}

Hello Bash

替換第一個匹配項

全部替換

${string//old/new}

str="banana"; echo ${str//a/o}

bonono

替換所有匹配項

模式刪除

${string#pattern}

str="file.tar.gz"; echo ${str#*.}

tar.gz

從開頭刪除最短匹配模式

${string##pattern}

str="file.tar.gz"; echo ${str##*.}

gz

從開頭刪除最長匹配模式

${string%pattern}

str="file.tar.gz"; echo ${str%.*}

file.tar

從結尾刪除最短匹配模式

${string%%pattern}

str="file.tar.gz"; echo ${str%%.*}

file

從結尾刪除最長匹配模式

示例:字符串操作

#!/bin/bashmy_string="Linux Shell Scripting is FUN!"echo "原始字符串: $my_string"
echo "字符串長度: ${#my_string}"echo "提取子串 (從第6個字符開始,長度5): ${my_string:6:5}" # Shell
echo "提取子串 (從第12個字符開始到結尾): ${my_string:12}" # Scripting is FUN!echo "替換第一個 'i' 為 'I': ${my_string/i/I}"
echo "替換所有 'i' 為 'I': ${my_string//i/I}"file_name="my_document.tar.gz"
echo "文件名: $file_name"
echo "刪除最短前綴到第一個點: ${file_name#*.}" # tar.gz
echo "刪除最長前綴到最后一個點: ${file_name##*.}" # gz
echo "刪除最短后綴到第一個點: ${file_name%.*}" # my_document.tar
echo "刪除最長后綴到最后一個點: ${file_name%%.*}" # my_document

3.3 分支語句:讓腳本“思考”和“決策”

分支語句允許腳本根據不同的條件執行不同的代碼塊,實現邏輯判斷。

3.3.1 if 語句:最常用的條件判斷
  • 基本語法:

    if condition; then# 如果條件為真,執行這里的命令
    fi
  • if-else 語法:

    if condition; then# 如果條件為真
    else# 如果條件為假
    fi
  • if-elif-else 語法:

    if condition1; then# 如果條件1為真
    elif condition2; then# 如果條件2為真
    else# 如果所有條件都為假
    fi
  • 條件表達式:

    • 條件通常放在 [ condition ][[ condition ]]test condition 中。

    • [ ] 是一個命令,需要注意空格和字符串比較時的引號。

    • [[ ]] 是Bash的關鍵字,功能更強大,支持正則匹配,且不需要嚴格的空格和引號。推薦使用。

表格:常用條件判斷操作符

操作符

類型

含義

示例

備注

字符串比較

== / =

字符串

等于

[[ "$STR1" == "$STR2" ]]

=[ ]中,==[[ ]]中更常用

!=

字符串

不等于

[[ "$STR1" != "$STR2" ]]

<

字符串

小于 (按ASCII值)

[[ "$STR1" < "$STR2" ]]

需在[[ ]]中使用,或[ "$STR1" \< "$STR2" ]轉義

>

字符串

大于 (按ASCII值)

[[ "$STR1" > "$STR2" ]]

需在[[ ]]中使用,或[ "$STR1" \> "$STR2" ]轉義

-z

字符串

字符串長度為零 (空字符串)

[ -z "$STR" ]

-n

字符串

字符串長度不為零 (非空字符串)

[ -n "$STR" ]

數字比較

僅用于整數比較

-eq

整數

等于 (equal)

[ $NUM1 -eq $NUM2 ]

-ne

整數

不等于 (not equal)

[ $NUM1 -ne $NUM2 ]

-gt

整數

大于 (greater than)

[ $NUM1 -gt $NUM2 ]

-ge

整數

大于等于 (greater than or equal)

[ $NUM1 -ge $NUM2 ]

-lt

整數

小于 (less than)

[ $NUM1 -lt $NUM2 ]

-le

整數

小于等于 (less than or equal)

[ $NUM1 -le $NUM2 ]

文件測試

-e

文件/目錄

文件或目錄存在

[ -e /path/to/file ]

-f

文件

是一個普通文件

[ -f /path/to/file.txt ]

-d

目錄

是一個目錄

[ -d /path/to/dir ]

-s

文件

文件不為空 (大小大于0)

[ -s /path/to/file.txt ]

-r

文件

文件可讀

[ -r /path/to/file.txt ]

-w

文件

文件可寫

[ -w /path/to/file.txt ]

-x

文件

文件可執行

[ -x /path/to/script.sh ]

邏輯操作符

&&

邏輯與

邏輯與 (AND)

CMD1 && CMD2

CMD1成功才執行CMD2

`

`

邏輯或

邏輯或 (OR)

-a

邏輯與

[ ]中表示邏輯與

[ condition1 -a condition2 ]

推薦使用&&[[ ]]

-o

邏輯或

[ ]中表示邏輯或

[ condition1 -o condition2 ]

推薦使用`

!

邏輯非

邏輯非 (NOT)

[ ! condition ]

示例:if 語句與條件判斷

#!/bin/bash# 檢查參數個數
if [[ $# -eq 0 ]]; thenecho "用法: $0 <文件名>"exit 1 # 退出腳本,返回非0表示失敗
fiFILE_TO_CHECK="$1"# 檢查文件是否存在且可讀
if [[ -f "$FILE_TO_CHECK" && -r "$FILE_TO_CHECK" ]]; thenecho "文件 '$FILE_TO_CHECK' 存在且可讀。"# 檢查文件是否為空if [[ -s "$FILE_TO_CHECK" ]]; thenecho "文件 '$FILE_TO_CHECK' 不為空。"echo "文件內容如下:"cat "$FILE_TO_CHECK"elseecho "文件 '$FILE_TO_CHECK' 存在但為空。"fi
elif [[ -d "$FILE_TO_CHECK" ]]; thenecho "'$FILE_TO_CHECK' 是一個目錄。"
elseecho "'$FILE_TO_CHECK' 不存在或不可訪問。"
fi# 數字比較示例
NUM=15
if [[ "$NUM" -gt 10 && "$NUM" -lt 20 ]]; thenecho "$NUM 在 10 到 20 之間。"
elseecho "$NUM 不在 10 到 20 之間。"
fi# 字符串比較示例
OS_TYPE="Linux"
if [[ "$OS_TYPE" == "Linux" ]]; thenecho "你正在使用Linux系統。"
elif [[ "$OS_TYPE" == "Windows" ]]; thenecho "你正在使用Windows系統。"
elseecho "未知操作系統。"
fi
3.3.2 case 語句:多重選擇的利器

當有多個互斥的條件需要判斷時,case 語句比多個if-elif更簡潔、更易讀。

  • 語法:

    case 變量或表達式 in模式1)# 匹配模式1時執行的命令;; # 兩個分號表示該模式結束模式2)# 匹配模式2時執行的命令;;*) # 默認模式,匹配所有其他情況# 執行默認命令;;
    esac # case語句的結束
  • 模式支持通配符: * (任意字符), ? (單個字符), [] (字符范圍)

示例:case 語句的使用

#!/bin/bashecho "請輸入你的選擇 (1-開始, 2-停止, 3-重啟, 其他-退出):"
read CHOICEcase "$CHOICE" in1)echo "正在啟動服務..."# 這里可以放啟動服務的命令;;2)echo "正在停止服務..."# 這里可以放停止服務的命令;;3)echo "正在重啟服務..."# 這里可以放重啟服務的命令;;[4-9]) # 匹配4到9的數字echo "選擇的數字在 4 到 9 之間,但不是有效操作。";;[a-zA-Z]*) # 匹配以字母開頭的任何字符串echo "請輸入數字選項,而不是字母!";;*) # 匹配所有其他情況echo "無效選擇,程序退出。"exit 1;;
esacecho "操作完成。"

3.4 循環語句:讓腳本“重復”執行任務

循環語句允許腳本重復執行一段代碼塊,直到滿足某個條件或遍歷完一個列表。

3.4.1 for 循環:遍歷列表或范圍
  • 遍歷列表:

    for 變量 in 列表; do# 對列表中的每個元素執行命令
    done
    • 列表可以是空格分隔的字符串、命令的輸出、文件名等。

  • C語言風格的for循環(Bash特有):

    for (( 初始化; 條件; 步進 )); do# 執行命令
    done

示例:for 循環的使用

#!/bin/bashecho "--- 遍歷列表 ---"
for fruit in apple banana orange; doecho "我喜歡吃 $fruit"
doneecho "--- 遍歷命令輸出 ---"
for file in $(ls *.txt 2>/dev/null); do # 查找所有txt文件if [[ -f "$file" ]]; thenecho "處理文件: $file"# 可以在這里對文件進行操作,例如 cat "$file"fi
doneecho "--- C語言風格的for循環 ---"
for (( i=1; i<=5; i++ )); doecho "計數: $i"
doneecho "--- 遍歷目錄下的文件 (更健壯的方式) ---"
# 使用 find 命令結合 while read 循環,處理文件名中包含空格的情況
find . -maxdepth 1 -type f -name "*.sh" | while IFS= read -r script; doecho "找到腳本: $script"
done
3.4.2 while 循環:條件為真時重復
  • 語法:

    while condition; do# 如果條件為真,執行這里的命令
    done
  • 條件可以是任何命令,只要其退出狀態為0(成功),循環就繼續。

示例:while 循環的使用

#!/bin/bash# 倒計時
count=5
while [[ $count -gt 0 ]]; doecho "倒計時: $count"sleep 1 # 暫停1秒((count--)) # 遞減計數器
done
echo "發射!"# 從文件中逐行讀取
echo -e "Line 1\nLine 2\nLine 3" > temp_lines.txt
echo "--- 逐行讀取文件 ---"
while IFS= read -r line; doecho "讀取到行: $line"
done < temp_lines.txt # 將temp_lines.txt的內容重定向為while循環的輸入
rm temp_lines.txt
3.4.3 until 循環:條件為假時重復
  • 語法:

    until condition; do# 如果條件為假,執行這里的命令
    done
  • while相反,當條件退出狀態為非0(失敗)時,循環繼續;當條件退出狀態為0(成功)時,循環終止。

示例:until 循環的使用

#!/bin/bash# 等待文件出現
FILE_TO_WAIT="my_data.txt"
echo "正在等待文件 '$FILE_TO_WAIT' 的出現..."until [[ -f "$FILE_TO_WAIT" ]]; doecho "文件未找到,等待中..."sleep 2
doneecho "文件 '$FILE_TO_WAIT' 已找到!"
touch "$FILE_TO_WAIT" # 模擬創建文件
3.4.4 breakcontinue:控制循環流程
  • break 立即終止當前循環,跳出循環體,執行循環后面的代碼。

  • continue 終止當前循環的本次迭代,跳到循環的下一次迭代。

示例:breakcontinue 的使用

#!/bin/bashecho "--- break 示例 ---"
for i in {1..10}; doif [[ $i -eq 6 ]]; thenecho "達到 6,終止循環!"break # 終止整個for循環fiecho "當前數字: $i"
doneecho "--- continue 示例 ---"
for i in {1..10}; doif [[ $((i % 2)) -eq 0 ]]; then # 如果是偶數echo "跳過偶數: $i"continue # 跳過本次迭代,進入下一次迭代fiecho "當前奇數: $i"
done

3.5 函數:模塊化你的腳本代碼

函數允許你將一段常用的代碼封裝起來,賦予它一個名稱,然后在腳本中多次調用,實現代碼的模塊化和復用。這對于編寫大型、復雜的腳本非常有用。

3.5.1 定義函數
  • 語法1(推薦):

    function_name() {# 函數體# 命令...
    }
  • 語法2:

    function function_name {# 函數體# 命令...
    }
3.5.2 調用函數
  • 直接使用函數名即可調用,像執行一個普通命令一樣。

    function_name [參數1] [參數2] ...
3.5.3 函數參數
  • 函數內部可以通過特殊變量 $1, $2, ... 來訪問傳遞給函數的參數。

  • $#:函數內部的參數個數。

  • $*, $@:函數內部的所有參數。

  • $0:在函數內部仍然是腳本本身的名稱。

3.5.4 函數返回值
  • 函數通過 return 命令返回一個退出狀態碼(0-255)。

  • 函數執行完畢后,可以通過 $? 變量獲取其退出狀態碼。

  • 如果需要返回具體的值(字符串或數字),通常通過echo打印,然后使用命令替換($(function_name))來捕獲。

示例:函數的使用

#!/bin/bash# 定義一個簡單的問候函數
greet_user() {echo "Hello, $1!" # $1 是傳遞給函數的第一個參數echo "歡迎使用我的腳本。"
}# 定義一個帶返回值的函數
add_numbers() {local num1=$1 # 使用local關鍵字聲明局部變量,避免與腳本全局變量沖突local num2=$2local sum=$((num1 + num2))echo "計算結果: $sum" # 通過echo打印結果return 0 # 返回0表示成功
}# 定義一個檢查文件類型的函數
check_file_type() {local file="$1"if [[ -f "$file" ]]; thenecho "$file 是一個普通文件。"return 0 # 成功elif [[ -d "$file" ]]; thenecho "$file 是一個目錄。"return 0 # 成功elseecho "$file 不存在或類型未知。"return 1 # 失敗fi
}echo "--- 調用函數 ---"greet_user "張三" # 調用函數并傳遞參數
greet_user "李四"echo "--- 調用帶返回值的函數 ---"
result=$(add_numbers 10 20) # 使用命令替換捕獲函數的輸出
echo "函數 add_numbers 的輸出: $result"
add_numbers 5 8
status=$? # 獲取函數的退出狀態碼
echo "函數 add_numbers 的退出狀態: $status"echo "--- 調用文件檢查函數 ---"
touch my_test_file.txt
check_file_type "my_test_file.txt"
check_file_type "/tmp"
check_file_type "no_such_file.xyz"
rm my_test_file.txtecho "腳本執行完畢。"

3.6 C語言模擬:一個簡易的Shell腳本解釋器

為了讓你更深入地理解Shell腳本在底層是如何工作的,我們將用C語言來模擬一個非常簡化的Shell腳本解釋器。這個解釋器將能夠:

  1. 讀取腳本文件: 逐行讀取.sh腳本文件。

  2. 解析命令: 將每一行解析成命令和參數。

  3. 變量管理: 實現一個簡單的機制來存儲和檢索Shell變量。

  4. 執行內置命令: 模擬echoread

  5. 實現簡易的if語句: 能夠解析并執行簡單的條件判斷。

  6. 實現簡易的for循環: 能夠遍歷一個簡單的列表。

  7. 實現簡易的函數: 能夠定義和調用函數。

這個模擬器會比較復雜,因為它要模擬Shell的很多內部邏輯。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <ctype.h> // For isspace// --- 宏定義 ---
#define MAX_LINE_LEN 256
#define MAX_TOKENS 32 // 每行最多32個詞法單元
#define MAX_VAR_NAME_LEN 64
#define MAX_VAR_VALUE_LEN 256
#define MAX_VARS 100 // 最大變量數
#define MAX_FUNC_NAME_LEN 64
#define MAX_FUNC_LINES 100 // 每個函數最大行數
#define MAX_FUNCS 20 // 最大函數數// --- 結構體:模擬Shell變量 ---
typedef struct {char name[MAX_VAR_NAME_LEN];char value[MAX_VAR_VALUE_LEN];
} ShellVar;ShellVar shell_vars[MAX_VARS];
int num_shell_vars = 0;// --- 結構體:模擬Shell函數 ---
typedef struct {char name[MAX_FUNC_NAME_LEN];char lines[MAX_FUNC_LINES][MAX_LINE_LEN]; // 函數體內容int num_lines;
} ShellFunc;ShellFunc shell_funcs[MAX_FUNCS];
int num_shell_funcs = 0;// --- 輔助函數:查找變量 ---
ShellVar* find_var(const char* name) {for (int i = 0; i < num_shell_vars; i++) {if (strcmp(shell_vars[i].name, name) == 0) {return &shell_vars[i];}}return NULL;
}// --- 輔助函數:設置變量值 ---
void set_var(const char* name, const char* value) {ShellVar* var = find_var(name);if (var != NULL) {strncpy(var->value, value, MAX_VAR_VALUE_LEN - 1);var->value[MAX_VAR_VALUE_LEN - 1] = '\0';} else {if (num_shell_vars < MAX_VARS) {strncpy(shell_vars[num_shell_vars].name, name, MAX_VAR_NAME_LEN - 1);shell_vars[num_shell_vars].name[MAX_VAR_NAME_LEN - 1] = '\0';strncpy(shell_vars[num_shell_vars].value, value, MAX_VAR_VALUE_LEN - 1);shell_vars[num_shell_vars].value[MAX_VAR_VALUE_LEN - 1] = '\0';num_shell_vars++;} else {fprintf(stderr, "模擬Shell: 變量空間不足。\n");}}
}// --- 輔助函數:獲取變量值 ---
const char* get_var_value(const char* name) {ShellVar* var = find_var(name);if (var != NULL) {return var->value;}return ""; // 未找到變量返回空字符串
}// --- 輔助函數:查找函數 ---
ShellFunc* find_func(const char* name) {for (int i = 0; i < num_shell_funcs; i++) {if (strcmp(shell_funcs[i].name, name) == 0) {return &shell_funcs[i];}}return NULL;
}// --- 輔助函數:去除字符串兩端空格 ---
char* trim_whitespace(char* str) {char *end;while(isspace((unsigned char)*str)) str++;if(*str == 0) return str;end = str + strlen(str) - 1;while(end > str && isspace((unsigned char)*end)) end--;*(end+1) = 0;return str;
}// --- 輔助函數:替換字符串中的變量引用 (簡易版) ---
// 例如 "Hello $NAME" -> "Hello World"
void expand_variables(char* line) {char expanded_line[MAX_LINE_LEN * 2]; // 預留更大空間expanded_line[0] = '\0';char* ptr = line;char* start_var;while (*ptr != '\0') {if (*ptr == '$') {start_var = ptr + 1;char var_name[MAX_VAR_NAME_LEN];int i = 0;// 簡單處理:只識別字母數字下劃線組成的變量名while (isalnum((unsigned char)*start_var) || *start_var == '_') {if (i < MAX_VAR_NAME_LEN - 1) {var_name[i++] = *start_var;}start_var++;}var_name[i] = '\0';const char* var_value = get_var_value(var_name);strncat(expanded_line, var_value, sizeof(expanded_line) - strlen(expanded_line) - 1);ptr = start_var;} else {strncat(expanded_line, ptr, 1);ptr++;}}strncpy(line, expanded_line, MAX_LINE_LEN - 1);line[MAX_LINE_LEN - 1] = '\0';
}// --- 核心執行函數 ---
// 返回0表示成功,非0表示失敗
int execute_command(char* tokens[], int num_tokens);// --- 模擬Shell內部執行邏輯 ---
int sim_shell_execute(const char* line) {char line_copy[MAX_LINE_LEN];strncpy(line_copy, line, MAX_LINE_LEN - 1);line_copy[MAX_LINE_LEN - 1] = '\0';// 1. 去除注釋char* comment_start = strchr(line_copy, '#');if (comment_start != NULL) {*comment_start = '\0';}// 2. 變量展開 (簡易版)expand_variables(line_copy);char* trimmed_line = trim_whitespace(line_copy);if (strlen(trimmed_line) == 0) {return 0; // 空行或只有注釋的行}// 3. 詞法分析/分割命令和參數char* tokens[MAX_TOKENS];int num_tokens = 0;char* token = strtok(trimmed_line, " \t"); // 按空格和制表符分割while (token != NULL && num_tokens < MAX_TOKENS) {tokens[num_tokens++] = token;token = strtok(NULL, " \t");}tokens[num_tokens] = NULL; // 標記結束if (num_tokens == 0) return 0; // 再次檢查是否為空// 4. 命令執行return execute_command(tokens, num_tokens);
}// --- 核心執行函數實現 ---
int execute_command(char* tokens[], int num_tokens) {const char* cmd = tokens[0];if (strcmp(cmd, "echo") == 0) {for (int i = 1; i < num_tokens; i++) {printf("%s%s", tokens[i], (i == num_tokens - 1) ? "" : " ");}printf("\n");return 0; // 成功} else if (strcmp(cmd, "read") == 0) {if (num_tokens > 1) {char input_buffer[MAX_VAR_VALUE_LEN];if (fgets(input_buffer, sizeof(input_buffer), stdin) != NULL) {input_buffer[strcspn(input_buffer, "\n")] = 0; // 移除換行符set_var(tokens[1], input_buffer);return 0;}}fprintf(stderr, "read: 缺少變量名。\n");return 1;} else if (strcmp(cmd, "set") == 0) { // 模擬變量賦值: set VAR_NAME=VALUEif (num_tokens > 1) {char* eq_sign = strchr(tokens[1], '=');if (eq_sign != NULL) {*eq_sign = '\0'; // 分割變量名和值set_var(tokens[1], eq_sign + 1);return 0;}}fprintf(stderr, "set: 無效的變量賦值語法。\n");return 1;} else if (strcmp(cmd, "if") == 0) {// 模擬 if [ condition ]// 簡化:只支持 if [ $VAR -eq VALUE ] 或 if [ -f FILE ]if (num_tokens >= 4 && strcmp(tokens[1], "[") == 0 && strcmp(tokens[num_tokens - 1], "]") == 0) {// 移除方括號tokens[1] = NULL; // 忽略 '['tokens[num_tokens - 1] = NULL; // 忽略 ']'// 重新組織參數,從 tokens[2] 開始char* cond_tokens[MAX_TOKENS];int cond_num_tokens = 0;for (int i = 2; i < num_tokens - 1; i++) {cond_tokens[cond_num_tokens++] = tokens[i];}cond_tokens[cond_num_tokens] = NULL;if (cond_num_tokens == 3 && strcmp(cond_tokens[1], "-eq") == 0) {// 模擬數字相等判斷: [ $VAR -eq VALUE ]const char* var_val_str = get_var_value(cond_tokens[0]);int var_val = atoi(var_val_str);int compare_val = atoi(cond_tokens[2]);return (var_val == compare_val) ? 0 : 1; // 0為真,1為假} else if (cond_num_tokens == 2 && strcmp(cond_tokens[0], "-f") == 0) {// 模擬文件存在判斷: [ -f FILE ]// 這里我們沒有真實文件系統,所以總是返回假fprintf(stderr, "if: -f 模擬總是返回假。\n");return 1; // 模擬文件不存在} else {fprintf(stderr, "if: 不支持的條件格式。\n");return 1;}} else {fprintf(stderr, "if: 語法錯誤。\n");return 1;}} else {fprintf(stderr, "模擬Shell: 未知命令或未實現: %s\n", cmd);return 1; // 失敗}
}// --- 模擬Shell腳本解釋器 ---
void sim_shell_interpreter(FILE* script_file) {char line[MAX_LINE_LEN];char current_func_name[MAX_FUNC_NAME_LEN];bool in_function_def = false;int current_func_line_idx = 0;while (fgets(line, sizeof(line), script_file) != NULL) {char trimmed_line[MAX_LINE_LEN];strncpy(trimmed_line, line, MAX_LINE_LEN - 1);trimmed_line[MAX_LINE_LEN - 1] = '\0';char* processed_line = trim_whitespace(trimmed_line);// 檢查Shebang行if (processed_line[0] == '#' && processed_line[1] == '!') {printf("[Shell Interpreter] 發現Shebang行: %s\n", processed_line);continue; // 跳過Shebang行}// 檢查函數定義開始if (strstr(processed_line, "() {") != NULL) {char* func_name_end = strstr(processed_line, "()");if (func_name_end != NULL) {*func_name_end = '\0';strncpy(current_func_name, processed_line, MAX_FUNC_NAME_LEN - 1);current_func_name[MAX_FUNC_NAME_LEN - 1] = '\0';trim_whitespace(current_func_name); // 確保函數名沒有多余空格if (num_shell_funcs < MAX_FUNCS) {strncpy(shell_funcs[num_shell_funcs].name, current_func_name, MAX_FUNC_NAME_LEN - 1);shell_funcs[num_shell_funcs].name[MAX_FUNC_NAME_LEN - 1] = '\0';shell_funcs[num_shell_funcs].num_lines = 0;current_func_line_idx = 0;in_function_def = true;printf("[Shell Interpreter] 開始定義函數: %s\n", current_func_name);} else {fprintf(stderr, "模擬Shell: 函數空間不足,無法定義函數 %s。\n", current_func_name);in_function_def = false; // 停止定義}continue;}}// 檢查函數定義結束if (in_function_def && strcmp(processed_line, "}") == 0) {shell_funcs[num_shell_funcs].num_lines = current_func_line_idx;num_shell_funcs++;in_function_def = false;printf("[Shell Interpreter] 函數 %s 定義結束。\n", current_func_name);continue;}if (in_function_def) {if (current_func_line_idx < MAX_FUNC_LINES) {strncpy(shell_funcs[num_shell_funcs].lines[current_func_line_idx], processed_line, MAX_LINE_LEN - 1);shell_funcs[num_shell_funcs].lines[current_func_line_idx][MAX_LINE_LEN - 1] = '\0';current_func_line_idx++;} else {fprintf(stderr, "模擬Shell: 函數 %s 行數過多,超出限制。\n", current_func_name);in_function_def = false; // 停止定義}continue;}// 處理 if, then, fi (簡化邏輯,只處理單行if)if (strncmp(processed_line, "if ", 3) == 0) {char* cond_start = strstr(processed_line, "[");char* cond_end = strstr(processed_line, "]");char* then_kw = strstr(processed_line, "; then");if (cond_start != NULL && cond_end != NULL && then_kw != NULL && cond_start < cond_end && cond_end < then_kw) {*cond_end = '\0'; // 截斷條件部分char condition_str[MAX_LINE_LEN];strncpy(condition_str, cond_start, sizeof(condition_str) - 1);condition_str[sizeof(condition_str) - 1] = '\0';char* tokens[MAX_TOKENS];int num_tokens = 0;char* temp_cond_str = strdup(condition_str);char* token_ptr = strtok(temp_cond_str, " \t");while (token_ptr != NULL && num_tokens < MAX_TOKENS) {tokens[num_tokens++] = token_ptr;token_ptr = strtok(NULL, " \t");}tokens[num_tokens] = NULL;printf("[Shell Interpreter] 正在評估條件: %s\n", condition_str);int condition_result = execute_command(tokens, num_tokens); // 評估條件free(temp_cond_str);char* then_cmd_start = then_kw + strlen("; then");char then_cmd_line[MAX_LINE_LEN];strncpy(then_cmd_line, then_cmd_start, sizeof(then_cmd_line) - 1);then_cmd_line[sizeof(then_cmd_line) - 1] = '\0';trim_whitespace(then_cmd_line);if (condition_result == 0) { // 條件為真printf("[Shell Interpreter] 條件為真,執行: %s\n", then_cmd_line);sim_shell_execute(then_cmd_line);} else {printf("[Shell Interpreter] 條件為假,跳過: %s\n", then_cmd_line);}continue; // 處理完if語句,跳到下一行}}// 處理 for 循環 (簡化邏輯,只支持 for var in list; do cmd; done)if (strncmp(processed_line, "for ", 4) == 0) {char* in_kw = strstr(processed_line, " in ");char* do_kw = strstr(processed_line, "; do ");char* done_kw = strstr(processed_line, "; done");if (in_kw != NULL && do_kw != NULL && done_kw != NULL && in_kw < do_kw && do_kw < done_kw) {char var_name[MAX_VAR_NAME_LEN];char list_str[MAX_LINE_LEN];char loop_cmd_str[MAX_LINE_LEN];// 提取變量名char* temp_line = strdup(processed_line + 4); // 跳過 "for "char* var_token = strtok(temp_line, " ");if (var_token != NULL) {strncpy(var_name, var_token, MAX_VAR_NAME_LEN - 1);var_name[MAX_VAR_NAME_LEN - 1] = '\0';} else {fprintf(stderr, "for: 語法錯誤,缺少變量名。\n");free(temp_line);continue;}// 提取列表char* list_start = in_kw + strlen(" in ");char* list_end = do_kw;strncpy(list_str, list_start, list_end - list_start);list_str[list_end - list_start] = '\0';trim_whitespace(list_str);// 提取循環體命令char* cmd_start = do_kw + strlen("; do ");char* cmd_end = done_kw;strncpy(loop_cmd_str, cmd_start, cmd_end - cmd_start);loop_cmd_str[cmd_end - cmd_start] = '\0';trim_whitespace(loop_cmd_str);printf("[Shell Interpreter] 正在執行 for 循環 (變量: %s, 列表: %s, 命令: %s)\n", var_name, list_str, loop_cmd_str);// 分割列表并循環char* item_token = strtok(list_str, " \t");while (item_token != NULL) {set_var(var_name, item_token); // 設置循環變量printf("[Shell Interpreter] 循環迭代: %s=%s, 執行命令: %s\n", var_name, item_token, loop_cmd_str);sim_shell_execute(loop_cmd_str); // 執行循環體命令item_token = strtok(NULL, " \t");}free(temp_line);continue; // 處理完for語句,跳到下一行}}// 檢查是否是函數調用ShellFunc* func_to_call = find_func(processed_line); // 簡化:假設函數調用不帶參數if (func_to_call != NULL) {printf("[Shell Interpreter] 正在調用函數: %s\n", func_to_call->name);for (int i = 0; i < func_to_call->num_lines; i++) {printf("[Shell Interpreter]   執行函數行: %s\n", func_to_call->lines[i]);sim_shell_execute(func_to_call->lines[i]); // 執行函數體內的每一行}printf("[Shell Interpreter] 函數 %s 調用結束。\n", func_to_call->name);continue;}// 其他命令,直接執行printf("[Shell Interpreter] 執行普通命令: %s\n", processed_line);sim_shell_execute(processed_line);}
}int main(int argc, char* argv[]) {if (argc < 2) {fprintf(stderr, "用法: %s <script_file.sh>\n", argv[0]);return 1;}FILE* script_file = fopen(argv[1], "r");if (script_file == NULL) {perror("無法打開腳本文件");return 1;}printf("====== 簡易Shell腳本解釋器模擬器 ======\n");printf("正在解釋執行腳本: %s\n", argv[1]);// 初始化一些特殊變量 (模擬)set_var("0", argv[1]); // 腳本名稱// 模擬參數,這里簡化不處理命令行參數傳遞給腳本set_var("NAME", "模擬用戶"); // 預設一個變量sim_shell_interpreter(script_file);fclose(script_file);printf("\n====== 腳本執行完畢 ======\n");return 0;
}

代碼分析與邏輯透析:

這份C語言代碼構建了一個簡易的Shell腳本解釋器模擬器,它能夠讀取并執行一個簡單的Shell腳本文件。雖然它無法與真實的Bash Shell相提并論,但它能讓你從底層理解Shell腳本的解析、變量管理、條件判斷和循環執行的核心原理

  1. 數據結構:

    • ShellVar 結構體:模擬Shell中的變量,包含name(變量名)和value(變量值)。

    • shell_vars 數組:全局變量,作為模擬的變量表(Symbol Table),存儲所有已定義的Shell變量。

    • ShellFunc 結構體:模擬Shell函數,包含name(函數名)和lines(函數體中的命令列表)。

    • shell_funcs 數組:全局變量,作為模擬的函數定義存儲區

  2. 輔助函數:

    • find_var, set_var, get_var_value:實現了變量的查找、設置和獲取功能,模擬了Shell對變量的內存管理。

    • find_func:用于查找已定義的函數。

    • trim_whitespace:去除字符串兩端的空格,這是解析命令時常用的預處理。

    • expand_variables核心功能之一! 它模擬了Shell的變量展開過程。當Shell遇到$VAR_NAME時,它會查找VAR_NAME的值并替換掉$VAR_NAME。這個函數遍歷一行文本,找到$開頭的變量引用,然后從shell_vars中查找其值并進行替換。

  3. execute_command 函數:

    • 這是模擬Shell執行內置命令的核心。它接收一個tokens數組(命令和參數)。

    • 目前它實現了:

      • echo:簡單地打印參數。

      • read:從標準輸入讀取一行,并將其值賦給指定的變量(通過set_var)。

      • set:模擬變量賦值,例如set MY_VAR=Hello

      • if簡化版的條件判斷。目前只支持if [ $VAR -eq VALUE ]if [ -f FILE ](文件存在判斷,但本模擬中文件系統是假的,所以-f總是返回假)。它會調用自身來評估條件表達式的退出狀態。

      • 其他未實現的命令會打印錯誤信息。

    • 退出狀態: 成功返回0,失敗返回非0,這與真實Shell命令的退出狀態一致。

  4. sim_shell_interpreter 函數:

    • 這是整個解釋器的主循環,它逐行讀取腳本文件。

    • Shebang行處理: 識別并跳過#!開頭的行。

    • 注釋處理: 識別#并忽略其后的內容。

    • 變量展開: 在執行任何命令之前,先調用expand_variables對行進行變量替換。

    • 函數定義識別:

      • 通過查找(){來識別函數定義的開始,進入in_function_def狀態。

      • 在函數定義狀態下,將后續行存儲到ShellFunc結構體的lines數組中。

      • 通過識別}來標記函數定義的結束。

    • if語句識別和處理: 識別if ...; then ...這種簡化形式,提取條件和命令,并調用execute_command評估條件和執行命令。

    • for循環識別和處理: 識別for var in list; do cmd; done這種簡化形式,提取變量名、列表和循環體命令。然后遍歷列表,每次迭代設置循環變量,并調用sim_shell_execute執行循環體命令。

    • 函數調用識別: 檢查當前行是否與已定義的函數名匹配,如果匹配則遍歷函數體內的命令并逐行執行(通過遞歸調用sim_shell_execute)。

    • 普通命令執行: 對于其他命令,直接調用sim_shell_execute進行處理。

  5. main 函數:

    • 接收腳本文件路徑作為命令行參數。

    • 打開腳本文件。

    • 初始化一些模擬的特殊變量和用戶變量。

    • 調用sim_shell_interpreter開始解釋執行腳本。

通過這個模擬器,你可以:

  • 理解變量展開: 觀察expand_variables如何將$NAME替換為實際值。

  • 理解命令解析: strtok如何將一行命令分割成命令和參數。

  • 理解條件判斷: execute_commandif邏輯如何根據條件返回0或1,從而控制流程。

  • 理解循環: for循環如何遍歷列表并重復執行命令。

  • 理解函數: 函數定義如何被存儲,函數調用如何觸發其內部命令的執行。

  • 親手調試: 你可以嘗試在這個C代碼中添加printf語句,跟蹤執行流程,更好地理解每一步。

如何使用這個C語言模擬器:

  1. 將上述C代碼保存為 sim_shell.c

  2. 編譯:gcc sim_shell.c -o sim_shell

  3. 創建一個簡單的Shell腳本文件,例如 my_script.sh

    #!/bin/bash
    # 這是一個測試腳本echo "--- 腳本開始 ---"set MY_VAR=Hello_World
    echo "MY_VAR的值是: $MY_VAR"echo "請輸入你的名字:"
    read USER_NAME
    echo "你好, $USER_NAME!"# 模擬if語句
    set NUM_VAL=10
    if [ $NUM_VAL -eq 10 ]; then echo "NUM_VAL 等于 10"; fi# 模擬for循環
    for item in apple banana orange; do echo "處理水果: $item"; done# 模擬函數
    my_func() {echo "這是我的函數內部。"echo "函數參數: $1" # 模擬參數,但本模擬器簡化,函數調用時不支持傳參
    }my_func # 調用函數echo "--- 腳本結束 ---"
  4. 運行模擬器:./sim_shell my_script.sh

  5. 觀察輸出,你會看到模擬器如何一步步解析和執行腳本。

3.7 小結與展望

恭喜你,老鐵!你已經成功闖過了“Linux與C高級編程”學習之路的第三關:Shell腳本編程

在這一部分中,我們:

  • 深入理解了Shell腳本的概念、作用和基本結構,讓你明白它為何是Linux自動化和嵌入式開發中的“效率神器”。

  • 掌握了變量的定義、使用以及各種特殊變量,學會了Shell腳本的“記憶”和“計算”能力。

  • 學習了算術運算和豐富的字符串操作,讓你的腳本能夠處理各種數據。

  • 精通了**if-elsecase分支語句**,讓你的腳本能夠根據條件做出“思考”和“決策”。

  • 掌握了**forwhileuntil循環語句**以及breakcontinue,讓你的腳本能夠“重復”執行任務。

  • 學會了函數的定義、調用、參數傳遞和返回值,讓你的腳本代碼更加模塊化、可復用。

  • 最重要的是,我們通過一個龐大且邏輯復雜的C語言模擬器,讓你從底層理解了Shell腳本的解析過程、變量展開、條件判斷、循環執行和函數調用的內部機制。這不僅僅是學會了命令,更是理解了其“骨架”和“血肉”!

現在,你不僅能夠編寫出自動化腳本來提高效率,還能更深入地理解這些腳本在Linux系統中的運行原理。這對于你未來在嵌入式設備上進行系統開發、調試和維護,將是巨大的優勢!

接下來,我們將進入更具挑戰性的第四部分:Linux TFTP服務搭建及使用,Linux NFS服務搭建及使用!這將帶你進入網絡文件傳輸和共享的世界,這對于嵌入式設備的遠程開發和調試至關重要!

請記住,學習Shell腳本,最好的方式就是多寫、多練、多調試!從簡單的自動化任務開始,逐步嘗試更復雜的邏輯。

敬請期待我的下一次更新!如果你在學習過程中有任何疑問,或者對代碼有任何改進的想法,隨時在評論區告訴我,咱們一起交流,一起成為Linux與C編程的“大神”!

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------更新于2025.6.19 下午

【硬核揭秘】Linux與C高級編程:從入門到精通,你的全棧之路!

第四部分:Linux TFTP與NFS服務——嵌入式遠程開發與調試的“神兵利器”

嘿,各位C語言的“卷王”們!

在前面的學習中,我們已經掌握了Linux的基本操作、高級Shell命令,甚至能編寫自動化腳本。這些都是你在Linux世界中“單打獨斗”的必備技能。但是,當你的戰場從開發機轉移到嵌入式目標板時,你會發現,很多時候你需要和目標板進行“遠程協作”——傳輸文件、共享目錄,甚至直接在目標板上運行和調試程序。

這時候,傳統的U盤拷貝、串口傳輸就顯得力不從心了。我們需要更高效、更專業的網絡服務!今天,咱們就來揭秘嵌入式遠程開發與調試的兩大“神兵利器”:TFTP(簡單文件傳輸協議)和NFS(網絡文件系統)

本篇是“Linux與C高級編程”系列的第四部分,我們將帶你:

  • TFTP: 了解其工作原理,手把手搭建TFTP服務器,并用C語言模擬其核心傳輸邏輯。

  • NFS: 深入理解其文件系統共享機制,搭建NFS服務器,并用C語言模擬遠程文件操作。

每一個知識點,咱們都會結合詳細的步驟、實用的Shell命令,并用大量帶注釋的C語言代碼,讓你不僅知其然,更知其所以然!

準備好了嗎?咱們這就開始,讓你的嵌入式遠程開發,變得像本地操作一樣流暢!

4.1 Linux TFTP服務搭建及使用:輕量級文件傳輸的“急先鋒”

4.1.1 什么是TFTP?為什么在嵌入式開發中常用?
  • TFTP (Trivial File Transfer Protocol),簡單文件傳輸協議,顧名思義,它是一個非常簡單的文件傳輸協議。它基于UDP(用戶數據報協議)工作,端口號為69。

  • “Trivial”在哪?

    • 簡單: 協議頭部非常小,功能非常少。

    • 無認證: 默認不提供用戶認證、權限控制等安全機制。

    • 無目錄列表: 不能像FTP那樣列出目錄內容。

    • 無斷點續傳: 不支持文件傳輸中斷后從上次中斷處繼續傳輸。

  • 與FTP/HTTP的區別和優勢:

特性

TFTP

FTP (File Transfer Protocol)

HTTP (HyperText Transfer Protocol)

協議類型

應用層協議

應用層協議

應用層協議

傳輸層協議

UDP (用戶數據報協議)

TCP (傳輸控制協議)

TCP (傳輸控制協議)

端口號

69

20 (數據), 21 (控制)

80 (HTTP), 443 (HTTPS)

安全性

無認證,不安全

用戶名/密碼認證,可支持TLS/SSL加密

可支持TLS/SSL加密 (HTTPS)

功能

僅支持文件上傳/下載,無目錄列表,無斷點續傳

支持文件上傳/下載、目錄列表、權限控制等

支持超文本傳輸、文件下載、Web服務等

復雜性

非常簡單,協議開銷小

相對復雜

復雜,功能強大

應用場景

嵌入式設備啟動加載、固件升級、網絡啟動

文件服務器、網站文件管理

網頁瀏覽、Web服務、API通信

  • 為什么在嵌入式開發中常用?

    • 輕量級: TFTP協議棧非常小,占用資源少,非常適合資源有限的嵌入式設備(如Bootloader階段)。

    • 無需復雜配置: 很多嵌入式設備的Bootloader(如U-Boot)內置了TFTP客戶端功能,無需復雜的網絡配置,只需簡單設置IP地址即可使用。

    • 快速傳輸小文件: 對于內核鏡像、根文件系統鏡像等相對較小的文件,TFTP傳輸速度快,效率高。

    • 網絡啟動(PXE): 許多嵌入式設備支持通過TFTP從網絡啟動,無需本地存儲。

思維導圖:TFTP在嵌入式中的應用

graph TDA[TFTP在嵌入式中的應用] --> B[Bootloader階段]B --> B1[U-Boot下載內核鏡像]B --> B2[U-Boot下載根文件系統鏡像]B --> B3[U-Boot下載設備樹文件]A --> C[固件升級]C --> C1[通過TFTP傳輸新固件到設備]A --> D[網絡啟動 (PXE)]D --> D1[無盤工作站/嵌入式設備從網絡加載啟動文件]A --> E[開發調試]E --> E1[快速傳輸測試文件、配置文件]
4.1.2 TFTP服務器搭建(Ubuntu為例)

在開發過程中,我們通常會在開發機(Host)上搭建TFTP服務器,用于向目標板(Target)提供文件。

步驟1:安裝TFTP服務器軟件

sudo apt update
sudo apt install tftpd-hpa tftp-hpa # tftpd-hpa是服務器,tftp-hpa是客戶端

步驟2:配置TFTP服務

TFTP服務器的配置文件通常在 /etc/default/tftpd-hpa

sudo vim /etc/default/tftpd-hpa

編輯內容如下(如果文件不存在,則創建):

# /etc/default/tftpd-hpa# TFTP_USERNAME:TFTP服務運行的用戶,通常是tftp
TFTP_USERNAME="tftp"# TFTP_DIRECTORY:TFTP服務器的根目錄,所有可傳輸的文件都必須放在這個目錄下
# 強烈建議將此目錄設置在用戶目錄下,例如 /home/your_user/tftpboot
TFTP_DIRECTORY="/home/your_user/tftpboot" # 將 your_user 替換為你的實際用戶名# TFTP_ADDRESS:TFTP服務監聽的IP地址和端口,默認是0.0.0.0:69 (監聽所有接口)
# 如果你有多個網卡,可以指定特定IP,例如 "192.168.1.100:69"
TFTP_ADDRESS="0.0.0.0:69"# TFTP_OPTIONS:TFTP服務器的選項
# -l:以獨立模式運行,而不是由inetd管理
# -c:允許客戶端創建新文件 (上傳)
# -s:安全模式,TFTP_DIRECTORY作為根目錄,不允許訪問其父目錄
# -p:允許端口范圍 (通常用于客戶端連接)
# -v:詳細輸出 (用于調試)
TFTP_OPTIONS="-l -c -s" # 允許上傳 (-c)

重要提示:

  • TFTP_DIRECTORY 這個目錄是TFTP服務器的“文件倉庫”。所有你想通過TFTP傳輸的文件,都必須放在這個目錄下。

  • TFTP_OPTIONS="-l -c -s"

    • -l:獨立模式運行,這樣TFTP服務會一直運行,而不是按需啟動。

    • -c:允許客戶端上傳文件。如果你只需要下載,可以不加此選項。

    • -s:安全模式。TFTP服務器會將TFTP_DIRECTORY視為根目錄,客戶端無法訪問其父目錄,這增加了安全性。

步驟3:創建TFTP根目錄并設置權限

根據你在配置文件中設置的TFTP_DIRECTORY,創建該目錄并設置正確的權限。TFTP服務通常以tftp用戶身份運行,所以需要確保tftp用戶對該目錄有讀寫權限。

mkdir -p /home/your_user/tftpboot # 創建目錄 (如果your_user是你的用戶名)
sudo chown -R tftp:tftp /home/your_user/tftpboot # 將目錄所有者改為tftp用戶和組
sudo chmod -R 777 /home/your_user/tftpboot # 給予tftp目錄所有者讀寫執行權限,以及其他用戶讀寫執行權限 (方便測試,生產環境按需調整)

步驟4:啟動/重啟TFTP服務

sudo systemctl restart tftpd-hpa # 重啟TFTP服務使配置生效
sudo systemctl enable tftpd-hpa  # 設置開機自啟動 (可選)
sudo systemctl status tftpd-hpa  # 查看服務狀態,確保Running

步驟5:防火墻設置(如果需要)

如果你的Ubuntu系統開啟了防火墻(如UFW),需要允許TFTP服務的UDP 69端口通過。

sudo ufw allow 69/udp
sudo ufw reload
sudo ufw status # 檢查防火墻狀態
4.1.3 TFTP客戶端使用

在開發機上安裝了TFTP服務器后,你可以在開發機上測試,也可以在目標板(如果其Bootloader或Linux系統支持TFTP客戶端)上使用。

步驟1:安裝TFTP客戶端軟件

如果你在步驟1中已經安裝了tftp-hpa,則無需再次安裝。

sudo apt install tftp-hpa # 如果未安裝

步驟2:使用tftp命令進行文件上傳和下載

假設你的TFTP服務器IP地址是 192.168.1.100,TFTP根目錄是 /home/your_user/tftpboot

  1. 準備測試文件:

    echo "This is a test file for TFTP download." > /home/your_user/tftpboot/test_download.txt
    echo "This file will be uploaded via TFTP." > /tmp/test_upload.txt # 客戶端本地文件
  2. 進入TFTP客戶端交互模式:

    tftp 192.168.1.100
    • 進入后,你會看到tftp>提示符。

  3. 下載文件 (get):

    • 將TFTP服務器上的test_download.txt文件下載到當前目錄。

    tftp> get test_download.txt
    Received 36 bytes in 0.000 seconds [360000 bps] # 成功下載
    tftp> quit
    • 此時,你的當前目錄下應該有了test_download.txt文件。

  4. 上傳文件 (put):

    • 將本地的test_upload.txt文件上傳到TFTP服務器。

    tftp> put /tmp/test_upload.txt # 注意這里是本地文件的完整路徑或相對路徑
    Sent 35 bytes in 0.000 seconds [350000 bps] # 成功上傳
    tftp> quit
    • 此時,在TFTP服務器的/home/your_user/tftpboot目錄下應該有了test_upload.txt文件。

  5. 非交互模式(單次操作):

    tftp 192.168.1.100 -c get test_download.txt # 直接下載
    tftp 192.168.1.100 -c put /tmp/test_upload.txt # 直接上傳

注意事項:

  • 權限: 確保TFTP服務器的根目錄及其文件有正確的權限,TFTP用戶能夠讀寫。

  • 防火墻: 確保TFTP服務器的UDP 69端口是開放的。

  • 網絡連通性: 確保開發機和目標板之間網絡是通的(可以ping)。

  • 文件路徑: 在TFTP客戶端中,getput命令后面的文件名是相對于TFTP服務器根目錄的路徑。

4.1.4 C語言模擬:簡易TFTP客戶端(UDP通信)

TFTP協議雖然簡單,但它涉及到網絡編程,特別是UDP套接字的使用。我們將用C語言模擬一個簡易的TFTP客戶端,它能夠向TFTP服務器發送**讀請求(RRQ)**并接收文件數據。

TFTP協議數據包結構(簡化):

TFTP協議定義了5種類型的報文:

  1. RRQ (Read Request):讀請求,客戶端請求從服務器下載文件。

  2. WRQ (Write Request):寫請求,客戶端請求向服務器上傳文件。

  3. DATA (數據):服務器向客戶端發送文件數據,或客戶端向服務器發送文件數據。

  4. ACK (Acknowledgment):確認報文,確認收到了數據包。

  5. ERROR (錯誤):錯誤報文,指示發生了錯誤。

RRQ報文格式:

字段

字節數

描述

Opcode

2

操作碼,RRQ為1

Filename

變長

請求的文件名,以NULL (0x00) 終止

Mode

變長

傳輸模式,通常為"netascii"或"octet",以NULL終止

DATA報文格式:

字段

字節數

描述

Opcode

2

操作碼,DATA為3

Block #

2

數據塊編號,從1開始遞增

Data

0-512

數據內容

ACK報文格式:

字段

字節數

描述

Opcode

2

操作碼,ACK為4

Block #

2

確認收到的數據塊編號

我們的C語言模擬器將實現以下簡化邏輯:

  1. 創建UDP套接字。

  2. 構建RRQ報文并發送給TFTP服務器。

  3. 循環接收DATA報文,并保存到本地文件。

  4. 每收到一個DATA報文,發送對應的ACK報文。

  5. 直到收到小于512字節的數據塊(表示文件結束)。

  6. 處理簡單的超時機制。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>     // For close()
#include <sys/socket.h> // For socket(), bind(), sendto(), recvfrom()
#include <netinet/in.h> // For sockaddr_in, htons(), htonl()
#include <arpa/inet.h>  // For inet_addr()
#include <errno.h>      // For errno
#include <sys/time.h>   // For select() timeout// --- TFTP協議宏定義 ---
#define TFTP_PORT 69             // TFTP服務器默認端口
#define TFTP_DATA_BLOCK_SIZE 512 // TFTP數據塊大小
#define TFTP_MAX_PACKET_SIZE (4 + TFTP_DATA_BLOCK_SIZE) // Opcode(2) + Block#(2) + Data(512)// TFTP操作碼 (Opcode)
#define TFTP_OPCODE_RRQ   1 // Read Request
#define TFTP_OPCODE_WRQ   2 // Write Request
#define TFTP_OPCODE_DATA  3 // Data
#define TFTP_OPCODE_ACK   4 // Acknowledgment
#define TFTP_OPCODE_ERROR 5 // Error// TFTP傳輸模式
#define TFTP_MODE_OCTET "octet" // 二進制模式
#define TFTP_MODE_NETASCII "netascii" // 文本模式// --- 錯誤碼定義 ---
#define ERR_NONE            0   // No error
#define ERR_NOT_DEFINED     1   // Not defined, see error message (if any).
#define ERR_FILE_NOT_FOUND  2   // File not found.
#define ERR_ACCESS_VIOLATION 3  // Access violation.
#define ERR_DISK_FULL       4   // Disk full or allocation exceeded.
#define ERR_ILLEGAL_OPERATION 5 // Illegal TFTP operation.
#define ERR_UNKNOWN_TRANSFER_ID 6 // Unknown transfer ID.
#define ERR_FILE_ALREADY_EXISTS 7 // File already exists.
#define ERR_NO_SUCH_USER    8   // No such user.// --- 函數:發送TFTP RRQ (Read Request) 報文 ---
// 構建并發送一個RRQ報文到TFTP服務器
// 參數:
//   sockfd: 套接字文件描述符
//   server_addr: 服務器地址結構體
//   filename: 請求下載的文件名
//   mode: 傳輸模式 (例如 "octet")
// 返回值: 0 成功, -1 失敗
int send_tftp_rrq(int sockfd, struct sockaddr_in* server_addr, const char* filename, const char* mode) {char packet[TFTP_MAX_PACKET_SIZE];int offset = 0;// Opcode (2 bytes) - RRQ = 1uint16_t opcode = htons(TFTP_OPCODE_RRQ); // 轉換為網絡字節序memcpy(packet + offset, &opcode, 2);offset += 2;// Filename (variable length, null-terminated)strcpy(packet + offset, filename);offset += strlen(filename) + 1; // +1 for null terminator// Mode (variable length, null-terminated)strcpy(packet + offset, mode);offset += strlen(mode) + 1; // +1 for null terminatorprintf("[TFTP Client] 發送 RRQ 請求文件: '%s', 模式: '%s'\n", filename, mode);if (sendto(sockfd, packet, offset, 0, (struct sockaddr*)server_addr, sizeof(struct sockaddr_in)) == -1) {perror("[TFTP Client] sendto RRQ 失敗");return -1;}return 0;
}// --- 函數:發送TFTP ACK (Acknowledgment) 報文 ---
// 構建并發送一個ACK報文到TFTP服務器
// 參數:
//   sockfd: 套接字文件描述符
//   dest_addr: 目標地址結構體 (通常是TFTP服務器的臨時端口)
//   block_num: 確認的數據塊編號
// 返回值: 0 成功, -1 失敗
int send_tftp_ack(int sockfd, struct sockaddr_in* dest_addr, uint16_t block_num) {char packet[4]; // Opcode(2) + Block#(2)// Opcode (2 bytes) - ACK = 4uint16_t opcode = htons(TFTP_OPCODE_ACK);memcpy(packet, &opcode, 2);// Block # (2 bytes)uint16_t net_block_num = htons(block_num);memcpy(packet + 2, &net_block_num, 2);printf("[TFTP Client] 發送 ACK (塊號: %u)\n", block_num);if (sendto(sockfd, packet, 4, 0, (struct sockaddr*)dest_addr, sizeof(struct sockaddr_in)) == -1) {perror("[TFTP Client] sendto ACK 失敗");return -1;}return 0;
}// --- 函數:接收TFTP報文并處理 ---
// 接收來自TFTP服務器的報文,并解析其類型
// 參數:
//   sockfd: 套接字文件描述符
//   recv_buffer: 接收數據的緩沖區
//   buffer_size: 緩沖區大小
//   server_addr: 用于存儲TFTP服務器的臨時地址和端口 (重要,因為數據傳輸會使用新端口)
//   timeout_sec: 接收超時時間 (秒)
// 返回值: 接收到的字節數, -1 失敗, 0 超時
int receive_tftp_packet(int sockfd, char* recv_buffer, int buffer_size, struct sockaddr_in* server_addr, int timeout_sec) {socklen_t addr_len = sizeof(struct sockaddr_in);// 設置select的超時時間fd_set read_fds;struct timeval timeout;FD_ZERO(&read_fds);FD_SET(sockfd, &read_fds);timeout.tv_sec = timeout_sec;timeout.tv_usec = 0;int ret = select(sockfd + 1, &read_fds, NULL, NULL, &timeout);if (ret == -1) {perror("[TFTP Client] select 錯誤");return -1;} else if (ret == 0) {printf("[TFTP Client] 接收超時 (%d 秒)。\n", timeout_sec);return 0; // 超時} else {// 有數據可讀int bytes_received = recvfrom(sockfd, recv_buffer, buffer_size, 0, (struct sockaddr*)server_addr, &addr_len);if (bytes_received == -1) {perror("[TFTP Client] recvfrom 失敗");return -1;}return bytes_received;}
}// --- 函數:解析TFTP錯誤報文 ---
void parse_tftp_error(const char* packet, int packet_len) {if (packet_len < 4) {fprintf(stderr, "[TFTP Client] 接收到無效的錯誤報文 (長度不足)。\n");return;}uint16_t error_code = ntohs(*(uint16_t*)(packet + 2)); // 錯誤碼在Opcode后2字節const char* error_message = packet + 4; // 錯誤消息在錯誤碼后fprintf(stderr, "[TFTP Client] 接收到錯誤報文!錯誤碼: %u (%s), 錯誤信息: '%s'\n",error_code,(error_code == ERR_FILE_NOT_FOUND) ? "文件未找到" :(error_code == ERR_ACCESS_VIOLATION) ? "訪問違規" :(error_code == ERR_ILLEGAL_OPERATION) ? "非法操作" : "未知錯誤",error_message);
}// --- 主函數:TFTP客戶端下載文件 ---
// 模擬TFTP客戶端下載文件的過程
// 參數:
//   server_ip: TFTP服務器的IP地址
//   filename: 要下載的文件名
//   local_filename: 保存到本地的文件名
// 返回值: 0 成功, -1 失敗
int tftp_download_file(const char* server_ip, const char* filename, const char* local_filename) {int sockfd;struct sockaddr_in server_addr;char recv_buffer[TFTP_MAX_PACKET_SIZE];FILE* fp = NULL;uint16_t expected_block = 1; // 期望接收的數據塊編號int retries = 0; // 重試次數const int MAX_RETRIES = 5; // 最大重試次數// 1. 創建UDP套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == -1) {perror("[TFTP Client] 創建套接字失敗");return -1;}// 2. 配置服務器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(TFTP_PORT); // TFTP默認端口69if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) <= 0) {perror("[TFTP Client] 無效的服務器IP地址");close(sockfd);return -1;}// 3. 發送RRQ請求if (send_tftp_rrq(sockfd, &server_addr, filename, TFTP_MODE_OCTET) == -1) {close(sockfd);return -1;}// 4. 打開本地文件用于寫入fp = fopen(local_filename, "wb"); // 以二進制寫入模式打開if (fp == NULL) {perror("[TFTP Client] 打開本地文件失敗");close(sockfd);return -1;}// 5. 循環接收數據塊while (true) {int bytes_received = receive_tftp_packet(sockfd, recv_buffer, sizeof(recv_buffer), &server_addr, 5); // 5秒超時if (bytes_received == -1) { // 接收錯誤fclose(fp);close(sockfd);return -1;} else if (bytes_received == 0) { // 超時retries++;if (retries >= MAX_RETRIES) {fprintf(stderr, "[TFTP Client] 達到最大重試次數,文件下載失敗。\n");fclose(fp);close(sockfd);return -1;}printf("[TFTP Client] 超時,重發 ACK (塊號: %u) 或 RRQ (如果剛開始)。\n", expected_block - 1);// 重發上一個ACK (如果已經收到過數據),或者重發RRQ (如果還沒收到第一個數據包)if (expected_block == 1) { // 還沒收到第一個數據包,重發RRQif (send_tftp_rrq(sockfd, &server_addr, filename, TFTP_MODE_OCTET) == -1) {fclose(fp);close(sockfd);return -1;}} else { // 已經收到過數據,重發上一個ACKif (send_tftp_ack(sockfd, &server_addr, expected_block - 1) == -1) {fclose(fp);close(sockfd);return -1;}}continue; // 繼續等待}retries = 0; // 成功接收數據,重置重試計數uint16_t opcode = ntohs(*(uint16_t*)recv_buffer); // 獲取操作碼if (opcode == TFTP_OPCODE_DATA) {uint16_t block_num = ntohs(*(uint16_t*)(recv_buffer + 2)); // 獲取數據塊編號char* data_ptr = recv_buffer + 4; // 數據內容起始地址int data_len = bytes_received - 4; // 數據內容長度printf("[TFTP Client] 接收到 DATA 報文 (塊號: %u, 長度: %d)\n", block_num, data_len);if (block_num == expected_block) {// 收到期望的數據塊,寫入文件fwrite(data_ptr, 1, data_len, fp);send_tftp_ack(sockfd, &server_addr, block_num); // 發送ACK確認if (data_len < TFTP_DATA_BLOCK_SIZE) {// 收到小于512字節的數據塊,表示文件傳輸結束printf("[TFTP Client] 文件 '%s' 下載完成!\n", filename);break; // 退出循環}expected_block++; // 期望下一個數據塊} else if (block_num < expected_block) {// 收到重復的數據塊 (可能是ACK丟失導致服務器重發),重新發送ACKprintf("[TFTP Client] 收到重復 DATA 報文 (塊號: %u),重發 ACK (塊號: %u)。\n", block_num, block_num);send_tftp_ack(sockfd, &server_addr, block_num);} else {// 收到不期望的數據塊 (跳塊或亂序),發送錯誤報文 (本模擬簡化,直接報錯)fprintf(stderr, "[TFTP Client] 收到不期望的 DATA 報文 (塊號: %u),期望 %u。下載失敗。\n", block_num, expected_block);fclose(fp);close(sockfd);return -1;}} else if (opcode == TFTP_OPCODE_ERROR) {parse_tftp_error(recv_buffer, bytes_received);fclose(fp);close(sockfd);return -1;} else {fprintf(stderr, "[TFTP Client] 接收到未知操作碼報文: %u。下載失敗。\n", opcode);fclose(fp);close(sockfd);return -1;}}fclose(fp);close(sockfd);return 0;
}int main(int argc, char* argv[]) {if (argc != 4) {fprintf(stderr, "用法: %s <TFTP服務器IP> <遠程文件名> <本地保存文件名>\n", argv[0]);fprintf(stderr, "示例: %s 192.168.1.100 test.txt my_local_test.txt\n", argv[0]);return 1;}const char* server_ip = argv[1];const char* remote_filename = argv[2];const char* local_filename = argv[3];printf("====== 簡易TFTP客戶端模擬器 ======\n");printf("嘗試從 %s 下載文件 '%s' 到本地 '%s'...\n", server_ip, remote_filename, local_filename);if (tftp_download_file(server_ip, remote_filename, local_filename) == 0) {printf("\nTFTP文件下載成功!\n");} else {fprintf(stderr, "\nTFTP文件下載失敗!\n");return 1;}printf("====== 模擬結束 ======\n");return 0;
}

代碼分析與邏輯透析:

這份C語言代碼實現了一個簡易的TFTP客戶端,主要功能是向TFTP服務器發送讀請求(RRQ)并接收數據塊,最終將文件保存到本地。它模擬了TFTP協議的核心報文交互流程。

  1. 宏定義:

    • TFTP_PORT:TFTP服務器的默認端口(69)。

    • TFTP_DATA_BLOCK_SIZE:TFTP協議規定每個數據塊最大為512字節。

    • TFTP_MAX_PACKET_SIZE:計算了TFTP數據包的最大可能大小(操作碼+塊號+數據)。

    • TFTP_OPCODE_*:定義了TFTP協議的各種操作碼,用于識別報文類型。

    • TFTP_MODE_*:定義了傳輸模式,octet表示二進制模式。

    • ERR_*:定義了TFTP協議中可能出現的錯誤碼,用于解析錯誤報文。

  2. send_tftp_rrq 函數:

    • 功能: 構建并發送一個TFTP讀請求(RRQ)報文。

    • 報文結構: 按照TFTP RRQ報文的格式(Opcode + Filename + Mode),將數據填充到packet緩沖區。

      • htons(TFTP_OPCODE_RRQ)htons(host to network short)將主機的短整型字節序轉換為網絡字節序。網絡傳輸通常使用大端字節序,而不同CPU的主機字節序可能不同,所以進行轉換是必要的。

      • 文件名和模式字符串后都跟著一個NULL終止符,這是TFTP協議的規定。

    • sendto():用于發送UDP數據報。它不需要先建立連接,直接指定目標地址。

  3. send_tftp_ack 函數:

    • 功能: 構建并發送一個TFTP確認(ACK)報文。

    • 報文結構: 按照TFTP ACK報文的格式(Opcode + Block #),將數據填充到packet緩沖區。

      • htons(TFTP_OPCODE_ACK):操作碼。

      • htons(block_num):確認收到的數據塊編號,同樣需要轉換為網絡字節序。

  4. receive_tftp_packet 函數:

    • 功能: 接收TFTP服務器發送的報文。

    • select() 這是一個關鍵的系統調用,用于實現超時機制。在UDP通信中,客戶端發送請求后不能無限期等待響應,需要設置超時。

      • fd_set read_fds:文件描述符集合,這里只關心sockfd是否有數據可讀。

      • struct timeval timeout:設置超時時間。

      • select()會阻塞直到sockfd有數據可讀,或者超時時間到達。

    • recvfrom():用于接收UDP數據報,并同時獲取發送方的地址信息(server_addr)。注意: TFTP服務器在收到RRQ/WRQ后,會從一個新的臨時端口發送DATA/ACK報文,所以recvfrom返回的server_addr會包含這個新的端口號。后續的ACK報文需要發送到這個新端口。

  5. parse_tftp_error 函數:

    • 功能: 解析TFTP錯誤報文,并打印錯誤碼和錯誤信息。

  6. tftp_download_file 函數(核心下載邏輯):

    • 初始化: 創建UDP套接字,配置服務器地址。

    • 發送RRQ: 調用send_tftp_rrq發送讀請求。

    • 文件操作: fopen(local_filename, "wb")以二進制寫入模式打開本地文件,用于保存下載的數據。

    • 數據接收循環:

      • while(true):持續循環接收數據塊。

      • receive_tftp_packet():接收報文,并處理超時。

      • 超時重傳: 如果超時,會增加retries計數。如果達到最大重試次數,則下載失敗。否則,會重發上一個ACK(如果已經收到數據)或重發RRQ(如果還沒收到第一個數據包),以應對網絡丟包。

      • 報文類型判斷: 根據接收到的opcode判斷是DATA報文還是ERROR報文。

      • DATA報文處理:

        • 解析block_num

        • 塊號校驗: if (block_num == expected_block):這是TFTP協議中保證數據順序和完整性的關鍵。如果收到的塊號與期望的塊號一致,才將數據寫入文件,并遞增expected_block

        • fwrite():將數據寫入本地文件。

        • send_tftp_ack()每收到一個正確的DATA報文,就立即發送一個ACK報文確認。這是TFTP可靠性(雖然基于UDP)的實現方式。

        • 文件結束判斷: if (data_len < TFTP_DATA_BLOCK_SIZE):如果收到的數據塊長度小于512字節,表示這是文件的最后一個數據塊,文件傳輸完成,跳出循環。

        • 重復塊處理: else if (block_num < expected_block):如果收到小于期望塊號的報文,說明是服務器重發了之前的數據塊(可能是客戶端的ACK丟失了)。此時客戶端應該重新發送該塊號的ACK

        • 亂序/跳塊處理: elseblock_num > expected_block):收到大于期望塊號的報文,表示亂序或跳塊,本模擬簡化為報錯。

      • ERROR報文處理: 調用parse_tftp_error打印錯誤信息并返回失敗。

    • 資源清理: fclose(fp)關閉文件,close(sockfd)關閉套接字。

  7. main 函數:

    • 解析命令行參數:TFTP服務器IP、遠程文件名、本地保存文件名。

    • 調用tftp_download_file開始下載。

    • 打印下載結果。

如何使用這個C語言TFTP客戶端模擬器:

  1. 準備環境:

    • 確保你已經搭建好了TFTP服務器(如Ubuntu上的tftpd-hpa),并且TFTP根目錄中有你想要下載的文件(例如test.txt)。

    • 確保TFTP服務器的IP地址是可達的。

  2. 保存代碼: 將上述C代碼保存為 tftp_client_sim.c

  3. 編譯: gcc tftp_client_sim.c -o tftp_client_sim

  4. 運行:

    ./tftp_client_sim 192.168.1.100 test.txt downloaded_test.txt
    • 192.168.1.100替換為你的TFTP服務器實際IP。

    • test.txt是TFTP服務器根目錄下的文件。

    • downloaded_test.txt是文件下載到本地后的名稱。

  5. 觀察輸出: 你會看到客戶端發送RRQ,接收DATA,發送ACK的詳細過程。下載完成后,本地會生成downloaded_test.txt文件。

通過這個模擬器,你不僅能練習C語言的網絡編程(UDP套接字、字節序轉換、select超時),還能對TFTP協議的報文結構、傳輸流程和可靠性機制(基于ACK的確認)有一個深入的理解!

4.2 Linux NFS服務搭建及使用:網絡文件系統的“共享利器”

4.2.1 什么是NFS?為什么在嵌入式開發中常用?
  • NFS (Network File System),網絡文件系統,允許網絡上的計算機之間共享文件和目錄。它使得遠程目錄看起來就像是本地文件系統的一部分。

  • NFS基于RPC(Remote Procedure Call,遠程過程調用)協議工作,通常使用TCP協議,端口號為2049。

  • 與TFTP的區別和優勢:

特性

TFTP

NFS (Network File System)

傳輸層協議

UDP

TCP (通常)

功能

簡單文件傳輸(上傳/下載)

完整的遠程文件系統訪問,如同本地文件

安全性

無認證,不安全

基于IP地址/主機名認證,可配置更細粒度權限

目錄操作

不支持目錄列表

支持完整的目錄操作(創建、刪除、列出)

文件操作

只能傳輸整個文件

支持遠程文件的打開、讀寫、seek等操作

應用場景

Bootloader階段的內核/文件系統下載、固件升級

嵌入式根文件系統掛載、遠程應用開發調試、共享開發資料

  • 為什么在嵌入式開發中常用?

    • 遠程根文件系統: 最重要的應用!在嵌入式開發中,我們可以將開發機上的一個目錄作為目標板的根文件系統,通過NFS掛載到目標板上。這樣,在開發機上修改文件(如應用程序、配置文件),目標板無需重新燒寫即可立即生效,大大加快了開發調試周期。

    • 應用程序開發與調試: 可以在開發機上編譯好應用程序,然后直接將可執行文件放到NFS共享目錄中。目標板啟動后,可以直接運行這些遠程的可執行文件,方便調試。

    • 共享開發資料: 團隊成員之間可以共享代碼庫、文檔、測試數據等。

    • 節省目標板存儲: 目標板無需內置大容量存儲來存放整個根文件系統,只需一個精簡的Bootloader和內核,即可通過NFS掛載遠程文件系統。

思維導圖:NFS在嵌入式中的應用

graph TDA[NFS在嵌入式中的應用] --> B[遠程根文件系統]B --> B1[開發機作為目標板的根文件系統]B --> B2[修改文件立即生效,無需燒寫]B --> B3[加速開發調試周期]A --> C[遠程應用程序開發與調試]C --> C1[開發機編譯,目標板直接運行]C --> C2[方便調試和測試]A --> D[共享開發資料]D --> D1[團隊協作共享代碼、文檔]A --> E[節省目標板存儲]E --> E1[目標板無需大容量存儲]
4.2.2 NFS服務器搭建(Ubuntu為例)

在開發過程中,我們通常會在開發機(Host)上搭建NFS服務器,用于向目標板(Target)提供共享文件系統。

步驟1:安裝NFS服務器軟件

sudo apt update
sudo apt install nfs-kernel-server

步驟2:配置NFS共享目錄

NFS共享目錄的配置文件是 /etc/exports

sudo vim /etc/exports

在文件末尾添加一行,定義要共享的目錄、允許訪問的客戶端以及權限。

# /etc/exports
# 格式: <共享目錄> <客戶端IP或網段>(權限選項,權限選項,...)/home/your_user/nfsroot *(rw,sync,no_subtree_check,no_root_squash)
# 或者更具體地指定客戶端IP,例如:
# /home/your_user/nfsroot 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)

參數解釋:

  • /home/your_user/nfsroot:這是你希望共享的本地目錄。請將your_user替換為你的實際用戶名。這個目錄將作為目標板的根文件系統或應用程序目錄。

  • *:表示允許任何客戶端IP地址訪問。在開發環境中為了方便,可以使用*。在生產環境中,強烈建議替換為具體的客戶端IP地址或IP網段(例如192.168.1.0/24)。

  • 權限選項:

    • rw:讀寫權限。允許客戶端對共享目錄進行讀寫操作。

    • ro:只讀權限。

    • sync:同步寫入。數據寫入NFS服務器時,會立即寫入磁盤,而不是先寫入緩存。這保證了數據的一致性,但可能會降低性能。

    • async:異步寫入。數據先寫入緩存,再寫入磁盤。性能較好,但有數據丟失風險。

    • no_subtree_check:禁用子目錄檢查。當共享一個父目錄下的子目錄時,NFS會檢查每個子目錄的父目錄是否被導出。禁用此選項可以提高性能,但可能存在安全隱患(在某些特定配置下)。對于根文件系統共享,通常建議禁用。

    • subtree_check:啟用子目錄檢查(默認)。

    • no_root_squash非常重要! 禁用root用戶映射。默認情況下,NFS會將客戶端的root用戶映射為NFS服務器上的匿名用戶(通常是nfsnobody),以增強安全性。但對于嵌入式根文件系統,目標板上的root用戶需要對文件系統有完整的權限,所以必須禁用此選項。

    • root_squash:啟用root用戶映射(默認)。

步驟3:創建NFS共享目錄并設置權限

mkdir -p /home/your_user/nfsroot # 創建目錄
sudo chmod -R 777 /home/your_user/nfsroot # 給予所有用戶讀寫執行權限 (方便測試,生產環境按需調整)
# 或者更嚴格的權限,例如:
# sudo chown -R your_user:your_group /home/your_user/nfsroot
# sudo chmod -R 755 /home/your_user/nfsroot

步驟4:導出共享目錄并啟動/重啟NFS服務

修改/etc/exports文件后,需要重新導出共享目錄并重啟NFS服務。

sudo exportfs -arv # 重新導出所有共享目錄 (-a: all, -r: re-export, -v: verbose)
sudo systemctl restart nfs-kernel-server # 重啟NFS服務
sudo systemctl enable nfs-kernel-server # 設置開機自啟動 (可選)
sudo systemctl status nfs-kernel-server  # 查看服務狀態

步驟5:防火墻設置(如果需要)

NFS服務需要開放多個端口。最簡單的方法是允許NFS服務通過防火墻。

sudo ufw allow nfs
sudo ufw reload
sudo ufw status # 檢查防火墻狀態

如果ufw allow nfs不起作用,或者你想手動開放端口,NFS通常使用以下端口:

  • portmapper (rpcbind): TCP/UDP 111

  • nfsd: TCP/UDP 2049

  • mountd: TCP/UDP 隨機端口 (通常在1024以上,但可以通過配置固定)

  • statd: TCP/UDP 隨機端口

  • lockd: TCP/UDP 隨機端口

為了避免隨機端口問題,可以嘗試以下命令開放相關服務:

sudo ufw allow portmapper
sudo ufw allow nfs
sudo ufw allow mountd
# 如果還不行,可以嘗試開放所有相關端口,但通常不推薦
# sudo ufw allow from any to any port 111 proto tcp
# sudo ufw allow from any to any port 111 proto udp
# sudo ufw allow from any to any port 2049 proto tcp
# sudo ufw allow from any to any port 2049 proto udp
# sudo ufw allow from any to any port 32767 proto tcp # mountd通常的隨機端口
# sudo ufw allow from any to any port 32767 proto udp
4.2.3 NFS客戶端使用

NFS客戶端通常是你的嵌入式目標板。這里我們以另一臺Linux機器作為客戶端進行模擬。

步驟1:安裝NFS客戶端軟件

sudo apt update
sudo apt install nfs-common

步驟2:手動掛載NFS共享目錄

假設NFS服務器IP地址是 192.168.1.100,共享目錄是 /home/your_user/nfsroot

  1. 在客戶端創建掛載點:

    sudo mkdir -p /mnt/nfs_share
  2. 執行掛載命令:

    sudo mount -t nfs 192.168.1.100:/home/your_user/nfsroot /mnt/nfs_share
    • -t nfs:指定文件系統類型為NFS。

    • 192.168.1.100:/home/your_user/nfsroot:NFS服務器的IP地址和共享目錄的路徑。

    • /mnt/nfs_share:客戶端本地的掛載點。

  3. 驗證掛載:

    df -h # 查看磁盤使用情況,應該能看到NFS掛載點
    ls /mnt/nfs_share # 查看共享目錄內容
    • 你可以在/mnt/nfs_share中創建、修改、刪除文件,這些操作會同步到NFS服務器的/home/your_user/nfsroot目錄。

  4. 取消掛載:

    sudo umount /mnt/nfs_share

步驟3:開機自動掛載(/etc/fstab

在嵌入式目標板上,我們通常希望NFS共享目錄在開機時自動掛載。這可以通過修改/etc/fstab文件來實現。

sudo vim /etc/fstab

在文件末尾添加一行:

# /etc/fstab
# 格式: <NFS服務器地址:共享目錄> <本地掛載點> <文件系統類型> <掛載選項> <dump> <fsck>192.168.1.100:/home/your_user/nfsroot /mnt/nfs_share nfs defaults 0 0

選項解釋:

  • defaults:包含rw, suid, dev, exec, auto, nouser, async等默認選項。

  • _netdev:表示只有在網絡可用時才掛載。這對于網絡啟動的嵌入式設備非常重要。

  • soft:軟掛載,如果NFS服務器無響應,客戶端會超時并返回錯誤。

  • hard:硬掛載,如果NFS服務器無響應,客戶端會無限期重試,直到服務器響應。

  • intr:允許中斷硬掛載。

添加后測試:

sudo mount -a # 嘗試掛載/etc/fstab中所有未掛載的文件系統
df -h # 檢查是否成功掛載

注意事項:

  • IP地址: 確保NFS服務器的IP地址是固定的,或者使用主機名(如果DNS配置正確)。

  • 網絡連通性: 確保客戶端和服務器之間網絡是通的。

  • 權限: 確保NFS服務器共享目錄的權限設置正確,允許客戶端進行所需操作。

  • no_root_squash 如果目標板以root用戶身份訪問NFS共享目錄,并且需要root權限,務必在服務器端設置no_root_squash

4.2.4 C語言模擬:簡易NFS客戶端(遠程文件讀寫概念模擬)

NFS協議非常復雜,直接用C語言實現一個完整的NFS客戶端幾乎是不可能的(涉及到復雜的RPC、XDR、文件系統抽象等)。但是,我們可以通過模擬的方式,用C語言實現一個程序,來概念性地演示遠程文件讀寫的工作原理。

這個模擬器將不涉及真正的NFS協議棧,而是通過TCP套接字連接到一個簡易的“文件服務器”(也用C語言實現),然后通過自定義的簡單協議進行文件操作。這能幫助你理解:

  1. 客戶端如何通過網絡連接到服務器。

  2. 客戶端如何向服務器發送文件操作請求(例如“打開文件”、“讀取數據”、“寫入數據”、“關閉文件”)。

  3. 服務器如何接收請求并執行相應的本地文件操作。

  4. 數據如何在網絡上進行傳輸。

模擬協議定義:

我們定義一個非常簡單的協議,通過TCP連接傳輸:

字段

字節數

描述

Opcode

1

操作碼:1=OPEN, 2=READ, 3=WRITE, 4=CLOSE

FilenameLen

1

文件名長度

Filename

變長

文件名

DataLen

4

數據長度 (僅READ/WRITE請求和響應)

Data

變長

數據內容 (僅READ/WRITE請求和響應)

Status

1

響應狀態:0=成功, 1=失敗 (僅服務器響應)

C語言代碼:簡易NFS服務器模擬器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/stat.h> // For mkdir
#include <dirent.h>   // For opendir, readdir// --- 宏定義 ---
#define SERVER_PORT 8888         // 模擬NFS服務器監聽端口
#define MAX_BUFFER_SIZE 1024     // 傳輸緩沖區大小
#define MAX_FILENAME_LEN 255     // 文件名最大長度
#define MAX_SHARED_PATH_LEN 256  // 共享目錄路徑最大長度// --- 模擬協議操作碼 ---
#define OP_OPEN  1
#define OP_READ  2
#define OP_WRITE 3
#define OP_CLOSE 4
#define OP_LIST  5 // 新增:列出目錄內容// --- 模擬協議響應狀態 ---
#define STATUS_SUCCESS 0
#define STATUS_FAILURE 1// --- 全局變量:模擬共享目錄 ---
char shared_root_path[MAX_SHARED_PATH_LEN] = "./sim_nfs_root";// --- 輔助函數:構建完整文件路徑 (防止路徑穿越) ---
// 確保客戶端請求的文件路徑在共享根目錄下
// 返回值: 成功返回完整路徑指針,失敗返回NULL
char* get_safe_filepath(const char* filename) {static char full_path[MAX_SHARED_PATH_LEN + MAX_FILENAME_LEN];snprintf(full_path, sizeof(full_path), "%s/%s", shared_root_path, filename);// 檢查路徑是否以共享根目錄開頭 (防止 ../../ 路徑穿越)if (strncmp(full_path, shared_root_path, strlen(shared_root_path)) != 0) {fprintf(stderr, "[NFS Server] 警告: 路徑穿越嘗試: %s\n", full_path);return NULL;}// 進一步檢查規范化路徑,確保沒有中間的 /../ 或 /./// 實際生產級服務器會使用 realpath() 或更復雜的路徑規范化char canonical_path[MAX_SHARED_PATH_LEN + MAX_FILENAME_LEN];if (realpath(full_path, canonical_path) == NULL) {// 如果文件不存在,realpath會失敗,但這不是路徑穿越// 只有當full_path本身有問題時才表示穿越if (errno == ENOENT) { // 文件不存在return full_path; // 允許繼續嘗試打開不存在的文件}fprintf(stderr, "[NFS Server] 警告: 路徑規范化失敗或無效路徑: %s (errno: %d)\n", full_path, errno);return NULL;}if (strncmp(canonical_path, shared_root_path, strlen(shared_root_path)) != 0) {fprintf(stderr, "[NFS Server] 警告: 路徑穿越嘗試 (規范化后): %s\n", canonical_path);return NULL;}strcpy(full_path, canonical_path); // 使用規范化后的路徑return full_path;
}// --- 函數:處理客戶端請求 ---
void handle_client_request(int client_sockfd) {char buffer[MAX_BUFFER_SIZE];ssize_t bytes_received;FILE* opened_file = NULL; // 服務器端打開的文件句柄printf("[NFS Server] 客戶端已連接。\n");while (true) {bytes_received = recv(client_sockfd, buffer, MAX_BUFFER_SIZE, 0);if (bytes_received <= 0) {if (bytes_received == 0) {printf("[NFS Server] 客戶端斷開連接。\n");} else {perror("[NFS Server] recv 錯誤");}break;}uint8_t opcode = buffer[0];uint8_t filename_len = buffer[1];char filename[MAX_FILENAME_LEN];memset(filename, 0, sizeof(filename));strncpy(filename, buffer + 2, filename_len);filename[filename_len] = '\0'; // 確保文件名終止char* safe_filepath = get_safe_filepath(filename);if (safe_filepath == NULL) {// 發送失敗響應uint8_t response[2] = {opcode, STATUS_FAILURE};send(client_sockfd, response, sizeof(response), 0);continue;}printf("[NFS Server] 收到請求: Opcode=%u, 文件名='%s'\n", opcode, filename);switch (opcode) {case OP_OPEN: {char mode_str[4]; // "rb", "wb", "ab"uint8_t open_mode = buffer[2 + filename_len]; // 0=read, 1=write, 2=appendif (open_mode == 0) strcpy(mode_str, "rb");else if (open_mode == 1) strcpy(mode_str, "wb");else if (open_mode == 2) strcpy(mode_str, "ab");else {fprintf(stderr, "[NFS Server] 錯誤: 無效的打開模式。\n");uint8_t response[2] = {opcode, STATUS_FAILURE};send(client_sockfd, response, sizeof(response), 0);break;}opened_file = fopen(safe_filepath, mode_str);uint8_t status = (opened_file != NULL) ? STATUS_SUCCESS : STATUS_FAILURE;uint8_t response[2] = {opcode, status};send(client_sockfd, response, sizeof(response), 0);printf("[NFS Server] OPEN '%s' (%s) -> %s\n", filename, mode_str, (status == STATUS_SUCCESS) ? "成功" : "失敗");break;}case OP_READ: {uint32_t bytes_to_read = ntohl(*(uint32_t*)(buffer + 2 + filename_len)); // 從請求中獲取要讀取的字節數char read_buffer[MAX_BUFFER_SIZE];uint8_t response[MAX_BUFFER_SIZE + 5]; // Opcode(1) + Status(1) + DataLen(4) + Data(MAX_BUFFER_SIZE)if (opened_file == NULL) {fprintf(stderr, "[NFS Server] 錯誤: 文件未打開,無法讀取。\n");response[0] = opcode; response[1] = STATUS_FAILURE;uint32_t zero_len = 0; memcpy(response + 2, &zero_len, 4);send(client_sockfd, response, 6, 0); // 發送失敗響應break;}// 實際讀取的字節數size_t actual_read_bytes = fread(read_buffer, 1, bytes_to_read, opened_file);uint8_t status = STATUS_SUCCESS;if (ferror(opened_file)) {status = STATUS_FAILURE;perror("[NFS Server] fread 錯誤");}response[0] = opcode;response[1] = status;uint32_t net_actual_read_bytes = htonl(actual_read_bytes);memcpy(response + 2, &net_actual_read_bytes, 4);memcpy(response + 6, read_buffer, actual_read_bytes);send(client_sockfd, response, 6 + actual_read_bytes, 0);printf("[NFS Server] READ '%s' -> %zu 字節 (%s)\n", filename, actual_read_bytes, (status == STATUS_SUCCESS) ? "成功" : "失敗");break;}case OP_WRITE: {uint32_t data_len = ntohl(*(uint32_t*)(buffer + 2 + filename_len));char* data_ptr = buffer + 2 + filename_len + 4; // 數據起始位置uint8_t response[MAX_BUFFER_SIZE + 5]; // Opcode(1) + Status(1) + WrittenLen(4)if (opened_file == NULL) {fprintf(stderr, "[NFS Server] 錯誤: 文件未打開,無法寫入。\n");response[0] = opcode; response[1] = STATUS_FAILURE;uint32_t zero_len = 0; memcpy(response + 2, &zero_len, 4);send(client_sockfd, response, 6, 0);break;}size_t actual_written_bytes = fwrite(data_ptr, 1, data_len, opened_file);uint8_t status = STATUS_SUCCESS;if (ferror(opened_file)) {status = STATUS_FAILURE;perror("[NFS Server] fwrite 錯誤");} else {fflush(opened_file); // 確保數據寫入磁盤}response[0] = opcode;response[1] = status;uint32_t net_actual_written_bytes = htonl(actual_written_bytes);memcpy(response + 2, &net_actual_written_bytes, 4);send(client_sockfd, response, 6, 0);printf("[NFS Server] WRITE '%s' -> %zu 字節 (%s)\n", filename, actual_written_bytes, (status == STATUS_SUCCESS) ? "成功" : "失敗");break;}case OP_CLOSE: {uint8_t status = STATUS_SUCCESS;if (opened_file != NULL) {if (fclose(opened_file) != 0) {status = STATUS_FAILURE;perror("[NFS Server] fclose 錯誤");}opened_file = NULL;} else {fprintf(stderr, "[NFS Server] 警告: 嘗試關閉未打開的文件。\n");}uint8_t response[2] = {opcode, status};send(client_sockfd, response, sizeof(response), 0);printf("[NFS Server] CLOSE '%s' -> %s\n", filename, (status == STATUS_SUCCESS) ? "成功" : "失敗");break;}case OP_LIST: {DIR *dir;struct dirent *entry;char list_buffer[MAX_BUFFER_SIZE];int current_len = 0;uint8_t response[MAX_BUFFER_SIZE + 2]; // Opcode(1) + Status(1) + Datadir = opendir(safe_filepath);if (dir == NULL) {response[0] = opcode; response[1] = STATUS_FAILURE;send(client_sockfd, response, 2, 0);perror("[NFS Server] opendir 錯誤");break;}while ((entry = readdir(dir)) != NULL) {if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {continue;}int item_len = strlen(entry->d_name);if (current_len + item_len + 1 >= MAX_BUFFER_SIZE) { // +1 for newline// 緩沖區滿了,發送當前內容并清空response[0] = opcode; response[1] = STATUS_SUCCESS;memcpy(response + 2, list_buffer, current_len);send(client_sockfd, response, 2 + current_len, 0);current_len = 0;list_buffer[0] = '\0';}strncat(list_buffer, entry->d_name, MAX_BUFFER_SIZE - current_len - 1);strncat(list_buffer, "\n", MAX_BUFFER_SIZE - strlen(list_buffer) - 1);current_len = strlen(list_buffer);}closedir(dir);// 發送剩余內容response[0] = opcode; response[1] = STATUS_SUCCESS;memcpy(response + 2, list_buffer, current_len);send(client_sockfd, response, 2 + current_len, 0);printf("[NFS Server] LIST '%s' -> 成功 (發送 %d 字節列表)\n", filename, current_len);break;}default:fprintf(stderr, "[NFS Server] 未知操作碼: %u\n", opcode);uint8_t response[2] = {opcode, STATUS_FAILURE};send(client_sockfd, response, sizeof(response), 0);break;}}if (opened_file != NULL) {fclose(opened_file); // 確保文件關閉}close(client_sockfd); // 關閉客戶端套接字
}int main(int argc, char* argv[]) {int listen_sockfd, client_sockfd;struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len = sizeof(client_addr);// 檢查并創建共享目錄if (mkdir(shared_root_path, 0777) == -1 && errno != EEXIST) {perror("[NFS Server] 創建共享目錄失敗");return 1;}printf("[NFS Server] 共享根目錄: %s\n", shared_root_path);printf("[NFS Server] 請在 %s 目錄下放置文件供客戶端訪問。\n", shared_root_path);// 1. 創建監聽套接字listen_sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP套接字if (listen_sockfd == -1) {perror("[NFS Server] 創建監聽套接字失敗");return 1;}// 允許端口重用 (解決 Address already in use 問題)int optval = 1;if (setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {perror("[NFS Server] setsockopt SO_REUSEADDR 失敗");close(listen_sockfd);return 1;}// 2. 綁定地址和端口memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);server_addr.sin_addr.s_addr = INADDR_ANY; // 監聽所有可用IP地址if (bind(listen_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("[NFS Server] 綁定地址失敗");close(listen_sockfd);return 1;}// 3. 監聽連接if (listen(listen_sockfd, 5) == -1) { // 最多5個待處理連接perror("[NFS Server] 監聽失敗");close(listen_sockfd);return 1;}printf("====== 簡易NFS服務器模擬器 ======\n");printf("NFS服務器正在監聽端口 %d...\n", SERVER_PORT);while (true) {// 4. 接受客戶端連接client_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &client_addr_len);if (client_sockfd == -1) {perror("[NFS Server] 接受連接失敗");continue;}char client_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN);printf("[NFS Server] 接收到來自 %s:%d 的連接。\n", client_ip, ntohs(client_addr.sin_port));// 5. 處理客戶端請求 (多線程/多進程更佳,這里簡化為單線程阻塞處理)handle_client_request(client_sockfd);}close(listen_sockfd);printf("====== 服務器退出 ======\n");return 0;
}

C語言代碼:簡易NFS客戶端模擬器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>// --- 宏定義 ---
#define SERVER_PORT 8888         // 模擬NFS服務器監聽端口
#define MAX_BUFFER_SIZE 1024     // 傳輸緩沖區大小
#define MAX_FILENAME_LEN 255     // 文件名最大長度// --- 模擬協議操作碼 ---
#define OP_OPEN  1
#define OP_READ  2
#define OP_WRITE 3
#define OP_CLOSE 4
#define OP_LIST  5// --- 模擬協議響應狀態 ---
#define STATUS_SUCCESS 0
#define STATUS_FAILURE 1// --- 函數:發送請求并接收響應 ---
// 通用函數,用于向服務器發送請求并等待響應
// 參數:
//   sockfd: 套接字文件描述符
//   request_buffer: 請求數據緩沖區
//   request_len: 請求數據長度
//   response_buffer: 接收響應的緩沖區
//   response_buffer_size: 響應緩沖區大小
// 返回值: 接收到的響應字節數, -1 失敗
ssize_t send_request_and_receive_response(int sockfd, const char* request_buffer, size_t request_len,char* response_buffer, size_t response_buffer_size) {if (send(sockfd, request_buffer, request_len, 0) == -1) {perror("[NFS Client] 發送請求失敗");return -1;}ssize_t bytes_received = recv(sockfd, response_buffer, response_buffer_size, 0);if (bytes_received == -1) {perror("[NFS Client] 接收響應失敗");} else if (bytes_received == 0) {printf("[NFS Client] 服務器斷開連接。\n");}return bytes_received;
}// --- 函數:模擬遠程文件打開 ---
// 參數:
//   sockfd: 套接字文件描述符
//   filename: 要打開的文件名
//   mode: 0=read, 1=write, 2=append
// 返回值: true 成功, false 失敗
bool remote_open(int sockfd, const char* filename, uint8_t mode) {char request[MAX_BUFFER_SIZE];int offset = 0;request[offset++] = OP_OPEN; // Opcoderequest[offset++] = (uint8_t)strlen(filename); // FilenameLenstrncpy(request + offset, filename, MAX_FILENAME_LEN - 1); // Filenameoffset += strlen(filename);request[offset++] = mode; // Open Modechar response[MAX_BUFFER_SIZE];ssize_t bytes_received = send_request_and_receive_response(sockfd, request, offset, response, sizeof(response));if (bytes_received == 2 && response[0] == OP_OPEN && response[1] == STATUS_SUCCESS) {printf("[NFS Client] 遠程文件 '%s' 打開成功 (模式: %u)。\n", filename, mode);return true;} else {fprintf(stderr, "[NFS Client] 遠程文件 '%s' 打開失敗。\n", filename);return false;}
}// --- 函數:模擬遠程文件讀取 ---
// 參數:
//   sockfd: 套接字文件描述符
//   filename: 文件名 (用于日志)
//   read_buffer: 存儲讀取數據的緩沖區
//   bytes_to_read: 期望讀取的字節數
// 返回值: 實際讀取的字節數, -1 失敗
ssize_t remote_read(int sockfd, const char* filename, char* read_buffer, uint32_t bytes_to_read) {char request[MAX_BUFFER_SIZE];int offset = 0;request[offset++] = OP_READ; // Opcoderequest[offset++] = (uint8_t)strlen(filename); // FilenameLen (用于服務器識別是哪個文件,雖然已打開)strncpy(request + offset, filename, MAX_FILENAME_LEN - 1);offset += strlen(filename);uint32_t net_bytes_to_read = htonl(bytes_to_read); // 轉換為網絡字節序memcpy(request + offset, &net_bytes_to_read, 4);offset += 4;char response[MAX_BUFFER_SIZE + 5]; // Opcode(1) + Status(1) + DataLen(4) + Datassize_t bytes_received = send_request_and_receive_response(sockfd, request, offset, response, sizeof(response));if (bytes_received >= 6 && response[0] == OP_READ && response[1] == STATUS_SUCCESS) {uint32_t actual_read_bytes = ntohl(*(uint32_t*)(response + 2)); // 實際讀取的字節數memcpy(read_buffer, response + 6, actual_read_bytes);read_buffer[actual_read_bytes] = '\0'; // 確保字符串終止printf("[NFS Client] 遠程文件 '%s' 讀取 %u 字節。\n", filename, actual_read_bytes);return actual_read_bytes;} else {fprintf(stderr, "[NFS Client] 遠程文件 '%s' 讀取失敗。\n", filename);return -1;}
}// --- 函數:模擬遠程文件寫入 ---
// 參數:
//   sockfd: 套接字文件描述符
//   filename: 文件名 (用于日志)
//   write_buffer: 待寫入的數據緩沖區
//   bytes_to_write: 期望寫入的字節數
// 返回值: 實際寫入的字節數, -1 失敗
ssize_t remote_write(int sockfd, const char* filename, const char* write_buffer, uint32_t bytes_to_write) {char request[MAX_BUFFER_SIZE];int offset = 0;request[offset++] = OP_WRITE; // Opcoderequest[offset++] = (uint8_t)strlen(filename); // FilenameLenstrncpy(request + offset, filename, MAX_FILENAME_LEN - 1);offset += strlen(filename);uint32_t net_bytes_to_write = htonl(bytes_to_write);memcpy(request + offset, &net_bytes_to_write, 4);offset += 4;memcpy(request + offset, write_buffer, bytes_to_write);offset += bytes_to_write;char response[MAX_BUFFER_SIZE]; // Opcode(1) + Status(1) + WrittenLen(4)ssize_t bytes_received = send_request_and_receive_response(sockfd, request, offset, response, sizeof(response));if (bytes_received == 6 && response[0] == OP_WRITE && response[1] == STATUS_SUCCESS) {uint32_t actual_written_bytes = ntohl(*(uint32_t*)(response + 2));printf("[NFS Client] 遠程文件 '%s' 寫入 %u 字節。\n", filename, actual_written_bytes);return actual_written_bytes;} else {fprintf(stderr, "[NFS Client] 遠程文件 '%s' 寫入失敗。\n", filename);return -1;}
}// --- 函數:模擬遠程文件關閉 ---
// 參數:
//   sockfd: 套接字文件描述符
//   filename: 文件名 (用于日志)
// 返回值: true 成功, false 失敗
bool remote_close(int sockfd, const char* filename) {char request[MAX_BUFFER_SIZE];int offset = 0;request[offset++] = OP_CLOSE; // Opcoderequest[offset++] = (uint8_t)strlen(filename); // FilenameLenstrncpy(request + offset, filename, MAX_FILENAME_LEN - 1);offset += strlen(filename);char response[MAX_BUFFER_SIZE];ssize_t bytes_received = send_request_and_receive_response(sockfd, request, offset, response, sizeof(response));if (bytes_received == 2 && response[0] == OP_CLOSE && response[1] == STATUS_SUCCESS) {printf("[NFS Client] 遠程文件 '%s' 關閉成功。\n", filename);return true;} else {fprintf(stderr, "[NFS Client] 遠程文件 '%s' 關閉失敗。\n", filename);return false;}
}// --- 函數:模擬遠程目錄列表 ---
// 參數:
//   sockfd: 套接字文件描述符
//   dirname: 要列出的目錄名
// 返回值: true 成功, false 失敗
bool remote_list_dir(int sockfd, const char* dirname) {char request[MAX_BUFFER_SIZE];int offset = 0;request[offset++] = OP_LIST; // Opcoderequest[offset++] = (uint8_t)strlen(dirname); // FilenameLen (這里是目錄名長度)strncpy(request + offset, dirname, MAX_FILENAME_LEN - 1);offset += strlen(dirname);char response[MAX_BUFFER_SIZE + 2]; // Opcode(1) + Status(1) + Datassize_t bytes_received = send_request_and_receive_response(sockfd, request, offset, response, sizeof(response));if (bytes_received >= 2 && response[0] == OP_LIST && response[1] == STATUS_SUCCESS) {printf("[NFS Client] 遠程目錄 '%s' 內容:\n", dirname);if (bytes_received > 2) {printf("%s\n", response + 2); // 打印列表內容} else {printf("(目錄為空或無內容)\n");}return true;} else {fprintf(stderr, "[NFS Client] 遠程目錄 '%s' 列表失敗。\n", dirname);return false;}
}int main(int argc, char* argv[]) {if (argc != 2) {fprintf(stderr, "用法: %s <NFS服務器IP>\n", argv[0]);fprintf(stderr, "示例: %s 127.0.0.1\n", argv[0]);return 1;}const char* server_ip = argv[1];int sockfd;struct sockaddr_in server_addr;// 1. 創建TCP套接字sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP套接字if (sockfd == -1) {perror("[NFS Client] 創建套接字失敗");return 1;}// 2. 配置服務器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) <= 0) {perror("[NFS Client] 無效的服務器IP地址");close(sockfd);return 1;}// 3. 連接到服務器if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("[NFS Client] 連接服務器失敗");close(sockfd);return 1;}printf("====== 簡易NFS客戶端模擬器 ======\n");printf("已連接到 NFS 服務器 %s:%d。\n", server_ip, SERVER_PORT);char read_buffer[MAX_BUFFER_SIZE];char write_data[] = "Hello from NFS client! This is a test line.\n";char append_data[] = "Appending another line.\n";// 模擬操作:// 1. 列出根目錄內容remote_list_dir(sockfd, ".");// 2. 寫入一個新文件printf("\n--- 模擬寫入文件 'test_nfs_write.txt' ---\n");if (remote_open(sockfd, "test_nfs_write.txt", 1 /* write mode */)) {remote_write(sockfd, "test_nfs_write.txt", write_data, strlen(write_data));remote_close(sockfd, "test_nfs_write.txt");}// 3. 追加內容到文件printf("\n--- 模擬追加內容到文件 'test_nfs_write.txt' ---\n");if (remote_open(sockfd, "test_nfs_write.txt", 2 /* append mode */)) {remote_write(sockfd, "test_nfs_write.txt", append_data, strlen(append_data));remote_close(sockfd, "test_nfs_write.txt");}// 4. 讀取文件內容printf("\n--- 模擬讀取文件 'test_nfs_write.txt' ---\n");if (remote_open(sockfd, "test_nfs_write.txt", 0 /* read mode */)) {ssize_t bytes_read = remote_read(sockfd, "test_nfs_write.txt", read_buffer, sizeof(read_buffer) - 1);if (bytes_read != -1) {printf("讀取到的內容:\n%s\n", read_buffer);}remote_close(sockfd, "test_nfs_write.txt");}// 5. 嘗試讀取一個不存在的文件printf("\n--- 模擬讀取不存在的文件 'non_existent.txt' ---\n");remote_open(sockfd, "non_existent.txt", 0); // 應該失敗// 6. 再次列出根目錄內容,確認新文件printf("\n--- 再次列出根目錄內容 ---\n");remote_list_dir(sockfd, ".");close(sockfd);printf("====== 模擬結束 ======\n");return 0;
}

代碼分析與邏輯透析:

這兩段C語言代碼共同構成了一個簡易的NFS(網絡文件系統)模擬器。它不實現真正的NFS協議,而是通過自定義的TCP協議,模擬了客戶端和服務器之間遠程文件操作(打開、讀、寫、關、列表)的交互過程。這對于理解NFS的**“遠程文件訪問”概念**非常有幫助。

NFS服務器模擬器 (sim_nfs_server.c):

  1. 宏定義與全局變量:

    • SERVER_PORT:服務器監聽的端口。

    • MAX_BUFFER_SIZE:用于網絡傳輸的緩沖區大小。

    • MAX_FILENAME_LENMAX_SHARED_PATH_LEN:文件路徑和共享路徑的最大長度。

    • OP_*:定義了客戶端請求的操作碼(OPEN, READ, WRITE, CLOSE, LIST)。

    • STATUS_*:定義了服務器響應的狀態碼(SUCCESS, FAILURE)。

    • shared_root_path:服務器上被共享的根目錄,所有操作都在這個目錄下進行。

  2. get_safe_filepath 函數:

    • 安全性核心! 這是一個非常重要的輔助函數,用于防止**路徑穿越(Path Traversal)**攻擊。

    • 客戶端可能會發送../../etc/passwd這樣的惡意路徑來訪問服務器上的敏感文件。

    • 這個函數通過snprintf構建完整路徑,然后通過strncmp檢查路徑是否以共享根目錄開頭。

    • 更健壯的服務器會使用realpath()來獲取文件的規范化絕對路徑,并再次檢查是否在共享目錄內,以徹底防止a/../b等形式的穿越。這里也加入了realpath的簡化使用。

  3. handle_client_request 函數:

    • 核心請求處理邏輯。 它在一個循環中接收客戶端發送的請求。

    • recv():接收TCP數據。

    • 報文解析: 根據自定義協議的格式(Opcode, FilenameLen, Filename等),解析接收到的數據。

    • switch (opcode) 根據操作碼執行不同的文件操作。

      • OP_OPEN

        • 解析文件名和打開模式(讀、寫、追加)。

        • 調用fopen()在服務器本地打開文件。

        • 發送STATUS_SUCCESSSTATUS_FAILURE響應。

      • OP_READ

        • 檢查文件是否已打開(opened_file != NULL)。

        • 解析客戶端請求讀取的字節數。

        • 調用fread()從已打開的文件中讀取數據。

        • 將讀取到的數據連同操作碼、狀態和實際讀取字節數一起發送回客戶端。

      • OP_WRITE

        • 檢查文件是否已打開。

        • 解析客戶端發送的數據長度和數據內容。

        • 調用fwrite()將數據寫入文件。

        • fflush(opened_file):確保數據立即寫入磁盤,而不是停留在緩沖區。

        • 發送STATUS_SUCCESSSTATUS_FAILURE響應。

      • OP_CLOSE

        • 調用fclose()關閉文件。

        • 發送STATUS_SUCCESSSTATUS_FAILURE響應。

      • OP_LIST (新增功能)

        • 使用opendir()readdir()函數遍歷指定目錄下的文件和子目錄。

        • 將目錄內容拼接成一個字符串,并通過TCP發送給客戶端。

        • 處理緩沖區滿的情況,分批發送。

    • 資源清理: 客戶端斷開連接或發生錯誤時,確保關閉文件句柄和客戶端套接字。

  4. main 函數:

    • 創建共享目錄: 在程序啟動時,創建./sim_nfs_root目錄,用于存放模擬的共享文件。

    • 創建監聽套接字 (socket(AF_INET, SOCK_STREAM, 0)): 創建一個TCP套接字,用于監聽客戶端連接。

    • setsockopt(SO_REUSEADDR) 允許端口重用,避免程序重啟時出現“Address already in use”錯誤。

    • 綁定地址和端口 (bind()): 將套接字綁定到服務器的IP地址和端口。

    • 監聽連接 (listen()): 使套接字進入監聽狀態,等待客戶端連接。

    • 接受連接循環 (accept()): 在一個無限循環中接受新的客戶端連接。每當有客戶端連接時,會創建一個新的客戶端套接字,并調用handle_client_request來處理該客戶端的請求。

NFS客戶端模擬器 (sim_nfs_client.c):

  1. 宏定義與公共函數:

    • 與服務器端相同的宏定義,確保協議一致性。

    • send_request_and_receive_response:這是一個通用輔助函數,封裝了發送TCP請求和接收TCP響應的邏輯。

  2. 遠程文件操作函數 (remote_open, remote_read, remote_write, remote_close, remote_list_dir):

    • 這些函數對應服務器端的不同操作碼,模擬了客戶端向服務器發送請求并處理響應的邏輯。

    • 請求構建: 每個函數都按照自定義協議的格式構建請求報文,包括操作碼、文件名長度、文件名、數據長度(讀寫操作)和數據內容(寫操作)。

    • 網絡字節序轉換: htonl()(host to network long)用于將主機字節序的32位整數轉換為網絡字節序,確保數據在不同架構的機器之間正確傳輸。

    • 響應解析: 接收到服務器響應后,解析響應報文中的操作碼、狀態碼、數據長度和數據內容。

    • 錯誤處理: 根據服務器返回的STATUS_FAILURE進行錯誤提示。

  3. main 函數:

    • 創建TCP套接字 (socket(AF_INET, SOCK_STREAM, 0))。

    • 配置服務器地址。

    • 連接到服務器 (connect()): 客戶端主動與服務器建立TCP連接。

    • 模擬操作序列: 在連接成功后,客戶端會按照預設的順序執行一系列模擬的遠程文件操作:

      • 列出根目錄內容。

      • 打開文件并寫入內容。

      • 再次打開文件并追加內容。

      • 再次打開文件并讀取內容。

      • 嘗試讀取一個不存在的文件(演示失敗情況)。

      • 再次列出根目錄內容,確認寫入的文件。

    • 資源清理: close(sockfd)關閉套接字。

如何使用這個C語言NFS模擬器:

  1. 準備環境:

    • 確保你的Linux系統上安裝了gcc

    • 在服務器端(例如你的開發機)創建一個名為sim_nfs_root的目錄,你可以在里面放一些測試文件。

  2. 保存代碼:

    • 將服務器代碼保存為 sim_nfs_server.c

    • 將客戶端代碼保存為 sim_nfs_client.c

  3. 編譯服務器: gcc sim_nfs_server.c -o sim_nfs_server

  4. 編譯客戶端: gcc sim_nfs_client.c -o sim_nfs_client

  5. 運行服務器: 在一個終端中運行服務器程序。

    ./sim_nfs_server
    • 它會提示你將文件放在./sim_nfs_root目錄下。

  6. 運行客戶端: 在另一個終端中運行客戶端程序,并指定服務器的IP地址(如果是同一臺機器,用127.0.0.1)。

    ./sim_nfs_client 127.0.0.1
  7. 觀察輸出: 你會看到客戶端和服務器之間詳細的請求和響應日志,以及文件內容的變化。在服務器的sim_nfs_root目錄下,你會看到test_nfs_write.txt文件被創建和修改。

通過這個模擬器,你將對網絡文件系統的抽象概念有更深刻的理解:客戶端發送請求,服務器執行本地操作,并通過網絡傳輸數據。這正是NFS在嵌入式開發中實現遠程根文件系統和應用程序調試的底層邏輯。

4.3 小結與展望

恭喜你,老鐵!你已經成功闖過了“Linux與C高級編程”學習之路的第四關:Linux TFTP與NFS服務

在這一部分中,我們:

  • 深入理解了TFTP協議的輕量級特性和在嵌入式設備啟動、固件升級中的重要作用。

  • 手把手教你搭建TFTP服務器,并使用TFTP客戶端進行文件上傳和下載。

  • 通過一個硬核的C語言TFTP客戶端模擬器,讓你從UDP套接字、報文構建、超時重傳、數據塊校驗等層面,透徹理解了TFTP文件傳輸的底層原理。

  • 深入理解了NFS協議作為網絡文件系統的強大功能,以及它在嵌入式根文件系統掛載、遠程應用開發調試中的核心地位。

  • 詳細講解了NFS服務器的搭建和配置,以及NFS客戶端的掛載和使用。

  • 通過一套簡易的C語言NFS服務器和客戶端模擬器,讓你從TCP套接字、自定義協議、請求響應機制和文件操作模擬等層面,概念性地理解了遠程文件系統訪問的底層邏輯。

現在,你不僅能夠熟練地使用TFTP和NFS服務來加速你的嵌入式開發流程,還能對這些網絡文件服務的工作原理有更深刻的認識。這對于你未來在嵌入式設備上進行網絡配置、系統部署和故障排查,將是巨大的優勢!

接下來,我們將進入更具挑戰性的第五部分:C語言高級編程(結構體、共用體、枚舉、內存管理、GDB調試、Makefile)!這將是本系列最核心的C語言部分,讓你從“會寫C代碼”到“寫出高質量、高效率、可維護的C代碼”的蛻變!

請記住,網絡服務和文件系統是復雜的領域,多實踐、多嘗試、多調試是掌握它們的最佳途徑!

敬請期待我的下一次更新!如果你在學習過程中有任何疑問,或者對代碼有任何改進的想法,隨時在評論區告訴我,咱們一起交流,一起成為Linux與C編程的“大神”!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/89978.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/89978.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/89978.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

鎖和事務的關系

事務的4大特性(ACID) 原子性&#xff08;Atomicity&#xff09;&#xff1a;事務被視為一個單一的、不可分割的工作單元一致性&#xff08;Consistency&#xff09;&#xff1a;事務執行前后&#xff0c;數據庫從一個一致狀態轉變為另一個一致狀態&#xff0c;并且強制執行所有…

電動車信用免押小程序免押租賃小程序php方案

電動車信用免押租賃小程序&#xff0c;免押租小程序&#xff0c;信用免押接口申請、對接開發&#xff0c;可源碼搭建&#xff0c;可二開或定制。開發語言后端php&#xff0c;前端uniapp。可二開定制 在線選擇門店&#xff0c;選擇車輛類型&#xff0c;選擇租賃方式&#xff08…

機器學習在智能安防中的應用:視頻監控與異常行為檢測

隨著人工智能技術的飛速發展&#xff0c;智能安防領域正經歷著一場深刻的變革。智能安防通過整合先進的信息技術&#xff0c;如物聯網&#xff08;IoT&#xff09;、大數據和機器學習&#xff0c;能夠實現從傳統的被動防御到主動預防的轉變。機器學習技術在智能安防中的應用尤為…

MySQL中DROP、DELETE與TRUNCATE的深度解析

在MySQL數據庫操作中&#xff0c;DROP、DELETE和TRUNCATE是三個常用的數據操作命令&#xff0c;它們都可以用于刪除數據&#xff0c;但在功能、執行效率、事務處理以及對表結構的影響等方面存在顯著差異。本文將從多個維度對這三個命令進行詳細對比和解析&#xff0c;幫助讀者更…

一條 SQL 語句的內部執行流程詳解(MySQL為例)

當執行如下 SQL&#xff1a; SELECT * FROM users WHERE id 1;在數據庫內部&#xff0c;其實會經歷多個復雜且有序的階段。以下是 MySQL&#xff08;InnoDB 引擎&#xff09;中 SQL 查詢語句從發送到結果返回的完整執行流程。 客戶端連接階段 客戶端&#xff08;如 JDBC、My…

超詳細yolo8/11-detect目標檢測全流程概述:配置環境、數據標注、訓練、驗證/預測、onnx部署(c++/python)詳解

文章目錄 一、配置環境二、數據標注三、模型訓練四、驗證預測五、onnx部署c 版python版本 一、配置環境 我的都是在Linux系統下&#xff0c;訓練部署的&#xff1b;模型訓練之前&#xff0c;需要配置好環境&#xff0c;Anaconda、顯卡驅動、cuda、cudnn、pytorch等&#xff1b…

阿里云Flink:開啟大數據實時處理新時代

走進阿里云 Flink 在大數據處理的廣袤領域中&#xff0c;阿里云 Flink 猶如一顆璀璨的明星&#xff0c;占據著舉足輕重的地位。隨著數據量呈指數級增長&#xff0c;企業對數據處理的實時性、高效性和準確性提出了前所未有的挑戰 。傳統的數據處理方式逐漸難以滿足這些嚴苛的需…

【Linux】基礎開發工具(1)

1. 軟件包管理器 1.1 什么是軟件包 在Linux下安裝軟件, ?個常用的辦法是下載到程序的源代碼, 并進行編譯, 得到可執行程序. 但是這樣太麻煩了, 于是有些人把?些常?的軟件提前編譯好, 做成軟件包(可以理解成windows上 的安裝程序)放在?個服務器上, 通過包管理器可以很?便…

藍橋杯51單片機設計

#超聲波原理# ①超聲波測距原理&#xff1a;聲波反射原理 聲波分類&#xff1a; 超聲波測距原理 超聲波頻率越高&#xff0c;波長越短&#xff0c;反身性越強&#xff0c;衍射性越弱 ②超聲波模塊原理 發射原理 跳線帽 接收原理 問題&#xff1a; &#xff11;.超聲波發射模塊需…

【LeetCode 熱題 100】240. 搜索二維矩陣 II——排除法

Problem: 240. 搜索二維矩陣 II 編寫一個高效的算法來搜索 m x n 矩陣 matrix 中的一個目標值 target 。該矩陣具有以下特性&#xff1a; 每行的元素從左到右升序排列。 每列的元素從上到下升序排列。 文章目錄 整體思路完整代碼時空復雜度時間復雜度&#xff1a;O(M N)空間復…

Android Input 系列專題【inputflinger事件的讀取與分發】

Android輸入系統在native中的核心工作就是&#xff0c;從Linux驅動設備節點中讀取事件&#xff0c;然后將這個事件進行分發&#xff0c;這兩項工作分別交給了InputReader和InputDispatcher來做。 他們的源碼都屬于native層inputflinger里面的一部分&#xff0c;如下架構&#…

【大模型LLM】GPU計算效率評估指標與優化方法:吞吐率

GPU計算效率評估指標與優化方法&#xff1a;吞吐率 一、核心效率指標二、大模型吞吐率&#xff08;Large Model Throughput&#xff09;三、關鍵性能瓶頸分析四、實際測量工具五、優化策略總結 一、核心效率指標 吞吐率&#xff08;Throughput&#xff09; 定義&#xff1a;單位…

Nestjs框架: 集成 Prisma

概述 在 NestJS 的官方文檔中&#xff0c;有兩處對數據庫進行了介紹 第一處位于左側“Techniques&#xff08;技術&#xff09;”部分下的“數據庫”板塊&#xff0c;中文文檔里同樣有這個位置。 Database 第二處是下面的“Recipes (秘籍)”板塊&#xff0c;這里有多個部分都與…

CppCon 2018 學習:What Do We Mean When We Say Nothing At All?

提供的內容深入探討了C編程中的一些關鍵概念&#xff0c;特別是如何編寫清晰、易維護的代碼&#xff0c;并展示了一些C17的新特性。我將對這些內容做中文的解釋和總結。 1. 良好的代碼設計原則 什么是“良好的代碼”&#xff1f; 能工作&#xff1a;代碼實現了預期功能。能在…

C語言中的輸入輸出函數:構建程序交互的基石

在C語言的世界里&#xff0c;輸入輸出&#xff08;I/O&#xff09;操作是程序與用戶或外部數據源進行交互的基本方式。無論是從鍵盤接收用戶輸入&#xff0c;還是將處理結果顯示到屏幕上&#xff0c;亦或是讀寫文件&#xff0c;都離不開C語言提供的輸入輸出函數。本文將深入探討…

高速信號眼圖

橫軸體系時域的抖動大小&#xff1b;縱軸體現電壓的噪聲。 噪聲越大&#xff0c;眼高越小。 抖動越大&#xff0c;眼寬越窄。 眼圖的模板是定義好的最大jitter和噪聲的模板范圍。就是信號的不可觸碰區域。信號波形不能夠觸碰到模板或者進行模板中。也就是眼圖中的線軌跡要在眼…

VisualSVN Server 禁止的特殊符號 導致的。具體分析如下:錯誤提示解讀

是由于 文件夾名稱中包含了 VisualSVN Server 禁止的特殊符號 導致的。具體分析如下&#xff1a; 錯誤提示解讀 錯誤信息明確說明&#xff1a; Folder name cannot contain following symbols < > : " / | and start or end by period. 即 文件夾名稱不能包含以下…

再見,WebSecurityConfigurerAdapter!你好,SecurityFilterChain

對于許多經驗豐富的 Spring開發者來說&#xff0c;WebSecurityConfigurerAdapter 是一個再熟悉不過的名字。在很長一段時間里&#xff0c;它幾乎是所有 Spring Security 配置的起點和核心。然而&#xff0c;隨著 Spring Boot 3.x 和 Spring Security 6.x 的普及&#xff0c;這個…

web前端面試-- MVC、MVP、MVVM 架構模式對比

MVC、MVP、MVVM 架構模式對比 基本概念 這三種都是用于分離用戶界面(UI)與業務邏輯的架構模式&#xff0c;旨在提高代碼的可維護性、可測試性和可擴展性。 1. MVC (Model-View-Controller) 核心結構&#xff1a; Model&#xff1a;數據模型和業務邏輯View&#xff1a;用戶界面展…

【C#】MVVM知識點匯總-2

在C#中實現MVVM&#xff08;Model-View-ViewModel&#xff09;架構時&#xff0c;可以總結以下幾個關鍵知識點&#xff0c;并通過具體的代碼示例來進行說明。 1. 模型 (Model) 模型包含應用程序中的數據和業務邏輯。通常與數據庫交互。 public class User { public int Id {…