进程概念详解
一、进程的基本概念
进程在书本上的定义是:计算机中正在运行的程序实例。仅此描述可能让很多人感到困惑。
我们磁盘上存储着.exe文件,启动文件时,文件会从磁盘加载到内存,由CPU对文件的数据和代码进行运算。但进程并非仅仅是数据和代码。
在计算机中有许许多多个应用程序在指向,有些程序可能是新执行进来的,有些程序可能是执行完了将要结束,可能还有些程序需要阻塞等待。有这么多种程序也有这么多种情况,那么操作系统是需要对这些程序进行管理,但又应该怎么管理呢?
计算机中有众多应用程序,它们的执行情况各不相同,操作系统需要对这些程序进行管理。每个程序都有各种属性,如代码地址、数据地址和最重要的ID(用来标识进程的唯一标识符)。操作系统将这些属性通过struct整合,用来描述一个应用程序当前的状态,并且每个struct都有一个next指针指向下一个整合属性,从而将对进程的管理转化为对链表的管理。我们把每一个struct都称之为内核数据结构对象
因此,进程 = 内核数据结构对象 + 数据和代码。
二、认识进程
进程的计算机业里统称为Process Control Block(PCB).
PCB通常包含以下内容:
- 进程状态:如就绪、运行、阻塞等。
- 进程ID:用来表示进程唯一性的标识符。
- 程序计数器:指示进程下一条指令的地址。
- CPU寄存器:保存进程在执行过程中使用的寄存器内容。
- 内存管理信息:如进程的地址空间、页表等。
- 调度信息:包括优先级、调度队列等信息。
在Linux中,PCB具体指的是task_struct,以下是其部分源代码
struct task_struct {volatile long state; // 进程状态struct thread_info *thread_info; // 指向线程信息的指针struct mm_struct *mm; // 进程的内存描述符struct files_struct *files; // 进程打开的文件描述符struct fs_struct *fs; // 进程的文件系统信息struct signal_struct *signal; // 信号处理相关struct sighand_struct *sighand; // 信号处理程序struct task_struct *parent; // 父进程struct list_head children; // 子进程链表struct list_head sibling; // 同级兄弟进程链表pid_t pid; // 进程标识符pid_t tgid; // 线程组标识符(线程的主线程)int exit_state; // 进程退出状态unsigned long flags; // 标记进程状态的标志位unsigned int prio; // 进程优先级unsigned int static_prio; // 静态优先级unsigned int normal_prio; // 正常优先级unsigned int rt_priority; // 实时优先级struct sched_entity se; // 调度实体struct mm_struct *active_mm; // 活动的内存描述符// 其他许多字段...};
三、查看进程信息
(一)通过系统调用函数获取
getpid函数是一个系统调用函数,用于获取当前进程的进程ID(PID,Process ID)。
例如,我们可以在Linux中编写一个简单的死循环输出打印的程序,执行程序后,再打开一个Linux窗口,使用ps ajx | head -1 && ps axj |grep mycode命令显示当前系统中运行的进程信息。可以看到循环中输出的pid值与显示的pid值正好对应。
(二)通过/proc系统文件查看
我们还可以通过/proc系统文件查看进程信息。例如,使用ll /proc/9430 -1命令查看当前进程包含的内容,其中exe指的是当前进程对应的可执行文件,cwd表示当前进程运行所处的文件的绝对路径。这也解释了为什么通过fopen函数创建文件时,如果不带上绝对路径,文件就会在当前路径下创建。
此外,在使用ps ajx命令时,除了显示PID还有一个PPID,PPID是当前进程的父进程ID。我们可以通过调用getppid()函数查看当前进程的PPID。
通过上图运行可以看到,我们一直运行mycode的文件时,它的PID是依次递增的,但一直不变的是PPID。那mycode的PPID又是谁呢?
通过上图运行可以看到,我们一直运行mycode的文件时,它的PID是依次递增的,但一直不变的是PPID。那mycode的PPID又是谁呢?
通过ps ajx命令观察到,PID为8729的进程所代表的名称为 bash。
那么 Bash 在Linux中是一个命令行解释器,用于解释用户输入的各种命令比如ls,pwd等。从中不难得出一个结论:我们历史上所执行的指令,工具,以及自己编写的程序,只要运行起来就都是进程。
四、创建子进程
在Linux中,有一个系统调用接口fork()用于创建子进程。
fork函数创建子进程后,子进程将继承父进程的代码,并且子进程将拥有一个唯一的进程ID。fork函数的返回值为pid_t,pid_t其实是被typedef过的int。
(一)fork()函数的使用示例
分析上图代码,一开始我们输出当前进程的PID和PPID,接着通过fork进行创建子进程后再次打印PID和PPID,我们会发现此时除了第一条打印的语句我们理解,还多了一条打印的语句。并且观察PID,会发现PID为 19215 的PPID正是一开始打印的PID 19214,那么就能够证明了fork会创建子进程,并且子进程会继承父进程fork()之后的代码,所以会打印两句输出语句。
调用fork()创建子进程后,返回值id会根据是父进程还是子进程进行区分:子进程返回的id为0,父进程返回的id是子进程的PID。
那这里可能就会产生一些疑惑。1.为什么fork给父进程返回的是子进程的PID,而子进程返回0。 2.为什么一个 fork() 函数会返回两次。 3.为什么一个id变量,即==0,又满足>0,if else if 同时成立。
(二)fork()函数的返回值及原因分析
问题1:
对于为什么fork给父进程返回的是子进程的PID,而子进程返回0。这是因为在所有进程就类似于一个进程树。
对于父进程来说,我需要知道我的子进程PID是什么,以进行区分因为一个父进程可以有多个子进程。 对于子进程来说,我没有子进程,只需要返回0即可。那可能会有疑惑,那我子进程是怎么找到父进程的呢?其实在之前的代码就可以知道,是有一个系统调用接口 getppid() 就可以知道自己的父进程是谁。
所以对于父子进程来说,是一个 1:n 之间的关系。
问题2:
为什么一个 fork() 函数会返回两次呢?其实这个问题也很好理解
那么在fork函数内会申请新的pcb,malloc一块新的空间给子进程以及拷贝父进程pcb,那么此时子进程已经创建,就会和父进程一起执行代码,到最后返回pid。所以并不是一个函数返回两次,而是有两个进程在执行同一段代码,子进程返回0,父进程返回子进程的pid。
问题3:
为什么一个id变量,即==0,又满足>0,if else if 同时成立。在讨论这个问题之前,我们先想一下,我此时同时打开了抖音以及微信,如果微信崩了,会不会影响抖音?显然是不会的,那么就可以得出一个结论,进程是具有独立性的。
父进程创建子进程后,两个进程同时指向一份代码,但这部分代码是只读的。如果父子任何一方,对数据进行修改,那么操作系统会把被修改的数据在底层拷贝一份,让目标进程修改这份拷贝。这种操作称之为写时拷贝
(三)实验验证进程的独立性和写时拷贝
gval是一个全局变量,父子进程都可以看到,当我们fork之后,子进程会开始对gval值进行修改,而父进程只对gval值进行查看。并且通过运行代码很明显能看到,子进程修改了gval值后,父进程的gval值并没有发生改变,这更加证明了父子进程之间是相互独立,并且如果父子一方对数据进行修改并不会影响到另一方。
那么本片文章到这里就结束了,感谢各位观看!!!