linux操作系统-进程概念和进程控制

了解冯诺依曼体系结构

在这里插入图片描述
上面是一张冯诺依曼体系结构图,从第一台计算机问世到如今我们所使用的笔记本电脑或者台式机都遵守了这个规则,主要是有五大部分组成:控制器,运算器,存储器,输入设备,输出设备。其中控制器和运算器组成了我们常说的CPU,存储器是内存,输入设备一般是键盘、摄像头、磁盘等,输出设备就是显示器、磁盘等,其中磁盘既是输出设备也是输入设备。CPU只能和内存交互,输入设备和输出设备只能和内存交互。

进程概念

在多道程序环境下,程序的执行属于并发执行,因此它们会失去封闭性,并具有间断性和运行结果不可再现性。通常,程序是不能参与并发执行的,否则,程序的执行就失去了意义。为了使程序可以并发执行,并且可以对并发执行的程序加以描述和控制,人们在OS中引人了“进程”这一概念。

为了使参与并发执行的每个程序(含数据)都能独立地运行,在OS中必须为之配置一个专门的数据结构,称之为进程控制块 (process control block,PCB)。Linux下称为task_struct。系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。这样,由程序段、相关的数据段和PCB这3部分便构成了进程实体(又称为进程映像)。一般情况下,我们把进程实体简称为进程,例如,所谓创建进程,实质上是指创建进程的PCB;而撤销进程,实质上是指撤销进程的PCB。

对于进程,从不同的角度可以给出不同的定义,其中较典型的定义有以下3种。
1. 进程是程序的一次执行。
2. 进程是一个程序及其数据在处理机上顺序执行时所发生的活动。
3. 程是具有独立功能的程序在一个数据集上执行的过程,它是系统进行资源分配和调度的一个独立单位。
在引人进程的概念后,我们可以把传统OS中的进程定义为:“进程是程序的执行过程,是系统进行资源分配和调度的一个独立单位”。

进程PCB

进程的PCB在内存中是用于管理一个进程的,它是一个struct结构体,结构体内存放进程的各种信息。例如:

  • 标示符(pid): 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  • I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息。

一个进程对应一个PCB也就是一个结构体,OS对PCB的管理就是对进程的管理,如果把进程的PCB都用链表连接起来,那么对进程的增删查改就变成了对PCB的增删查改。

进程的创建

在OS中是允许一个进程创建另外一个进程的,被创建出来的这个进程就叫做子进程。子进程还可以创建孙进程。是一种多叉树的关系。
子进程会继承父进程的资源和数据。例如,继承父进程所打开的文件和分配到的缓冲区等。
当子进程被撤销时,应将其从父进程那里获得的子进程的资源归还给父进程。撤销父进程时,也应该撤销所有的子进程。

Linux下C语言中有一个函数可以创建子进程。
在这里插入图片描述
fork函数会给子进程和父进程分别返回一个返回值。

#include <stdio.h>
#include <unistd.h>
int main(){
    pid_t id = fork();
    if(id==0){
         printf("我是子进程,pid为:%d,我的父进程是:%d\n",getpid(),getppid());//获取自己的pid函数为getpid(),获取父进程的pid函数为getppid();
    }else{
        printf("我是父进程,pid为:%d,我的父进程是:%d\n",getpid(),getppid());
    }
    return 0;
}

在这里插入图片描述
这就是创建子进程的过程,fork会给子进程返回0,会给父进程返回子进程的pid。(pid就是进程的唯一标识符,是OS用来区别进程的)。

//查看进程的相关指令
ps axj | grep 可执行文件名   //包含grep本身这个进程
ps axj | grep 可执行文件名 | grep -v grep  //不包含grep
ps axj | head -1 && ps axj | grep 可执行文件名 |grep -v grep //显示列表名


ls /proc 	//查看所有进程的目录,只有操作系统运行起来才会存在这个目录
getpid(); //获取当前进程id
getppid(); //获取父进程id
kill -9 PID值 //杀死对应进程
killall 进程名称 //根据名字杀死进程

进程状态

一个进程运行的时候可以有很多种状态。例如:

  • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
  • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

僵尸进程以及危害

有一种进程叫做僵尸进程。为什么会有僵尸进程呢?怎么形成的?

