目录
冯诺依曼体系结构
操作系统
概念
设计os的目的
定位
如何理解管理
总结
系统调用和库函数概念
进程
描述进程-pcb
组织进程
查看进程
通过系统调用获取进程标示符
通过系统调用创建进程-fork初识
进程状态
阻塞和挂起
Z(zombie)-僵尸进程
冯诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成
输入单元:包括键盘, 鼠标,扫描仪, 写板等
中央处理器(CPU):含有运算器和控制器等
输出单元:显示器,打印机等
关于冯诺依曼,必须强调几点:
这里的存储器指的是内存
不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
一句话,所有设备都只能直接和内存打交道。
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,假如,从你登录上qq开始和某位朋友聊 天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他收到消息之后的数据流动过程。如果是在qq上发 送文件呢?
我们通过键盘输入一条信息,然后会传给存储器,存储器再传给运算器和控制器,运算器和控制器统称为cpu,cpu处理完我们的信息,就会发送到qq,通过网络上传给我们的朋友。
操作系统
概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)
这就是进程管理,系统会给每个进程合理安排资源。
设计os的目的
与硬件交互,管理所有的软硬件资源
为用户程序(应用程序)提供一个良好的执行环境
定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件
也就是说,在软件方面分配好内存给它们运行,在硬件上合理分配软件去使用内存,还有用户的读写操作。
如何理解管理
这个时候就可以 以我们自己为例子了,大家学生时代基本都是在学校的吧,那么,学校就是一个计算机,校长就是操作系统,我们就是一堆堆的“软件".毕竟铁打的学校流水的学生嘛。
那么学校是每个学期都要安排考试,如果有人考试没过,下学期就要安排补考(大家都逢考必过),校长怎么知道有什么人需要补考呢,这个时候就需要通知我们的辅导员,调取我们的成绩信息,让没过的同学在安排好的教室和时间去补考。这个时候辅导员就是一个小的操作系统,它管理着我们,校长管理着辅导员。我们每个学期都要通过考试证明我们是合格的,当我们各项指标都符合要求时,就可以毕业,而校长和辅导员,就是管理我们的os,他们是管理方,审核我们的毕业要求,而我们就是被管理方,去申请这个系统的资源。
总结
计算机管理硬件 1. 描述起来,用struct结构体 2. 组织起来,用链表或其他高效的数据结构
也就是先描述再组织
系统调用和库函数概念
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分 由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统 调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
进程
基本概念
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
进程不只是包含我们的软件,还有一些后台进程,也就是只要我们电脑开机了,就有进程运行
描述进程-pcb
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是task struct
task_struct-PCB的一种
task_struct 在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
linux的内核源码可以在网上下载学习
查看进程
进程的信息可以通过 /proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
大多数进程信息同样可以使用top和ps这些用户级工具来获取
比如我们现在写一个循环运行的文件
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1){
sleep(1);
}
return 0;
}
通过系统调用获取进程标示符
进程id(PID) 父进程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
通过系统调用创建进程-fork初识
运行man fork认识fork
fork有两个返回值
父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
比如说我们现在写一个这样的程序
fork 之后通常要用if进行分流
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if(ret < 0){
perror("fork");
return 1;
}
else if(ret == 0){ //child
printf("I am child : %d!, ret: %d\n", getpid(), ret);
}else{ //father
printf("I am father : %d!, ret: %d\n", getpid(), ret);
}
sleep(1);return 0;
}
也就是我们说的fork有两个返回值,当ret为0时,进入分支语句打印一次,当ret不为0,也就是父进程,打印一次。
进程状态
在学习进程状态前,我们先说一下两个关于进程运行的情况
阻塞和挂起
阻塞: 进程因为等待某种条件(资源)就绪,而导致的一种不推进的状态(如我们常说的卡住了一般:页面无法响应、因网络中断下载任务无法继续执行等)。或者说,阻塞就是当前进程不被CPU调度。事实上,进程要通过等待的方式,等某个具体的资源被别人用完或者有了某个资源之后,再使用该资源。
我们知道,操作系统对软硬件做管理,其方式可以被总结为:先描述,再组织 。其中进程被描述为结构体 task_struct ,硬件被管理时同样也是被描述为一个结构体如 struct dev ,每个软硬件对应的结构体中都包括了关于自身的信息。值得注意的是,在每个硬件对应的结构体中还包含了指向进程控制块 PCB(task_struct) 的指针,可以认为该指针指向了一个进程队列的队头,通过该指针可以对某个进程队列进行管理。事实上,一个进程处在运行状态时,可以表示该进程处在CPU进程调度的运行队列中,而当某个进程因等待某种资源而无法继续推进时(通常是等待某种硬件资源,如磁盘、网卡、键盘等),CPU就会将该进程调出当前的运行队列,并调入其所等待资源对应的等待队列中(此时该进程就处在一种 阻塞 状态。换句话说,当某个进程处于阻塞状态时,就表示该进程对应的结构体 task_struct 正在某种被操作系统管理的资源下排队),当该资源准备就绪后,再将该进程调回CPU的运行队列中继续排队运行。
挂起: 当因为等待某种资源就绪,进程对应PCB由运行队列转至资源下的等待队列时,考虑到内存空间紧张,CPU会将因为等待而暂时无法运行的进程对应的代码和数据先由内存转移到磁盘中,此时进程即为挂起状态,等到该进程可以被运行时再将对应的代码和数据由磁盘转移回内存中。
看看Linux内核源代码怎么说
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux内核里,进程有时候也叫做任务)。 下面的状态在kernel源代码里定义:(可以在网上找到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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
进程状态查看命令 ps aux / ps axj (二选一)
ps -aux 是以BSD方式显示
a 显示所有用户的进程(show processes for all users)
u 显示用户(display the process's user/owner)
x 显示无控制终端的进程(also show processes not attached to a terminal)
ps-axj
a: 显示所有用户进程
x:显示没有控制终端的进程
j:显示与作业有关的信息(显示的列):会话期ID(SID),进程组ID(PGID),控制终端(TT),终端进程组ID(TRGID)
我们可以写一段代码来观察进程的状态
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("我是一个进程\n");
sleep(1);
}
return 0;
}
我们可以看到,程序一直在打印这段文字,那么这个时候程序是不是一直在运行呢?
通过命令 ps axj | head -1 && ps axj | grep test | grep -v grep
查看对应的进程状态:
可以发现:虽然该进程看上去似乎一直在运行,但所显示的进程状态却表示其处于 S+(睡眠状态) 。这是由于在这段死循环中代码中,我们需要显示器资源来显示输出内容(这是一个进程!),显然显示器资源不会一直只供该进程使用,即该进程的运行需要等待显示器资源的就绪,也就是上述所说的睡眠状态。
下面我们又将代码中的输出语句注释,使该死循环中为空,重新编译运行,再次查看相应进程状态,此时可以看到,该进程处于 R+(运行状态) ,这是因为此时该进程不需要等待某个资源就绪,因此其一直处于运行状态。
我们可以看到,我们的状态都有一个+号
这是因为我们的程序都在前台运行,也就是我们的窗口一直在被占用,要解除这种状态我们就ctrl+C,就可以退出程序了。
这个时候就没有这个进程了。
也可以通过kill -9指令去终止(杀死)这个进程
kill- l 查看kill指令
我们可以把程序再跑起来,再用指令去终止
T就是停止状态
这个时候没有+号,就是在后台运行,ctrl+C就终止不了了
我们就通过kill -9杀死进程。
还有一个小t状态,当进程正在被跟踪时,就处于 t 这个特殊状态,其本质上也是一种停止状态。例如调试程序时,触发断点而停止运行,此时对应进程就处在 t 状态。
这里补充一下,如果我们在gdb调试可执行文件时,报这样的错误
No symbol table is loaded. Use the "file" command.
问题应该是我们编译时没有加上ggdb3,我们再加上编译一遍,基本就可以了
gcc -ggdb3 -o test test.c
这就是我们运行程序,调试程序,还有打了断点之后,程序所处的不同状态
X(dead)死亡状态
该状态只是一个返回状态(瞬时状态),我们不会在任务列表里看到这个状态。事实上,我们创建进程,无非是想通过进程完成一些任务,而对于任务完成结果,我们可能关心,也可能不关心,这就涉及到一个概念 – 退出码 。所谓 退出码 ,其实就是我们编写的代码中最常见的
main()主函数中的{return 0}(也就是return的那个数字)
我们可以通过 echo $? 命令来查看进程退出码。
#include <stdio.h>
#include <unistd.h>
int main()
{
int number =3;if(number ==3)
return 1;
sleep(1);
return 0;
}
Z(zombie)-僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。
当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
我们直接写一个程序看看原理
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id > 0){ //parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}else{
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
也就是说,如果我们的父进程进入休眠,没有读取子进程的返回码,子进程一直无法把返回码给到父进程,那么子进程就会进到僵尸状态
僵尸状态有什么危害呢?
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎 么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的! 维护退出状态本身就是要用数据维护,也属于进程基本信息,
所以保存在task_struct(PCB)中,换句话说
Z状态一直不退出,PCB一直都要维护?是的! 那一个父进程创建了很多子进程,就是不回收,
是不是就会造成内存资源的浪费?是的!因为数据结构 对象本身就要占用内存
想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空 间!
内存泄漏?是的!
有一些知识点我放在下一篇再写啦,一时间写不过来啦哈哈哈哈