北邮 操作系统(二)

第二章 系统接口

操作系统是管理计算机硬件的一层软件系统,学习操作系统的核心就是学习操作系统如何使得用户对计算机的使用更加方便和高效,前提是我们知道用户是如何使用计算机系统的,这就是系统接口的基本概念;

系统接口是用户使用操作系统(计算机系统)的基本入口,系统接口也是通向操作系统内核的窗口,很多关乎内核模块的理解都需要通过系统接口进入;

1.计算机系统的使用

计算机系统的使用方式主要可以分为如下三种:

  • 第一种,直接打开一个应用程序使用,通常都是图形界面的,如Word、QQ等;
  • 第二种,使用命令行,如在Linux下输入命令ls、gcc-o hello hello.c等;
  • 第三种,自己编写一个程序,然后再编译执行,如大家熟悉的编程显示Hello World信息;

无论哪种方式,最终真正能让计算机产生效果,即真正使用计算机硬件的那一部分实际上是一样的,即调用函数

无论是命令行、应用程序或图形界面,在本质上都是一样的,即用户编写应用程序,在应用程序中会调用一些由操作系统提供的重要接口函数。然后这些应用程序相互配合,实现对计算机的使用;

接口函数就是应用程序在使用计算机时要调用的函数,由于这些函数是系统提供的,又是以函数调用的形式出现的,而且这些函数非常重要,所以这些函数对应一个非常重要的概念——系统调用;

Q:操作系统接口和系统调用等价吗?

A:等价,操作系统接口表现为函数调用,因为函数由系统提供,所以操作系统接口就是系统调用 —— “系统调用实际上就是操作系统提供给应用程序的接口函数”

2.基本的系统调用

操作系统提供的服务分为面向客户(命令接口)和面向软件/编程人员(系统调用);

在用户程序中凡是与资源有关的操作都必须通过系统调用的方式向操作系统提出服务请求,由操作系统代替完成,系统调用按照功能可以大致分为如下几类:

  • 设备管理,完成设备的请求或释放,以及设备启动等功能;

  • 文件管理,完成文件的读、写、创建及删除等功能;

  • 进程控制,完成进程的创建、撤销、阻塞及唤醒等功能;

  • 进程通信,完成进程之间的消息传递或信号传递等功能;

  • 内存管理,完成内存的分配、回收以及获取作业占用内存区大小及始址等功能;

系统调用的处理需要由操作系统内核程序负责完成,这需要运行在核心态;

2.1 fork、exec、wait、exit

fork、exec、wait和exit这四个系统调用是和进程有关的最为重要的四个系统调用:

  • fork用来创建进程;
  • exec从磁盘上载入并执行某个可执行程序;
  • exit是进程自己退出时要调用的函数;
  • 调用wait的进程会等到子进程退出时才继续执行;

fork

fork系统调用的函数原型定义为:

int fork();

这个函数没有参数,调用该函数的进程会再创建一个进程,新创建的进程是原进程的子进程;
两个进程都从fork()这个地方继续往下执行,并且执行“同样”的代码。但是父进程执行fork()会返回子进程的ID,而子进程调用fork()会返回0,父子进程正是通过对这个返回值的判断(用if 语句)来决定分别执行哪段代码;

exec

系统调用exec()的功能是在当前进程中执行一段新程序,进程的PID保持不变。可以这样形象地理解,一个进程就像一个壳子,在这个壳子里可以装各种可执行代码。fork()创建了这个壳子,并且将父进程的代码装在这个壳子中执行,而exec()是用一个磁盘上的可执行程序(exec()的参数告诉操作系统是哪个可执行程序)替换了这个壳子里原有的内容;

exec()函数分为两类,分别以execl和execv开头,其函数原型定义如下:

void execl(const char*filepath,const char*arg1,char*arg2,…);
void execlp(const char*filename,const char*argl,char*arg2,…); 
void execv(const char* filepath,char* argv[]); 
void execvp(const char* filename,char* argv[]);

这些函数基本上一样,只是execl中对应可执行程序入口函数的参数,即其中的arg1、arg2等,是一个一个列举出来的,而execv是将这些参数组织成一个数组告诉操作系统的;

可执行程序入口函数的参数就是可执行程序的main(int argc,char*argv[])函数中的参数argv,我们都知道这些参数是通过命令行输入的,命令行中的参数实际上就是用系统调用execl/execv函数中的参数传进去的;

函数名中带p和不带p的差别在于:execv()中filepath是绝对路径,而execvp()中的filename是相对路径;

exit

系统调用exit用来终止一个进程,在进程中可以显式调用exit来终止自己,也可以隐式调用exit;

操作系统在编译main()函数时,当遇到main()函数的最后一个}时会“塞入”一个exit;

exit()函数的原型定义为:

void exit(int status);

exit中的参数status是退出进程返回给其父进程的退出码。同时,退出的进程会向其父进程发送一个SIGCHILD信号,一个进程执行wait系统调用时就会暂停自己的执行来等待这个信号。所以wait和exit合在一起可以完成这样一种进程之间的同步合作:父进程启动了一个子进程,调用wait 等待子进程执行完毕;子进程执行完毕以后调用exit给父进程发送一个信号SIGCHILD,父进程被唤醒继续执行;

wait

wait系统调用的函数原型定义为:

int wait(int *stat_addr);

其返回值是exit子进程的PID,stat_addr是进程中定义的一个变量,用于存放子进程调用exit时的退出码,即exit 系统调用的参数status;

2.2 open、read、write

