免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: 雨丝风片
打印 上一主题 下一主题

[FreeBSD] 【FreeBSD system programming 】中文翻译计划及所有异义提交处 [复制链接]

论坛徽章:
0
61 [报告]
发表于 2006-02-15 09:52 |只看该作者
把第二章给我吧,谢谢

论坛徽章:
0
62 [报告]
发表于 2006-02-15 09:55 |只看该作者
原帖由 meilincore 于 2006-2-15 09:52 发表
把第二章给我吧,谢谢


已在一楼更新,多谢支持和参与!

论坛徽章:
0
63 [报告]
发表于 2006-02-15 11:11 |只看该作者
1.10已提交。

论坛徽章:
0
64 [报告]
发表于 2006-02-15 11:15 |只看该作者
都是好人啊.最崇拜这种人啊

论坛徽章:
0
65 [报告]
发表于 2006-02-15 13:00 |只看该作者
第三章
       FreeBSD System Programming

该章节的例程:
http://www.khmere.com/freebsd_book/src/03/index.html

3.1 进程和内核服务
process:一系列用于实现目标的行为和操作;特别指:制造中指定的连续的工作和处理。

  前面的章节有提到BSD系统的启动过程。现在让我们看看一旦内核被导入并运行会发生什么。像大部分其他的现代操作系统,BSD是一个多用户多任务操作系统,这意味着它支持多用户运行不同的多进程来使用系统资源。进程的概念提出了一个对操作系统管理的所有活动的有力的提取。这个概念作为在多道程序设计环境中面向工作的一个特定术语是在19世纪60年代由Multics(一个失败的分时系统计划)的设计者提出的。

3.2 调度

  一个进程是一个运行程序的单独实例。例如,当你在BSD系统中运行Netscape,它将在被运行时创建一个进程。
如果有3个用户登陆BSD系统并全部同时运行相同的Netscape程序,每一个用户将拥有区别于其他用户的自己的
Netscape实例。BSD系统可以支持同时多个这样的进程。每一个进程将和一个进程标志号(PID)关联。这些进程
需要资源并且可能不得不使用一些设备比如外部存储。

  当多个程序在一个系统上运行的时候,操作系统操控出他们都在运行的假象。操作系统特定的调度算法将管理
程序的优先权指派。该领域的计算科学是广阔的,和十分专业的。更多的信息请看资源章节。

  操作系统实际上将运行的多进程不断调入或调出CPU(s),通过这种方法,每个进程得到一段特定的CPU(s)运行
时间。这段时间被叫做时间片。时间片的长短基本上由内核的调度算法决定。该算法有一个可以调整的值叫‘nice’,它提供给进程修改运行优先权的程序能力。这些优先权的值如下:
      Nice values                Priority
        -20    - 0            Higher priority for execution
         0      - 20          Lower priority for execution
      
注意:一个高的nice值导致低的运行级别,这可能看上去有点怪异。然而,考虑到下面这个计算:
(scheduling algo calculation) + (nice value)
这个简单的算术表明:添加一个负值到一个正值将得到一个较小的数,而当这些数被排序的时候,较小的数将排在
运行队列的前面。

所有进程默认的nice值是0,运行的进程可以提高自己的nice值(就是降低自己的优先权),但是只有以root运行的
进程可以降低nice(就是提高优先权)。BSD系列提供了两个基本的借口用于改变和获得这些值。它们是:


  1. int getpriority(int which, int who);
  2. int setpriority(int which, int who, int prio);
复制代码



  1. int nice(int incr);
复制代码


nice函数会把调用它的进程的nice值设置成传给它的参数incr的值。可以看到nice函数是比较不好用的,因为它不是
十分灵活,实际上nice函数不过是对前两个函数的封装。首选的方法是使用setpriority()。

