Linux进程概念和控制(必备知识)

1、冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。

在这里插入图片描述
我们所认识的计算机,都是有一个个的硬件组件组成
输入单元:包括键盘, 鼠标,扫描仪, 写板等
中央处理器(CPU):含有运算器和控制器等
输出单元:显示器,打印机等

注意:
1、这里的存储器指的是内存
2、不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
3、外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
4、一句话,所有设备都只能直接和内存打交道

对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。(从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。)

在这里插入图片描述

2、操作系统

概念

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。
笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)

操作系统的定位是:一款纯正的"搞管理的软件"

在这里插入图片描述

设计OS的目的

与硬件交互,管理所有的软硬件资源
为用户程序(应用程序)提供一个良好的执行环境

管理的概念

先描述在组织(可以转化成对目标的管理,也可以转化成对数据的管理)

操作系统如何为用户提供服务

通过窗口的形式(OS不信任任何用户) 也就是系统调用接口,如银行它并不信任任何客户,但它必须为客户提供服务,于是我们取钱的时候都是通过窗口的形式。

一个好的操作系统应具备哪些特征

对上为用户提供良好的运行环境(普通用户),为程序员提供各种基本功能。
对下管理好软硬件资源

3、进程

什么是进程

加载到内存的程序就叫做进程,实际上就是 程序文件内容 + 操作系统维护进程的相关数据结构(PCB)。进程是承担分配系统资源(CPU时间,内存)的基本实体。

为什么要有PCB

在任何进程形成之时,操作系统都要为该进程创建PCB进程控制块,因为系统中可能存在多个进程,而每一个进程被管理起来都要先描述在组织。(描述进程的结构体,PCB进程控制块。组织:将被管理对象使用特性的数据结构组织起来)

什么是PCB

