在了解进程概念前我们还得了解下冯诺依曼体系结构和操作系统的概念与定位。
1 冯诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系:
- 输入单元:包括网卡,键盘, 鼠标,扫描仪, 写板,话筒等 ;
- 中央处理器(CPU):含有运算器和控制器等 ;
- 输出单元:网卡,显示器,打印机等;
关于冯诺依曼,必须强调几点:
- 这里的存储器指的是内存 ;
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备) ;
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取;
- 一句话,所有设备都只能直接和内存打交道;
对冯诺依曼的理解,不能只停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。
在不考虑网络层情况下,小明用qq向小红发送了一条消息,小明的电脑从键盘上读取信息然后加载到内存,再从内存将数据通过一系列操作发送到输出设备上(网卡),然后通过一系列的网络操作将数据发送到小红的输入设备上(网卡),小红的电脑再从输入设备中将数据读到内存,然后通过输出设备(显示器)将信息刷新到小红的电脑上,这里数据刷新是两个方面的,再成功发送后小明的电脑也会显示出已经成功发送后的信息。
2 操作系统(Operator System)
概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell程序等等)
设计OS的目的
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。
总结
计算机管理硬件
- 先描述起来,用struct结构体
- 再组织起来,用链表或其他高效的数据结构
系统调用和库函数概念
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
3 进程
有了上面对冯诺依曼体系结构和操作系统的理解,我们自然可以想到进程也是先描述,再组织。
3.1 基本概念
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
有些教材书上甚至是给出这样的定义的:进程就是程序加载到内存中。但是我觉得这种描述是狭隘的不够具体的,具体的我们下面会给出解释.
3.2 描述进程-PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct是PCB****的一种:
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
我们将所有进程的属性用一个队列来维护,当我们想要加载程序时就将它的PCB链接到该运行队列中,这样就很好的维护了进程。
那现在我们再来回答什么是进程?
进程=当前程序的代码和数据+内核关于进程的相关数据结构
task_ struct****内容分类:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
3.2 组织进程
- 可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
3.3 查看进程
我们在Linux环境中创建了一个profile.cpp的Cpp文件,然后编译运行生成了一个叫做profile的可执行文件,我们可以通过一下命令来查找进程:
ps ajx | head -1 && ps ajx | grep "查找进程的名字"
当我们运行profile后来查看:
不难发现我们查询到了profile进程的一些基本信息,如果我们想不加上下面那一行的信息可以将命令后面多加一些内容:
ps ajx | head -1 && ps ajx | grep "查找进程的名字" | grep -v grep
当然,文件名可加可不接双引号。
我们还可以在./proc中查询:
ls ./proc
3.4 通过系统调用获取进程标示符
- 进程id(PID)
- 父进程id(PPID)
我们向profile.cpp中写入以下代码:
1 #include<iostream>
2 #include<sys/types.h>
3 #include<unistd.h>
4 using namespace std;
5
6
7 int main()
8 {
9 while(1)
10 {
11 pid_t ret=getpid();
12 cout<<"hello"<<ret<<" "<<endl;
13 pid_t t=fork();
14 if(t==0)
15 {
16 while(1)
17 {
18 cout<<"我是一个子进程"<<" pid:"<<getpid()<<" ppid:"<<getppid()<<endl;
19 sleep(1);
20 }
21 }
22 else if(t>0)
23 {
24 while(1)
25 {
26 cout<<"我是一个父进程"<<" pid:"<<getpid()<<" ppid:"<<getppid()<<endl;
27 sleep(1);
28 }
29 }
30 }
31 return 0;
32 }
当我们查看进程时:
fork()后执行流会变成两个,是先执行父进程还是子进程是由调度器决定的,fork()后的代码共享,我们通常是用if else 来进行分流的。
- 运行 man fork 认识fork
RETURN VALUE On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.
- fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
当我们只读数据不写数据时,父子进程是共享代码的,而当有其中一个执行流尝试修改数据时OS就会在当前进程触发写时拷贝另外生成一份。
如何理解有两个返回值呢?
创建子进程本质上就是OS提供的一个函数,当函数内部进行return 时我们主体功能已经完成了。
3.5 进程状态
在了解进程状态前我们还得了解下什么是阻塞和挂起?
相信大家在看一些操作系统的书的时候就见过类似于这样的图片:
阻塞状态是一种等待某种资源就绪,而导致的一种不被推进的过程。这么说有点儿抽象,我们来举一个栗子:
当有大量进程存在时我们是不是要先描述,再组织,前面我们说过组织进程靠的是内核中以某种数据结构来维护进程的PCB。假设你在应用市场要下载一个软件,但是下到一半时网络突然中断了,那么操作系统会一直等到网络资源恢复后再去运行其他进程吗?显然是不会的,假如操作系统这样设计的话那么难道我们电脑上的其他程序就不运行了吗?就只等你一个?所以当网络资源中断时操作系统会将该进程从CPU的运行队列中拿走,放到对应硬件资源的等待队列中,等到网络资源恢复后再将该进程链接到CPU的运行队列中执行,而这种等待某种资源就绪而不被推进的一种状态就叫做阻塞状态。
而挂起又是什么意思呢?
由于机器的资源是有限的,在资源不足的情况下操作系统可以暂时将一些在内存中的进程淘汰出局,当条件允许的时候又会被操作系统给调回来,这个比较好理解就不在多做解释了。
看看Linux内核源代码怎么说
下面的状态在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 * 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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
第一个R状态并不是表示进程在运行中,有可能是在运行队列里面。接下来给出大家一个程序大家猜猜这时一种什么状态?
1#include<iostream>
2 #include<unistd.h>
3 using namespace std;
4
5 int main()
6 {
7 while(1)
8 {
9 cout<<"我是一个进程 我的pid是:"<<getpid()<<endl;
10 }
11 return 0;
12 }
我相信第一次大家或许都会想:既然是死循环那么程序肯定在运行状态了,也就是R状态,事实真的是这样吗?我们一起来看看:
我们居然惊奇的发现,该进程居然处于S状态,这不合理吧?
其实大家想想,我们使用cout往显示屏中打印字符串,这里的输出设备就是我们的屏幕,但是输入输出设备是很慢的相对于CPU来说,可能当输入输出一个字符时CPU就已经跑了上百万行代码了。所以当我们往显示屏打印字符串时,操作系统并不会直接将该进程直接运行,而是等到当某种资源就绪后会将该进程链接到运行队列中,那这S状态与我们上面讲的阻塞状态有点儿类似呀,其实S状态本质就是阻塞状态。(要想获得R状态就得在资源准备就绪的一瞬间来用命令查看,太难抓了,我就不演示了)
那后面的+号是什么意思呢?
这里+表示该进程是前台运行的,当我们使用ctrl+c的时候能够终止掉该进程,不写+表示的是后台运行的,这时用ctrl+c是无法终止掉该程序的,要用命令杀掉进程来终止,我们接下来会介绍的。
当我们注释掉代码里的打印字符时:
1 #include<iostream>
2 #include<unistd.h>
3 using namespace std;
4
5 int main()
6 {
7 while(1)
8 {
9 // cout<<"我是一个进程 我的pid是:"<<getpid()<<endl;
10 }
11 return 0;
12 }
再来看看:
我们会发现代码进程的状态已经变成了R.
同理,当我们往键盘中输入数据时几乎绝大多数都是S状态,只有刚输入数据那一瞬间才是R状态。
D状态是一种不可中断休眠状态,这时就算是强如操作系统都不能够干掉他,但是这种场景一般很少见,除非机器快宕机了。
T状态是一种停止状态,我们可以通过一个命令来改变当前的状态位T状态,大家可以查看有哪些kill命令:
kill -l
这时就会出现很多与kill相匹配的选项:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
这里我们使用19号命令来暂停进程:
kill -19 进程的pid
那如何恢复呢?
可以用kill命令配带的18号选项:
kill -18 进程的pid
但是大家发现没有,这里的S状态是没有加+,也就是该进程是在后台运行的,不可以被ctrl+c终止
这时应该怎么处理呢?
我们可以试试kill带的9号选项:
kill -9 进程的pid
这时该进程已经被干掉了。
除了T状态还有一个t状态,这里的t表示的一种追踪暂停,类似于我们打断点运行到断点处:
我们可以打开Makefile加入断点信息,然后调试起来
不难发现此时的状态已经变成了t.
至于后面的X和Z状态将会放在下一节课来讲解,如果该文对你有帮助的话能不能3连支持一下博主呢?
版权归原作者 Fox! 所有, 如有侵权,请联系我们删除。