Linux:进程概念
- 博客主页:一起去看日落吗
- 分享博主的在Linux中学习到的知识和遇到的问题
博主的能力有限,出现错误希望大家不吝赐教
- 分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义,祝我们都能在鸡零狗碎里找到闪闪的快乐。
- 冯·诺依曼结构也称普林斯顿结构,
是一种将程序指令存储器和数据存储器合并在一起的存储器结构
。程序指令存储地址和数据存储地址指向同一个存储器的不同物理位置,因此程序指令和数据的宽度相同,如英特尔公司的8086中央处理器的程序指令和数据都是16位宽。- 数学家冯·诺依曼提出了计算机制造的三个基本原则,
即采用二进制逻辑、程序存储执行以及计算机由五个部分组成(运算器、控制器、存储器、输入设备、输出设备)
,这套理论被称为冯·诺依曼体系结构。
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系.
我们所认识的计算机,都是有一个个的硬件组件组成:
输入单元:包括键盘, 鼠标,扫描仪, 写板等
中央处理器(CPU):含有运算器和控制器等
输出单元:显示器,打印机等
注意
- 这里的存储器指的是内存
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
- 一句话,所有设备都只能直接和内存打交道。
冯诺依曼体系结构规定了硬件层面上的数据流向,
所有的输入单元的数据必须先写到存储器中 ,然后 CPU 通过某种方式访问存储器,将数据读取到 CPU 内部,运算器进行运算,控制器进行控制,然后将结果写回到内存,最后将结果传输到输出设备中
。
我们先看一下计算机的存储分级:
当一个快的设备和一个慢的设备一起协同时,最终的运算效率肯定是以慢的设备为主,就如 “ 木桶原理 ” —— 要去衡量木桶能装多少水,并不是由最高的木片决定的,而是由最短的木片决定的,一般 CPU 去计算时,它的短板就在磁盘上,所以整个计算机体系的效率就一定会被磁盘拖累。所以我们必须在运行时把数据加载到内存中,然后 CPU 再计算,而在计算的期间可以同时让输入单元加载到内存,这样可以让加载的时间和计算的时间重合,以提升效率
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。 从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?
何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)
与硬件交互,管理所有的软硬件资源
为用户程序(应用程序)提供一个良好的执行环境
注意
- 硬件遵守冯诺依曼体系
OS不信任任何用户,任何对系统硬件或者软件访问,都必须通过OS的手
- 计算机体系是一个层状结构,任何访问硬件或者软件的行为,都必须通过OS接口,贯穿OS进行访问
- 库函数:语言或者第三方库(第一方:系统的、第二方:自己的,其余是第三方的)给我们提供的接口
- 系统调用:OS提供的接口(我们使用 print、scanf 等库函数时,都使用了系统接口,称之为系统调用。)
- 在开发角度,
操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用
,这部分由操作系统提供的接口,叫做系统调用
。系统调用在使用上,功能比较基础,对用户的要求相对也比较高
,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
那在还没有学习进程之前,就问大家,操作系统是怎么管理进行进程管理的呢?很简单,先把进程描述起来,再把进程组织起来!
用学校来举例:
- 学生(被管理者) —— 软件和硬件
- 辅导员(执行者) —— 驱动
- 校长(管理者) —— 操作系统
管理者和被管理者并不直接打交道,本质管理者是通过 “ 数据 ” 来进行管理的,管理数据,就一定要把学生信息抽取出来,而学生信息可以用一个结构体来描述,每一名同学创建一个结构体变量,然后利用指针把所有的同学关联起来,构成一个双向循环链表。操作系统并不和硬件打交道,而是通过驱动程序进行操作。
总结就是一句话:先把进程描述起来,再把进程组织起来!
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
说明:
- 所有的程序启动,本质上都是
在系统上面创建进程。
- 有了进程控制快,
所有的进程管理任务与进程对应的代码和数据毫无关系,与内核创建的该进程的PCB强相关。
- 把进程控制块PCB用双向链表组织在一起,于是操作系统对进程的管理,变为对数据的管理,
本质上就是对双链表的增删查改。
结论:进程 = 你的程序 + 内核申请的数据结构(PCB)
先描述,再组织
- 描述:每个进程对应的 PCB 几乎包含了进程相关的所有属性信息。
- 组织:OS 对进程的管理转化成了对进程之间数据结构的管理。
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
我们先写一段代码来测试一下进程
- 进程id:
pid
- 父进程id:
ppid
ps ajx
: 查看系统当前所有进程。
- [Ctrl + C]
- [control + C]
- kill -9 [pid] 向目标进程发送9号信号 – 同时也证明pid能标识系统上的唯一进程
/proc
是Linux系统下查看进程的目录ls /proc
ls /proc/pid
查看该进程属性信息
ll /proc/pid
cwd:这就是为什么文件操作时,不指定路径,会默认在当前目录下创建文件。
上面我们写了一个死循环代码,然后 “ ./ ” 运行,一般我们称之为命令式创建进程,实际上我们也可以用代码来创建子进程。
通过命令
man fork
来查找 fork 的相关手册:
仔细观察:
fork 之后,站在操作系统的角度就是多了一个进程,以我们目前有限的知识,我们知道
进程 = 程序代码 + 内核数据结构(task_struct)
,其中操作系统需要先为子进程创建内核数据结构,在系统角度创建子进程,通常以父进程为模板,子进程中默认使用的是父进程中的代码和数据。
默认情况下,子进程会“继承”父进程的代码和数据
- 代码:fork之后,产生的子进程和父进程代码是共享的。代码是不可被修改的,这意味着父子代码只有一份完全共享。
- 数据:默认情况下,数据也是“共享的”,不过修改时会发生写时拷贝来维护数据的独立性。子进程内核的数据结构task_struct,也会以父进程的为模板初始化自身
在上面的代码中,我们发现一个程序有两个返回值,我们来进一步理解
#include
#include
#include
#include
void DoThing()
{
int count = 0;
while(count < 5)
{
printf("pid : %d, ppid : %d, count : %d\n", getpid(), getppid(), count);
count++;
sleep(1);
}
}
int main()
{
pid_t ids[5];
printf("I am father : %d\n", getpid());
for(int i = 0; i < 5; i++)
{
ids[i] = fork();
if(ids[i] == 0)
{
//child
DoThing();
exit(1);
}
}
printf("%d %d %d %d %d\n", ids[0], ids[1], ids[2], ids[3], ids[4]);
getchar();
return 0;
}
多进程代码,让父子执行不同的事情
一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器,有时虽有空闲处理器但因等待某个时间的发生而无法执行,这一切都说明进程和程序不相同,
进程是活动的且有状态变化的,于是就有了进程状态这一概念
。进程的状态信息也是在task_struct(PCB)中。进程状态的意义在于,方便OS快速判断进程,并完成特定的功能,比如调度。本质上是一种分类
。
下面的状态在kernel源代码里定义。
/*
* 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 *task_state_array[] = {
"R (running)", /* 0*/
"S (sleeping)", /* 1*/
"D (disk sleep)", /* 2*/
"T (stopped)", /* 4*/
"T (tracing stop)", /* 8*/
"Z (zombie)", /* 16*/
"X (dead)" /* 32*/
};
进程变迁图:
ps aux
ps axj
R运行状态(running)
一个进程处于运行状态(running),并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个R状态的进程。
查看运行状态:
写一个死循环并且运行
#include
#include
#include
int main()
{
while(true);
return 0;
}
S 浅度睡眠状态(sleeping)
一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。等待非CPU资源就绪。我们可以 Ctrl + C 退出循环,而此时的进程就没了,也就是说它虽然是一种休眠状态,但是它随时可以接收外部的信号,处理外部的请求。
查看运行状态:
#include
#include
int main()
{
sleep(100) ; //睡眠100秒
return 0;
}
挂起状态(也属于S 状态)
当内存不足的时候,OS提供适当的置换进程的代码和数据到磁盘中,PCB不换进程的状态就叫做挂起
你现在正在等待某种资源的时候,正巧内存不足了,内存不够是你正在阻塞状态,所以把你的代码数据置换到磁盘里,所以叫做“挂起阻塞”
也叫不可中断睡眠状态(uninterruptible sleep),进程处于D状态,
不可以被杀掉! ,在这个状态的进程通常会等待IO的结束
。
当进程压力过大的时候,操作系统会通过一些特殊的方式杀掉一些进程
T暂停状态(stopped)
可以通过发送 SIGSTOP
(kill - 19)
信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
我们先看看kill指令:
9)杀死 18)继续 19) 停止
暂停进程
此时,发送信号恢复状态,会发现S后面没有+号,[ctrl + C] 也没法终止程序,这是因为你的暂停和继续让进程变成了后台运行。
那要怎么样才能暂停呢?
$ kill -9 pid即可
前台进程和后台进程的区别:
前台进程:./myproc,输入指令无效,[ctrl + c] 可终止进程
后台进程:./myproc &,可以执行指令,[ctrl + c] 不能终止进程,退出进程要用kill
X死亡状态(dead)
收进程资源。进程相关的内核数据结构&代码和数据。
Z僵尸状态(Zombie)
为什么要有僵尸状态?因为需要辨别退出/死亡原因,把进程退出的信息(数据)写入到task_struct中,供系统/父进程读取。
写一个监控命令行脚本,语法类似C语言:
while :; do ps axj | head -1 && ps axj | grep practice | grep -v grep; sleep 1; echo "########################"; done
子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就进入Z状态。
下面一段代码,在50秒内,我把子进程杀掉,父进程不退出休眠啥也不干,此时子进程成为僵尸进程
。
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 pid_t id = fork();
7 if(id == 0)
8 {
9 //child
10 while(1)
11 {
12 printf("I am a child, running!\n");
13 sleep(2);
14 }
15 }
16 else
17 {
18 //parent
19 printf( "father do nothing!\n");
20 sleep(50);
21 }
22 return 0;
23 }
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 内存泄漏
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号init进程领养,当然要有init进程回收喽。
代码演示:
#include
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int main()
6 {
7 pid_t id = fork();
8 if(id == 0)
9 {
10 //child
11 while(1)
12 {
13 printf("I am a child, running!\n");
14 sleep(2);
15 }
16 }
17 else
18 {
19 //parent
20 printf( "father do nothing!\n");
21 sleep(10);
22 exit(1);
23 }
24 return 0;
25 }
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
ps -l
我们很容易注意到其中的几个重要信息,有下:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别。
- 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
- 可以理解nice值是进程优先级的修正修正数据
优先级 = 老优先级 + nice
用top命令更改已存在进程的nice值(频繁操作可能需要sudo)
top
进入top后按"r" → 输入进程PID → 输入nice值
按r,输入PID值
输入nice值,然后按q退出
此时我们再用ps命令查看进程的优先级信息,即可发现进程的NI变成了10,PRI变成了90(80+NI)。
注意:若是想将NI值调为负值,也就是将进程的优先级调高,需要使用sudo命令提升权限。
竞争性
: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级。独立性
: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。并行
: 多个进程在多个CPU下分别同时进行运行,这称之为并行。并发
: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(
即用户登陆到Linux系统中时,默认的目录
)- SHELL : 当前Shell,
它的值通常是/bin/bash
。
我们可以通过echo命令来查看环境变量,方式如下:
echo $NAME //NAME为待查看的环境变量名称
查看环境变量PATH
echo $PATH
测试PATH
创建文件
#include
int main()
{
printf("hello world!\n");
return 0;
}
- 大家有没有想过这样一个问题:为什么执行ls命令的时候不用带./就可以执行,而我们自己生成的可执行程序必须要在前面带上./才可以执行?
- 容易理解的是,要执行一个可执行程序必须要先找到它在哪里,既然不带./就可以执行ls命令,说明系统能够通过ls名称找到ls的位置,而系统是无法找到我们自己的可执行程序的,所以我们必须带上./,以此告诉系统该可执行程序位于当前目录下。
- 环境变量PATH当中有多条路径,这些路径由冒号隔开,当你使用ls命令时,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径当中进行查找。
而ls命令实际就位于PATH当中的某一个路径下,所以就算ls命令不带路径执行,系统也是能够找到的。
- 将可执行程序拷贝到环境变量PATH的某一路径下。
- 将可执行程序所在的目录导入到环境变量PATH当中。
测试HOME
环境变量HOME当中即保存的该用户的主工作目录。
echo | 显示某个环境变量值 |
---|---|
export | 设置一个新的环境变量 |
env | 显示所有环境变量 |
unset | 清除环境变量 |
set | 显示本地定义的shell变量和环境变量 |
每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串,最后一个字符指针为空。
main函数其实有三个参数,只是我们平时基本不用它们,所以一般情况下都没有写出来。
方法一:命令行第三个参数
#include
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
运行结果就是各个环境变量的值:
方法二:通过第三方变量environ来获取。
#include
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
除了通过main函数的第三个参数和第三方变量environ来获取环境变量外,我们还可以通过系统调用getenv函数来获取环境变量。getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。