在OS上,PCB进程控制块就是一个结构体类型。在Linux操作系统中,PCB是struct task_struct{//进程的所有属性}。
有了进程控制块,所有的进程管理任务与进程对应程序毫无关系,内核创建的该进程的PCB强相关。

task_ struct内容分类:
<1>标示符: 描述本进程的唯一标示符,用来区别其他进程。
<2>状态: 任务状态,退出代码,退出信号等。
<3>优先级: 相对于其他进程的优先级。
<4>程序计数器: 程序中即将被执行的下一条指令的地址。
<5>内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
<6>上下文数据: 进程执行时处理器的寄存器中的数据

为什么要有上下文数据?
电脑运行多个程序本质是通过,CPU快速切换完成的(时间片),因为寄存器只有一份。进程切换出去时要保留运行信息,上下文数据起到保护和恢复的作用。

<1>进程的创建

通过系统调用创建进程-fork
#include <unistd.h>
pid_t fork(void);

返回值:自进程中返回0,父进程返回子进程id,出错返回-1

来个代码验证一下:
在这里插入图片描述
运行结果:
在这里插入图片描述
我们可以看到,if和else中的语句均被执行了。这在一个进程中是绝对不可能做到的。而且也证明一件事,子进程和父进程的代码是共享的。我们创建子进程难道是为了让他和父进程执行一样的代码吗?
当然不是,因此我们可以通过,if分流来让子进程和父进程来做不同的事情

fork常规用法

1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
2、一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数(下面将进程替换会提到)。

fork调用失败的原因

系统中有太多的进程
实际用户的进程数超过了限制

<2>进程查看

方法一:通过系统调用来获取进程的标识符

getpid ( )获取当前进程ID, getppid ( )获取父进程ID。
在这里插入图片描述
运行结果
在这里插入图片描述
方法二:通过查看proc文件下的内容 ps /proc/

我们可以看到,我们打印出来的进程ID在这个文件目录下都是可以找到的。
在这里插入图片描述
方法三:ps命令查看正在运行的进程

ps基本格式
[root@VM-4-16-centos practice4]# ps aux
#查看系统中所有的进程,使用 BS 操作系统格式
[root@VM-4-16-centos practice4]# ps -le
#查看系统中所有的进程,使用 Linux 标准命令格式

选项:
a:显示一个终端的所有进程,除会话引线外;
u:显示进程的归属用户及内存的使用情况;
x:显示没有控制终端的进程;
-l:长格式显示更加详细的信息;
-e:显示所有进程;
可以看到,ps 命令有些与众不同,它的部分选项不能加入"-“,比如命令"ps aux”,其中"aux"是选项,但是前面不能带“-”

常用几种组合
“ps aux” 可以查看系统中所有的进程;
“ps -le” 可以查看系统中所有的进程,而且还能看到进程的父进程的 PID 和进程优先级;
“ps -l” 只能看到当前 Shell 产生的进程;
"ps -al"以详细信息显示终端下的进程

当我们想看具体某个进程的时候也可以通过管道来过滤下
在这里插入图片描述
USER:该进程是由哪个用户产生的。
PID:进程的 ID。
%CPU:该进程占用 CPU 资源的百分比,占用的百分比越高,进程越耗费资源。
%MEM:该进程占用物理内存的百分比,占用的百分比越高,进程越耗费资源。
VSZ: 该进程占用虚拟内存的大小,单位为 KB。
RSS: 该进程占用实际物理内存的大小,单位为 KB。
TTY: 该进程是在哪个终端运行的。其中,tty1 ~ tty7 代表本地控制台终端(可以通过 Alt+F1 ~ F7 快捷键切换不同的终端),tty1~tty6 是本地的字符界面终端,tty7 是图形终端。pts/0 ~ 255 代表虚拟终端,一般是远程连接的终端,第一个远程连接占用 pts/0,第二个远程连接占用 pts/1,依次増长。
STAT:进程状态
START:该进程的启动时间。
TIME:该进程占用 CPU 的运算时间,注意不是系统时间。
COMMAND:产生此进程的命令名。

<3>进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。

下面的状态在kernel源代码里定义: static const char * const task_state_array[] = {
“R (running)” , /* 0 /
“S (sleeping)”, /
1 /
“D (disk sleep)”, /
2*/
“T (stopped)”, /* 4 /
“t (tracing stop)”, /
8 /
“X (dead)”, /
16 /
“Z (zombie)”, /
32 */
};

R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡(interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
Z(zombie)-僵尸进程:
僵死状态(Zombies:是一个比较特殊的状态。僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

僵尸进程示例:
代码:
在这里插入图片描述

程序运行:
在这里插入图片描述
脚本监控
在这里插入图片描述

僵尸进程的危害
1、进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。父进程如果一直不读取,那子进程就一直处于Z状态。
2、维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
3、想象一下如果一个父进程创建了很多子进程,就是不回收,就就会造成大量内存资源的浪费,也就是内存泄漏。

孤儿进程:父进程先退出,子进程就成为“孤儿进程”。孤儿进程被1号init进程领养,由init进程回收。

<4>进程优先级

概念

cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

在这里插入图片描述

PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值

1、PRI即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
2、NI就是nice值,其表示进程可被执行的优先级的修正数值。
3、PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
4、调整进程优先级,在Linux下,就是调整进程nice值nice其取值范围是-20至19,一共40个级别
进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。

用top命令修改已存在进程的nice值
1、top
2、进入top后按“r”–>输入进程PID–>输入nice值

<5> 进程地址空间

我们先看一段代码
在这里插入图片描述
运行结果
在这里插入图片描述
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
1、变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。
2、但地址值是一样的,说明,该地址绝对不是物理地址!
3、在Linux地址下,这种地址叫做 虚拟地址
4、我们在用C/C++语言所看到的地址,全部都是虚拟地址! 物理地址,用户一概看不到,由OS统一管理。

进程地址空间是什么?

本质是内核中一种数据类型(mm_struct),用软件的方式来模拟内存。

为什么要有地址空间?

1.通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了,保护物理内存以及各个进程的数据安全。
2.将内存申请和内存使用的概念在时间上划分清除,通过虚拟地址空间,来屏蔽底层内存申请的过程,达到进程读写内存和OS进行内存管理操作,进行软件上面的分离
3.站在CPU和应用层的角度,进程可以看做统一使用4GB空间,而且每个区域的相对位置是比较确定的(每个进程都认为自己是独占系统资源的)。

那进程地址空间是怎样和物理内存建立联系的呢?

在这里插入图片描述
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
在这里插入图片描述

4、环境变量

概念

1、环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
2、如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
3、环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性,可以被子进程继承下去。

常见环境变量

PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash。

查看环境变量方法

echo $NAME //NAME:你的环境变量名称

测试PATH
为什么我们平时二进制程序需要带路径才能执行,而有些命令却不需要带路径就可以执行,如ls、pwd等,原因就是这些指令其实已经添加到了环境变量中了,那把我们的二进制程序也添加到环境变量中去是不是就可以不带路径执行了?(export PATH=$PATH:hello程序所在路径)是的。如下图:
在这里插入图片描述
测试HOME
我们用root和普通用户,分别执行 echo $HOME ,对比差异,执行 cd ~; pwd ,对应 ~ 和 HOME 的关系。通过下图我们可以发现普通用户cd ~是跳转到普通用户的目录下,root用户则是跳转到root目录下。
在这里插入图片描述

环境变量相关的命令

1、echo: 显示某个环境变量值
2、export: 设置一个新的环境变量
3、env: 显示所有环境变量
4、unset: 清除环境变量
5、set: 显示本地定义的shell变量和环境变量

环境变量的组织方式

在这里插入图片描述

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串

通过代码来获取环境变量

在这里插入图片描述
在这里插入图片描述
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。

运行结果:
在这里插入图片描述

通过系统调用获取环境变量

在这里插入图片描述
运行结果:
在这里插入图片描述

5、进程控制

进程创建上面我们说过了,就是使用调用系统中fork函数就可以了。下面我们说一说进程终止、等待以及替换。

<1>进程终止

终止场景

1、代码运行完毕,结果正确
2、代码运行完毕,结果不正确
3、代码异常终止

常见进程退出方法

异常退出:ctrl + c,信号终止。
正常终止(可以通过 echo $? 查看进程退出码)。

在这里插入图片描述

_exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值。
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。

exit函数
#include <unistd.h> void exit(int status);

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
1、执行用户通过 atexit或on_exit定义的清理函数。
2、关闭所有打开的流,所有的缓存数据均被写入
3、调用_exit

如图:
在这里插入图片描述

return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

进程退出,OS层面都做了什么呢?

系统层面少了一个进程,free PCB、mm_struct、页表和各种映射关系。代码和数据申请的空间也要释放掉。

<2>进程等待

什么是进程等待?

父进程fork后,需要通过wait/waitpid等待进程退出。

为什么要让父进程等待?

1、通过获取子进程的退出信息,来得知子进程的运行结果。
2、可以保证时序问题,子进程先退出,父进程后退出。
3、进程退出的时候会先进入僵尸状态,会造成内存泄漏问题,需要父进程等待,来释放该子进程占用的资源。(进程一旦编程僵尸进程就"刀枪不入" kill -9 也无能为力,因为谁也没办法杀死一个已经死去的进程)。

进程等待的方法

wait方法(阻塞式等待)
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status); 函数原型
返回值: 成功则返回被等待进程pid,失败则返回-1。
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid方法 (options设为0阻塞等待)
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;(这时候父进程可以干其他事情)
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态(不被信号所杀kill),则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。(非阻塞等待,options设为0阻塞等待)

1、如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
2、如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。如果不存在该子进程,则立即出错返回。

来一段代码感受一下wait和waitpid函数
在这里插入图片描述
options设为0时(等价下面一行wait代码)执行结果
在这里插入图片描述
options设为WNOHANG时执行结果
在这里插入图片描述
可见这时候的程序运行效率会更高一些,父进程在等待子进程的同时还可以去做其他的事。

获取进程状态(status)

方法一:
使用WIFEXITED获取子子进程退出状态,WEXITSTATUS获取子进程退出码。
在这里插入图片描述
方法二:
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特
位):
在这里插入图片描述
获得退出状态则先向右移动8位在与上0XFF,获得终止信号则直接与上0X7F就可以了。
如上述代码中的
在这里插入图片描述

阻塞等待 VS 非阻塞等待

阻塞等待: 父进程专心等待什么事都不做,可能要多次检测,基于非阻塞等待的轮训方案(间隔时间向子进程发消息询问)。
阻塞等待的本质: 本质是将父进程的PCB放到了等待队列,并将状态该为S状态。
返回的本质: 进程的PCB从等待队列拿到运行队列,从而被CPU调度。

非阻塞等待:父进程在等待的同时还可以分心执行其他任务。

<3>进程替换

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。**调用exec并不创建新进程,**所以调用exec前后该进程的id并未改变。
在这里插入图片描述
程序替换代码示例
在这里插入图片描述
运行结果:
在这里插入图片描述
注意:
1、程序替换会更改代码区的代码会发生写时拷贝。
2、程序替换成功后,不会执行后序代码。
3、程序替换不会创建新程序。

exec函数
#include <unistd.h>`
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg, …,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,这些函数之间的关系如下图所示:
在这里插入图片描述

函数解释

1、这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
2、如果调用出错则返回-1。
3、所以exec函数只有出错的返回值而没有成功的返回值。

命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

在这里插入图片描述

这几个函数比较相似我们选几个来看看。

在这里插入图片描述
来段代码感受一下:
这里面不带e的也就是不需要自己维护环境变量的比较简单,只需要牢记一点。你想执行谁,想要在命令行上怎么执行(注意传参的形式就可以了) 在下面代码中会有注释,这里我们以execle来演示。
在这里插入图片描述
执行结果:
在这里插入图片描述
进程中的环境变量说明
在Linux中,Shell进程是所有执行码的父进程。当一个执行码执行时,Shell进程会fork子进程然后调用exec函数去执行执行码。Shell进程堆栈中存放着该用户下的所有环境变量,使用execl、execv、execlp、execvp函数使执行码重生时,Shell进程会将所有环境变量复制给生成的新进程;而使用execle、execve时新进程不继承任何Shell进程的环境变量,而由envp[]数组自行设置环境变量。

  • 17
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 15
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小朱同学..

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值