(系统调用实验)了解系统调用不同的封装形式

实验原理

API

API(Application Programming Interface,应用程序编程接口),指的是我们用户程序编程调用的如read(),write(),malloc(),free()之类的调用的是glibc库提供的库函数。API直接提供给用户编程使用,运行在用户态。这里要另外提一下,POSIX针对API提出标准,即针对API的函数名,返回值,参数类型进行规范约束,但是并不管API具体如何实现。

系统调用

系统操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用,用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务。通过软中断或系统调用指令向内核发出一个明确的请求,内核将调用内核相关函数来实现(如sys_read(),sys_write())。用户程序不能直接调用这些Sys_read,sys_write等函数。这些函数运行在内核态。

两者联系与区别

区别:API只是一个函数定义,系统调用通过软终端向内核发出一个明确的请求
联系:
1.Libc库定义了一些API引用了封装例程,目的在于发布系统调用。
2.一般每个系统调用对应了一个封装例程,库再用这些封装例程定义出给用户的API。通常API函数库中的函数会调用封装例程,封装例程负责发起系统调用,这些都运行在用户态;内核开始接受系统调用后,CPU从用户态切换到内核态;内核调用相关的内核函数来处理再逐步返回给封装例程,cpu进行一次内核态到用户态的切换,API函数从封装例程拿到结果,在处理完毕后返回用户。
3.当API函数不一定都需要进行系统调用

系统调用号

一个系统调用号,对应一个函数入口地址,glibc和内核里面的这个系统调用号是一致的,所以glibc调用汇编之类把系统调用号传给内核的时候,内核找到这个具体的系统调用服务例程对应的函数入口地址,如sys_read。

系统调用和函数调用的区别

  • 系统调用和库函数的关系
    a. 系统调用通过软终端int 0x80从用户态进入内核态。 函数库中的某些函数调用了系统调用
    b. 函数库中的函数可以没有调用系统调用,也可以调用多个系统调用 编程人员可以通过函数库调用系统调用 高级编程也可以直接采用int
    c.0x80进入系统调用,而不必通过函数库作为中介 如果是在核心编程,也可以通过int
    d.0x80进入系统调用,此时不能使用函数库。因为函数库中的函数是内核访问不到的,
  • 从用户调用库函数到系统调用执行的流程
    a.用户调用getpid()库函数
    b.库函数执行int 0x80中断,由于中断使得进程从用户态进入内核态,参数通过寄存器传送
    c.0x80中断对应的中断例程被称为system call handler。其工作为存储大多数寄存器到内核堆栈中,这是汇编代码写的
    d.执行真正的系统调用函数-system call service routine,这是C代码
    e.通过ret_from_sys_call()返回,回到用户态的库函数,这是汇编代码写的

软中断

  • 软中断与硬中断
    硬中断:由与系统相连的外设(比如网卡、硬盘)自动产生的。主要是用来通知操作系统系统外设的变化,比如网卡收到数据包的时候,就会发送一个中断。通常我们说的中断指的是硬中断(hardirq)
    软中断:为了满足实时系统的要求,中断处理应该是越快越好。Linux为了实现这个特点,当中断发生的时候硬中断处理那些短时间就可以完成的工作,而将那些处理事情比较长的工作,放到中断之后来完成,也就是软中断(softirq)
  • 软中断指令
    Int是软中断指令。中断向量表是中断号和中断处理函数地址的对应表,int n-触发软中断n。相应的中断处理函数的地址为:中断向量表地址+4*n

int与函数调用call的区别

中断int过程:取得中断类型码,把标志位压入栈指中,把CD压入栈中,把IP压入栈中,更改CS和IP,转到中断程序。而CALL是将当前IP或者CS和IP压入栈中,到底是把IP还是IP和CS压入栈中。就要看CALL是一个字还是2个字。

Task 1

参考下列网址中的程序。阅读分别运行用API接口函数getpid()直接调用和用汇编中断调用两种方法调用Linux操作系统的同一个系统调用getpid的程序。请问getpid的系统调用号是多少,Linux系统调用的终端向量号是多少。

