Chapter 1 操作系统概述 上(现代操作系统笔记)

来源:互联网 发布:tina备份软件 编辑:程序博客网 时间:2024/05/08 21:43
  • 计算机系统漫游
- 系统硬件的组成
1. I/O设备
2. 主存memory, 在执行程序时,用来存放程序和程序处理的数据。和磁盘之间可以采用DRAM直接存取(存储器逻辑上是一个线性的字节数组组成,每个字节有自己的唯一的地址,从0开始)
3. 处理器, 解释主存中的命令,它的核心是程序计数器Program Counter, PC指向主存中某条机器语言指令。I/O的读写也依赖于处理器。

- 指令的执行
当敲入回车键时,shell就知道命令的输入已经结束,然后shell执行一系列的指令:
1.将目标文件从磁盘拷贝到主存,加载目标文件
2.设置PC,执行指令(主存到寄存器,寄存器到显示设备)

-OS与硬件
1.文件是对I/O设备的抽象表示
2.虚拟存储器是对主存,磁盘I/O设备的抽象表示
3.进程是对处理器,主存和I/O的抽象表示
1)上下文切换,一个进程的指令跟另一个进程的指令交错进行,此时需要保存当前进程的上下文,恢复新进程的上下文,将控制权转移到新进程。进程是资源分配的基本单位,线程共享资源。
例:shell进程与hello进程
当shell执行hello时,shell通过系统调用执行请求,将控制权交给OS,OS保存shell的上下文,创建fork一个新进程和上下文,然后将控制权交给hello程序
2) 虚拟地址空间,最上面1/4地址空间是预留给操作系统的代码和数据的,底层3/4用来存放用户进程的代码和数据
3) 程序代码与数据,代码从固定地址开始,接着是C全局变量对应的数据区,代码和数据区是有可执行的目的文件初始化的
4)堆
5)共享库
6)栈
7)内核虚拟存储器:是操作系统驻留在存储器的部分,地址空间顶部的四分之一部分,为内核预留
8)进程虚拟存储器的内容存储在磁盘上,主存是磁盘的高速缓存
9)高速缓存

- 网络系统可以被看作一个I/O设备,当系统从主存拷贝一串字符到网络适配器时,数据流经网络到达另一台机器,另一台读取到达的数据,拷贝到自己的主存

- 高级程序语言->低级机器语言(可执行目标程序,以二进制的磁盘文件形式存放)
Unix上,依赖编译驱动程序 e.g., gcc -o hello hello.c,
1.预处理器,将被包含的头文件定义的内容插入到程序文本中,通常以i为后缀
2.编译器,将程序编译成汇编程序,以s为后缀
3.汇编器,将汇编程序翻译成机器语言指令,这些指令被打包成一种可重定位(relocatable)目标程序,以o为后缀
4.链接器,将定义好的库函数并入到程序的二进制文件中,库函数存于另外的单独的预编译目标文件中,如printf.o
=====================================================================================================
  • 计算机系统
- 硬件
1.存储器
2.I/O
3.总线
1)内部总线
2)外部总线
4.CPU,从内存读取指令,解码执行
1)寄存器存在于CPU中,分为通用寄存器,段寄存器等,比如AX,BX,CS,DS,SS,ES,IP(PC),SP等
2)每当程序被迫终止时,寄存器的值会被保存起来,等待恢复
3)状态寄存器(PSW),用二进制位控制,当处于核心态时,可以处理指令集的每一条指令,当处于用户态时,关于I/O,内存保护等指令会被拒绝执行
4)一般寄存器用来存储部分关键或临时变量,因为从内存读取数据然后执行需要花费较长的时间

- 软件
1.OS,处于核心态,有权对所有硬件进行访问
2.交互应用软件,比如shell, 图形,高级语言程序等,处于用户态

- I/O设备
1.设备本身,有一个标准的接口
2.设备控制器,为操作系统提供简单的接口
每一个控制器含有少量用于通信的寄存器,驱动程序从OS读取命令,然后翻译成对应的值,写入这些设备寄存器中,所有这些设备寄存器的集合构成I/O端口空间
3.不同的设备控制器,需要不同的软件进行控制,负责发出命令并接收响应,与控制器对话,称为device driver设备驱动,必须把驱动装到OS中,核心态运行
将驱动装入OS的三种途径:
1)内核与驱动重新链接,然后重启系统(Linux)
2)在OS文件设置一个入口,并通知该文件需要一个驱动程序,系统重启,OS寻找这个程序,并装载(Windows)
3) OS运行时,接收新的设备驱动,并安装,无需重启

