截至目前我们所认识的计算机,都是由一个个硬件组件组成
输入单元:包括键盘,鼠标,扫描仪等
中央处理器(CPU):含有运算器和控制器等
输出单元:显示器,打印机等
关于冯诺依曼体系要注意以下几点:
这里的存储器指的是内存
不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
外设(输入或输出设备)要输入或输出数据,也只能写入内存或从内存中读取
一句话总结:所有设备都直接和内存打交道
操作系统是怎么管理进行进程管理的呢?
很简单,先把进程描述起来,再把进程组织起来,也就是六个字,先描述后组织
进程的概念:
课本上:程序的一个执行实例或正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体
就是把进程信息放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。在课本上称为PCB,Linux操作系统下的PCB是task_struct.
在Linux下描述进程的结构体叫做task_struct,它是Linux内核中的一个数据结构,它会被装载到内存(RAM)里并且包含着进程信息.
标识符:描述本进程的唯一标识符,用来与其它进程进行区分
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息。
在命令模式中输入Ctrl + v,然后通过h,j,k,l进行选中区域,注意不能使用上下左右键来选中区域,然后按shift + i也就是大i切换到输入模式,再输入//,最后按Esc进行批量化注释
一个已经加载到内存当中的程序,就叫做进程(任务)
进程 = 内核PCB数据结构对象 + 你自己的代码和数据
内核PCB数据结构对象:描述你这个进程的所有的属性值
对进程做管理本质上就是对内核PCB内核数据结构做管理
什么是标识符?如何获取标识符?
怎么手动创建进程?
通过系统调用fork()函数创建进程
getpid()获取进程id
getppid获取父进程id
#include
#include
#include
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函数创建进程之后,有个奇怪的现象,为什么fork函数的返回值会有两个?
1.fork有两个返回值
2.父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
我们要理解以下几个问题:
1.为什么fork要给子进程返回0,给父进程返回子进程pid?
因为它的的子进程不一定只有一个,返回这个子进程的pid就是让父亲知道这个当前控制的是哪个子进程
返回不同的返回值是为了区分让不同的执行流,执行不同的代码块
2.一个函数是如何返回两次的?
3.一个变量怎么会有不同的内容?怎么理解?
4.fork函数究竟在干什么?干了什么?
5.我们为什么要创建子进程
为了让父和子执行不同的代码,让儿子代替父亲做一些事
fork函数创建子进程,给子进程返回0,给当前进程返回创建的子进程id,为什么会返回两个值呢?因为在fork函数return前,在fork函数体内就已经把要创建的子进程创建好了,所以在return前有两个进程,两个进程分别执行return语句,所以就产生了两个值。
创建的子进程和父进程共享代码和数据,只为子进程创建了对应的PCB数据结构对象,但是我们创建子进程就是为了让它帮助父进程做一些事,如果代码和数据完全一样的话,我们直接让父进程去执行不就可以了吗?所以我们要想办法让他们拥有不同的数据,而如果我们单独给子进程开辟一份空间去保存它对应的数据,就可能会造成资源浪费,因为有可能某些数据子进程并不会用到,此时我们选择用写时拷贝的方法去解决它,也就是当子进程数据要发生改变时我们单独为这个数据开辟一份空间用来存放子进程的这个数据,而其他的数据仍然是父子共享的,具体为什么同一个变量ret为什么会有两个不同的值,就需要我们在地址空间变量那来学习
在操作系统学科中分为三大类:运行,阻塞和挂起
运行:就是当前进程在被调度或者在调度队列中
阻塞:比如需要等待键盘输入时,当前进程就处于阻塞状态
挂起:有时在CPU快满时,就会把某些正在等待的进程中的数据和代码进行取出来,只是让PCB在那里等待,等到要运行到该进程时就会再把数据和代码放进去,这种把数据和代码先取出来让PCB在调度队列的状态就叫做挂起状态。
进程 = 内核PCB数据结构对象 + 你自己的代码和数据
在Linux内核当中进程也叫做任务。
Linux中进程的状态分为以下几种:
R运行状态:分为两种:1.表示当前进程在运行中2.处于在运行队列中
S睡眠状态:因为着进程在等待某事件完成(这里的睡眠状态有时也可称为可中断睡眠)
D磁盘休眠状态(Disk sleep):有时也叫不可中断睡眠,在这个状态的进程通常会等待IO的结束。
S状态称为前度睡眠,D状态称为深度睡眠,就是当CPU快满时,操作系统可以将S状态的进程杀死,但是不可以杀死D状态的进程,只有等它自己结束才可以
T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止(T)进程,这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
kill -9杀死进程
kill -19停止进程
kill -18运行进程
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
什么是僵尸进程?
先举一个例子,可能不是特别恰当但是可以帮助我们更好地去理解他,就比如一个人在路上出车祸当场去世了,他躺在马路上的时候,医生并不会把它当场埋了,此时躺在路上的时候就处于僵尸状态,他要等他的父亲来领取他,然后埋了才是死亡状态。
在进程中就是当它因为异常或者某些原因要退出时,要等它的父进程来把他的数据保存起来的时候,这就称为僵尸状态。
只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
进程的退出状态必须要维护下去,因为他要告诉关心他的进程(父进程),你交给我的状态我班的怎么样了,可如果父进程一直不读取,那子进程就一直处于Z状态?是的
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB就要一直维护?是的
那一个父进程创建了很多子进程就是不回收,是不是就会造成资源的浪费?是的,因为数据结构对象本身就要占用内存。属于内存泄漏?是的
模拟僵尸进程让子进程先退出,父进程在子进程退出后仍处于sleep状态
#include
#include
#include
#include
int main()
{
int id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id > 0)
{
printf("parent[%d] is sleeping\n", getppid());
sleep(30);
}
else{
printf("child[%d] is begin Z\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
while : ; do ps aux | grep mycode | grep -v grep; sleep 1;echo “#####################”; done
孤儿状态就是它的父亲退出了,此时子进程的状态就为孤儿进程,子进程最终会被操作系统(1号init进程也可能是systemed,不同平台不同)领养注意不是bash。
模拟孤儿进程:让父进程先退出,父进程退出后子进程仍处于sleep状态
进程优先级就是CPU对该进程进行分配资源的先后顺序,优先级越高越先执行.
进程为什么要有优先级?
可以让重要的进程先被执行,不重要的放到后面去执行,且这种做法对于多进程的Linux环境而言很有用,极大地改善系统的性能
通过ps -l命令来查看系统进程
与权限一样我们依次来认识一下这些字母都代表着什么
UID:代表执行者的身份
PID:当前进程的代号
PPID:当前进程的父进程的代号
PRI:代表当前进程被执行的优先级,其值越小越好
NI:代表这个进程的nice值
前面几个都容易理解,而这个nice值是什么呢?
nice值表示这个进程被执行的优先级的修正数值,也就是PRI = PRI(old) + nice,而其中PRI(old)始终是80,只是给的表达式是这样,在实际加的时候并不是用它的原值而是80
当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行.所以,调整进程优先级,在Linux下,就是调整进程nice值,nice其取值范围是[-20,19],一共40个级别
如何修改nice值呢?
在命令中输入top命令,然后按r,输入进程的pid,然后再输入nice值如果输入的小于-20或者大于19,那么默认按照极值-20或者19进行处理
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
PATH:指定命令的搜索路径
HOME:指定用户的主工作目录(即用户登录到Linux时,默认的目录)
SHELL:当前Shell,它的值通常是/bin/bash
echo $NAME //NAME:你的环境变量名称,来获取对应的环境变量
2.
通过命令行第三个参数查看:
#include
int main(int argc, char* argv[], char* env[])
{
int i = 0;
for(; env[i]; i++)
{
printf("%d: %s\n", i, env[i]);
}
printf("\n");
return 0;
}
3.
通过第三方变量environ获取
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
#include
int main()
{
extern char **environ;
int i = 0;
for(; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
4.
我们再来介绍一个系统调用函数getenv(),它也是用来获取环境变量的
#include
#include
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
来获取对应的环境变量值,这里获取的是PATH对应的环境变量值
环境变量的组织方式:
每个函数都会收到一张函数表,环境表是一个字符指针数组,每个指针指向以\0为结尾的字符串
函数表数组的最后一个指针值为NULL,因此在上边打印时直接把for循环中的判断条件替换为environ[i]。
通过K="V"的形式来设置环境变量,注意=两边不能有空格
通过查看我们可以看到环境变量SW设置成功了,但是我们我们再来看以下代码
#include
#include
int main()
{
printf("%s\n", getenv("SW"));
return 0;
}
通过getenv函数调用我们发现竟然出现了段错误,这是为什么?
先给出结论,因为这样创建的环境变量具有局域性,属于本地变量,本地变量并不会被继承,只会在本bash有效。而执行命令./mytest是通过创建子进程来完成的,那么echo是怎么拿到的呢?因为echo命令没有创建子进程,而是有bash本身去完成的。
这就延伸出指令的分类:指令分为两种:
1.常规命令------bash通过创建子进程来完成的
2.内建命令------bash不创建子进程,而是由它自己来执行,类似于bash调用了自己或系统提供的函数
那么下一个问题是如何创建出可继承的环境变量呢?
只需用export导出环境名即可
此时我们可以看到通过./mytest指令也可看到对应的环境变量了,证明该环境变量继承给了子进程
1.echo: 显示某个环境变量值
2. export: 设置一个新的环境变量
3. env: 显示所有环境变量
4. unset: 清除环境变量
5. set: 显示本地定义的shell变量和环境变量
env获取环境变量不包括本地变量,set获取变量包括本地变量