目录
一、任务、进程和线程
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博客