前言
上一篇文章最后我们引出一个概念:什么是并发?
并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
在os中有很多进程,肯定不是每一个进程占有cpu就一直运行,而是每隔一段时间,就从cpu中剥离下来,这里的一段时间叫做时间片。
在cpu上是只能运行一个进程的,但是由于os的进程调度机制,它能够快速地在多个进程间来回切换,使得每一个进程都得到一定的时间片,从而给用户造成多个进程同时运行的错觉。
就像QQ音乐在后台播放,你却在使用微信聊天,用户感觉是两个进程在同时运行,其实不是的,这是os在一直进行进程切换,只不过速度超级快。
一、寄存器存储临时变量
#include<stdio.h>
int add(int x,int y)
{
int sum=0;
sum=x+y;
return sum;
}
int main()
{
printf("两个数的和为%d\n",add(1,2));
return 0;
}
我们知道栈上开辟的临时变量会随着函数的生命周期的结束而结束的,但是函数的返回值为什么外部可以接收到呢?
其实上这是和cpu上的寄存器是息息相关的。
在cpu中有很多寄存器。比如:eax,ebx,ecx,ss,ds,cs等等。
其中eax寄存器,就是用来存储临时变量的。它充当代码的临时空间。
当我们的进程运行的时候,os怎么知道代码运行到什么位置了,这里也是和一个寄存器相关的-eip寄存器,也叫pc指针/程序计数器。
我们的进程会产生很多数据,这些数据也是在寄存器中保存起来的。
其中每一个进程在cpu寄存器中形成的临时数据(可能都是不一样的),叫做进程的硬件上下文。
cpu中的寄存器是硬件设备,它一直在那里,只有一套。
但是对于进程来说,有几个进程,进程的硬件上下文就有几套。
【那这里就有一个问题了,一套寄存器如何保存多套进程的硬件上下文的?】
这里就要分第一次加载进程和第二次加载进程了。
第一次加载进程:
当前有个进程A,第一次加载到内存中,寄存器会保存该进程的一些数据。
第二次加载进程:
现在有个进程B需要加载了,但是进程A还没有运行完毕,这时就需要进程切换,寄存器中的内容现在保存的是进程A的,包括数据内容和pc指针的信息等等,会被重新加载进程的PCB中,寄存器中的内容本身不会消除,进程B运行起来之后,需要将数据保存到寄存器中,是直接覆盖写的。
当进程A再次进来运行后,进程B也执行和之前进程A同样的操作,而进程A需要把之前从寄存器保存到PCB中的内容重新拷贝到寄存器中,在pc指针的位置继续运行。
二、Linux2.6内核进程调度队列
上述的过程进程的切换都是通过内核进程的调度队列实现的。
1.优先级
-
普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
-
实时优先级:0~99(不关心)
2.活动队列
可以看到整个调度队列中有两组nr_active,bitmap[5],queue[140]。
一组是活动队列中的,一组是过期队列中的。
-
时间片还没有结束的所有进程都按照优先级放在该队列
-
nr_active: 总共有多少个运行状态的进程
-
queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
-
从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 从0下标开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了!
其中还有一个bitmap[5],也叫位图,类型是int,一共5*32=160个比特位,一个140个优先级,就有140个进程队列,为了提高查找非空队列的效率,使用160个比特位来表示队列是否为空,大大提高效率!(俗称大O(1)算法)
3.过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
4.active指针和expired指针
active指针永远指向活动队列。
expired指针永远指向过期队列。
当活动队列上的进程没有了之后,交换active指针和expired指针的指向,就相当于有了一批新的活动进程了。
5.为什么要有活动和过期两个队列
这就是和进程的优先级息息相关了,活动队列中有80和60优先级的进程,os首先执行60优先级的进程,但是现在又来了很多60优先级的进程,它们还能链入活动队列吗?这样的话80优先级的进程是不是就一直不能被调度,我们知道os要较为均衡的调度所有进程,所以我们还需要一个过期队列,将新来的进程链入到过期队列中。
我们一直调度活动队列中的进程,进程越来越少,过期队列中的进程越来越多,当活动队列中进程被调度完,交换active指针和expired指针即可!!!
三、命令行参数
1.什么是命令行参数
我们知道main函数作为程序的入口,我们通常都是写的main(),无参的写法,作为一个函数,它也是要被别人调用的,那它有没有参数呢?
答案是有,它的参数如下:
int main(int argc,char*argv[])
这里的参数就叫做命令行参数,第一个参数argc是个数,第二个参数是字符指针数组。
argv中有什么,这是我们想知道的,打印出来看看。
#include<stdio.h>
int main(int argc,char*argv[])
{
for(int i=0;i<argc;i++)
{
printf("%d:%s\n",i,argv[i]);
}
return 0;
}
这是怎么回事呢?
整个连起来组成一个大的字符串,中间使用空格分成若干个小的字符串,在命令行输入的这些字符串会传入main中的参数中去,这就是命令行参数,这是怎么做到的呢?这是shell或OS做到的。
2.命令行参数的使用
使用命令行参数我们可以自己实现一个简单的计算器。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{
if (argc != 4)
{
printf("Use error/Usage %s [-add|-sub|-mul|-div] op1 op2\n", argv[0]);
return 1;
}
int x = atoi(argv[2]);
int y = atoi(argv[3]);
int result = 0;
if (strcmp(argv[1], "-add") == 0)
{
result = x + y;
printf("%d+%d=%d\n", x, y, result);
}
else if (strcmp(argv[1], "-sub") == 0)
{
result = x - y;
printf("%d=%d-%d\n", x, y, result);
}
else if (strcmp(argv[1], "-mul") == 0)
{
printf("%d*%d=%d\n", x, y, x * y);
}
else if (strcmp(argv[1], "-div") == 0)
{
if (0 == y)
printf("%d/%d=error! div zero\n", x, y);
else
printf("%d/%d=%d\n", x, y, x / y);
}
else
{
printf("Use error, you should use right command line Usage:
%s op[-add|sub|mul|div] d2 d2\n", argv[0]);
}
return 0;
}
我们可以自己控制输入命令行参数的方式,达到我们想要的结果。
系统中的命令ls -l和ls -a -l是如何实现不同功能的呢?
这里就是命令行参数在起作用,输入不同的指令,返回给main的命令行参数就不同,然后代码执行逻辑就不同,从而实现出不同指令打印出不同的内容。
懂了命令行参数的实现逻辑,我们还可以重写touch命令等等其他命令。
#include<stdio.h>
int main(int argc,char*argv[])
{
if(argc!=2)
{
printf("touch: missing file operand\n");
return 1;
}
FILE *fp = fopen(argv[1], "w");
if(fp != NULL)
fclose(fp);
return 0;
}
总结:命令行参数,可以支持各种指令级别的命令行选项的设置!
我们也终于理解我们以前学的指令,选项是什么意思了!!!