Linux - 进程
一、查看进程(ps)
1) 基本选项
ps
:显示当前会话的进程。ps -e
或ps -A
:显示所有进程。ps -f
:显示更详细的进程信息(如 PPID、UID)。ps -l
:显示长格式的进程信息,包括优先级、状态等。ps -u <username>
:显示指定用户的进程信息。ps -p <pid>
:仅显示指定进程 ID 的进程信息。ps -C <command_name>
:按命令名搜索进程。
2) 格式化选项
本质是遍历task_struct的双链表,打印相关信息。
-
ps aux
:查看所有进程的详细信息,其中:
a
:显示终端上所有用户的进程。u
:显示用户相关的详细信息。x
:显示没有控制终端的进程。
-
ps ajx
是一个常用组合,具有以下含义:a
:显示与所有用户相关的进程,而不仅仅是当前用户的进程。j
:以“任务格式”显示进程信息,包含PID(进程id)
、PPID(父进程id)
、PGID(进程组id)
、SID(会话id)
、TTY(终端)
等任务管理相关的列。x
:显示没有控制终端的进程(即后台进程)。
3) 常用组合
ps aux | grep <process_name>
:查看所有进程中指定名称的进程。ps -eo pid,cmd,%mem,%cpu --sort=-%cpu | head
:查看 CPU 使用率最高的进程。
4) 进程目录(/proc)
/proc
是 Linux 文件系统中的一个伪文件系统(也称虚拟文件系统),它用于在内存中动态生成有关系统和进程的信息。/proc
中的文件和目录并不占用实际的磁盘空间,而是由内核根据实时数据生成。用户和程序可以通过读取这些文件,方便地获取系统信息和控制系统配置。
# 显示进程pid为150464的相关PCB信息
(base) Raizeroko@bciserver:~$ ls /proc/150464 -l
... #其他进程信息
# cwd: 当前工作目录的符号链接,指向进程启动时或运行中的工作目录。
# 进程在创建时的PCB中就有当前路径!!!才能在执行时的各种当前路径相关的操作才能完成。
lrwxrwxrwx 1 Raizeroko Raizeroko 0 10月 25 19:06 cwd -> /home/Raizeroko/code/process
# environ: 包含进程的环境变量,记录进程启动时的环境配置,如路径、语言设置等。
-r-------- 1 Raizeroko Raizeroko 0 10月 25 19:07 environ
# exe: 进程正在执行的二进制文件的符号链接,指向该进程的可执行文件路径。
lrwxrwxrwx 1 Raizeroko Raizeroko 0 10月 25 19:07 exe -> /home/Raizeroko/code/process/myprocess
... #其他进程信息
二、描述进程(PCB)
PCB(Process Control Block,进程控制块)是操作系统用于存储和管理每个进程相关信息的数据结构。它是内核中用于追踪进程状态的关键数据块,每个进程在创建时都会生成一个 PCB,当进程终止时相应的 PCB 也会被销毁。PCB 是操作系统在多任务处理和调度中管理进程的核心。
由于PCB的存在,在操作系统中,对进程进行管理就变成了对单链表进行增删查改。PCB+程序代码=进程。
1) PCB 的作用
PCB 是操作系统中多任务调度的核心所在:
- 保存和恢复上下文:操作系统在执行进程切换时会先保存当前进程的 PCB,再加载新进程的 PCB,从而保证进程能够继续上次的状态。
- 资源分配和管理:操作系统利用 PCB 中的信息来管理和分配资源(如 CPU、内存、I/O 等)。
- 状态追踪:通过 PCB,操作系统能够实时监控和更新进程状态,以决定进程调度策略。
2) Linux的PCB(task_struct)
task_struct
是 Linux 内核中用于描述和管理每个进程的核心数据结构。它可以理解为 Linux 进程的 PCB(进程控制块),包含与进程有关的各种信息,用于内核调度、资源管理和进程控制。每个进程在创建时,Linux 内核会分配一个 task_struct
,并在进程结束后释放。Linux最基本的组织进程task_struct的方式采用的双向链表进行组织。task_struct的内容包括:
-
标识符类
-
唯一标识符:
pid
、tgid
,ppid
用于唯一标识进程。 -
父子进程标识:
parent
、children
、sibling
等,用于建立进程的父子关系和管理进程层级。
-
-
状态类
-
任务状态:
state
(当前进程状态:TASK_RUNNING
、TASK_INTERRUPTIBLE
等)、exit_state
(退出状态)。 -
信号和退出代码:
exit_code
(退出代码)、exit_signal
(退出信号),用于指示进程退出原因和信号状态。 -
等待信息:
wait_chldexit
(等待的子进程),以及其他等待状态信息,用于进程同步和资源控制。
-
-
优先级类
-
优先级:
prio
、static_prio
、normal_prio
等,表示进程相对其他进程的优先级。 -
调度策略:
policy
,调度策略,如SCHED_NORMAL
、SCHED_FIFO
等。
-
-
程序计数器
- 程序计数器:用于保存当前进程的执行位置,指向即将执行的下一条指令的地址。
-
内存指针类
-
内存管理结构:
mm
(指向内存管理结构体mm_struct
)、active_mm
(内存映射)。 -
代码和数据段指针:指向进程的代码、数据、堆栈等内存段,以及共享的内存块指针,用于实现虚拟内存和内存隔离。
-
-
上下文数据类
-
CPU 上下文:CPU 上下文用于在进程调度中保存和恢复当前进程的状态,包含处理器寄存器的值(如程序计数器、栈指针等)。
-
寄存器信息:各类 CPU 寄存器中的数据,用于在进程切换时保存当前进程的执行上下文,便于切换后继续执行。
-
-
I/O 状态信息类
-
I/O 请求:记录当前进程的 I/O 请求,包括等待的设备信息。
-
文件描述符:
files
,指向files_struct
结构,用于管理进程打开的文件列表。 -
设备信息:记录分配给该进程的设备资源,便于管理 I/O 资源的使用情况。
-
-
记账信息类
-
时间计数:
utime
、stime
,用户态和内核态的总时间。 -
使用统计:记录进程使用的资源总量,比如 CPU 时间总和、内存占用量等。
-
其他记账信息:用于监控进程使用的资源量,便于操作系统进行资源分配和管理。
-
-
其他信息
-
调试和跟踪:一些调试状态、进程栈信息,用于内核追踪进程状态和监控进程行为。
-
信号和安全信息:
signal
(信号信息)、security
(安全信息),用于处理进程的信号和安全策略。
-
三、创建进程
1) 系统层面创建进程
# 运行一个名为process的进程
./process
# process对应的process.cpp代码
int main()
{
while(1)
{
# getpid():返回当前进程的进程 ID(PID)。
cout<<"my pid is: "<< getpid() <<endl;
# getppid():返回当前进程的父进程的进程 ID(PPID)。
cout<<"my ppid is: "<< getppid() <<endl;
fflush(stdout);
sleep(3);
}
return 0;
}
当在命令行运行程序时,bash
(或其他命令行解释器,他的创建在每次登录系统时自动完成)会作为父进程来创建一个新的子进程,执行这个程序的代码。具体来说,bash
进程会使用以下过程来启动子进程:
-
创建子进程:
bash
使用fork()
系统调用来创建一个新的子进程,这个子进程会是原bash
进程的副本。此时,子进程与bash
的代码和数据几乎完全相同,但有自己独立的进程 ID(PID)。 -
执行程序:子进程使用
exec()
系列系统调用,将其代码和数据段替换为目标程序的代码和数据。这样,子进程的内容变成了我们要运行的程序,而 PID 和 PPID 不变。此时,子进程正式开始运行目标程序的代码。 -
等待子进程结束:在默认情况下,
bash
会等待子进程结束,获取它的退出状态,以便确认程序是否正常运行。等待子进程完成后,bash
进程继续等待用户输入新的命令。
2) 代码层面创建进程(fork)
pid_t fork(void)
fork()
是 Unix 系统中的一个重要系统调用,用于创建一个新进程。它的执行机制、用法和特性是理解多进程编程的核心内容之一。详细来说,fork()
会创建一个几乎完全相同的子进程,其中包含和父进程相同的程序代码、文件描述符、环境等。
# 运行后发现if和else if同时满足。
int main()
{
cout<<"我是一个进程: pid" << getpid() << ", ppid: "<< getppid() << endl;
pid_t id = fork();
if(id == 0)
{
// 子进程
cout<<"我是子进程: pid" << getpid() << ", ppid: "<< getppid() << endl;
}
else if(id > 0)
{
// 父进程
cout<<"我是父进程: pid" << getpid() << ", ppid: "<< getppid() << endl;
}
else
{
cout<<"error"<<endl;
}
return 0;
}
-
fork()
的工作原理:fork()
调用通过复制调用它的进程来生成一个新进程,也就是子进程。即先创建子进程的PCB,并修改PCB中部分属性完成创建,然后fork语句后的代码段共享。- 子进程是父进程的副本,但运行在独立的内存空间内。此时父子进程的程序代码、数据和堆栈段内容相同,但各自占用不同的内存地址。
- 子进程的代码执行时从fork()调用后的的下一条指令开始的。
-
父进程与子进程的区别:
-
子进程有一个独立的 PID(进程 ID)。子进程的
PPID
(父进程 ID)是父进程的 PID。 -
fork()
在父进程中返回子进程的 PID,在子进程中返回 0。如果fork()
出错(比如系统资源不足),则返回 -1。
-
-
fork()
的返回值:-
返回 >0:此时是在父进程中,返回值是子进程的 PID。
-
返回 0:此时是在子进程中。
-
返回 -1:此时
fork()
出错,通常是因为系统资源限制或其他原因。
-
-
fork()
的独立性:-
父子进程在
fork()
调用后共享一部分资源,但也有各自独立的资源。 -
共享的资源包括:文件描述符(文件指针位置独立)、环境变量、用户和组信息等。
-
独立的资源包括:各自的内存空间、各自的进程ID和父进程ID、各自的堆栈指针和局部变量。
-
-
fork()
的写时复制机制:fork()
不会立即复制父进程的所有内存空间,而是使用写时复制机制,只有当父或子进程需要修改某段内存时,才会将其真正复制到子进程。这种机制在保持进程隔离性的同时,提高了资源利用效率。
注意以下几个问题:
为什么fork要给子进程返回0, 给父进程返回子进程pid?
这种设计主要是为了方便程序员在创建进程后能够轻松区分当前代码是在父进程还是在子进程中执行:
- 返回 0:子进程收到
0
,可以通过这一返回值明确知道自己是子进程。这样,子进程可以直接在后续的代码中进行特定的操作,比如执行不同的任务、修改其状态等。- 返回子进程 PID:父进程收到子进程的 PID,能够通过这个值来管理和跟踪子进程。这允许父进程在需要时(例如等待子进程结束或获取其状态)使用这个 PID。
一个函数是如何做到返回两次的?
fork()
函数在被调用时,实际上是在原进程(父进程)和新进程(子进程)中分别执行。函数返回两次的过程可以理解为:
- 在父进程中,
fork()
返回子进程的 PID(大于 0 的值)。- 在子进程中,
fork()
返回 0。由于这两个进程的代码在
fork()
调用后继续执行,因此对于每个进程来说,函数都返回了一次结果。事实上,可以想象,调用
fork()
之后,程序的执行路径分开了,形成了两个独立的执行流。因此每个进程都可以独立处理自己的返回值。一个变量怎么会有不同的内容?
在
fork()
之后,父子进程各自有独立的内存空间,尽管在fork()
之前它们共享相同的内存内容,但在fork()
之后,对一个进程的变量的修改不会影响另一个进程。具体来说:
- 写时复制:当
fork()
被调用时,操作系统采用写时复制策略。父进程和子进程共享相同的物理内存页,但在其中一个进程试图修改这些内存页时,操作系统会复制该页到新的内存空间,从而使得两个进程各自拥有独立的副本。这就是为什么fork后得到的返回值在父子进程中虽然拥有相同的变量名,但内容不一样。fork后,父子进程谁先运行?
在调用
fork()
后,父进程和子进程的执行顺序是不确定的,因为调度器会随机决定哪个先运行。这就像在堵车时,每条车道的运行快慢不能具体确定,可能先走,也可能后走,完全取决于前方道路调度的安排。
四、 进程状态
1) 一般操作系统的进程状态
-
运行 (Running)
- 定义: 进程正在执行代码,并使用 CPU 资源。
- 特点: 只有一个进程能够在某个时刻处于运行状态(在单核 CPU 上)。在多核 CPU 上,可以有多个进程同时处于运行状态。
- 并发执行:进程被CPU调度运行时,并不是一直占用CPU直到代码运行完成才调出CPU并从就绪队列中挑选下一个程序运行,而是每一个进程中都拥有一个时间片属性,如果在CPU上运行时间超过了时间片就会从就绪队列中挑选其他进程加载到CPU上运行,因此CPU运行时会存在大量的进程切换。
-
就绪(Ready)
- 定义: 进程已准备好执行,但由于CPU资源被其他进程占用,暂时无法运行。
- 特点: 就绪队列中的所有进程都在等待调度器的分配,它们可以立即运行一旦获得CPU。
-
阻塞 (Blocked)
- 定义: 进程因等待某个事件(如 I/O 操作、信号量、资源等)而无法继续执行。
- 特点: 阻塞状态的进程不会使用 CPU,直到它所等待的事件发生并被唤醒。
-
挂起 (Suspended)
-
定义: 进程被操作系统或用户主动暂停执行,可以是由于系统资源紧张或人为干预。
-
特点: 挂起的进程通常在内存中,但不会消耗 CPU 时间。它可以在未来被恢复到就绪或运行状态。
-
换入换出:当操作系统内存严重不足时,在阻塞队列的进程由于不需要立即运行,为了缓解内存空间,操作系统就会将这部分进程的代码换出到磁盘中,只留下PCB在阻塞队列中排队,这就叫挂起,直到阻塞进程等待成功,或者内存空间得到缓解才会将代码重新换入到内存中。
-
2)Linux中的进程状态
在Linux内核中,进程状态使用位图(bitmap)表示。即每种状态以二进制位来表示,多个状态可以通过位的组合来表达。这种设计使得内核可以使用位操作来检测进程的各种状态。这段代码展示了task_state_array
数组,定义了各进程状态的字符串表示,每个字符串对应一个状态码。
-
“R (running)” - 代码
0
:运行或准备运行的进程。(对应运行状态+就绪状态) -
“S (sleeping)” - 代码
1
:可中断睡眠状态,即等待某个条件或事件,大部分进程实际上都处于该状态。(对应阻塞状态)- 浅度睡眠:随时可以响应外部变化,但是在等待时可以被操作系统杀死。
-
“D (disk sleep)” - 代码
2
:不可中断睡眠状态,通常是等待硬件响应,不能被信号唤醒。(也是一种阻塞状态)- 深度睡眠:类似于S状态,但是不能被操作系统杀死,一般出现一个D状态系统就离挂不远了。
-
“T (stopped)/t (tracing stop)”- 代码
4/8
:进程被暂停,通常是因为收到了停止信号(如SIGSTOP
)。kill -19 [pid] # 将进程暂停SIGSTOP kill -18 [pid] # 结束暂停SIGCONT
- 进程在这种状态下不会占用CPU,通常等待外部信号恢复(如
SIGCONT
)以重新进入运行或就绪状态 - 该状态常见于进程被手动暂停(例如使用
Ctrl+Z
)或被调试工具暂停时(如使用gdb
调试)。
- 进程在这种状态下不会占用CPU,通常等待外部信号恢复(如
-
“X (dead)” - 代码
16
:进程已经终止,等待被清理,此状态在内核中较少见(即被放到垃圾队列等待CPU处理的进程)。 -
“Z (zombie)” - 代码
32
:僵尸进程,已经完成执行,但父进程尚未回收。- 进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会一直处于Z状态,进程的相关资源尤其是task_struct不能被释放,此时僵尸进程会一直占用资源,这会导致内存泄漏。
- 孤儿进程:父子进程,父进程先退出,子进程的父进程会被改成1号进程(即操作系统),该子进程被系统领养,称为孤儿进程。
3) 进程优先级
在Linux中,进程的优先级是一个用于调度的参数,表示进程对CPU的使用优先程度。优先级越高,进程在调度中越容易获得CPU资源。
(base) Raizeroko@bciserver:~/code/zstate$ ps -al
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 1000 2086 2084 0 80 0 - 6341914 - tty2 00:00:03 Xorg
0 S 1000 2112 2084 0 80 0 - 56574 - tty2 00:00:00 gnome-session-b
0 S 1013 125575 125554 0 80 0 - 3298 - pts/9 00:00:00 bash
...
-
PRI: 即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序
- PRI表示进程的实际优先级,数值越小优先级越高,由调度器动态计算并赋值。
- 对于实时进程来说,PRI是固定不变的;对于普通进程,调度器会根据进程的
Nice
值和动态调度策略自动调整PRI,以实现负载平衡。 - PRI通常由系统自动控制,用户无法直接修改其值。
-
NI: 表示进程可被执行的优先级的修正数值
-
Nice值用于影响进程的调度优先级,可以直接通过
nice
或renice
命令来调整,数值范围从-20
到19
。 -
Nice值也能用
top
进行更改(进入top后按“r”–>输入进程PID–>输入nice值) -
Nice值的改变会间接影响PRI:
Nice
值越小,PRI越高,从而提升进程获得CPU时间片的机会。 -
那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice。PRI(old)默认为80。
-
- 优先级更改:但实际上,进程优先级不建议更改。
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
五、环境变量
环境变量是在操作系统中,用于存储系统配置信息的一种变量,这些变量可以影响到进程和程序的运行环境。环境变量通常用于指定文件路径、配置系统行为、储存应用程序的配置信息等,它通常拥有全局属性。
# 同样是程序pwd不需要加上'./'
(base) Raizeroko@bciserver:~/code$ pwd
/home/Raizeroko/code
(base) Raizeroko@bciserver:~/code$ which pwd
/usr/bin/pwd
# 而test就需要加上'./'
(base) Raizeroko@bciserver:~/code$ ./test #cout<<'test'<<endl;
test
可以注意到一个问题,pwd
和一个自己写的test
同样都是程序,但是前者在运行时不需要带上'./'
而后者就需要带上。这正是因为 pwd
是在系统路径(如 /usr/bin/
)中的程序,而 test
是你当前目录下的程序。
- 系统路径 (PATH): 系统在运行命令时会根据
PATH
环境变量中定义的目录列表来寻找可执行文件。pwd
位于/usr/bin/
,而/usr/bin/
目录通常包含在PATH
环境变量中,因此系统可以直接找到它。 - 当前目录不在 PATH 中: 当前目录 (
./
) 通常不包含在PATH
中,为了安全考虑,避免无意中执行当前目录中的某些程序。所以当你在当前目录执行程序时,需要明确指定路径,比如./test
。 - 每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
1)查看环境变量
env
: 直接列出当前会话中的所有环境变量及其值。echo $NAME
:可以用来查看某个具体环境变量的值,其中NAME
替换为要查看的变量名。getenv()
:适用于 C/C++、Python 等编程语言,用于在程序中获取环境变量的值。extern char **environ
: C提供的第三方变量,也能获取环境变量。
2)常见环境变量
-
PATH(Linux系统的指令搜索路径)
PATH
用于告诉操作系统在运行程序或命令时,在哪些目录下查找可执行文件。(base) Raizeroko@bciserver:~/code$ echo $PATH /home/Raizeroko/.vscode-server/cli/servers/Stable-912bb683695358a54ae0c670461738984cbb5b95/server/bin/remote-cli:/home/Raizeroko/miniconda3/bin:/home/Raizeroko/miniconda3/condabin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
-
PATH的值是一个由冒号分隔的目录列表。
-
添加环境变量:
PATH=$PATH:[path]
,在PATH中添加新的路径path
,但是这种修改是内存级环境不良,如果一不小心删除,虽然会导致大部分指令无法执行,但是只需要重启就能恢复。 -
永久修改 PATH:可以将上述命令加到用户主目录中的
.bashrc
或.bash_profile
文件中,以确保每次启动 shell 时都会加载新路径。不建议将当前目录 (
./
) 添加到 PATH,因为可能会无意中执行当前目录中的恶意文件。
-
-
HOME
HOME
表示当前用户的主目录路径。通常在 Unix 和 Linux 系统中,每个用户都会有一个独立的主目录,用于存储该用户的配置文件、个人文件和其他资源。(base) Raizeroko@bciserver:~/code$ echo $HOME /home/Raizeroko
-
PWD
PWD
用于表示当前工作目录的绝对路径。这个变量在用户通过命令行切换目录时会动态更新,以便随时指示用户在文件系统中的具体位置。(base) Raizeroko@bciserver:~/code$ echo $PWD /home/Raizeroko/code
-
HISTSIZE
HISTSIZE
用于定义在当前会话中可存储的命令历史条目的最大数量。这个变量控制的是保存在内存中的命令数量,而非写入历史文件(通常为~/.bash_history
)的命令数。默认值是1000。(base) Raizeroko@bciserver:~/code$ echo $HISTSIZE 1000
-
USER
USER
环境变量在 Unix 和 Linux 系统中用于存储当前登录用户的用户名。这个变量通常由系统在用户登录时自动设置,反映了当前正在运行会话的用户身份。USER
变量在许多脚本、命令和应用程序中都很有用,因为它允许程序自动识别和使用当前用户的名字。这是一个只读变量,即用户无法在同一会话中更改自己的USER
值。如果要运行程序作为其他用户,通常会用sudo
或su
命令来启动新的会话,这样新会话中的USER
变量就会更新为目标用户的名字。(base) Raizeroko@bciserver:~/code$ echo $USER Raizeroko
-
OLDPWD
OLDPWD
用于存储上一个工作目录的路径。它通常和cd
命令配合使用,帮助用户快速切换到上一次所在的目录。(base) Raizeroko@bciserver:~/code/env$ echo $OLDPWD /home/Raizeroko/code # 'cd -' 命令会回到上个目录,其实就是转换成了'cd $OLDPWD'指令
3) 环境变量的特点
-
全局性:环境变量是系统级的,任何在该环境下运行的进程都可以访问这些变量。它们为不同的程序提供了统一的配置信息。
-
可继承性:子进程会继承父进程的环境变量。这意味着当一个进程创建另一个进程时,后者会自动获得父进程的环境变量副本。
-
动态性:环境变量的值可以在程序运行时动态改变,允许程序根据需要调整其行为。例如,可以在运行时修改
PATH
环境变量来改变命令查找的路径。 -
命名约定:环境变量通常使用大写字母命名,以便与其他类型的变量(如局部变量)区分开来。例如,
HOME
、PATH
、USER
等。 -
作用范围:环境变量的作用范围主要是针对当前用户的会话。在某些情况下(如使用
sudo
),环境变量可能会被清除或重置。 -
优先级:当一个变量在不同的作用域中存在时(例如用户环境和系统环境),用户设置的环境变量通常会覆盖系统默认的变量值。
-
易于访问:在大多数编程语言中都有简便的方法可以访问环境变量,通常是通过内置函数或库,如
getenv()
在 C/C++ 中,os.getenv()
在 Python 中。 -
配置参数:环境变量常用于传递配置信息,比如数据库连接字符串、API 密钥等,尤其是在容器化和云环境中,它们为应用程序的配置提供了灵活性和可移植性。
4) 命令行参数
命令行参数是指在启动程序时,通过命令行传递给程序的输入参数。这些参数通常用于控制程序的行为或提供必要的信息。通过使用命令行参数,程序可以更灵活地适应不同的使用场景,提升用户体验。
int main(int argc, char *argv[], char *env[]) {
// argc:参数个数,包括程序名
// argv:参数指针数组,argv[0] 是程序名,argv[1] 是第一个参数,依此类推,而最后一个参数默认为NULL
// env:
for(int i=0; argv[i]; i++)
{
printf("argv[%d]->%s\n", i, argv[i]);
}
}
对于上面的代码执行时带上选项可以看到输出如下:
(base) Raizeroko@bciserver:~/code/env$ ./env -a -b -c -d
argv[0]->./env
argv[1]->-a
argv[2]->-b
argv[3]->-c
argv[4]->-d
env[0]->SHELL=/bin/bash
env[1]->COLORTERM=truecolor
... # 其他环境变量
在main函数的传参int main(int argc, char *argv[], char *env[])
中,其存在两张核心向量表:
-
命令行参数表(
int argc, char *argv[]
)-
Bash 在解析命令时,会将整个命令行输入作为一个字符串,然后通过空格(以及其他特定的分隔符)将其分割成各个部分。
-
命令行参数允许用户在运行程序时直接传递输入数据,而无需在程序内部进行硬编码。这使得程序更加灵活和可重用。例如,用户可以通过命令行参数指定文件名、搜索关键字、配置选项等。
-
通过命令行参数,用户可以定制程序的执行方式。例如,可以使用不同的参数来启用或禁用某些功能,改变程序的运行模式(如调试模式或正常模式),或者调整输出格式。
-
命令行参数使得程序可以更容易地与其他脚本和工具进行集成。用户可以编写脚本来自动化任务,通过传递不同的参数来实现批量处理。
-
-
环境变量表(
char *env[]
)- 当从 Bash 或其他 shell 启动一个程序时,程序会继承父进程的环境变量。不需要在每次调用时重新指定这些变量。
- Bash 在启动时会从配置文件(如
/etc/profile
或~/.bashrc
)中读取环境变量设置,并在启动子进程时将这些变量传递下去。 - 环境变量通常用于配置应用程序的运行环境,比如指定路径、语言设置、文件位置等,使得程序可以在不同的环境中运行而不需要修改代码。
5)本地变量
在 Linux 中,本地变量指的是在当前 shell 会话或脚本中定义的变量。这些变量的作用域通常限制在它们被定义的上下文中,比如函数或脚本内。
-
[NAEM]=[VALUE]
: 创建本地变量# 定义本地变量:'NAME'是变量名,'VALUE'是对应值 NAME=VALUE
-
export [NAME]
: 将本地变量转为环境变量。# 'NAME'是对应本地变量名 export NAME
本地变量只会在本
bash
内有效,不会被继承。命令行中启动的指令不一定要创建子进程。
常规命令:通过创建子进程完成。
内建命令:bash不创建子进程,而是自己亲自执行(类似于bash调用了自己写的,或系统提供的函数)。
创建了本地变量test,echo指令按道理来讲是创建了子进程,但是由于test是一个本地变量,因此不应该被识别并打印,但在这里却完成了。
(base) Raizeroko@bciserver:~/code/env$ test=100 (base) Raizeroko@bciserver:~/code/env$ echo $test 100
cd
,echo
,export
,pwd
,set
等都是内建命令。
六、程序地址空间
程序地址空间(Program Address Space)是操作系统为每个进程分配的虚拟内存空间。这个空间是进程运行时可访问的地址范围,使每个进程仿佛拥有独立的内存,避免了进程间的相互干扰。
1) 地址空间分布
-
代码段(Text Segment)
- 包含程序的可执行代码,即指令的二进制表示。
- 是只读的,通常共享的,以避免重复加载多个相同程序。
-
数据段(Data Segment)
-
存储静态分配的数据,分为已初始化数据段(已初始化的全局变量)和未初始化数据段(BSS段,用于未初始化的全局变量)。
-
这些数据在程序加载时被映射到内存中,并在程序运行期间不会被释放。
-
static
修饰的局部变量,在编译的时候已经被编译到全局数据区了。
-
-
堆区(Heap Segment)
-
用于动态内存分配,使用
malloc
、calloc
或new
等函数申请内存。 -
堆从低地址向高地址增长,并在程序运行时动态扩展或收缩。
-
-
栈区(Stack Segment)
-
用于存储函数调用相关的信息(如局部变量、返回地址等)。
-
栈从高地址向低地址增长,每次调用函数时,都会分配新的栈帧,当函数返回时,这部分内存被释放。
-
-
内核区(Kernel Space)
-
用户进程无法直接访问该区域,它在用户地址空间之外,通常只允许内核使用。
-
包含了操作系统的内核代码、数据和资源,保证系统安全和稳定。
-
高地址
┌──────────────┐
│ 内核区 │
├──────────────┤
│ 栈ᅟᅠ区 │
│ (Stack) │
├ ─ ─ ─ ── ─ ─ ┤
│ ↓ │
│ 共享区 │
│ ↑ │
├ ─ ─ ─ ── ─ ─ ┤
│ 堆ᅟ区 │
│ (Heap) │
├──────────────┤
│ 已初始化数据 │
│ (Data) │
├──────────────┤
│ 未初始化数据 │
│ (BSS) │
├──────────────┤
│ 代码段 │
│ (Text) │
└──────────────┘
低地址
2)虚拟地址与物理地址
当父子进程同时打印同一个变量的地址和值,并修改其中一个的值时会发现:
- 父进程与子进程的变量名相同
- 父进程与子进程的地址相同
- 父进程与子进程的值不同
由上可知:如果变量的地址是物理地址,那绝对不可能存在读取同一个地址的同一个变量时,它读到的值却不同,因此用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。
-
虚拟地址
- 虚拟地址是操作系统提供给应用程序的一种抽象地址空间,应用程序只能使用虚拟地址而不直接接触物理地址。
- 提供进程间的内存隔离,防止一个进程直接访问或修改另一个进程的内存。
- 操作系统可以灵活地管理内存,例如加载到不同物理内存位置。
- 虚拟内存大小不受实际物理内存大小的限制,使得程序可以访问比物理内存更大的地址空间(通过虚拟内存页换入换出实现)。
- 程序地址空间是虚拟的地址不是物理地址。
-
页表
页表是操作系统为每个进程维护的映射表,记录虚拟地址到物理地址的对应关系。页表中记录着虚拟页(虚拟地址的一部分)和物理页框的映射,以及一些管理信息(如页是否在内存中、是否可写等)。
-
虚拟页(Virtual Page):这是进程内的逻辑地址空间划分,通常每页大小为4KB或其他固定大小。虚拟页帮助程序生成和管理进程的地址空间。
-
物理页框(Physical Page Frame):这是实际的物理内存块,与虚拟页对应的映射位置。虚拟地址转换为物理地址后,通过物理页框访问实际内存数据。
-
管理标志位:
- 有效位(Present Bit):标记虚拟页是否在物理内存中,或是否被换出到磁盘。如果不在内存当中,操作系统会发生缺页中断,将会在磁盘中找到对应程序加载到内存上,重新在页表上建立映射关系。
- 读写权限(Read/Write Bit):控制虚拟页是否可以被读取、写入,或执行。
- 访问位(Accessed Bit)和修改位(Dirty Bit):分别表示页面是否被访问过或是否被修改过,通常用于页面置换算法优化。
内存并不知道一个数据能不能被写入或读取,正是因为页表管理标志位的存在,才能控制进程对内存的读取写入等操作。
-
-
物理地址
- 物理地址是计算机内存硬件的实际地址。CPU最终通过物理地址访问具体的内存单元。
- 程序提供虚拟地址,CPU通过页表转换得到对应的物理地址。
- 内存访问请求通过内存管理单元(MMU)进行地址转换。
由于上面三者的存在,所以操作系统可以借此实现一种关键机制——写时拷贝!!!(即上面发生相同地址但值不相同问题的原因。
假设
fork()
之前父进程的一个虚拟地址0x1234
指向物理地址0xABC
。在fork()
后:
- 子进程同样拥有虚拟地址
0x1234
,并且此地址最初指向相同的物理页0xABC
。- 当父进程或子进程对
0x1234
执行写操作时,操作系统会分配新的物理页(例如0xDEF
),并将页表项更新,使写入的进程的0x1234
指向0xDEF
。- 这样,父进程的
0x1234
仍然指向0xABC
,子进程的0x1234
则指向0xDEF
。通过以上机制,父进程和子进程在使用同一虚拟地址的情况下可以独立地对数据进行操作,让进程以统一的视角看待内存,并实现进程管理模块和内存管理模块进行解耦合。
3) 深入理解地址空间
地址空间是描述一个进程可访问的内存范围的抽象,本质上是操作系统管理的一个数据结构对象,就像进程控制块(PCB)一样,地址空间也是由内核维护的关键对象。
-
地址空间大小:CPU的位数(例如 32 位或 64 位)决定了其能够处理的地址宽度。例如,32 位的 CPU 最大支持 2^{32} 个地址(4 GB 的地址空间),而 64 位的 CPU 理论上支持 2^{64} 个地址(16 EB 的地址空间)。但实际上,64 位系统并未完全利用 64 位的地址宽度。
-
地址空间的划分:地址空间划分是将整个虚拟地址范围分割成不同的区域,以支持程序的不同需求。地址空间的划分通常通过一个结构体或数据结构来描述,它标识了进程内各个内存区域的大小、起始地址和特定的权限。