我们自己或者是OS创建进程的目的是为了让进程帮我们完成某些事情来达成某些目的。当进程结束的时候需要查看进程是否是正常返回。以及目的达成没有。所以子进程结束时需要给父进程一个返回值。如果子进程执行完了。但是一直等不到父进程来接收子进程的返回结果。那么这个子进程会变成僵尸进程(Z状态)。

僵尸进程有什么危害呢?

设想一下如果一个父进程有很多个子进程,但是子进程执行完要返回结果的时候,找不到父进程来接收,就会产生很多的僵尸进程。内存中进程的PCB和存放数据和代码的空间都是要占据内存空间的,如果进程一直不回收就有可能造成内存泄漏,这是很严重的问题。

孤儿进程

当子进程还没有结束,但是父进程却提前结束了,这个子进程就会变成孤儿进程,而孤儿进程默认会被1号进程init接收。这也是一种解决僵尸进程的方式,直接kill -9 pid 杀死父进程,让init进程接管子进程。

环境变量

环境变量是指操作系统运行环境需要的一些参数

echo $PATH   //查看在环境变量中可执行程序的搜索路径
echo $环境变量名 //查看环境变量值
export PATH=$PATH:可执行程序路径   //添加可执行程序到环境变量
env 		 //查看所有的环境变量
export 变量名  //把shell临时变量设置到环境变量中
set   //查看环境变量
unset  //删除某个环境变量
int main(char argc,char* argv[],char* envp[])
char* envp[]:传递给当前进程的环境变量表
char* argv[]:可执行程序的命令选项都会传递到这个指针数组中,shell中的命令选项就是这个原理,可以用来显示不同的结果
int argc  ://代表传递命令选项的个数

进程地址空间

什么是进程地址空间?

我们日常编写C语言或者C++代码时候,都会创建一个变量或者创建函数,这些变量、常量、函数等数据都是可以用取地址符号进行地址的读取查看,这些地址是内存条中真实的物理地址吗?如果不是那是什么地址?可以用代码验证一下。

#include <unistd.h>
#include <stdio.h>      
#include <assert.h>
int g_val = 100;
int main(){
//创建子进程
 pid_t id = fork();
 assert(id>=0);
 //子进程fork返回值为0,父进程返回值大于0
 if(id==0){
   while(1){
       printf("我是子进程,pid为:%d,我的父进程是:%d,g_val值为%d,&g_val值为%p\n"\
           ,getpid(),getppid(),g_val,&g_val);
       sleep(1);
      g_val++;
   }
 }else{
   while(1){
      printf("我是父进程,pid为:%d,我的父进程是:%d,g_val值为%d,&g_val值为%p\n"\
           ,getpid(),getppid(),g_val,&g_val);
      sleep(1);
  }
}
return 0;
}

> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YpWY5DBd-1680487737050)(C:\Users\JRG\Desktop\linux笔记\image\image-20230313145635360.png)]

通过以上代码我们可以看出来,子进程和父进程中,一个变量名相同,地址相同,但是里面存放的值居然是不同的,这符合常理吗?显然不是,因为在真实的物理空间中,一个地址不可能有两个值。就像一个人正常情况不可能有两个身份证号,只能是一个萝卜一个坑。所以这个值显示出来的地址不是真实的,是虚拟的。

这个虚拟的地址空间就叫做进程地址空间。顾名思义,每个进程都会有一个虚拟的进程地址空间

进程地址空间怎么来的,定义方式是什么?

进程地址空间是当一个进程被操作系统加载到内存的时候,操作系统会给每个进程创建一个独立的虚拟空间,就叫做进程地址空间,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ScuZ6C89-1680487737051)(C:\Users\JRG\Desktop\linux笔记\image\image-20230313151721938.png)]

这个地址空间也叫做线性地址空间,根据冯诺依曼结构体系,计算机的结构分为五大部件,各个部件之间必须有联系,不然没办法进行IO处理,连接各个部件的线叫做总线。以32位操作系统的计算机为例,CPU和内存之间就有32根系统总线连接,每根线只能表示0和1,32根线就有232 个排列组合,就对应内存中的地址有从0到232个地址,每个地址大小为1字节,也就是内存大小为4GB。每个进程地址空间都会被映射一个4GB的空间(理论上,实际上并没有这么多),由于地址空间是线性的(0000 0000-FFFF FFFF),宽度为1字节,所以操作系统管理虚拟地址空间就是用PCB指向一个mm_struct数据结构进行管理,里面会记录每个空间段的起始位置和结束位置,如果需要扩大区域,只需要修改start和end记录的位置即可。

