前文所提:应用程序从磁盘加载进内存,而操作系统的管理方法是描述 + 组织,所以通过该种管理方法形成的管理对象就是进程。
从用户的视角来看,进程是一个程序的运行实例;从操作系统的视角来看,进程是一个拥有资源分配能力的实体。
进程 = 内核数据结构对象 + 自己的代码和数据
在Linux中进程可以看做是PCB(task struct)和自己的代码和数据组成的。PCB中包含该进程的所有属性,与代码以及数据共同组成进程,PCB中存在指向其他进程的指针,通过指针的指向,进程通过双向链表的数据结构来进行链接,而进程的管理就是对链表的增删查改。并且每个进程都有独立的地址空间,以避免相互干扰。
我们所使用的指令、工具以及自己的程序,运行起来,都是进程!
操作系统使用**进程控制块(Process Control Block, PCB)**来描述和管理进程的所有信息。PCB 是一个重要的数据结构,操作系统通过它来追踪每个进程的状态。
在 Linux 操作系统中,PCB 被实现为一个名为 task_struct
的结构体,其主要内容包括:
在 Linux 内核中,所有进程的 PCB 以链表形式组织。通过 task_struct
中的 next
和 prev
指针,形成一个双向链表,对进程进行遍历和管理。
如下图所示:
在 Linux 系统中,可以通过 /proc
文件系统以及用户级工具来查看进程信息:
/proc
文件夹/proc
中都有一个对应的文件夹,文件夹名称是该进程的 PID。/proc/1
。ps
** 命令**:显示进程的详细信息。
bash
就是命令行解释器
,每启动一个XShell就会有一个bash进程启动,所以输入的指令等信息都是通过父进程bash
处理的,所以当使用命令行启动多个进程后可以发现它们的父进程(PPID)都是bash
。
top
** 命令**:实时显示系统运行的进程和资源使用情况。进程id(PID) :
**<font style="color:rgb(100,106,115);">getpid();</font>**
⽗进程id(PPID):**<font style="color:rgb(100,106,115);">getppid();</font>**
在sys/types.h
包含获取当前进程ID的函数,比如使用getpid();
获取当前进程的PID:
cwd
与exe
grep
作为指令也是进程,所以显示的时候也会显示grep
的进程信息。
/proc/[PID]
目录下的cwd
和exe
是与进程相关的重要符号链接,它们分别代表了进程的当前工作目录和可执行文件路径。理解这两个概念对于深入掌握进程的行为和状态非常有帮助。
cwd
(Current Working Directory)cwd
是一个符号链接,指向进程的当前工作目录。当前工作目录是指进程在执行过程中,其相对路径的基准目录。就好比你在终端中切换到某个目录,然后运行一个程序,这个被切换到的目录就是程序的当前工作目录。/home/user/projects
目录下启动了一个名为my_app
的程序,那么/proc/[PID]/cwd
就会指向/home/user/projects
目录。chdir
可以改变cwd
的指向路径。cwd
来解析的。比如,如果my_app
程序尝试创建data.txt
文件,直接使用(./data.txt
)而没有指定绝对路径,那么系统会直接在/home/user/projects
下建立/home/user/projects/data.txt
(假设cwd
是/home/user/projects
)。cwd
可以了解进程是在哪个目录下运行的,这对于调试程序(特别是当程序试图访问文件时出现路径错误等问题)和监控进程行为非常有用。例如,如果一个进程试图访问一个不存在的文件并报错,查看cwd
可以帮助确定它试图访问文件的完整路径,从而更容易地找到问题所在。exe
(Executable)exe
是一个符号链接,指向启动该进程的可执行文件的路径。这个可执行文件是进程运行的主体,包含了程序的机器代码和资源。/usr/bin/my_app
启动了一个程序,那么/proc/[PID]/exe
就会指向/usr/bin/my_app
。exe
链接,你可以清楚地知道是哪个可执行文件启动了这个进程。这对于系统监控工具来说非常重要,因为它们可以根据可执行文件的路径来识别和分类进程。例如,在一个包含多个不同版本应用程序的系统中,通过exe
可以区分是哪个版本的应用程序正在运行。exe
可以帮助确定是否有未经授权的程序在运行。如果发现exe
指向一个不熟悉或可疑的路径,这可能是一个安全风险的信号。此外,它也可以用于追踪软件的使用情况,比如统计某个特定可执行文件被启动的次数等。exe
的路径是非常有用的。可以直接通过这个路径来启动新的进程实例,或者使用调试工具(如gdb
)附加到这个可执行文件上进行分析。假设你正在运行一个名为example_app
的程序,你可以在终端中使用以下命令来查看其cwd
和exe
:
pid=$(pgrep example_app) # 获取example_app进程的PID
ls -l /proc/$pid/cwd # 查看cwd链接
ls -l /proc/$pid/exe # 查看exe链接
这将输出类似以下内容:
lrwxrwxrwx 1 user user 0 Jan 1 12:34 /proc/1234/cwd -> /home/user/projects
lrwxrwxrwx 1 user user 0 Jan 1 12:34 /proc/1234/exe -> /usr/local/bin/example_app
从这个输出中,你可以看到example_app
进程的当前工作目录是/home/user/projects
,而其可执行文件位于/usr/local/bin/example_app
。这些信息对于理解进程的行为和进行系统管理非常关键。
fork
以及进程的独立进程的创建和管理是操作系统的重要功能。在 Linux 中,创建进程主要通过 fork()
系统调用。
通过man
查看fork()
:
pid_t
类型<unistd.h>
可以通过以下代码获取进程的 PID 和其父进程的 PPID:
PID:
getpid();
PPID:getppid();
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
fork()
是 Linux 中用于创建新进程的函数。
得到的运行结果如下:
可以看出,在fork();
执行后出现了两个进程,其中一个进程的pid是fork
前的进程的pid,一个是新进程的pid。此时就成功的创建了子进程。但是要如何使用fork()
呢?
首先从fork()
函数本身开始理解:
以下是一个代码示例。
printf("父进程开始运行,pid:%d \n", getpid());
pid_t id = fork(); // 父子进程的独立过程是在调用 fork() 函数时完成,之后父子进程独立
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
while(1)
{
sleep(1);
printf("我是一个子进程!我的pid:%d,我的父进程id:%d\n",getpid(), getppid());
}
}
else
{
// father
while(1)
{
sleep(1);
printf("我是父进程!我的pid:%d,我的父进程id:%d\n",getpid(), getppid());
}
}
运行结果如下:
在
fork()
执行后创建了子进程,并且同上文所讲相同,父进程的父进程是bash
进程。
fork()
?fork()
是用于创建进程的系统调用。
fork()
的返回值fork()
返回两个值,因为它在两个进程中执行,分别是:
fork()
返回子进程的 PID(进程 ID),这是一个正整数(> 0
)。fork()
返回 0
。**fork()**
有两个返回值?操作系统在执行 fork()
时,会基于当前父进程的状态,创建一个几乎完全相同的子进程。
fork()
会把当前的程序和运行环境复制一份,创建一个新的进程。在fork()
函数内,return
也是代码语句,所以也会作为拷贝的代码,申请新的PCB,拷贝父进程的PCB给子进程。在fork中通过区分父子进程后,通过return
返回两个返回值,两个返回值都对id
进行修改,对变量进行修改,触发了写时拷贝,因此系统会进行空间及数据的分配。这就是为什么返回两个返回值的原因,下文会对该过程进行详细讲解。
**fork()**
,操作系统知道它是父进程,所以返回子进程的 PID,方便父进程管理。**fork()**
,它的视角是:我是子进程,我没有子进程,所以返回 0
。注意:
fork()
的执行结果是两套完全独立的运行环境。fork()
的返回值是区分父进程和子进程的关键。父子进程的独立过程是在调用 **fork()**
函数时完成的。具体地说,当 fork()
被调用时,操作系统会执行以下步骤,从而使父进程和子进程完全独立:
**fork()**
** 的调用时刻**:操作系统在执行 fork()
时,会基于当前父进程的状态,创建一个几乎完全相同的子进程。进程复制的内容:
一旦 fork()
返回,父子进程开始独立运行:
fork()
的返回值处分叉: fork()
返回子进程的 PID。fork()
返回 0
。父子进程的独立性体现在以下几点:
现代操作系统使用了一种优化机制,叫做 写时复制(COW),以减少不必要的资源浪费:
**fork()**
刚返回时,父子进程共享相同的物理内存页(只读),因此复制过程很快。因此,只有在需要时,内存的独立性才真正实现,也就是需要对对内存中数据进行修改的时候,但逻辑上,父子进程从 fork()
返回后就已经被视为完全独立了。
调用 fork()
后,父子进程的分离流程可以表示如下:
父进程:
ret = fork(); // 返回子进程 PID (> 0)
------------------------------
| 父进程逻辑 |
| printf("父进程部分"); |
| 独立运行,继续父进程代码 |
------------------------------
子进程:
ret = fork(); // 返回 0
------------------------------
| 子进程逻辑 |
| printf("子进程部分"); |
| 独立运行,继续子进程代码 |
------------------------------
写时拷贝修改ret
内容,进程独立。
**fork()**
** 是操作系统分离父子进程的起点**。基本的独立靠的是
**struct task_struct(PCB)**
独立。 当父子进程任何一方进行数据修改的时候触发写时拷贝,操作系统就把修改的数据在底层拷贝一份,让整个目标进程修改这个拷贝,脱离代码共享,实现完全独立。