实验过程

  • 编写运用API接口函数getpid()直接调用系统调用,代码如下
    操作系统实验一:操作系统初步
  • 编写汇编中断调用Linux操作系统同一个系统调用,代码如下
    操作系统实验一:操作系统初步
  • 编译程序,并运行操作系统实验一:操作系统初步
  • 查询getpid函数的系统调用号操作系统实验一:操作系统初步

实验结果

  • getpid()的系统调用号是172,Linux系统调用的中断向量号是0x80
  • getpid_call代码解析:pid用来保存API函数getpid()返回的进程号
  • getpid_int代码解析:
    “mov $0,%%ebx\n\t”:将ebx寄存器清零
    “mov $0x14 %%eax\n\t”:将20(getpid系统调用号为20)存入eax寄存器
    “int $0x80\n\t”:发送系统调用指令
    “mov %%eax,%0\n\t” :"=m"(pid):将eax寄存器的值(其中保存的是系统调用返回值)存入变量pid

Task 2

命令:printf_(“Hello World!\n”)可归入一个{C标准函数、 GNU C函数库、 Linux API}中的哪一个或者哪几个?请分别用相应的linux系统调用C函数形式和汇编代码两种形式来实现上述命令。

实验过程

  • 用linux系统编写C函数形式的”Hello World”程序为hello_c.c操作系统实验一:操作系统初步
  • 编译源码为hello_c,并执行文件,屏幕打印”Hello World!”操作系统实验一:操作系统初步
  • 用linux系统编写汇编代码形式的”Helloworld”程序为hello_asm.asm操作系统实验一:操作系统初步
  • 编译源码为hello_asm,并执行文件,屏幕打印“Hello,World!”操作系统实验一:操作系统初步

Task 3

阅读pintos操作系统源代码,画出系统调用实现的流程图。

Pintos简介

Pintos是80*86架构的简单操作系统框架。支持内核线程,加载和运行用户程序以及文件系统。我们可以通过http://cs140.stanford.edu/pintos.tar.gz获取pintos

Pintos/src/

Pintos/src中看到的目录结构有:“threads/”, “userprg/”, “vm/”, “filesys/”, “lib/”, “lib/kernel/”, “lib/user/”, “lib/tests/”, “eamples/”, “utils/”。
其中/Pintos/src/lib用于标准C库的子集实现,此目录中的代码被编译到Pintos内核中,并在运行的用户程序中。在内核代码和用户程序中,头文件在这些目录下课可以使用#include<…>标头,你几乎不需要修改此代码。

Pintos/src/bin/lib/user/syscall.c

  • 此文件中,syscall.h里定义了4个宏,分别是syscall0(NUMBER), syscall1(NUMBER,ARG0), syscall2(NUMBER,ARG0,ARG1), syscall3(NUMBER,ARG0,ARG1,ARG2)操作系统实验一:操作系统初步
  • 该文件定义了20中系统调用函数操作系统实验一:操作系统初步

Pintos/src/userprog/syscall.c

此文件中定义了syscall_init, syscall_handler两个函数操作系统实验一:操作系统初步
Syscall_init:负责系统调用初始化工作,并与函数内部调用了inter_register_int函数,注册软中断,从而调用系统调用处理函数。
Syscall_handler:处理系统调用

系统调用实现流程图

操作系统实验一:操作系统初步

(并发实验)根据以下代码完成下面的实验

Task 1

编译运行该程序(cpu.c),观察输出结果,说明程序功能。(编译命令: gcc -o cpu cpu.c –Wall)(执行命令:./cpu)

实验过程

  • 编写cpu.c程序,代码如下操作系统实验一:操作系统初步
  • 编译并执行cpu.c程序,查看结果操作系统实验一:操作系统初步

代码解释