虚拟地址空间怎么转换为物理地址空间的

我们想要把数据真正的存放在电脑上,还是需要把数据存放在物理空间内的,那么数据是怎么从虚拟空间转换到物理内存中的呢?其实在虚拟内存和物理内存中间还有一个叫做**页表(MMU)**的东西,当我们对虚拟内存中的数据进行访问或修改时,页表根据相对应的规则去物理空间读取数据,如果数据是只读的,页表也会有相对应的r权限。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-66bJcDNO-1680487737051)(C:\Users\JRG\Desktop\linux笔记\image\image-20230313160412423.png)]

到现在就可以解释为什么一个空间地址会有两个值,根本原因是因为我们在语言层面读取的地址都是虚拟的,物理内存中一定是两个不同值对应两个地址空间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RgNpo1vh-1680487737051)(C:\Users\JRG\Desktop\linux笔记\image\image-20230313170447691.png)]

创建子进程的时候,数据不变的时候子进程会和父进程共用数据和代码,一旦数据发生改变,改变的一方就会对数据进行写实拷贝,单独开辟空间,但是虚拟内存的位置不会变,只会在物理内存中单独开辟,这也就是为什么一个地址有两个值。

虚拟地址空间存在的意义

  1. 如果没有虚拟地址空间让CPU直接访问物理内存,在正常情况下其实是可以的,把需要的可执行程序加载到内存,用PCB把每一个进程管理起来,其实是可以的。但是如果我们自己写的可执行程序有问题呢?比如指针越界,修改了别的进程的数据就会影响进程的独立性。如果有虚拟地址空间,如果进程发生了问题,在虚拟空间和页表这里就会直接报错,不会影响物理内存,其他进程正常运行,保护了进程的独立性。防止地址随意访问,保护了物理内存与其他进程。
  2. 我们写代码向系统申请空间的时候,系统不会立马把空间给进程,只有真正要用的时候才会向物理内存申请空间,我们写代码看到的是申请完空间立马会用,但是对于CPU的执行速度来说就不一定了,有可能并发的时候进程的时间片结束了。CPU就会切换进程,申请的物理内存就有可能被闲置,这是一种浪费。所以操作系统的原则是不允许有任何的浪费或者不高效。所以申请空间时会先在虚拟内存申请,页表会记录,真正用的时候才会在物理内存申请空间。所以就可以分为进程管理和内存管理,我们的代码只会操作虚拟空间的地址,对虚拟空间进行查看访问,不会关心数据存放在物理地址的哪里,这就叫做进程管理。而物理内存只需要管理有没有剩余空间,数据存放在哪里,这就叫做内存管理。中间会有页表进行转换。这样可以将进程管理和内存管理进行解耦合,增强独立性。
  3. 在我们进行编程的时候,编译器也是需要遵守虚拟地址空间的规则,所以我们写好代码,编译生成可执行文件的时候,可执行程序内部的指令就已经生成了对应的虚拟地址,当可执行程序加载到内存时,CPU调度这个进程,会从虚拟地址的代码区有一个固定的程序入口通过页表访问到物理内存,CPU会拿到第一条指令,指令内会包含下一个调用指令的虚拟地址,再通过虚拟地址访问下一条指令的物理空间,程序就跑起来了。
  4. 这张图片也是很好的解释了一个进程从磁盘拿到内存中CPU是怎么去执行读取代码访问数据的。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c9hCmlf6-1680487737052)(C:\Users\JRG\Desktop\linux笔记\image\image-20230313181336239.png)]

进程控制

进程退出

使一个进程退出有两种方法

  1. 在main函数中使用return。
  2. 调用exit()或者_exit()函数。区别:exit()退出进程之前会刷新缓冲区的内容,__exit()直接退出进程,不会刷新缓冲区。exit()底层调用了 _exit()函数。

写实拷贝

当一个子进程被创建出来的时候,刚开始父子进程的代码和数据是同一片空间,只有一方的数据被修改的时候,系统才会再开辟空间拷贝一份数据给父或子进程。

