进程
概念:
windows
上面运行的各种软件和开发的程序运行起来我们叫把程序跑起来,而程序的本质就是进程,所以专业的叫法是进程跑起来而不是程序跑起来,打开windows
下面的任务管理系统上看到的程序其实都是进程。
cpu对进程的处理有个规定:每个进程都有一个固定的运行时间周期,一旦过了这个周期那么这个进程就被结束不管你跑没跑完,如果没跑完会有个程序计数器会存储地址,这样下次再跑这个进程就不会重头跑而是在结束前的位置继续跑。
而为什么有这个规定呢?其实原因很简单就像你去医院看病,得到前台挂号排队因为医生有限所以需要排队,而cpu也同理你进程这么多我cpu就一个如果我我是先跑一个程序再结束的话,如果那个程序特别长要执行很久从你用户的角度上看电脑像是卡死了,所以在有限的资源上表现出你每个进程都在运行的话就必须每隔一段时间久换一个进程而这个时间非常的短,在我们人类的角度上看几乎察觉不到这机制,这也使的我们的机器像是能同时运行多个进程的错觉。
描述进程-PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
- 我们称之为PCB,在Linux下的PCB是一个
task_struct
的结构体task_struct
是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
如何理解PCB?
PCB就是一个大结构体是用链表结构链接起来的,而PCB结构下都应该包含这些字段如下:
- pid和ppid
- 优先级
- 帮我找到我的代码和数据
- 时间片
- 上下文信息
- 连接信息
前台进程
我们可以跑一个进程看看,在此我写了一个死循环
int main()
{
while (1);
return 0;
}
我们可以看到当执行了这个进程之后无论,我们怎么输入指令系统都不会响应我们,因为前台进程只能运行一个而shell命令也是一个进程所以运行了死循环就运行不了shell脚本了。
如果我们想执行shell脚本就得让它变成后台进程所以我们在执行它的命令加上个&
就好了。
我们可以看到R+变成R了这代表什么意思?
+
字母带+说明是前台运行
R
进程正在运行
S
进程在休眠
优先级
为什么要有优先级?
因为cpu资源有限加上有很多进程,所以为了保证每个PCB都能执行到,cpu会进行优先级设置。
如何理解进程排队?
程序计数器
cpu内有很多种寄存器,而EIP
寄存器是专门来存储地址的,而这个存储器存的地址是cpu下一条要执行的指令的地址,因为cpu在运转的过程都是围绕着三个指令一直运转:取指令->分析指令->执行指令。
时间片
每个进程的都有一个固定的执行周期而这个周期叫做时间片,每过一段时间就要换个进程执行。
上下文数据
因为每过一个时间片就要换一个进程而它怎么知道之前的进程执行到哪一步了呢?
cpu在对进行清理的时候把执行到哪一步的位置有个指针存起来了,而这个指针是用的寄存器存起来的所以再清理这个进程的时候它会把它的临时数据也放到内存当中这样它下次再跑到这个进程的时候也能还原之前执行的位置了。
进程状态
每个进程在运行时都会有一个状态每种状态都有不同的含义
R状态
R
状态代表的是该进程在运行或正准备运行,也就是说R
状态进程不一定是在执行而有可能是在准备被执行,说人话就是虽然我没在运行但是我准备好了随时可以运行,因为不一定是要运行时才能是R
状态只要是能运行就可以是R
状态。
因为PCB
的结构里面的其它结构不一定是该结构的节点而有可能是其它结构不过它在PCB
下面充当PCB
的节点,说人话就是虽然PCB
是一个链式结构或者其它更高效的结构但是,它的每个节点不一定是一样的节点有可能它的某个节点是红黑树,然后另一个节点是顺序表这样的结构,你可以把PCB
理解成存储其它结构的链式结构。
而我们也说了,一个进程就有一个PCB
那么这么多进程,它怎么高效的找到下一个R
状态的进程执行呢?如果说是遍历的话这效率就是O(N)了,很显然Linux
不会直接遍历来找因为这样效率太低了,所以它会为R
状态的进程创建一个队列,这样按优先级排的进程就能先执行了同时效率还提升这样找就是O(1)而不是O(N)了。
S状态
S状态的进程是浅睡眠,浅睡眠它能被Kill掉也就是说别人还在睡觉我们就KIll了它,不仅能被kill掉还能随时被叫醒执行其它任务,它在LInux下的意思是待机或者挂起。
D状态
D状态和S状态类似不过D状态是深度睡眠,在深度睡眠状态不会被任何人kill掉包括操作系统,如果对它执行kill它只会在自动醒时候才会执行,因为在深度睡眠它不听到任何指令,所以任何人都不能对它下指令为什么会有D状态呢?
举个例子: 假设一个进程抱着一堆数据对着硬盘说你把这些写进去,硬盘得到指令就把它这堆数据拿去写了,而被拿去写的时候这个进程又不需要被执行或者其它操作那么它会进入S状态(浅睡眠),而这个时候操作系统的内存资源很紧张已经对操作系统的正常运行造成影响了而这时操作系统会对无用进程进行kill然后释放资源来缓解资源紧张问题,而这时它会对进程进行检查它发现有个进程是S状态,而这个时候操作系统会把它kill掉来缓解资源紧张(PS:可以理解成,公共厕所就2个,然后4个人在排队上厕所而你不用厕所却占着这个坑,所以公共厕所管理员会把你T出去让别人需要的来上)而这个时候硬盘写数据写失败了回来叫S进程问它接下来要如何处理,而这个时候S进程已经被kill掉了,所以硬盘如何喊都不会有任何响应。
这个时候硬盘就面临着很尴尬的问题那就是它找不到之前对它下命令的进程,而它手里抱着的数据写进去写失败了,它现在面临着两个选择是接着写呢?还是把这些数据丢弃了呢?结果很显然肯定是丢弃因为你这个进程都不在了我就干脆不写了就偷个懒把数据丢弃了反正你也不知道。
而为了解决这个问题就有一个新状态那就是D状态这个状态是深度睡眠不会被kill掉,所以像刚才那个情况系统再看到一个D状态的会认为它不能被kill掉就算要kill掉也只能等它醒了再kill掉相当于等它处理好它的事情再kill掉它。
T状态
T状态是暂停也就是相当于操作系统中的挂起,具体就是一个进程正常运行突然cpu发了一个电信号通知这个进程,让这个进程先别运行在一旁等我通知你再来运行,这个进程就被cpu踢出了队列状态同时变成T状态这个时候这个进程就是挂起。
Z状态
僵尸进程
僵尸进程的意思是这个进程已经死亡了,但是还不能直接结束因为要保存信息和退出码,而操作系统需要先拿到退出码因为操作系统需要知道它是正常死亡还是被kill死亡的,因此操作系统会叫调制器去拿退出码,只要这些信息都被操作系统收到了才会释放资源。
方便理解的例子: 一个人在跑步的人(进程)突然猝死,那么死亡后看到的人会报警而警察来了会先获取信息(调制器取信息)让法医判断它是怎么死亡的,然后再把信息带回去才能把人抬走,这些死亡信息会被做成档案存放在警察局。
而这个期间僵尸进程是Z状态,这个状态代表的是进程死了,但是还在运行因为它必须得把信息全交给调制器才会得到结束的信号,我们可以用Linux来模拟下这个僵尸进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
size_t id = fork();
if (!id)
{
while (1)
{
printf("pid:%d\n", getpid());
sleep(1);
exit(EXIT_SUCCESS);
}
}
else if (id > 0)
{
int count = 5;
while (count)
{
printf("ppid:%d\n", getppid());
sleep(30);
count--;
}
printf("I am pronc...\n");
}
return 0;
}
我们可以看到子进程变成了僵尸进程,因为子进程结束了但是父进程还没结束父进程还没结束就不能去子进程那取退出码,不能取退出码就没法结束就会变成僵尸进程。
僵尸进程的危害
进程的退出状态必须维持下去因为它要把它的结果告诉它的父进程,而进程一直不读取,那么子进程就一直是退出状态,维护退出状态是要数据维护的也数据基本信息,所以保存在PCB当中。
如果父进程创建很多子进程都不回收的话会造成泄露(ps:内存泄露指空间无法被回收利用从导致泄露)
X状态
X状态是退出状态不会被查看到
Linux内核状态源代码
/*
* The task state array is a strange "bitmap" of * reasons to sleep. Thus "running" is zero, and * you can test for combinations of others with * simple bit tests.
*/
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 */
};
孤儿进程
如果在子进程是僵尸进程的时候父进程直接终止,那么这个子进程就会成为孤儿进程,而一旦成为孤儿进程那么只能让操作系统来领养。一般是由1号init的进程来领养。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}else{//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
同时运行的进程
单核的cpu能同时运行进程吗?答案是可以的
#include <stdio.h>
#include <unistd.h>
int main()
{
size_t id = fork();
if (!id)
{
while (1)
{
printf("I am chile:%d\n", id);
sleep(1);
}
}
else if(id > 0)
{
while (1)
{
printf("I am fathe:%d\n", id);
sleep(1);
}
}
return 0;
}
我们可以看到,它两是同时运行的同时子进程的pid是0,而子生成的子进程自会执行生成它之后的代码,而之上的代码不会执行。
进程优先级
PRI&Nice
- PRI是进程优先级,默认值是80值越低优先级越高,值不可被修改。
- Nice是进程修正值,默认是0它的作用是用于修改优先级
有时候如果想提升某个进程的效率的话就调整它的优先级,但是相应的代价就是其它进程的运行效率会低,一般是不建议对进程进行优先级调整的默认的就可以了。
进程的优先级准确的来说是PRI+Nice=进程优先级,而我们对Nice进行修改即可对优先级进行调整,而Nice的值最高是19,最低是-20而PRI默认是80那么进程优先级最高就是99,最低是60。
如何修改Nice
我们使用top
指令进入实时检测进程,按下r再输入要调整进程的PID,再接着输入调整值如果调整值过低或过高的就得提升权限进行此操作。
其它概念
独立性
每个进程都是隔离的互不影响
竞争性
因为资源有限所以每个进程会有竞争,为了平衡恶意竞争就由进程优先级来限制。
并发
假设cpu是单核,一次只能执行一个进程,而在这段时间内cpu通过切片对多个进程任务进行推进,这段时间内多个进程任务被推进看起来像是同时运行,这个过程我们我们称之为并发。
并行
假设我们的cpu是多核,而这个时候有多个进程同时进行,那么这个行为我们称之为并行。
环境变量
基础概念:
PATH
用于指定命令搜索路径(例如:ls这些命令都是通过PATH协助系统搜索路径找到ls的执行程序的)
HOME
用于指定工作目录(Linux默认目录指定是home/use,如果不想默认目录就是当前用户可以指定修改)
SHELL
当前Shell,通常它都是bin/bash。
查看可执行程序的路径
像ls、rm……这些都是可执行程序,为什么它们都不用./就能执行?而我们却要./才能执行,因为./的意思告诉系统要执行的文件在当前目录内,而为什么那些没有给路径我们却能直接输入文件名就能执行了?其实是环境变量的原因系统要找这些指令的时候会通过PATH来查找因为可执行命令都会放在这,像安装一个程序都要点击下一步,这个过程其实是拷贝它们把程序拷贝到PATH环境变量下这样我们就能输入文件名执行了。
如果我们也想要完成想ls这样直接输入文件名就能执行的操作,我们可以把我们的程序直接拷贝到/usr/bin/目录下,但是不建议在这么做。
真要直接输入程序名就能执行的话我建议用:export PATH=$PATH:程序所在目录绝对路径
这样添加进去的是临时变量账号一重登就会被删除。
环境变量指令
- whics 程序名
找到该程序的路径
- export 环境变量名=$环境变量名:绝对路径目录
用于把可执行程序添加到,环境变量文件内后续可支持直接输入程序名执行改程序,账号一重登就会删除添加的内容
- export 本地变量名=n
设置一个新的环境变量不过只在本进程(bash)生效
- echo 环境变量名
显示环境变量名路径
- echo “内容” > /dev/pts/n
把内容发个另一个使用该机器的人n为使用改机器的显示器端
- unset 环境变量
用于清除环境变量
- set
用于显示本地定义的Shell变量和环境变量
环境变量组织
每个程序都会生成一个环境变量表,环境变量表是一个字符指针数组,每个指针指向一个以\0
结尾的环境字符串。
通过代码获取环境变量
main函数其实是有参数的很多人都不知道,因为一般都用不到但是在Linux下面main函数的参数我们是用的到的,比如可以写个脚本之类的操作,当然windows也是有用的,但是windows主要是以图形界面为主,所以参数几乎无用。
main函数有三个参数且它也是被调用的,分别是int argc
、char* argv[]
和char* envp[]
分别对应的是,指令个数、指令和指令环境变量表。
Linux下的指令的选项参数就是这样做出来的,所以mian函数有这些参数是C语言规定的。
进程空间地址
以往这叫程序空间地址是不准确的,准确的来讲应该是叫进程空间地址。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 0;
}
else if (!id)
{
g_val = 100;
printf("chilid[%d]:%d:%p\n", getpid(), g_val, &g_val);
}
else
{
sleep(3);
printf("parent[%d]:%d:%p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
[ls@VM-4-7-centos test3]$ ./test3
chilid[15274]:100:0x601058
parent[15273]:0:0x601058
[ls@VM-4-7-centos test3]$
我们可以看到子进程的改变对父进程没有任何影响,说明子进程也是一个独立的PCB,体现了独立性但是空间地址是一样的为什么不会影响呢?
因为地址一样,但是内容改变互不影响说明这不是物理地址而是虚拟地址,因为物理地址具有唯一性如果内容被更改其它进程来读的时候读到的是改变后的内容,而这的子进程的改变对父进程没有任何影响说明这是虚拟地址,虚拟地址通过页表转化就能获得物理地址,这也就说明它们调用的是同一块地址但是改变互不影响。
页表
我们通常看到的地址是真正的地址吗?其实并不是我们能看到的地址都是虚拟地址,物理地址我们是无法看到的因为被操作系统的页表保护起来了。
当我们申请一段空间时操作系统会去物理内存里面找一段闲置空间,然后用页表映射物理空间生成一个虚拟地址。
进程控制
fork创建子进程
fork是用于创建子进程来完成父进程所交代的任务,当子进程完成某种任务后会有两种返回值一种是父进程调用fork返回的pid
而另一个才是子进程的返回值0
,如果子进程返回-1
说明该进程创建失败。
子进程在生成时操作系统会做几件事,第一创建子进程的信息第二把子进程组织起来连接到PCB
中从而完成一个进程的创建,而子进程的创建是发生在fork调用的一瞬间吗?
其实并不是调用fork时它会先让系统创建信息组织信息等等……这一系列操作完成之后才真正的是两个进程这才和父进程彻底隔离开来,而子进程的数据都是源自于父进程。
写实拷贝
写实拷贝是发生在子进程的数据和父进程遭到修改才会发生,如果未修改那么会共用一块内存空间这样可以极大的增加空间的利用率,只有但子进程或父进程发生修改时才会独立生成一个块内存,否则都是共用的。
fork的返回值
-
父进程调用fork返回子进程的ID
因为子进程有多个而父进程只有一个且子进程需要被父进程进行管理,所以必须得返回子进程ID才能管理。
-
子进程的返回值
当子进程完成某种任务后必须返回执行的结果,所以也会有一个返回值该返回值是给父进程看的,父进程要知道子进程的任务完成的如何。
fork调用失败
fork调用失败有两种情况:
- 用户所创建的进程超过了上限
- 当前系统所存在的进程过多
进程终止
为什么main函数要有返回值?
以前从语法角度来讲是不好讲清楚的,但是从操作系统的视角来理解那么就很容易说通了。
因为操作系统必须要返回一个值,这是因为用户要知道该main函数执行的结果如何了,当然有些程序是不需要知道main函数的执行结果但是操作系统必须要提供这样的方式让用户获取到main函数执行的结果且操作系统也得要知道main函数执行的如何。
exit
exit
是用来终止进程的,当进程被exit
终止那么它会对该进程进行清理释放曾经占有的空间并且会把缓冲区的内容全部刷新出来。
_exit
_exit
和exit
没有太大的区别,唯一的区别是头文件不同且不会对缓冲区刷新关闭。
进程终止OS做了什么?
当一个进程被exit
终止那么OS会把它PCB
和各种队列中移除并释放资源。
进程等待
为什么进程要等待?
因为进程不等待的话无法收回子进程的信息,而子进程一直没被收回信息那么回发生僵尸进程且如果长期没被收回会发生内存泄漏问题。
一般进程等待都是由父进程等子进程,父进程等到子进程一般是先进行对子进程的资源回收和退出码等信息回收。
进行进程等待的方法
进程等待的必要性
如果父进程不等待子进程的话会导致子进程成为僵尸进程,而一旦成为僵尸进程就会导致内存泄漏。
wait
函数原型:
pid_t wait(int* status);
waitpid
函数原型:
pid_t waitpid(pit_t pid, int* status, int options);
使用案列:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main(void)
{
pid_t arr[10];
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
int count = 10;
while (count > 0)
{
printf("I am child,pid:%d, ppid: %d\n", getpid(), getppid());
sleep(1);
count--;
}
exit(11);
}
arr[i] = id;
}
int count = 0;
while (count < 10)
{
int status = 0;
pid_t ret = waitpid(arr[count], &status, 0);
//以下是推荐的进程等待用法
if (ret >= 0)
{
printf("waite child success!,%d\n", ret);
if (WIFEXITED(status))
{
printf("child get exit code:%d\n", WEXITSTATUS(status));
}
else
{
printf("child not exit normal!\n");
}
//以下是通过未计算来取得信息,低七位是信号退出码,次低八位是进程退出码
//printf("status: %d\n", status);
//printf("child get singal:%d\n", status & 0x7F);
//printf("child get exit code:%d\n", (status >> 8) & 0xFF);
}
count++;
}
return 0;
}
进程阻塞和非阻塞的区别
-
阻塞
父进程就什么事情都不进行一直等待着子进程,直到子进程成功退出才停止阻塞。
-
非阻塞
父进程不会一直等待子进程,而是一直做其它事情每隔一段时间就回来确认子进程是否完成,如果完成那么就会退出非阻塞状态。
进程程序替换
用fork
创建子进程后执行的是父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec
函数来执行另一个程序。
当进程调用一种exec
函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
调用exec
并不创建新进程,所以调用exec
前后该进程的id
并未改变。
替换函数
有六种以exec
开头的函数,统称为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* cosnt envp[]);
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以
exec
函数只有出错的返回值,而没有成功的返回值。
命令解释
这些函数原型看起来很容易混淆,但只要掌握了规律就很好记住。
- l(list):表示参数采用列表
- v(vector):参数用数组
- p(path);有p自动搜索环境变量
PATH
- e(env):表示自己维护环境变量