操作系统实验一:操作系统初步
argc为命令行总的参数个数
argv[] 为保存命令行参数的字符串指针,其中第0个参数是程序的全名,以后的参数为命令行后面跟的用户输入参数。argv参数为字符串指针数组,其各元素为命令行中各字符串的首地址。指针数组的长度即为参数个数argc
操作系统实验一:操作系统初步
当命令行总参数不为2时,即当除却程序名外,有且只有一个参数,否则提示“usage:cpu\n”,退出程序
操作系统实验一:操作系统初步
当除却程序名后只有一个参数时,将该参数赋值给str。并进入循环,循环中每打印一次参数,线程睡眠1秒。

实验结果

如上所示,当输入./cpu时出现“usage:cpu\n”的错误提示,表示必须在cpu后输入一个参数。当输入./cpu A后cpu.c程序运行,并按照1秒钟的时间间隔先屏幕中打印A。

Task 2

再次按下面的运行并观察结果:执行命令:./cpu A & ; ./cpu B & ; ./cpu C & ; ./cpu D &程序cpu运行了几次?他们运行的顺序有何特点和规律?请结合操作系统的特征进行解释。

实验过程

  • 输入./cpu A & ; ./cpu B & ; ./cpu C & ; ./cpu D &,并运行操作系统实验一:操作系统初步

实验结果

同时并发运行一个程序,发现四个程序的执行顺序没有规律

结果分析

在单批道处理系统中,一个作业单独进入内存并独占系统资源,知道运行结束后下一个作业才能够进入系统,当进行I/O操作的时候CPU处等待状态。它的特征是执行顺序是先进先出的。
用户所提交的作业先放在外存上,并排成一个对列(后备对列),由作业调度程序按照一定的算法,从后备对列中选择若干个作业调入内存,使其共享CPU和系统中的各种资源。同时在内存中装入若干程序,这样可以在A程序运行时,利用其IO操作而暂停的CPU空挡时间,再调度另一道程序B运行,同样可以利用B程序在IO操作时调用CPU空档调用程序C运行,使用多道程序交替运行,始终保持CPU忙碌的状态。
多道批处理系统具有多道和成批的特点。多道:在内存中同时存放多个作业,使之同时处于运行状态,这些作业共享CPU和外部设备等资源。成批:和他的作业之间没有交互性。用户自己不能干预自己的作业的运行,发现作业错误不能及时改正。
因为多批道处理系统具有成批性,内存中可以同时存在若干道作业,但是用户不能够干预自己作业的运行。并作业通过一定的作业调度算法来使用CPU所以作业的执行次序与进入内存次序无严格对应关系。
同时在多道批处理系统中,I/O设备具有异步性,我们无法判定一个程序何时结束何时开始。

(内存分配实验)根据以下代码完成实验

Task 1

阅读并编译运行该程序(mem.c),观察输出结果,说明程序功能。

实验过程

  • 编写mem.c程序,代码如下操作系统实验一:操作系统初步
  • 编译并执行mem.c程序,查看结果操作系统实验一:操作系统初步

代码解释

  • 操作系统实验一:操作系统初步
    定义一个指针p并将一个int类型单元的首地址存储到指针变量p中
  • 操作系统实验一:操作系统初步
    assert的作用是现计算表达式p!=NULL,如果其值为假(即为0),那么它先向stder打印出一条出错信息,然后通过调用abort来终止程序运行。
    getpid的作用是取得进程码,许多程序利用取得此值来建立临时文件,以避免临时文件带来相同的问题。
    %p的作用是按照十六进制输出地址
    这里检查指针p不为NULL,即成功被分配地址后,输出其进程码与指针p对应的地址位置
  • 操作系统实验一:操作系统初步
    将指针p执行的int空间的值修改为0,并通过while循环每隔1分钟对该值进行累加。

实验结果

此次mem程序执行的进程号为3280,指针p所存储的地址是0x1db8010,对p指向的值累加并打印到屏幕成功。

Task 2

再次按下面的命令运行并观察结果。两个分别运行的程序分配的内存地址是否相同?是否共享同一块物理内存区域?为什么?命令:./mem &; ./mem &

实验过程

  • 输入./mem &; ./mem &,并运行操作系统实验一:操作系统初步

实验结果

同时运行两个程序,发现为两个进程分别分配了两个物理地址,分别为0xd40010,0xd12010。