子进程被创建出来系统不立马拷贝数据和代码,而是用写实拷贝是因为有可能子进程不会用到父进程的一些数据,可以按需申请空间, 操作系统不允许任何浪费和不高效的行为发生。

进程等待

概念:通过系统调用,获取子进程退出码或者退出信号的方式,顺便释放内存问题。

原因:避免内存泄漏,获取子进程的执行结果,避免成为僵尸状态。

进程退出的三种结果:

  1. 程序执行完毕,执行结果正确。
  2. 程序执行完毕,执行结果错误。
  3. 程序执行异常,返回异常结果。

waitpid返回结果存储在status,status为整形,32个比特位,返回结果存储在高八位,退出信号存储在第0-7个比特位.
在这里插入图片描述
Linux提供了两个宏:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

方式:

wait(int* status) 等待任意一个子进程 返回结果为子进程pid,,失败返回-1,子进程退出结果存储进status变量中。

waitpid(pid_t pid,int* status,int options),pid指定的子进程pid,返回结果存储进status变量中,options传入0一直等待子进程结束,传入WNOHANG就会隔一段之间查看子进程的状态,可以做别的事情,waitpid返回值为0表示子进程还没退出,退出成功返回子进程pid。

进程替换

一个进程创建一个子进程可以做两种事情,一种是执行父进程的代码和数据。另一种就是执行另一个程序的代码和数据,进程本身pid和ppid不会改变,只是把进程里面的数据和代码进行替换和修改为另一个进程的代码和数据,所以进程替换并不会创建新的进程,最后的执行结果还是会返回到父进程中。

进程替换后从新的进程开始执行,新的进程结束后直接返回退出信号和退出码,替换前的进程不会再向后执行,因为数据和代码已经被替换成新的了。

在这里插入图片描述
进程创建的时候内存中是先有task_struct还是先有的代码和数据呢?

我们编写一个可执行程序的时候,都是bash的子进程,所以bash先创建一个子进程,然后再调用exec接口,这样就完成了程序的替换,所以是先有的task_struct,然后才调用系统接口(exec)把数据和代码加载到进程里面。

进程创建子进程之后,子进程进行exec,子进程发生写实拷贝,把新程序的数据和代码(代码也会写实拷贝,不会再和父进程共享)拷贝覆盖到子进程,不会影响父进程,程序必须整体替换,不能局部替换。

进程替换的接口函数
C/C++中让一个进程去做我们自己指定的事情,需要调用一些接口函数。

int execl(const char *path, const char *arg, ...);
 path传递的可执行程序的路径,arg是指令和指令选项,...代表后面能跟多个指令选项。
 例如:execl("\bin\ls","ls","-a","-l",NULL);
int execlp(const char *file, const char *arg, ...);
file只需要指定文件名就可以,系统会自动到环境变量中查找,arg是指令和指令选项,...代表后面能跟多个指令选项。例如:
execlp("ls","ls","-a","-l",NULL);
int execle(const char *path, const char *arg,..., char * const envp[]);
envp是给进程传递的环境变量,不能直接传递自己定义的环境变量,否则会覆盖系统自带的环境变量,如果想加入自己的环境变量,用putenv函数。例如:

extern char** environ;
putenv("MYENV=YouCanSeeMe");//把自定义环境变量加入到系统环境变量中,传给ls进程
execl("\bin\ls","ls","-a","-l",NULL,environ);
int execv(const char *path, char *const argv[]);
 path传递的可执行程序的路径,arg是指针数组 ,例如:
 char* myargv[]={"ls","-a","-l",NULL};
 execv("\bin\ls",myargv);
int execvp(const char *file, char *const argv[]);
file只需要指定文件名就可以,系统会自动到环境变量中查找,arg是指针数组 ,例如:
 char* myargv[]={"ls","-a","-l",NULL};
 execv("ls",myargv);
int execvpe(const char *file, char *const argv[],
                   char *const envp[]);
int execve(const char *filename, char *const argv[],
                  char *const envp[]);
//上面的六个函数接口都是对这个函数的封装,底层都是调用这个函数
	我们暂时学习了一下Linux下进程的概念和进程的一些等待和进程的替换。这些只是进程的入门级知识,进程还有许多知识没有分享出来,
我会继续和小伙伴们一起学习成长。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值