- 存储器
1.速度快,容量大,便宜等,这些条件不同同时满足,顶层容量越小,成本却越高
2.寄存器与CPU材料,速度一样,一般小于1kB
3.内存(主存)也存储常访问的资源,避免频繁访问磁盘
随机访问存储RAM:电源一断,内容消失
只读存储器ROM:一经存储,不可修改
闪寸Flash: 可擦除重写
4.虚拟内存管理
将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分,需要快速的映像内存地址,把程序生成的地址转换为有关字节在RAM中的物理地址
5. 磁盘
1)同心圆
2)磁道,每个磁盘可以分为许多扇区,每个扇区512B
3)柱面
6.高速缓存(多级)

- 系统调用
1. trap 系统调用陷入内核,进入核心态,调用OS,调用结束后,把控制权交回给进程
2. trap并不是系统调用专门发明的,硬件使用警告也可以使用这个调用,把控制权交给OS,决定怎么处理错误,有些线程希望自己处理,可以把控制权交回给线程

Basic:CPU一次只能处理一条指令
采用系统调用,OS可以提供系统的各种核心功能,需要在核心态执行的许多子服务。系统调用不仅提供给用户编程,OS自己也很有进程实在系统调用的基础上实现的。
思考:系统调用,一般过程调用,库函数调用区别?

系统调用的主要种类:

1.进程控制,如建立进程,撤销进程,进程挂起等

2.进程之间的通信,如信号,管道

3.存储管理,如获取作业进程占用的内存区域信息等

4.设备管理,如打开设备,获取或向设备输出等

5.文件管理,如创建文件,删除文件,打开文件,读写文件等

6.系统信息维护,如获取或设置系统当前日期等

系统调用的执行过程:
1.保持现场,设置寄存器(申请系统调用的功能号)
2.根据申请的功能号,查找相应的入口地址(OS通常根据功能号将各个系统调用所对应的子程序的入口地址排列为一张表)
3.执行相应的服务程序

C语言函数库对一些系统调用进行了一些包装和拓展,存进库函数


例2: 
count=read(fd, buffer_nbytes);
运行错误时,count=-1,全局变量errno会被放入错误号
步骤:
1. 将参数压入栈
2.调用read,将read代码读入寄存器
3.读取寄存器,OS检查系统调用编号,使用trap命令发出正确的系统调用
4.根据编号-系统调用表,执行命令,最后返回调用者
5.清楚堆栈,增加SP



#############
=>系统调用——进程
#############
每一个进程都有一个相关的虚拟地址空间(预留顶部1/4空间给内核,剩余3/4用于存放栈,共享库,堆,BSS,数据,代码等)
系统管理器给每一个进程一个UID,父进程和子进程拥有相同的UID,当UID是Superuser时,具有特殊权利。除了虚拟地址空间外,与进程相关的还有资源集(包括寄存器,进程ID,用户ID,数据段指针,打开文件的清单等),这些资源集一般存于一张表中,称为进程表,当前的进程都是进程表中的一个项。当进程挂起,寄存器被压入堆栈。

进程调度:为每一个进程分配一个时间片
当进程创建时,每一个进程都会被分配一个进程控制块(Process Control Block),PCB包含很多信息(比如进程ID,每个进程都有唯一的标识符),供给系统调度和进程本身使用。一个或多个进程组成进程组,构成一个session。Linux PS命令可以查看系统目前多少个进程在进行。

调用函数:
getid()——>返回当前进程ID

#include<sys/types.h> /* 提供类型pid_t的定义 */

#include<unistd.h> /* 提供函数的定义 */

    pid_t getpid(void);

pid_t与int类型兼容,完成可以用%d打印出来

fork()——>复制一个进程
当一个进程调用fork(),会出现几乎一模一样的进程。原先的进程被称为父进程,新出现的进程被称为子进程,但是PID是不同的,在父进程中,fork会返回子进程的PID,而子进程会返回0,出现错误时,fork返回一个负值。

#include<sys/types.h> /* 提供类型pid_t的定义 */