open、read、write是典型的操纵文件的系统调用。同时,这三个系统调用也是用户编程时最为常用的系统调用,这是因为文件是用户操作计算机的基本单位;

三个系统调用的函数原型定义为

int open(char *filename,int mode); 
int read(int fd,char *buf,int count); 
int write(int fd,char *buf,int count);

open

open系统调用用来打开文件,其中第一个参数filename是要打开的文件名,mode是打开方式,返回值fd是打开文件后产生的句柄,以后就用这个句柄来操作打开的文件;

read

read和write是操作打开文件的系统调用,read用来将句柄fd对应的文件读入到内存缓存区buf中,并且要读入count个字节,而真正读入的字节数会由read返回;

write

write用来向句柄fd对应的文件写内容,即从内存缓存区buf中取出count个字节写出到文件中,当然真正写出的字节数由write返回;

2.3 printf、scanf

printf和scanf是用来分别操纵显示器和键盘的函数,函数原型是:

void printf(格式化输出字符串,输出内容,…);//如printf("ID:%d",3)
void scanf(格式化输入字符串,输入内存地址,…);//如scanf("ID:%d",&id)

注意,这两个函数不是系统调用,仅仅只是两个库函数,这两个库函数的实现依赖于write和read实现“写显示器”和“读键盘”

Q:库函数和系统调用的区别在哪里?

A:

  • 系统调用是最底层的应用,是面向硬件的。而库函数的调用是面向开发的,相当于应用程序的API接口;
  • 各个操作系统的系统调用是不同的,因此系统调用一般是没有跨操作系统的可移植性,而库函数的移植性良好;
  • 库函数属于过程调用,调用开销小;系统调用需要在用户空间和内核上下文环境切换,开销较大;
  • 库函数调用函数库中的一段程序,这段程序最终还是通过系统调用来实现的;系统调用调用的是系统内核的服务;

3.系统调用的实现机理

系统调用不仅是为了给上层用户提供“统一接口”供其使用,同时作为操作系统的大门,上层应用只能通过这个大门才能进入操作系统;

3.1 双模态的概念

要实现系统调用的“大门”作用,首先要区分“门里”和“门外”,在操作系统中我们称其为内核态和用户态,一般情况下:

  • 内核态是操作系统代码执行时的状态;
  • 用户态是应用程序代码执行时的状态;

无论是操作系统内核代码还是应用程序代码都是装入内存后才执行的,而内核态代码和用户态代码在内存中放置的区域不同:

  • 放置内核代码的那一段内存区域是“内核态区域” - 一般进程的kernel代码都是相同的,映射到物理地址同一块区域;
  • 放置用户代码的那段内存区域就是“用户态区域”;

结论:一般来说kernel的代码都在高地址,方便用户态进程从低地址进行处理;

系统调用的“大门”作用体现在让执行在用户态区域的代码不能进入内核态区域(具体来说就是用户态代码不能通过jmp跳转到内核态内存区域中,用户态代码也不能通过mov访问存放在内核态内存中的数据)


Q:同样在内存中,如何区分是用户态内存还是内核态内存?

A:内存的使用由操作系统统一管理,操作系统在内存中划定一个区域并设置该区域的特权级,操作系统会及那个自己所在的内存区域的特权级别设置的非常高,将用户程序所在区域的特权级别设置的较低;

在用户程序执行时每访问一次内存都做一次审查。如果要访问的内存区域比自己的特权级高,CPU会拒绝执行,这就实现了操作系统的保护机制;

CPU提供了一种被称为特权环的机制来实现这个特权级检查;

由于很多指令在执行时都要进行这样的特权级检查,为了提高执行效率,应该用计算机硬件即CPU电路来实现这个权限检查,而不是用软件来完成这个检查;

特权级检查涉及两个重要的数值:

  • 当前特权级(CPL):表示当前执行指令的特权级
  • 描述符特权级(DPL):表示一个目标段的特权级,将目标内存区域的特权级信息放在目标段描述符中

3.2 双模态的实现

双模态是由操作系统和硬件一起实现的,硬件主要实现下面的功能:

3.2.1 特权指令

特权指令是指只有在CPU处于内核态的时候才能执行的指令,一个不严谨的说法是,影响其他进程的指令就是特权指令

特权指令不能通过普通应用直接执行,必须让操作系统帮助执行,操作系统向用户态应用提供了接口用来执行这些特权指令,这些接口被称为系统调用

硬件会帮助操作系统检测程序的优先级(上面已经介绍过);

3.2.2 内存保护

内存保护可以保证物理地址和虚拟地址都是隔离的,当然这里硬件需要进行的地址的隔离是指物理地址

分段法具有不容易实现共享、内存碎片化的问题,因此现代操作系统一般都采用分页的方法;

分页法涉及的一个很重要的概念是虚拟地址到物理地址的转换(实际上是虚拟页到物理页的映射,我们之后会详细介绍),这个映射过程是操作系统和硬件一起执行的 - 由硬件(CPU中的MMU)来执行虚拟地址到物理地址之间的映射,由软件OS来决定映射的策略,OS和CPU的中间体就是页表,页表存在内存中;

3.2.3 时间片中断

时间片中断是一种经典的调配策略,是一种让操作系统重新获取CPU控制权的方式

注意:计时器的设置只能在内核态进行

3.2.4 上下文切换

本质上就是进程/线程切换,我们将在后面详细介绍;

3.3 总结

代码(静态存放在存储器中):可以分为用户代码和操作系统代码;

进程(一段代码的上下文,不仅包括代码还有堆栈等):可以分为用户进程和操作系统进程;

CPU的运行状态可以分为用户态和内核态;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坂.y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值