目錄
一、任務、進程和線程
1.1任務
1.2進程
1.3線程
1.4線程和進程的關系
1.5 在linux系統下進程操作
二、Linux虛擬內存管理與stm32的真實物理內存區別
2.1 Linux虛擬內存管理
2.2?STM32的真實物理內存映射
2.3區別
三、?Linux系統調用函數 fork()、wait()、exec()
3.1 fork
3.2 wait
3.3 exec
四、在樹莓派中,創建組員賬號,完成練習
4.1 用戶創建和配置:
4.2登錄自己的樹莓派賬號練習
五、總結
一、任務、進程和線程
1.1任務
多任務系統指可以同一時間內運行多個應用程序的系統,每個應用程序被稱作一個任務。
任務是一個邏輯概念,指由一個軟件完成的任務,或者是一系列共同達到某一目的的操作。
任務的特點:
-
在實時操作系統(RTOS)中,任務通常是獨立的、無法返回的函數。
-
任務的調度和管理依賴于任務控制塊(TCB),它記錄任務的狀態、優先級等信息
1.2進程
進程是指一個具有獨立功能的程序在某個數據集上的一次動態執行過程,它是系統進行資源分配和調度的最小單元。
通俗來說,進程就是程序的一次執行過程,程序是靜態的,它作為系統中的一種資源是永遠存在的。而進程是動態的,它是動態的產生,變化和消亡的,擁有其自己的生命周期。
舉個例子:同時掛三個 QQ 號,它們就對應三個 QQ 進程,退出一個就會殺死一個對應的進程。但是,就算你把這三個 QQ 全都退出了,QQ 這個程序死亡了嗎?顯然沒有。
進程不僅包含正在運行的程序實體,并且包括這個運行的程序中占據的所有系統資源,比如說 CPU、內存、網絡資源等。很多小伙伴在回答進程的概念的時候,往往只會說它是一個運行的實體,而會忽略掉進程所占據的資源。比如說,同樣一個程序,同一時刻被兩次運行了,那么他們就是兩個獨立的進程。
-
特點:
-
每個進程擁有獨立的內存空間,包括代碼段、數據段、堆和棧。
-
進程之間相互隔離,一個進程的崩潰通常不會影響其他進程。
-
進程是資源分配的最小單位,但不是CPU調度的最小單位。
-
1.3線程
線程是進程內獨立的一條運行路線,是處理器調度的最小單元,也可以稱為輕量級進程。線程——程序執行的最小單位。
-
特點:
-
一個進程可以包含多個線程,線程共享所屬進程的內存空間和資源。
-
線程的切換開銷較小,因為它只需要切換寄存器狀態和棧信息。
-
線程是程序執行的最小單位,多個線程可以并發執行。
-
1.4線程和進程的關系
(1)一個線程只能屬于一個進程,而一個進程可以有多個線程,但至少有一個線程;
(2)資源分配給進程,同一進程內的所有線程共享該進程的所有資源;
(3)線程在執行過程中需要協作同步。不同進程中的線程之間要利用消息通信的方法實現同步;
(4)處理機分配給線程,即真正在處理機上運行的是線程;
(5)線程是進程的一個執行單元,也是進程內的可調用實體。
1.5 在linux系統下進程操作
操作:1) 用 ps -a 命令查看系統中各進程的編號pid ; 2) 用kill 命令終止一個進程pid。
ps -a
-
ps 是“process status”的縮寫,用于顯示當前系統中運行的進程信息。
-
選項
-a
:表示顯示當前終端(TTY)中所有用戶啟動的進程。
我可以通過用一個sleep也來弄一個進程方便我們將他kill,如下操作:
sleep 50&
-
sleep
命令用于讓當前進程暫停指定的時間(單位為秒)。這里讓進程暫停50秒。 -
&
符號:將命令放到后臺執行。這樣,用戶可以在命令執行的同時繼續在終端中輸入其他命令。
kill 1317303
-
向PID為
1317303
的sleep
進程發送終止信號,使其停止運行。 -
再次用ps -a查看是否終止進程。
-
二、Linux虛擬內存管理與stm32的真實物理內存區別
2.1 Linux虛擬內存管理
Linux操作系統采用虛擬內存管理技術,使得每個進程都有各自互不干涉的進程地址空間。
在Linux中,虛擬內存管理的基本理念是“每個程序認為它擁有獨立的內存”。虛擬內存通過以下方式實現:
-
虛擬地址與物理地址:Linux使用虛擬地址來訪問內存,這些虛擬地址通過內存管理單元(MMU)映射到物理地址。虛擬地址空間被分為用戶空間和內核空間,每個進程都有自己的虛擬地址空間。
-
分頁機制:Linux將內存劃分為固定大小的頁面(通常是4KB),并根據需要將頁面從磁盤交換到物理內存中。這種機制允許系統運行比物理內存更大的程序。
-
內存保護與隔離:虛擬內存機制提供了內存保護,確保一個進程無法訪問另一個進程的內存。內核空間的內存對用戶空間進程不可見。
-
動態內存分配:Linux內核使用懶惰分配(Lazy Allocation)技術,只有當進程實際訪問分配的內存時,才會分配物理內存。
-
交換空間(Swap):當物理內存不足時,Linux會將不常用的頁面交換到磁盤上的交換空間。
用戶感知:程序操作的是虛擬地址,物理地址對用戶透明;通過malloc()
分配內存時,實際可能僅在虛擬地址空間預留范圍(brk
或mmap
),直到訪問時才觸發缺頁異常分配物理頁。
2.2?STM32的真實物理內存映射
STM32是一種嵌入式微控制器,其內存管理相對簡單,主要基于物理內存的直接訪問。
-
物理內存映射:STM32使用物理內存映射,將內存和外設分配到不同的地址范圍。這種映射是固定的,沒有虛擬地址的概念。
-
內存保護缺失:STM32通常沒有內存保護機制,用戶空間代碼可以直接訪問內核空間的內存。這可能導致一個程序的錯誤操作影響整個系統。
-
簡單的內存管理:STM32的內存管理主要依賴于靜態分配,程序在啟動時分配所需的內存,并在運行時直接訪問這些內存。
-
無交換空間:STM32沒有交換空間的概念,所有內存操作都直接在物理內存上進行。
開發模式:程序員需手動管理內存,避免溢出。外設操作通過指針直接訪問寄存器,比如常見的:
// 直接操作STM32的GPIO寄存器
#define GPIOA_ODR (*(volatile uint32_t*)0x40020014)
GPIOA_ODR |= 0x00000001; // 設置PA0引腳為高電平
2.3區別
特性 | Linux虛擬內存 | STM32物理內存映射 |
---|---|---|
地址空間 | 虛擬地址(通過MMU轉換) | 物理地址(直接訪問) |
硬件依賴 | 必須支持MMU(如ARM Cortex-A系列) | 無MMU(如ARM Cortex-M系列) |
內存隔離 | 進程間隔離,防止非法訪問 | 無隔離,程序可直接修改任意內存/外設 |
動態分配 | 支持按需分配和交換(malloc /mmap ) | 靜態分配(鏈接腳本定義堆棧/全局變量) |
訪問權限控制 | 通過頁表實現讀/寫/執行權限 | 無權限控制,依賴程序員自律 |
典型應用場景 | 通用計算(多任務/復雜應用) | 實時嵌入式系統(確定性/低延遲) |
三、?Linux系統調用函數 fork()、wait()、exec()
3.1 fork
在Linux 中創建一個新進程的唯一方法是使用fork()函數。fork()函數用于從已存在的一個進程中創建一個新的進程,新進程稱為子進程,而原進程稱為父進程。
-
子進程特性:
-
子進程是父進程的復制品,繼承父進程的地址空間,包括代碼段、數據段、堆、棧等。
-
子進程繼承父進程的文件描述符、信號控制設定、進程優先級、進程組號、當前工作目錄等。
-
子進程擁有獨立的進程號(PID)和資源使用信息。
-
-
返回值:
-
在父進程中,
fork()
返回子進程的PID。 -
在子進程中,
fork()
返回0。
-
?1.在練習文件夾下面利用nano創建函數fork_example.c文件(建議也可以參考下文中樹莓派部分的fork函數,那個比較簡潔)
#include <stdio.h> // 標準輸入輸出庫,用于printf等函數
#include <sys/types.h> // 包含數據類型定義,如pid_t
#include <unistd.h> // 包含fork、getpid、getppid等函數
#include <stdlib.h> // 包含exit函數
#include <errno.h> // 包含錯誤號定義
#include <sys/wait.h> // 包含waitpid函數及相關宏int main() {pid_t pid; // 定義一個pid_t類型的變量,用于存儲fork返回的進程IDint ret = 1; // 定義一個返回值變量,未在代碼中使用int status; // 定義一個變量,用于存儲子進程退出狀態pid = fork(); // 調用fork函數創建一個子進程if (pid == -1) { // 如果fork返回-1,表示創建子進程失敗printf("can't fork, error occured\n"); // 輸出錯誤信息exit(EXIT_FAILURE); // 退出程序,返回值為EXIT_FAILURE} else if (pid == 0) { // 如果fork返回0,表示當前是子進程printf("child process, pid = %u\n", getpid()); // 輸出子進程的PIDprintf("parent of child process, pid = %u\n", getppid()); // 輸出子進程的父進程PIDchar *argv_list[] = {"ls", "-lart", "/home", NULL}; // 定義一個字符串數組,作為execv的參數// 調用execv替換當前進程映像為"ls"程序,并傳遞參數"-lart"和"/home"execv("ls", argv_list); // 如果execv成功,控制權將轉移到"ls"程序,不會返回到這里// 如果execv失敗,返回-1,并繼續執行下面的代碼exit(0); // 子進程退出} else { // 如果fork返回一個正數,表示當前是父進程,返回值是子進程的PIDprintf("Parent of parent process, pid = %u\n", getppid()); // 輸出父進程的父進程PIDprintf("parent process, pid = %u\n", getpid()); // 輸出父進程的PID// 父進程調用waitpid等待子進程結束if (waitpid(pid, &status, 0) > 0) { // 如果waitpid成功,返回子進程的PID// 檢查子進程是否正常退出if (WIFEXITED(status) && !WEXITSTATUS(status)) // 如果子進程正常退出且返回值為0printf("program execution successful\n");else if (WIFEXITED(status) && WEXITSTATUS(status)) { // 如果子進程正常退出但返回值非0if (WEXITSTATUS(status) == 127) { // 如果返回值為127,表示execv失敗printf("execv failed\n");} else // 如果返回值非127,表示程序執行完成但返回了非零狀態printf("program terminated normally, but returned a non-zero status\n");} else // 如果子進程沒有正常退出printf("program didn't terminate normally\n");} else { // 如果waitpid失敗printf("waitpid() failed\n");}exit(0); // 父進程退出}return 0; // 程序正常結束返回0
}
2.使用cmake
編譯fork_example.c
在根目錄下創建(想著用不同的方法來編譯,掌握多種編譯辦法。也可以直接采用下文的gcc方式來編譯)
CMakeLists.txt
cmake_minimum_required(VERSION 3.10) # 最低 CMake 版本要求
project(Fork_demo) # 項目名稱# 添加可執行文件
add_executable(fork_demo fork_example.c)# 設置 C 標準(可選)
set(CMAKE_C_STANDARD 11)
3.構建目錄生成文件
mkdir build && cd build
創建構建目錄并生成 Makefile:
cmake --build build
./build/fork_demo
運行即可,結果如圖:
3.2 wait
功能:wait()函數用于使父進程(也就是調用wait()的進程)阻塞,直到一個子進程結束或者該進程接收到了一個指定的信號為止。如果該父進程沒有子進程或者它的子進程已經結束,則wait()函數就會立即返回。
-
作用:
-
確保父進程在子進程結束后再繼續執行。
-
防止子進程變成僵尸進程(zombie process)。
-
-
返回值:
-
如果父進程沒有子進程或者子進程已經結束,
wait()
會立即返回。 -
返回值是子進程的PID。
-
?wait調用:
1.創建一個?fork_wait.c
?文件,內容如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h> // 必須包含此頭文件以使用 wait()int main() {pid_t pid = fork();if (pid < 0) {perror("fork 失敗");return 1;} else if (pid == 0) {// 子進程執行任務printf("子進程 PID = %d\n", getpid());sleep(2); // 模擬耗時操作printf("子進程結束\n");} else {// 父進程等待子進程結束printf("父進程 PID = %d,等待子進程 %d...\n", getpid(), pid);int status;wait(&status); // 阻塞等待子進程結束printf("子進程退出狀態: %d\n", WEXITSTATUS(status));}return 0;
}
同樣的創建Makefile,
# 定義編譯器和編譯選項
CC = gcc
CFLAGS = -Wall -Wextra -std=c11# 定義目標可執行文件名和源文件
TARGET = fork_wait_demo
SRC = fork_wait.c# 默認目標
all: $(TARGET)# 編譯規則
$(TARGET): $(SRC)$(CC) $(CFLAGS) -o $@ $^# 清理生成的文件
clean:rm -f $(TARGET)# 偽目標聲明(避免與同名文件沖突)
.PHONY: all clean
編譯運行效果:
make
./fork_wait_demo
結果演示:
3.3 exec
在Linux 中使用exec函數族主要有兩種情況:
1.當進程認為自己不能再為系統和用戶做出任何貢獻時,他就可以發揮最后一點余熱,調用任何一個exec,讓自己以新的面貌重生;
2.如果一個進程想執行另一個程序,那么它就可以調用fork() 函數新建一個進程,然后調用exec 函數族中的任意一個函數,這樣看起來就像通過執行應用程序而產生了一個新進程(這種情況非常普遍)。
exec調用:
可以先建一個文件夾哈(這樣方便看到保存每個函數的那個記錄,我上面兩個函數忘記創建了,之后做實驗最好一個實驗一個文件夾,這樣清爽一些)
mkdir exec && cd exec
1.創建?fork_exec.c
?文件,參考代碼:
#include <stdio.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid == 0) {// 子進程執行 ls -lexecl("/bin/ls", "ls", "-l", NULL);perror("exec failed"); // 若 exec 失敗才會執行return 1;}return 0;
}
2.可以用gcc或者make或者cmake等方式來進行編譯,我們這里就用常規簡單一點的gcc(cmake和make的方式上面兩個函數都有代碼,修改一下名稱就可以用啦)
gcc fork_exec.c -o fork_demo
./fork_demo
結果演示:
四、在樹莓派中,創建組員賬號,完成練習
4.1 用戶創建和配置:
提前進入到主要的賬號里邊給各個用戶加上相關的權限,操作如下(putty和Xterminal都可以)
1.使用adduser
命令創建用戶
sudo adduser zsc
-
執行后會提示設置密碼及用戶信息(非必填項可直接回車跳過)
-
默認自動生成同名主目錄?
/home/username
2.配置用戶權限
1.將用戶加入sudo組
sudo usermod -aG sudo zsc
2.加入常用硬件訪問組
sudo usermod -aG adm,dialout,plugdev zsc
3.?驗證用戶權限
id zsc # 查看用戶所屬組
groups zsc # 列出用戶所有附加組
4.2登錄自己的樹莓派賬號練習
在Xterminal中利用ssh連接登陸上自己的樹莓派賬號。(連接過程和之前的博客步驟一樣的,通過電腦移動熱點查詢物理地址,賬號密碼確認后即可登錄)
我們還可以查看樹莓派下面的其他用戶
compgen -u
進入到自己的樹莓派環境中
配置一下安裝一下環境:
在樹莓派Ubuntu系統中,默認可能未安裝GCC,安裝GCC及編譯所需的工具鏈(如make
、g++
等)
sudo apt update
sudo apt install build-essential
1.創建并打開目錄
mkdir test0404 && cd test0404
2.編寫fork.c程序(選用gcc的方式來編譯啦,這樣快捷一些)
nano fork_gcc.c
簡單的編寫一個代碼:
#include <stdio.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid < 0) {fprintf(stderr, "Fork失敗\n");return 1;} else if (pid == 0) {printf("子進程ID: %d\n", getpid());} else {printf("父進程ID: %d,子進程ID: %d\n", getpid(), pid);}return 0;
}
3.編譯并運行:
gcc fork_gcc.c -o fork_gcc//編譯需要卡一會./fork_gcc
五、總結
通過本次讓我收獲很大很大,本文主要闡述了任務、進程、線程的定義,區別和聯系,參考文獻1中有更加直白易懂的說法(感興趣的可以去看看),同時通過查閱資料闡述了虛擬機內存管理和STM32的物理內存中的一些差別,進一步了解虛擬機和存儲的方式;最后也是本次最重要的實踐環節,學習調用了fork、wait、exec函數,深入理解了每個函數在樹莓派的中是如何使用的,同時每個函數的調用用了不同的編譯方法(cmake、make、gcc)(個人建議設計內容少的函數可以直接用gcc是最方便的),加深了對Ubuntu的運用和理解,同時通過反反復復的敲代碼和命令,對整體的編程水平還是上升了不少,感興趣的朋友也可以自己試試手敲,收獲會很大的。
樹莓派的操作也越來越熟悉了,在XTerminal很好用,建議用這個,很不錯。
本文中原理部分有些圖片來源于網絡,如有侵權請及時與我聯系刪除,本人才疏學淺,如有描述不準確或出錯的地方還請海涵,感謝您的閱讀!
參考文獻:
https://zhuanlan.zhihu.com/p/391496775
https://zhuanlan.zhihu.com/p/403313422
Linux系統調用編程-CSDN博客
Linux Ubuntu 入門基本命令整理_linux ubuntu入門基本命令整理-CSDN博客