#include<unistd.h> /* 提供函数的定义 */

    pid_t fork(void);


例1:

/* fork_test.c */

#include<sys/types.h>

#inlcude<unistd.h>

main()

{

    pid_t pid;   

    /*此时仅有一个进程*/

    pid=fork();

    /*此时已经有两个进程在同时运行*/

    if(pid<0)

        printf("error in fork!");

    else if(pid==0)

        printf("I am the child process, my process ID is %d\n",getpid());

    else

        printf("I am the parent process, my process ID is %d\n",getpid());

}


fork()一般错误有两种原因:1、内存空间已满: ERRNO:ENOMEM 2、进程数已经达到系统规定:ERRNO:EGAIN
exit()终止一个进程,停止所有操作,清除PCB内的各种数据,参数0标识正常结束,其它数值表示出现错误。

#include<stdlib.h>

    void exit(int status);


exit()与_exit()区别:

#include<unistd.h>

    void _exit(int status);

exit()是在_exit()基础上进行的进一步的包装,增加了若干工序,其中最大的区别是就是exit()在被调用之前会检查文件的打开情况,把文件缓冲区中的内容写回文件(清理I/O缓存)。

缓存:
1、全缓存,写满缓存之后再一次性读出
2、行缓存,遇到/n或EOF之后读出


fork()采用exit(),vfork()采用_exit()。其中fork()的父子进程如果没有对数据段,堆栈段进行写操作时,共享信息,如果一方进行写操作,则复制出进程副本,而vfork()共享数据段,一方写操作时,会阻塞另一方,数据段的修改影响另一方。

采用exit()后,会留下一个僵尸进程Zombie的数据结构,包含进程的退出状态,进程占用的CPU时间,发送页错误的数目和收到信号的数目等信息,但几乎不占CPU和内存空间,没有可执行代码,也不可调用,但是系统的进程数是有限的,所以僵尸进程会占用数目,影响新进程的产生。这时我们采用wait()和waitpid()进行处理。

wait():

#include <sys/types.h> /* 提供类型pid_t的定义 */

#include <sys/wait.h>

    pid_t wait(int *status)


一旦调用wait(),会阻塞自己,直到找到一个已经变成僵尸的子进程,wait会收集该僵尸进程信息,返回进程号并把他销毁,如果没有找到这样的子进程会一直等待。如果我们对进程怎么死的不在意,可以把status设为NULL。因为status的存放是二进制的,所以读起来麻烦,因此,系统引进了:
WIFEXITED(status) 正常退出返回非零
WEXITSTATUS(status)

/* wait2.c */

#include <sys/types.h>

#include <sys/wait.h>

#include <unistd.h>

main()

{

    int status;

    pid_t pc,pr;

    pc=fork();

    if(pc<0) /* 如果出错 */

        printf("error ocurred!\n");

    else if(pc==0){ /* 子进程 */

        printf("This is child process with pid of %d.\n",getpid());

        exit(3);    /* 子进程返回3 */

    }

    else{       /* 父进程 */

        pr=wait(&status);

        if(WIFEXITED(status)){  /* 如果WIFEXITED返回非零值 */

            printf("the child process %d exit normally.\n",pr);

            printf("the return code is %d.\n",WEXITSTATUS(status));

        }else           /* 如果WIFEXITED返回零 */

            printf("the child process %d exit abnormally.\n",pr);

    }

}

waitpid():

#include <sys/types.h> /* 提供类型pid_t的定义 */

#include <sys/wait.h>

    pid_t waitpid(pid_t pid,int *status,int options)


当pid>0,只等待进程ID等于PID的子进程,不管其它已经有多少进程已经运行结束退出,只要指定的子进程还没有结束,waitpid就会一直等下去
当pid=-1,等待任何一个子进程退出,没有任何限制,此时waitpid()和wait()的作用一样
当pid=0,等待同一个进程组中的任何子进程,如果子进程已经加入别的进程组,则忽略
当pid<-1,等待一个指定进程组中的任何子进程,这个进程组id=pid的绝对值 

Exec函数系列:
根据指定的文件名找到可执行的文件,取代调用子进程的内容(调用进程的实体,包括代码段,数据段和堆栈都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,就像是旧的躯壳,却注入了新的灵魂,只有调用失败了才会返回一个-1,从原程序的调用点往下执行)
fork()会将调用进程的所有内容原封不懂的拷贝到新产生的子进程中去,这些拷贝的动作很耗时,而如果fork()完之后马上调用exec(),这些幸苦拷贝的东西会被马上抹掉,这看起来非常不划算,于是人们设计了一种“写时拷贝 copy on write”技术,使fork结束后不立刻复制父进程的内容,而是真正使用时才复制,这样如果下一条语句是exec,他就不回做无用功了,也提高了效率

#include <unistd.h>

int execl(const char *path, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

int execle(const char *path, const char *arg, ..., char *const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

int execve(const char *path, char *const argv[], char *const envp[]);


execve是真正的系统函数调用,其它的都是经过包装的。3个EXECL和3个EXECV开头的函数,其中EXECV开头的函数的命令行参数是char *argv[]的格式,以NULL结束,而EXECL开头的函数需要一个个列出。
EXECLE()和EXECVE()需要传递环境变量组Envp[],以NULL结束。如果Envp[]是NULL,则按系统默认的环境变量进行操作。
EXECLP()和EXECVP()的文件指定不需要绝对路径

int main(int argc, char *argv[],char *envp[])
输入: cp file1 file2
此时, *argv[0]=cp
             *argv[1]=file1
             *argv[2]=file2
             *argv[3]=NULL

它可能与绝大多数教科书上描述不一样,实际上,这才是main函数真正完整的形式,参数argc指定命令行参数的个数,数组argv[]存放命令行参数,而envp用于存放所有的环境变量。环境变量是一组值,从用户登陆后就一直存在,很多应用程序需要依靠他来确定系统的一些细节。我们最常见的环境变量是PATH,它指出了应该到哪里去搜索应用程序,如/bin; HOME也是比较常用的环境变量,它指出了我们在系统中的个人目录。

值得一提的是,argv数组和envp数组存放的都是指向字符串的指针,这两个数组都以一个NULL元素表示数组的结尾。


下面是网上摘录的一个对进程短暂一生的小小比喻:
随着一句fork,一个新进程呱呱落地,但它这时只是老进程的一个clone
随着exec,新进程脱胎换骨,离家独立,开始为人民服务
人有生老病死,进程也是一样,它可以自然死亡,即运行到main函数的最后一个“}”,从容的离开我们
它也可以自杀,自杀方式有两种,一种是exit函数调用,一种是使用return,无论哪一种方式,都可以留下遗书,放在返回值里保留下来
它还可以被谋杀,被其它进程通过另外一些方式结束它的生命
进程死掉之后,会留下一具僵尸,wait和waitpid充当了验尸工,把僵尸推去火化,使其最终归于无形

* exec.c */

#include <unistd.h>

main()

{

    char *envp[]={"PATH=/tmp",

            "USER=lei",

            "STATUS=testing",

            NULL};

    char *argv_execv[]={"echo", "excuted by execv", NULL};

    char *argv_execvp[]={"echo", "executed by execvp", NULL};

    char *argv_execve[]={"env", NULL};

    if(fork()==0)

        if(execl("/bin/echo", "echo", "executed by execl", NULL)<0)

            perror("Err on execl");

    if(fork()==0)

        if(execlp("echo", "echo", "executed by execlp", NULL)<0)

            perror("Err on execlp");

    if(fork()==0)

        if(execle("/usr/bin/env", "env", NULL, envp)<0)

            perror("Err on execle");

    if(fork()==0)

        if(execv("/bin/echo", argv_execv)<0)

            perror("Err on execv");

    if(fork()==0)

        if(execvp("echo", argv_execvp)<0)

            perror("Err on execvp");

    if(fork()==0)

        if(execve("/usr/bin/env", argv_execve, envp)<0)

            perror("Err on execve");

}


Daemon进程:后台守护程序,当终端关闭时,daemon进程还是可以在系统中长久的存活下去(脱去终端,能在后台周期性的运行某种任务或等待处理某些可能发生的事件),每一个系统与用户进行交流的界面称为终端,每个从终端开始运行的程序依附与这个终端。
称为daemon进程的步骤:
1、在终端创建进程,之后所有的工作都在子进程中完成,而终端可以执行其它命令(调用fork后,父进程退出,fork产生的新进程一定不会成为一个进程组的组长,称为孤儿,此时由1号(init)进程收养它,原先的子进程会变成init进程的子进程)
init进程:由0号进程创建,是系统中所有其它进程的祖先
2、进程组:是一个或多个进程的集合,进程组有进程组ID来标识,组长的PID和进程组ID一样
一个会话(session)开始于用户登陆,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话,除非进程调用setsid系统调用。
系统调用setsid不带任何参数,调用之后,调用进程就会成立一个新的会话,并自任该会话的组长。
setsid():

#include <unistd.h>

     pid_t setsid(void);

让调用进程完全独立出来,脱离其它进程的控制,调用setsid系统调用。这是整个过程中最重要的一步。它的作用是创建一个新的会话(session),并自任该会话的组长(session leader)。如果调用进程是一个进程组的组长,调用就会失败,但这已经在第1步得到了保证。调用setsid有3个作用:

  1. 让进程摆脱原会话的控制;
  2. 让进程摆脱原进程组的控制;
  3. 让进程摆脱原控制终端的控制;

#############
=>系统调用——文件

#############

open()用于打开文件,参数是绝对路径或相对路径,O_RDONLY, O_WRONLY或RDWR表示对文件的操作权限,O_CREATE创建一个新的文件,open()函数返回一个文件描述符fd,可以使用close(fd)关闭对应文件。read()和write()可以用来读写文件。

与每一个文件相关的是一个指向文件当前位置的指针,在顺序读写时,该指针通常指向要读或写的下一个字节。Lseek调用可以设置该位置指针的值,后续的read和write就可以随机访问一个文件的任意部分,Lseek有三个参数,fd,文件位置,offset,说明该文件位置是相对于文件起始位置,当前位置还是当前位置。Unix可以通过stat系统调用查看文件的信息,第一个参数指定了要被检查的文件名,第二个参数用来存放这些文件。

mkdir和rmdir用来创建和删除空目录,link允许一个文件以两个或以上的名称出现。




每一个文件都有一个i编号表示,对应着一个i节点的表格项,记录文件的拥有者,磁盘块的位置等。一个目录是包括<i: 两个字节,ASCII文件名:14个字节>对集合的一个文件。在link中,我们只使用了i编号,比如note与memo的i编号都是70,指向同一个文件,如果一个文件被移走,可以利用unlink系统调用,保留另一个。两个都被移走的话,UNIX 00发现文件没有相应的目录项,就把该文件移走。
mount系统调用把两个文件系统合并成一个
mount("dev/fd0", "/mnt", 0); 第三个参数表示是只读还是可读写的。


chmod可以改变文件的保护模式,如:
chmod("file",0844)

chdir的作用是改变当前工作目录。使用fork创建的子进程继承了父进程的当前工作目录,当前的工作目录所在的文件系统是不能被卸载的,会对以后造成麻烦,所以我们一般使用根目录。一旦进程开始运行后,当前工作目录保持不变,此时,可以通过chdir修改。
unmask可以设定一个文件权限,用户可以用它来屏蔽某些权限。

#include <sys/types.h>

#include <sys/stat.h>

  mode_t umask(mode_t mask);


关闭不需要的文件时,子进程会从父进程那里继承下来一些已经打开的文件,会浪费资源,并且可能导致所在文件系统无法卸下。
当守护进程和terminal失去联系,所以terminal输入的字符不可能到达守护进程。守护进程的输出也到达不了终端。

/* daemon.c */

#include<unistd.h>

#include<sys/types.h>

#include <sys/stat.h>

#define MAXFILE 65535

main()

{

    pid_t pid;

    int i;

        pid=fork();

    if(pid<0){

        printf("error in fork\n");

        exit(1);

    }else if(pid>0) 

        /* 父进程退出 */

        exit(0); 

    /* 调用setsid */

    setsid();

    /* 切换当前目录 */

    chdir("/");

    /* 设置文件权限掩码 */

    umask(0);

    /* 关闭所有可能打开的不需要的文件 */

    for(i=0;i<MAXFILE;i++)

        close(i);

    /* 

       到现在为止,进程已经成为一个完全的daemon进程,

       你可以在这里添加任何你要daemon做的事情,如:

    */ 

    for(;;)

        sleep(10);

}




Reference:
1. 现代操作系统(第三版)Andrew S. Tanenbaum
2.http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part4/index.html?ca=drs-






0 0
原创粉丝点击