结果分析

操作系统对在进行的每个进程分配空间时不会直接在memory上直接对应存储,而是通过‘虚拟内存技术’,为每个进程分配4G内存空间,0-3G属于用户空间,3-4G属于内核空间。每个进程得我用户空间不同,但内核空间相同。程序中的malloc函数是在用户空间中的堆上通过扩展堆的方式来得到得到虚拟内存地址的返回值,来维持一个页表来进行映射,每次访问空间的某个地址都需要把地址翻译为实际物理内存地址。
所以从进程的不同性上面来说,有可能对应相同的虚拟地址,但是真实的物理地址不会相同,这种虚拟内存也不保证不同进程的相互隔离,错误程序不会干扰别的正确的进程。

(共享问题)根据以下代码完成实验

实验原理

Pthread

定义:POSIX threads, 简称Pthreads,是线程的POSIX标准,定义了创建和操纵线程的一套API。
数据类型:pthread_t:线程ID, pthread_attr_t:线程属性
操纵函数:
pthread_create():创建一个线程
pthread_exit():终止当前线程
pthread_cancel():中断当前的线程,知道另一个线程运行结束
pthread_attr_init():初始化线程的属性
pthread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
pthread_attr_getdetachstate():获取脱离状态的属性
pthread_attr_destroy():删除线程的属性
pthread_kill():向线程发送一个信号

Task 1

阅读并编译运行该程序,观察输出结果,说明程序功能。(编译命令:gcc -o thread thread.c -Wall –pthread)(执行命令1:./thread 1000)
阅读并编译运行该程序(mem.c),观察输出结果,说明程序功能。

实验过程

  • 编写thread.c,代码如下操作系统实验一:操作系统初步
  • 编译并执行thread.c程序,查看结果操作系统实验一:操作系统初步

代码解析

  • 操作系统实验一:操作系统初步
    定义了两个全局变量counter、loops
  • 操作系统实验一:操作系统初步
    定义了work函数利用for循环对counter变量进行loops次累加
  • 操作系统实验一:操作系统初步
    要求输入的参数有且只有一个
  • 操作系统实验一:操作系统初步
    通过pthread_create()函数创建两个均进行利用worker函数进行counter累加工作的线程,然后利用pthread_join()函数来以阻塞的方式,等待指定线程结束,并回收被等待线程的资源。
  • 操作系统实验一:操作系统初步
    输出counter的累加结果

实验结果

当输入参数为1000时,程序执行得到的累加结果为2000

Task 2

尝试其他输入参数并执行,并总结执行结果的有何规律?你能尝试解释它吗?(例如执行命令2:./thread 100000)(或者其他参数。)

实验过程

  • 分别输入./thread 10000、 ./thread 100000、 ./thread 1000000、./thread 100, ./thread 10并运行操作系统实验一:操作系统初步

实验结果

发现同时运行两个线程的时候,当输入loops参数较小是,counter的结果基本为loops的两倍。当输入的loops较大时,会小于两倍。

结果分析

在该程序中,由counter、loops是全局变量,且worker()函数是共享的。
但两个线程在同一进程中,并且访问操作的对象是共享的时候,就会发生读脏数据的情况。现代计算机,多采用加锁的机制避免这种问题的发生,保证线程实现的完整性。
当输入参数比较小的手,一个CPU的核心足够处理,就是单核CPU运行多线程,由于每个核心有内存锁机制,所以计算结果没有问题。但是当输入参数比较大时,就会无法满足,进而导致出现读取脏数据的情况。

github链接

https://github.com/CloudnessZhang/16281244_ZhangYunxiao_OS_lab1.

相关文章:

  • 2022-01-07
  • 2021-06-18
  • 2021-12-24
  • 2022-01-01
  • 2022-01-01
  • 2021-08-20
  • 2021-07-09
猜你喜欢
  • 2021-09-24
  • 2021-05-28
  • 2021-05-16
  • 2021-05-15
  • 2021-10-13
  • 2021-07-01
相关资源
相似解决方案