因为可能的的优先权值可以为-1,所以getpriority的返回值不能作为程序运行成功与否的判断,而应该检查errno(错误号)是否被设置。因此在调用getpriority的时候要清空该值(errno)。(更多的关于使用errno信息,可以使用man(2)察看关于erron的页面和读例程:http://www.khmere.com/freebsd_book/src/03/nice_code.c.html)setpriority 和 getpriority可以通过设置‘which’和‘who’的值作用于外部进程的优先级,这以后在论述。

例程nice_code.c示范如何获取和设置当前进程的nice值。如果它被root以-20的命令行参数运行,系统将看上去没有反应。这是因为这个程序拥有了最高的运行级别,将会比系统优先。所以当设置丢了小于0的值时,需要谨慎使用和更多的耐心。取决于CPU,完全运行完可能要消耗20分钟。建议使用time指令来运行程序,如下:

  1. bash$ time nice_code 10
复制代码

然后,调整参数的值。这样进程消耗了多少运行时间就很明显了。比如调整参数使之小于0:

  1. $ time nice_code -1
复制代码

下面把参数调大:

  1. bash$ time nice_code 20
复制代码


同时,尝试用其他非root用户运行程序,并使用小于0的参数。系统将会拒绝操作。只有root运行的进程可以降低他们的nice值。(因为Unix系统时多用户系统,每个进程应该获得合理的CPU时间片,只有root可以改变进程的优先级到0以下,这样可以避免用户通过降低优先级来独占CPU资源.)

3.3 系统进程
保护模式的概念在前一章已有介绍,简单的说,它是现代CPU系列支持的一个特定的模式,通过该模式操作系统可以保护内存.据此,现在有两种这样的操作模式。一是内核态,意味着进程将运行在内核的内存空间,并因此,以有内核特权的保护模式运行于CPU中。二是用户态,即运行并且不是运作在内核保护模式的一切。

这区别是十分重要的,因为所有的给定进程都在内核态和用户态使用资源。这些资源有多种格式,就如用来标志进程的内核结构,进程指派的内存,打开的文件,和其他执行段。

在一个FreeBSD系统中,有一些关键的进程帮助内核来执行任务。这些进程中的一部份是完全运行于内核空间,而有些运行于用户态。这些进程如下:

PID  Name
0    swapper
1    init
2    pagedaemon
3    vmdaemon
4    bufdaemon
5    syncer

以上所有的进程除了init,都是运行于内核。这意味着它们不存在相应的二进制程序。这些进程有点类似用户态进程,并且因为它们在内核内运行,他们运作于内核特权模式。这种体系结构是出于多种设计考虑。例如,pagedaemon进程,为了减少负载,只在系统缺乏内存时被唤醒。因此如果系统许多空闲的内存,它就没有被唤醒的必要。如此,比这个进程运行于用户态的优点就是除非真正需要,可以避免使用CPU。但是,它在系统增加了一个需要被调度的进程。不过这些调度计算消耗如此小几乎可以忽略不计。

init进程是所有进程的宗主进程。除了运行于内核态特权模式的进程,每个进程都是init的后裔。并且,僵尸或孤儿进程都会被init接管。它也运行一些管理任务,比如调用产生系统ttys的gettys,和运行系统的有序关机。

3.4 进程创建和进程ID系统

就像上面所说,当一个进程被运行,内核指派给它一个唯一PID。这个PID是个正整数并且它的值在0到PID_MAX之间,由系统决定。(对于FreeBSD,在/usr/include/sys/proc.h中设定PID_MAX为99999。)内核将用下一个相续的可用的值来指派PID。因此,当PID_MAX到达的时候,值将会循环。这个循环是重要的,当使用PID于当前进程统计的时候。
每一个在系统上运行的进程都是由其他进程产生的。这是有一些系统调用完成的,将在下一章论述。当一个进程创建一个新的进程,原进程就成为父进程,新进程就是子进程。这种父子关系提供了很好的类推-每一个父进程可以有很多子进程而父进程们是由另外一个进程派生的。进程可以通过getpid/getppid函数得到自己或他们父进程的PID。

进程也可以用进程组来分组。这些进程组可以通过一个唯一的grpID来标志。进程组作为一个机制被引入BSD是为了使shells能够控制工作。看下面这个例子:

  1. bash$ ps auwx  | grep root  | awk {'print $2' }
复制代码

这些程序,ps,grep,awk和print都归属于一个相同的组,这允许所有的这些指令可以通过一个单一的工作来控制。

一个进程可以获得它的组ID/和父ID通过调用getpgrp或getppid:

  1.   #include <sys/types.h>
  2.   #include <unistd.h>
  3.   
  4.      pid_t   getpid(void);
  5.      pid_t   getppid(void);
  6.      pid_t   getpgrp(void);
复制代码

所有以上例出的函数都使绝对可以运行成功的。然而,FreeBSD man pages 强烈建议,PID不应该用来创建一个唯一文件,因为这个PID只在创建的时候唯一。一旦进程退出,PID值会回到未使用PID池,将会被接着被使用(当然是在系统一直运行情况下)。

一个获得这些值的源程序代码在 proc_ids.c中列出。
http://www.khmere.com/freebsd_book/src/03/proc_ids.c.html

当你运行下面程序:

  1. bash$ sleep 10 |  ps auwx |  awk {'print $2'}  | ./proc_ids
复制代码

同时在另一个终端运行:

  1. bash$ ps -j
复制代码


只有这些指令将被一个shell运行并且每个都有相同的PPID和PGID。

3.5 子进程

进程可以由其他进程创建,在BSD中有3个方法来实现这个目的。他们是:fork,vfork 和rfork 。其他的调用例如system不过是对这3个的封装。

fork

当一个进程调用frok,一个新的复制父进程的进程奖被创建:


  1. #include <sys/types.h>
  2.   #include <unistd.h>

  3.      pid_t   fork(void);
复制代码

不像其他的函数调用,在成功的时候,fork会反馈两次-一次在父进程,返回的是新创建的子进程的PID,而第二次在子进程,返回将是0。这样一来,你就可以区分两个进程。当一个新的进程被fork创建,它几乎是父进程的精确复制。他们的共同点包括一下:(不是创建的顺序)

Controlling terminal
Working directory
Root directory
Nice value
Resource limits
Real, effective and saved user ID
Real, effective and saved group ID
Process group ID
Supplementary group ID
The set-user-id bits
The set-group-id bits
All saved environment variables
All open file descriptors and current offsets
Close-on-exec flags
File mode creation (umask)
Signals handling settings (SIG_DFL, SIG_IGN, addresses)
Signals mask

在子进程中不同的是它的PID,而PPID被设置成父进程的PID,进程的资源利用值是0,并且拷贝了父进程的文件描述符。子进程可以关闭文件描述符而不干扰父进程。如果子进程希望读写它们,将会保持父进程的偏移量。注意一个潜在的问题,如果父子进程同时试图读写相同的文件描述符会导致输出异常,或程序崩溃。

当一个新的进程被创建后,运行的顺序将不可知。最好控制运行顺序的方法是使用semiphores,pipes,或signals,这以后论述。通过这些,读写就可以控制,使得一个进程可以避免扰乱其他进程导致一起崩溃。

wait

父进程应该使用下面wait系统调用的一种来搜集子进程的退出状态:


  1.     #include <sys/types.h>
  2.     #include <sys/wait.h>

  3.      pid_t     wait(int *status);


  4.      #include <sys/time.h>
  5.      #include <sys/resource.h>

  6.      pid_t     waitpid(pid_t wpid, int *status, int options);
  7.      pid_t     wait3(int *status, int options, struct rusage *rusage);
  8.      pid_t     wait4(pid_t  wpid, int *status, int options, struct rusage *rusage);

复制代码


在调用wait的时候,options参数是一个位值,或者是以下的一个:

WNOHANG -在调用的时候不阻塞程序,他会导致wait马上反馈即使没有子进程被终止。

WUNTRACED -当你想要等待停止并且不可跟踪的子进程(可能由SIGTTIN, SIGTTOU, SIGTSTP或SIGSTOP信号导致)的状态时设置该值。

WLINUXCLONE -如果你想等待通过linux_clone产生的内核线程。

当使用wait4和waitpid调用时,要注意wpid参数是要等待的PID值。如果将其设置成-1将会导致该调用等待所有可能的子进程的终止。在调用时使用rusage结构,如果结构不是指向NULL将返回终止进程的资源使用统计。当然如果进程已经停止,那么这些使用信息就没有什么用处。

wait函数调用提供给父进程一个方法去获取它子进程退出的信息。一当调用,调用它的进程将被阻塞知道一个子进程终止或接收到一个信号。这可以通过设置WNOHANG 值来避免。当调用成功时,status参数将包含结束进程的信息。如果调用进程对退出状态没有兴趣,可以将status设置成NULL。关于使用WNOHANG 的更多细节将在信号章节描述。

如果对退出状态信息有兴趣,它们的宏定义在/usr/include/sys/wait.h中。最好使用它们来获得更好的跨平台兼容性。下面列出它们3个和使用说明:

WIFEXITED(status) -如果返回true(也就是返回非0值)那么进程是通过调用exit()或_exit()正常终止的。

WIFSIGNALED(status) -如果返回true那么进程是由信号终止的。

WIFSTOPPED(status) -如果返回true那么进程已经停止并且可以重新开始。这个宏应该和WUNTRACED一起使用,或当子进程被跟踪的时候(就像使用ptrace)

如果有必要,以下的宏可以进一步提取保存的状态中信息

WEXITSTATUS(status) - 这只能在WIFEXITED(status) 宏评估为true时使用,他会评估exit()和_exit()传递参数的低8位。

WTERMSIG(status) -这只能在WIFSIGNALED(status)评估为true时使用,他会得到导致进程终止的信号的值。

WCOREDUMP(status) -这只能在WIFSIGNALED(status)评估为true时使用,如果终止的进程在收到信号的点创建了core dump文件那么该宏会返回true。

WSTOPSIG(status) -这只能在WIFSTOPPED(status) 评估为true时使用。该红会得到导致进程停止的信号。

如果父进程没有收集子进程的退出状态,或父进程在子进程结束前就已结束,init将会接管子进程并收集它们的退出状态。

vfork 和 rfork

vfork函数和fork相似,是在2.9BSD被引入的。它们两者的区别是,vfork会停止父进程的运行并且使用父进程的运行线程。这是为了调用execv函数设计的,这样可以避免低效的复制父进程的地址空间。实际上vfork是不被建议使用的,因为它不是跨平台的。例如Irix的5.x就没有vfork.下面是vfork的调用例子:


  1. #include <sys/types.h>
  2. #include <unistd.h>
  3.   
  4.     int  vfork(void);
复制代码


rfork函数也和fork与vfork相似.他是在Plan9引入的。它的主要目的是增加更成熟的方法来控制进程的创建和创建内核线程。FreeBSD/OpenBSD支持伪线程和Linux clone调用。换句话说,rfork允许比fork更快更小的进程创建。这个调用可以设置子进程可以和它们共享的资源。下面是一个rfork调用例程:


  1.   #include <unistd.h>

  2.     int    rfork(int flags);
复制代码


rfork可以选择的资源如下:

RFPROC -设置该值表示你想创建一个新的进程;而其他的标志将只影响当前进程。该值是默认值。

RFNOWAIT -设置该值表示你希望创建一个和父进程分离的子进程。一当该子进程被退出,他不会留下状态等待父进程收集。

RFFDG -设置该值表示你希望复制父进程的文件描述符表。否则父子进程将会共享一个文件描述符表。

RFCFDG  -该标志和RFDG标志互斥。设置该值表示你希望子进程拥有一个干净的文件描述符表。

RFMEM -设置该值表示你想强制内核共享完整的地址空间。这是直接共享硬件页面表的典型做法。这不能直接在C中调用,因为子进程会返回和父进程相同堆栈。如果你想这么做,最好使用下面列出的rfork_thread函数:


  1. #include <unistd.h>

  2.    int     rfork_thread(int flags, void *stack, int (*func)(void*arg), void *arg);
复制代码


这将创建一个新的进程运行于指定的堆栈,并且调用参数指定的函数。和fork不同,成功的时候只会返回新创建的进程PID给父进程,因为子进程将直接运行指定的函数。如果调用失败将返回-1,并且设置errno的值。

RFSIGSHARE -这是一个FreeBSD特有的标志并且最近众所周知地被用于FreeBSD4.3的缓冲区溢出。该值将允许子进程和父进程共享信号。(通过共享sigacts结构)。

RFLINUXTHPN -这是另一个FreeBSD特有的值。这将导致内核在子进程退出时使用信号SIGUSR1代替SIGCHILD。我们会在下一章论述它,现在可以把它看作rfork模仿linux clone调用。

rfork的返回值和fork相似。子进程获得0值而父进程获得子进程的PID。一个微小的区别是-rfork会在需要时休眠直到必要的系统资源可用。而一个fork调用和使用RFFDG | RFPROC来调用rfork相似。但是他不设计向后兼容。如果rfork调用失败会返回-1并且设置errno。(更多的error 标号信息可以看man page和头部文件。)

显然,rfork调用提供了一个较小消耗的fork版本。缺点是有一些众所周知的安全漏洞。并且他即使在跨BSD系列平台上也不是很兼容,在不同的Unix版本中是独一无二的。比如当前的NetBSD-1.5就不支持rfork,而且不是所有FreeBSD可用的标志都可以用于OpenBSD。正因为如此,推荐的线程接口是使用pthreads。

3.6 运行二进制程序

一个进程如果仅仅是父进程的拷贝是没有很大用途的。因此,需要使用exec函数。这些是设计来使用新的进程镜像代替当前进程的镜像。好,举个例子,shell运行ls指令。首先shell会按照执行运行fork或vfork接着它会调用exec函数。一旦exec被成功调用一个新的进程将由ls代替运行,并且exec自己没有返回。如果像shell或perl这样的脚本是目标程序,这个过程就像二进制程序运行。那里有一个附加的程序调用解析器。那就是,头两个字符将是#! 例如,下面展示了一个带参数的解析器调用:

  1. #!  interpreter [arg]

复制代码

下面的指令将使用-w参数调用Perl解析器:


  1. #!/usr/bin/perl  -w
复制代码


下面列出了所有的exec调用。源代码 exec.c http://www.khmere.com/freebsd_book/src/03/exec.c.html
调用它们的基本结构是(目标程序),(参数),(必要的环境变量)。


  1.   #include <sys/types.h>
  2.   #include <unistd.h>
  3.    
  4.     extern char **environ;
  5.    
  6.     int     execl(const char *path, const char *arg0, ...  const char *argn,   NULL);
  7.     int     execle(const char *path, const char *arg0, ... const char  *argn, NULL, char *const envp[]);
  8.     int     execlp(const char *file, const char *arg0, ... const char *argn , NULL );
  9.     int     exect(const char *path, char *const argv[], char *const envp[]);
  10.     int     execv(const char *path, char *const argv[]);
  11.     int     execvp(const char *file, char *const argv[]);
复制代码


注意;在使用带有参数arg的exec时,arg参数必须是以NULL截止的字符串,就如在arg0,arg1,arg2 ..argn中必须以NULL或0结尾。如果没有确定结束符调用会失败。这字符串将成为目标程序运行的参数。

调用exec使用*argv[]参数时其必须是一组包含结束符的字符串的数组,它们将成为目标程序运行的参数。

另一个区分二进制或脚本运行exec的是目标程序是如何指定的。如果目标没有带路径,函数execlp和execvp会搜寻你的环境目录来查找目标程序。而其他函数调用将要求绝对路径。

简索:

数据参数和序列参数

array: exect, execv, execvp

sequence: execl, execle, eseclp

路径搜索和直接文件

path: execl, execle, exect, execv

file: execlp, exevp

指定环境和继承环境

specify: execle, exect

inherit: execl, execlp, execv, execvp

system


  1.    
  2.    #include <stdlib.h>

  3.      int     system(const char *string);
复制代码


另外一个关键的运行函数调用是system调用。这个函数十分直观。提供的参数将直接传送给shell。如果指定的参数
为NULL,如果shell可用函数会返回1否则返回0。一旦调用,进城会等待shell结束并返回shell的结束状态。如果返回-1表示fork或waitpid失败,127表示shell运行失败。

一旦调用,一些信号例如SIGINT和SIGQUIT将被忽略,并且会阻塞SIGCHILD。同样如果子进程写输出到标准错误输出可能会使调用进程崩溃。

显然,BSD结构提供了简单同时丰富的进程创建接口。这种成熟的设计可以和任何一个现在操作系统媲美。下一章将包括信号和进程管理,包含资源使用,线程和进程限制。

[ 本帖最后由 孙轩 于 2006-2-16 16:02 编辑 ]

论坛徽章:
0
66 [报告]
发表于 2006-02-15 13:14 |只看该作者
原帖由 孙轩 于 2006-2-15 13:00 发表
第三章
       FreeBSD System Programming

该章节的例程:http://www.khmere.com/freebsd_book/src/03/index.html

3.1 进程和内核服务
process:一系列用于实现目标的行为和操作;特别指:制造 ...


偶先提个建议,能不能考虑把原文中示例的代码和shell命令等用“插入code”功能框起来,
一是可以保持原有的缩进,二也方便大家阅读时和正文区分,不知意下如何?

论坛徽章:
0
67 [报告]
发表于 2006-02-15 14:58 |只看该作者
支持一下,受益匪浅

论坛徽章:
0
68 [报告]
发表于 2006-02-15 15:13 |只看该作者
原帖由 孙轩 于 2006-2-15 13:00 发表
第三章
       FreeBSD System Programming

该章节的例程:
http://www.khmere.com/freebsd_book/src/03/index.html

3.1 进程和内核服务
process:一系列用于实现目标的行为和操作;特别指:制 ...


已经把你完成的部分加到发布贴里面去了!

论坛徽章:
0
69 [报告]
发表于 2006-02-15 15:30 |只看该作者
第一章已翻译完毕。

第六章还没人认领,由我来继续弄吧!

[ 本帖最后由 雨丝风片 于 2006-2-16 08:11 编辑 ]

论坛徽章:
0
70 [报告]
发表于 2006-02-15 21:35 |只看该作者
第二章  自举BSD

翻译(meilincore@chinaunix)

2.1 自举BSD
自举:在很少或者没有协助的情况下主动,努力的提升和发展(把她自己自举到顶点)
Bootstrap : to promote or develop by initiative and effort with little or no assistance <bootstrapped herself to the top> www.m-w.com

自举计算机是指加载操作系统的过程,该过程为:初始化硬件,读取一小部分代码到内存并执行.这一点代码接着加载一个大的操作系统.一旦操作系统被加载,它就需要创建自己的整个环境.这个过程,称做自举计算机,是一个复杂的,高度平台相关的过程.

本章我们将从细节上探索FreeBSD在i386平台上的自举过程.相关的概念和过程与NetBSD和OpenBSD在i386平台上的自举程序相类似.注意一些汇编代码对于从实际上完成i386基本系统的启动任务是必须的.然而,我们不会从细节上回顾汇编代码而是主要集中在高层概念上,因此就算你不是专家讨论也是有意义的.

注意:虽然本章中讨论的一些概念,特别是"实模式"和"保护模式",并不存在与现代象PPC或Alpha这些硬件构架当中.但是i386 BSD基本系统是目前为止最为广泛的系统并会继续如此(一个值得注意的例外是Mac OS X),而且适应很多情况.如果你对启动系统细节有兴趣,你可能需要自定义内核,自定义文件系统,设备驱动.同时.i386构架还广泛应用与嵌入式系统.给定一个安装平台(Given the install base),i386平台及其成果将继续在未来一些年里适用.甚至新的64位CPU到目前我们所知道的为止还是一样的启动过程.

2.2 FreeBSD的自举过程

FreeBSD使用一种三段启动过程,当你打开计算机电源或者重新启动.一旦BIOS完成系统检测.它将从DISK0加载第一个轨道到内存中.(每一个过程使用512字节的程序.刚好占用硬盘的一个块)第一轨道就是我们熟知的主引导记录(MBR),也就是boot0,第一个被计算机加载执行的程序.第二个程序,boot1,又是固定大小为512字节并知道如何读取slice信息和加载boot2.一旦boot2被加载它就有能力直接启动系统或者加载加载器(loader program),加载器固定大小为512字节.相当精致并被设计来允许对系统精确启动作更多的控制.

boot0

从BIOS加载的第一个程序,boot0,是一个固定大小为512字节的小程序,位于主引导记录(MBR).你可以在/usr/src/sys/boot/i386/boot找到该程序的源代码.当然现代计算机的BIOS可以设置成从各种不同的驱动器包括光驱,软驱和IDE硬盘启动.对于本章来说,我们将假设计算机是从第一个硬盘启动的.也就是磁盘驱动器0,C:,或者,对BIOS而言,0X80.

从第一个磁盘的第一个扇区,512字节被读到内存位置0X7C00.然后,BIOS会检验内存位置0X7DFE的数字0XAA55(启动块代码的最后两个字节).这个位置索引数字0XAA55对i386是如此重要而被赋予了一个适当的名字----魔数.这意味着,只有在这个数字存在于内存位置0X7DFE的情况下BIOS才会把控制权转移给boot0被安置的内存位置0X7C00.

这提出了在Intel i386系统上编写启动代码的一个要点:记着你代码中的第一个内存位置处(0X0)必须是一条指令.并且,当BIOS转移控制权给内存位置0X7C00的时候该位置必须包含一条指令.这可以是一个简单的跳转到其他位置或者启动程序主过程的入口.另外当启动代码被执行时你对CPU到底在干什么是完全没有控制的.因为这时候寄存器的状态也是未知的,你别指望已经设置好了适当的段和栈寄存器.这个小工作必须由启动代码来完成.因为目前操作系统还没有加载起来;所有的I/O必须通过BIOS过程来完成(Intel CPU 文档有一个完整列表)

在boot0被加载并获得控制权以后,它将设置自己的寄存器和栈信息.接着,boot0重定位自己到一个低的内存位置并跳转到它主过程的新的偏移地址.这时候boot0必须还能够定位和启动其他磁盘和分区,在它的最后,boot0代码有一个小的常见的可启动类型列表,它们都必须在其最后两个字节包含魔数才可启动.

最终,当boot0完成搜索可启动磁盘和分区以后,将提示用户作一个选择.如果在一个短的时间段里面没有任何选择或者某个键被按下,boot0将加载下一个启动块到内存并转移控制权给它.还是那样,这可以是任何操作系统的启动代码----你可以设置它加载Linux的自举代码或者甚至DOS.对BSD而言,下阶段的启动程序是boot1.

boot1

与boot0相似,boot1是一个非常简单且总共才512字节的程序;它也必须在最后两个字节包含魔数.其目的是定位和读取FreeBSD磁盘分区,接着定位和加载boot2.

虽然在大多数情况下,boot1是被boot0加载的,但是这个顺序不必是唯一存在的选项.由于FreeBSD的灵活性你可以使用被称为专用磁盘的选项(说的就是那个更为声名狼籍的危险的专用磁盘)一个专用磁盘就是整个磁盘,或者BIOS的每一个扇区都属于FreeBSD.通常,你会在PC机磁盘上发现一个fdisk表,或者一个slice表,用来允许多个操作系统从单个PC机磁盘上启动.你可以选择使用一个专用磁盘并且直接从boot1启动;boot0完全没有必要安装在磁盘上.不管你采用那种实现,boot1都是非常重要的启动块并需要被加载

Boot1被加载到内存位置0X7C00并操作在实模式;环境没有设置,寄存器也在未知状态.boot1必须设置好栈,定义所有的寄存器,并使用BIOS接口来做所有的I/O.一旦boot1被加载到内存并且控制权已经转移过来.它必须在自己的第一个内存位置(0X0)包含一条指令.所有这些都成功以后,boot1将读取系统磁盘搜索boot2.

一旦boot2被加载.程序必须设置好boot2的环境;boot2是一个BTX客户端(boot Extender 启动扩展器)并且比前面的boot0和boot1稍微复杂一些.boot1需要加载boot2到内存位置0X9000并且它的入口是0X9010.然后,甚至在boot1加载并转移控制权给boot2以后,还有一个被boot2使用的过程存在于boot1.如果你阅读boot1的源代码你会注意到一个函数呼叫xread.这个函数用来通过BIOS从磁盘读取数据.因此,boot1在内存中的位置是非常重要的并且boot2必须知道它的位置才能正常工作.

boot2

目前为止我们已经加载了两个启动块和一个大的程序到内存中.转移了两次控制权且每次都重新设置了一个小的环境(栈,段寄存器等等...),并通过BIOS执行了一些有限的I/O.我们依然没有触及到加载操作系统的点子上来.如果你看过FreeBSD启动过程中的屏幕,到目前为止你可能看见F1和可爱的ASCII螺旋线.你可能印象当中不会觉得这都是,但他的却是原原本本,精确原生的汇编代码让这个启动过程看起来如此幽雅而轻巧

现在到最后一个自举过程了,boot2.最后阶段很简单并且可以发生两件事中的一件:boot2加载加载器(我们将在下一节讨论这个)或者,boot2加载内核并直接启动而完全不使用加载器.如果你曾经打断boot2的加载过程,你可能已经看过这个boot2输出到屏幕的东西:

  1. >>FreeBSD/i386 BOOT
  2. Defualt: 0:ad(0,a)/boot/loader
  3. boot:
复制代码


如果你按下回车,boot2会简单的加载默认加载器,象列出的那样.当然,如果你只是键入"boot kernel"接着它就会加载内核(/kernel)并启动.你可以,当然,改变这些默认值.如果你想找到更多信息请阅读boot( 8 )的文档

早先我们提到过boot2是一个BTX客户端(Boot Extender 启动扩展器).这留下什么呢?BTX提供者是一个基础的虚拟86寻址环境.该来讨论一下Intel硬件内存寻址的历史了.

目前为止我们都避免提及内存寻址方式.这可能会比较混淆因为Intel CPU要忍受历史问题,并且启动代码设计通常是留给那些非写不可的开发人员.除非你要移植一个系统到新的构架因而你的代码必须完全是平台依赖的,通常,一个程序员永远也不会被派去写一个启动加载器.然而启动过程对于需要编写设备驱动代码或者内核相关编程的开发人员来说是非常重要的.这就是一些开发人员会在这里碰到BTX加载器.

大约从8088到80186,Intel处理器只有一种方式进行内存寻址,叫做实模式.这些早期的CPU有巨大的16位寄存器和20位内存地址.然后问题来了,你如何在16位的寄存器中构成一个20位的地址呢?答案就是,用两个16位寄存器,一个提供基地址而另一个提供这个基地址的偏移量.基地址寄存器被左移4位这样当两个合并的时候一个令人惊异的20位地址就被算出来了.用所有这些灵巧的段寄存器和位移,早期的Intel处理器可以寻址总共1M空间.今天这甚至还不够容纳一个Word文档,作为一个傲慢的例子.

一旦80386席卷而来,寻址1M就不够了;用户需要更多的内存并且程序开始使用更多的内存.一种新的寻址模式叫做保护模式被发明了出来.新的保护模式允许寻址高达4G内存.

新方式的另一个好处就是对于汇编程序员来说更容易实现.主要的区别就是你的扩展寄存器(它们还是那些16为寄存器不过386现在是32位了)可以包含一个完整的32位地址.甚至你以前的段寄存器现在都被保护了.程序不能写入或者读取他们.这些段寄存器现在被用来定位你的内存中的真实地址,这个过程包括权限验校位(读写等等...)并引入了MMU(memory management unit内存管理单元)

现在回到BTX客户端的问题.我们使用BTX程序有什么好处呢?很简单:灵活性.BTX提供了足够的服务因此一个小的,拥有漂亮接口的程序可以被写出并能在加载内核的过程中非常灵活.在FreeBSD系统中这就是加载器.从下一节有将看到加载器真的是多么的漂亮和灵活.因此本节余下的部分我们将涵盖基本的BTX服务.

BTX服务可以被归类到两个基本组.第一组是由直接函数呼叫(类似于系统呼叫)提供的系统服务.另外一组是不由客户端直接呼叫的环境服务.这些服务类似于一个操作系统,然而BTX程序是象单任务环境那样操作的.

直接呼叫提供的BTX服务由两个系统呼叫组成.exit 和 exec.当exit被呼叫的时候BTX加载器结束并且系统重启.最后的一个系统呼叫exec将转移控制权给提供的地址.在个控制权移交是在超级用户模式下完成,并且新的程序可以离开被保护的CPU模式

BTX加载器提供的环境服务是非常基础的.BTX加载器处理所有的中断.这些中断被送到合适的BIOS处理器.BTX也模拟BIOS扩展内存传输呼叫.并且最后还模拟一些汇编指令,它们是pushf,popf,cli,sti.iret和hlt.

最后注意:所有编写来运行在BTX环境中的程序都必须被编译并连接到btxld.更多信息请阅读BTX加载器联机手册

2.3加载器

最后的启动阶段是由加载器组成的.加载器程序是一个标准命令(引用为"内建命令"和一个Forth解释器的联合(基于ficl).加载器将允许用户同系统启动进行交互或者允许系统恢复和维护.通过加载器用户可以选择载入或者卸载内核模块.用户也可以设置或者取消设置相关变量,比如rootdevice(根设备)和module_path(模块路径).这些也可以在/boot/loader.conf里面改变.加载器读取的默认文件在/boot/defaults/loader.conf.默认文件也包含了许多可用的选项.这写文件都被构造得和/etc/rc.conf类似.

加载器对于内核和设备驱动除错是非常有用的.通过加载器你可以通知内核在除错器起用的状态下启动.或者,你可以为设备驱动测试加载相关的内核模块.如果你准备编写任何内核模块或者设备驱动的话你最好是阅读加载器的所有文档.首先从联机手册开始,然后复习/boot/loader.conf中的所有选项.一路上加载器在你需要扩展BSD或者诊断内核崩溃的时候将非常有用.

2.4开始内核服务
我们终于到了内核已经被载入内存,CPU的控制权的也被传给它的阶段了.一旦内核被加载,它需要从初始化开始运行并准备系统进入多任务模式.该初始化包括三个主要组成部分.前两个是机器相关的,由c和汇编混合编写成.前两个阶段准备系统和初始化CPU内存管理单元(MMU,如果它存在)并处理硬件初始化.最后一个阶段继续建立基本内核环境并让系统准备好运行进程1(/sbin/init).前两个阶段是高度平台依赖的.因为每一种构架都有相关需要.我们将提供一个对这两个阶段的高层次浏览并在本书稍后当我们涵盖到设备驱动的时候从细节上来了解这些概念.

阶段 1 & 2 内核装配和c起始过程

虽然内核一旦被加载就不再对系统做任何假设,加载器还是会传送一些信息给内核,比如启动设备和启动标记.另外,内核必须创建自己的环境并让系统准备好运行进程0(下面解释).这些任务包括CPU检测,运行时刻任务建立,和内存数量检测.

CPU识别是非常重要的一步,因为每个平台可以有多种不同的CPU(i386是其中之一),不是所有的CPU都会支持同样的特性.比如,拿MMX指令集来说,虽然它对于内核来说不是一个重要特性,但是对于浮点单元就是,因此如果这个特性不被该CPU所支持的话就必须用软件来模拟.这对于所有其他不被支持的特性和已知错误或者CPU的固有特性来说都是成立的

第二个阶段会初始化系统硬件和内存.这包括探测硬件,初始化I/O设备,为内核结构分配内存,建立系统消息缓存.该阶段你会看到启动屏幕中闪过很多硬件列表.按照BSD传统,这个阶段是通过呼叫cpu_startup()函数来初始化.

第3阶段 和 进程 0

一旦cpu_startup()函数返回,内核就需要建立进程0.进程0通常称为交换器,如果你运行ps命令会看到它在活动.然而从感官上来说并没有这样一个名为swapper的二进制文件附属于该进程.这对于其他四个在现代FreeBSD系统上找到进程都是成立的:pagedaemon,vmdaemon,bufdaemon和syncer.为避免复杂化,我们只说这些进程是VM子系统的一部分;我们在进程一章中再讨论它们.着重理解他们是在启动过程中由内核创建而不是文件系统中的二进制程序,并且是相当平台依赖性的.他们是用c语言写成并在起始平台环境初始化完毕以后开始的

init和系统shell脚本

在所有的汇编和平台依赖性代码执行完毕以后,内核终于执行第一个真正的程序:/sbin/init.该程序是相当简短的(在FreeBSD上,总共约1,700行).象我们在BSD进程一章所讨论的那样,这就是那个所有进程都遗传自的进程.这样设计的实力在于,因为/sbin/init只是文件系统中的一个二进制程序,你可以自己写一个自定义版的.对于/sbin/init在启动时的主要目标就是运行系统启动脚本并为系统进入多用户模式做准备.留意信号:/sbin/init应当能优雅的处理信号,否则你的系统可能结束在/sbin/init程序的一个奇怪的状态并且在系统启动时崩溃掉.同时运行中/sbin/init可以接受信号来执行某些任务.比如,如果你想让系统不为某个相关的终端生产进程.象列在/etc/ttys里面的.你可以标记想要的终端为关闭状态并执行下面的命令使init读取/etc/ttys并只为列出的标记状态为开的终端生产进程.

  1. bash$kill -HUP 1
复制代码


注意除非你很小心,否则可能会以一个你不能登录的系统告终(到关于信号的一章里面查看详细信息.)

init程序会在启动过程中设置系统以进入多用户模式.这是很灵巧的.引入了象开始每一个守护进程并设置网络信息这样枯燥的任务.在UNIX系统中.有一些这样的途径,主要引入shell脚本.在某些版本的Linux和System V系统中,对应与某个运行级别的可启动脚本位于/etc/rc<n>.d.然而BSD使用了简单得多的办法.这就是在/etc/里面找到的的rc脚本

这些脚本通常是不能被编辑的,而是,在/etc/fc.conf里面设置变量(来改变行为).象PicoBSD的自定义安装你可能需要建立自己的脚本icoBSD是高度磁盘空间敏感的,并有相关的文件系统需要.一个重要的提示,/usr/local/etc/rc.d/文件夹是特别的.该文件夹的特别意义在于其中找到的每个.sh可执行文件都会在/etc/rc脚本之后被执行.为了方便系统管理这个文件夹替代了老的/etc/rc.local文件(/etc/rc.local是以前在系统启动尾声的时候启动自定义脚本或者程序的办法)

BSD rc脚本包括值得注意的条目列表如下:

  1. /etc/rc - 主脚本并且第一个被呼叫,挂接文件系统并运行所有需要的rc脚本
  2. /etc/rc.atm - 用来配置ATM网络.
  3. /etc/rc.devfs - 设置/dev/权限和连接
  4. /etc/rc.diskless1 - 第一个diskless客户端脚本
  5. /etc/rc.diskless2 - 第二个diskless客户端脚本
  6. /etc/rc.firewall - 用于防火墙规则
  7. /etc/rc.firewall6 - 用于IPV6防火墙规则
  8. /etc/rc.i386 - intel系统相关需要
  9. /etc/rc.isdn - isdn网络设置
  10. /etc/rc.network - IPV4网络设置
  11. /etc/rc.network6 - IPV6网络设置
  12. /etc/rc.pccard - 膝上电脑pc card控制器
  13. /etc/rc.serial - 设置串口设备
  14. /etc/rc.sysctl - 在系统启动时设置sysctl选项
  15. /usr/local/etc/rc.d/ - 存有自定义启动脚本的普通文件夹
复制代码


这里有一个例子:

如果你想让rsync作为守护进程这个脚本会起作用:

  1. #!/bin/sh

  2. if [ -x /usr/local/bin/rsync ]; then
  3.         /usr/local/bin/rsync --deamon
  4. fi
复制代码

这将首先检测rsync是否存在然后以守护模式运行它



//初步完成,有一些地方拿不准,特别是开头bootstrap这个意思的翻译.请多指教
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP