Kernel chapter 4-7This is a featured page

第四章 系统调用

为了保证不同的进程不会互相影响,进程在运行的时候就没有优先级,不可以进入其它的进程或者直接进入硬件。唯一的办法让进程可以进入硬件或者其它的进程就是告诉内核执行进程。比如,读取一个在硬盘里的文件,一个进程不可以直接读取硬盘。所以它就要告诉内核它要打开一个文件,然内核打开它并且帮助它读取。有几个原因:
●不需要了解硬件或者文件系统。一个进程就可以简单的要求内核间接的进入。
●危险的进程不能影响到其它的进程或者系统,因为内核检查一个进程是否有权限行动。比如,当一个用户的进程要求内核终止另一个用户的,内核就要首先检查它的权限并且拒绝这个要求。
●内核可以获取有效的管理资源。内核可以操作调度表,更好地做运算,管理进程的需求。比如,当太多的进程是着读取硬盘上不同的文件时,内核就会让所有的阅读要求最小化的延迟。所以,就加快了执行的速度。
我们看过一些让内核做用户操作的要求。在Linux中,用户进程和内核在不同的权限下运行。用户进程在无优先权的等级,所以不可以直接访问硬件或者进入内存或者其它的进程。内核在有优先权的等级,所以可以直接进入硬件和所有进程的内存。所以,一个进程就永远要被迫要求内核在它需要进入其它进程的时候去执行命令。所有这些需要都是通过系统调度。系统调度允许进程要求内核的服务,也是唯一的办法。
在图4.1中我们可以看到总体的概况。用户进程使用系统调度,要求内核提供服务。机械部分会在以后的章节中提到。


第一节 生成系统调度
系统调度用软件中断生成。中断导致系统调度操作执行,所以系统调度管理器调用一个系统调度通过系统调图平台。
我们知道系统调度是唯一的用户访问进程资源的方法。当一个用户的空间应用程序发出了系统调度时,参数就通过寄存器和应用程序执行init0x80指令。这就导致了一个引入系统模式和处理器到系统调度项。
1. 保存寄存器
2. 设置%ds,%es到KERNEL_DS,所以所有的数据(和多余的部分)就在内核地址空间中。
3. 如果%eax的值大于NR_syscalls(当前是256),就会因为ENOSYS错误失败
4. 如果任务被进程跟踪(Ptraced=Process traced)(tsk-¿ptrace&PF_TRACESYS),执行特殊的进程。这个支持程序和Strace很相似(analogue of SVR4 truss(1))或者调试器。
5. 执行系统调用要根据在系统调用平台的syscall_number进入。
6. 恢复寄存器并返回。
系统调用控制在启动的时候安装(在arch/i386/kernel/traps.c trap_init()).当系统启动的时候,功能trap_init()被调用启动中断解说符号表格Interrupt Descriptor Table(IDT),所以中断0x80点到system_call地址的项从/i386/kernel/entry.S.以下的就是从trap_init功能中提出的traps.c:
Set_system_gate(SYSCALL_VECTOR,&system_call);
这个编码设置系统调用的中断。SYSCALL_CECTOR定义为0x80,&system_call是进入system_call在arch/i386/kernel/entry.S.的地址。这个结果使CPU可以在system_call中有interrupt0x80的时候执行这个编码。
1. 系统调用表格
只有一个系统调用的软件中断(0x80),但是有很多的系统服务这个进程的需要。那么内核又怎么知道并且正确的满足它们的需要呢?
有一个系统调用表格在所有的系统调用功能项(如系统调用功能的地址)。当有系统调用的时候,内核就会从系统调用表格中查找到一个系统调用功能的项,根据系统调用给的数字。比如,当进程想要打开一个文件的时候,它就首先要告诉内核它想要调用系统调用,用调用表格的调用5(sys_call_table).在查找之后,内核就会得到系统调用sys_open的进入并且调用它。
系统调用表格(sys_call_table)的项在(/usr/src/linux/arch/i386/kernel/entry.
S)保存在所有的系统调用当中。以下的部分就是系统调用表格:
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /*0 – old”septup()”*/
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
.long SYMBOL_NAME(sys_open)
.long SYMBOL_NAME(sys_close)
.long SYMBOL_NAME(sys_waitpid)

系统调用表格是一个储存在地址调用功能里面的地址数组。ENTRY宏表示sys_call_table的。在这个表格当中,我们有sys_ni_syscall在第一个项0,就是表示系统调用数字0何sys_ni_syscall匹配。下一个,我们就有了system call number1匹配sys_exit和系统调用数字2匹配sys_fork等等。.long的意思就是每一个项是32位的整数,SYMBOL_NAME宏定义了功能的代表名称,SYMBOL_NAME宏定义了代表的地址。SYMBOL_NAME(sys_fork)的意思就是功能sys_fork的内核地址。当一个软件中断0x80,系统调用处理器就可以从表格中查找合适的系统调用。让我们看一看系统调用的表格的结尾:

.long SYMBOL_NAME(sys_ni_syscall) /*reserved for fremovexattr*/
.long SYMBOL_NAME(sys_tkill)
.long SYMBOL_NAME(sys_ni_syscall) /*reserved for sendfile64 */
.long SYMBOL_NAME(sys_ni_syscall) /*240 reserved for futex */
.long SYMBOL_NAME(sys_ni_syscall) /*reserved for sched_setaffinity */
.long SYMBOL_NAME(sys_ni_syscall) /* reserved for sched_getaffinity */
.rept NR_syscalls_(.-sys_call_table)/4
.long SYMBOL_NAME(sys_ni_syscall)
.endr
我们可以看到.rept这个关键词,它的意思就是重复。在这种情况下,重复的次数就是
NR_SYMBOL_NAME(sys_ni_syscall)/4
这句话重复就是
.long SYMBOL_NAME(sys_ni_syscall)
重复的样式基本上是用sys_ni_syscall填满剩余的系统调度表格,到NR_syscalls的数字。解释一下,那个(.-sys_call_table)的意思就是当前的地址减去系统调用表格的地址。这个结构占用所有的之前的系统调用项的空间。然后我们把这个数字用4除开,得到的是刚才的项数字。最后,重复的数字就是NR_syscalls减去之前进入的数字,就是说用sys_ni_syscall填满余下的项.
你可能注意到了,系统调用系统数字242或者更少已经决定了sys_call_table,但是不是所有的数字都是有效的。很多用sys_ni_syscall填入,证明系统调用没有被操作。Sys_ni_syscall功能当时返回一个错误,系统调用没有存在。
2. 案例
以下的案例帮助我们了解系统调度的任务原则。在这个案例中,我们使用getuid系统调度。让我们看看以下的C程序:


#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>

Int main(){
Int p;
P = getuid();
Printf( “uid=%d\n”,p)
Return 0 ;
}
这个简单的程序打印了进程所有者的使用身份证明。这个程序看起来好像与系统调度没有任何关系,但是它确实完成了系统调度。原因就是一般的程序员不会直接的调用系统调度,而是使用库library功能来完成这个任务。在这个案例当中,getuid就是得到用户身份的功能。如果我们仔细看到getuid的内部处理,它就实现了访问内和要求系统调度调出用户身份。所以,尽管程序员并没有直接使用系统调度,进程还是必须访问内核作出系统调度。
得到用户身份,程序就要使用系统调度sys_getuid要求从内核得到用户身份。Getuid系统调度负责24号系统调度,我们就使用了getuid功能,我们也可以直接的作出系统调度24号,得到用户身份:
#include<stdio.h>
#include<unistd.h>
#include<sys/syscall.h>

Int main(){
Int p ;
P = syscall( 24 );
Printf( “uid=%d\n”,p );
Return 0 ;
}
以上的案例就调用了系统调度24,输出的结果应该也是一样的。
理解系统调度在内核空间,执行系统模式非常重要。图4.2就显示了在执行系统调度getuid时候更改模式。
我们看到了发出系统调度要要求生成软件中断0x80.中断引起了系统调度控制执行,同时,转换系统模式。所以,我们可以直接作出系统调度,生成案例中080软件中断:
#include<stdio.h>
Int main(){
Int p;
__asm__volatile (
“movl $24,%%eax \n” \
“int $0x80 \n” \
“movl %%eax,%0 \n” \
:”=r” (p) \
:
);
Printf(“uid=%d\n”,p );
Return 0 ;
}
在以上的案例中我们生成了软件中断0x80直接做出了系统调度24。__asm__是一个特殊的gcc扩展,包括C程序中的汇编码。在这个程序中,有三行汇编码。第一行的移动指令设置了CPU的EAX寄存器值为24,下一行就是生成0x80中断。当中断生成以后,系统调度控制者system_call就被执行,从系统调度表格中找出4号系统调度的项并且执行。当系统调度结束的时候,系统调度就返回原值,并且把结果储存到EAX寄存器当中。我们可以最后使用移动指令,保存EAX寄存器当中的内容,寄存多种的P,并且使用Printf显示其内容。因为我们以getuid作了系统调度,回复得值就应该是进程的用户身份。结果应该和前两个一样。

第二节 实现一个新的系统调度
在操作系统的工作领域可以有效执行系统调度。在这一节中我们要看一看在Linux环境实现系统调度。
如何在实际工作当中实现系统调度?系统调度在基于用户态转到系统模式的转变滞后。在Linux环境,只能用中断完成。中断0x80就是为了系统调度的。
一般来说,用户使用库功能作出系统调度(fork())执行某些任务。Library功能(就像从_syscall宏生成的<asm-i386/unistd.h规律一样),写入一个参数,之后系统调度来决定传输寄存器其然后启动0x80中断。当相应的中断服务历程返回的时候,回复的值从适当的传输器存器和库功能当中产生。中断历程在输入地址_system_call()开始,在arch/i386/kernel/entry.S文档中。
实现一个新的系统调用
在表格当中添加一个项:首先系统调度必须要起一个名字并且有一个特殊号码。因为系统要知道每一个系统调度,文档include/asm/unistd.h包括了项的表格。
#define __NR_<system call name> <system call number>

我们要在下一个有效编码位置添加我们的系统调度:
#define __NR_pedagogictime 259
Arch/i386/kernel/entry.S文档有初始表格:
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /* 0 –old’’setup()’’system call*/
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_ni_syscall) /*255 sys_epoll_ctl */
.long SYMBOL_NAME(sys_ni_syscall) /*sys_epoll_wait*/
.long SYMBOL_NAME(sys_ni_syscall) /*sys_remap_file_pages*/
.long SYMBOL_NAME(sys_set_tid_address)
这里我们在259位置添加一个系统调度控制的功能性指针,这里,结果就会是:
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /*0 – old””setup()’’system call*/
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)

.long SYMBOL_NAME(sys_ni_syscall) /*255 sys_epoll_ctl */
.long SYMBOL_NAME(sys_ni_syscall) /*sys_epoll_wait */
.long SYMBOL_NAME(sys_ni_syscall) /*sys_remap_file_pages */
.long SYMBOL_NAME(sys_set_tid_address)
.long SYMBOL_NAME(sys_pedagogictime)
生成一个系统调度stub:然后你就可以生成一个系统调度stub。通过宏调度从用户空间程序中生成。宏对于stub是有效的,有六个参数。你可以加以下的编码在你的用户程序中:
#include<sys/time.h>
#include<linux/unistd.h>
#include<sys/syscall.h>
#include<unistd.h>

//Generate system call stub
_syscall1 (int, pedagogictime, int flag);
所以你就可以简单的调用功能
Pedagogictime(printflag);
它会受到sys_call_bable[NR_pedagogictime]的限制,为内核功能指向你的项点。或者你可以这样调度:
Syscall(259, printflag)
如果你还没有生成这个stub。
写内核功能:在这里我们开始写内核功能。它和一般的C语言编写的管理者功能差不多。比如系统调用功能:
#include<linux/kernel.h>
#include<asm/uaccess.h>

Asmlinkage int sys_pedagogictime(int flag){
Printk(“This is an example”);
}
当内核功能被间接调用时,C编译器并不是每一个参数的种类都检查。清在使用参数之前检查一遍。
重新编译内核:最后,我们要重新编译内核。你可以把你的新的内核功能放在一个新的文件里面或者放在旧的文件里面。比如time.c.但是一个新的文件(asm1.c)添加之后,修改/usr/src/linux/kernel中的makefile。把asm1.o添加到obj-y=
记住要运行“make dep”
ThizLinux使用自己的内核,所以如果用户需要安装新的内核资源,需要把旧的内核删除,然后连接新的内核标题。
/usr/include/linux-> /usr/src/linux/include/linux
/usr/include/asm->/usr/src/linux/include/asm
否则,你的用户程序就不能修改标题了。



第三节 练习
实现一个简单的“hello world”系统调度。这个系统调度的名字要是helloworld功能名字应该是sys_helloworld.系统调度需要函数,并且要最后归零。请确定添加合适的项.S以及unistd.h
系统调度只能用printk显示信息“Hello World”,其它命令都不可以。
Printk(KERN_INFO”Hello World\n”);
KERN_INFO特殊化了这个信息。使用命令dmesg确定你的新的系统调用。
在这个练习中,你将学到:
如何编译内核
添加一个新的系统调度(entry.S, unistd.h)
Printk的用法
Dmesg察看内核信息的用法
了解内核信息阶层
提高它们对系统调度的理解程度

第五章 进程管理

第一节 介绍
进程负责操作系统内的任务。一个程序是机械编码指令的集合,数据存储在硬盘内可执行的文件映像中,一个进程可以看作是计算机程序的行动。
一个动态的实体,不停的在更改处理器执行的机械编码指令。也是一个程序的指令和数据,处理器也包括程序计算器和所有的CPU寄存器,还有处理数据包含如历程参数、回复地址以及其它存储项目的即时数据堆栈。当前的执行程序或者是进程包括所有的当前微处理器的活动。Linux是一个多任务处理的操作系统。进程内的任务被依据职责分开。如果其中一个进程崩溃,并不会影响系统内其它的进程。所有的独立进程都在自己的虚拟地址空间内运行,没有和其它的进程混在一起,除了通过安全,内核管理系统它们不可能和其它的进程交流。
在一个进程的运行时间内,它将会使用很多的系统资源。要使用系统内的CPU来运行它的指令,使用系统的物理内存来保存它的数据。打开并使用在文件系统内的文件并且直接或者间接的使用系统的物理设备。Linux不许跟踪进程和系统资源,才能公平的控制管理系统内其它的进程。避免其中一个进程独占了大多数的系统物理设备或者CPU资源。
系统内或者CPU内的大多数的珍贵资源一般只有一个。Linux是一个多任务操作系统,它的目标就是让进程最大/最优化的使用系统内的资源和CPU资源。如果其进程多于CPU的处理,其它的进程必须要等待CPU空闲的时候才可以被执行。多任务是一个简单的概念;一个进程要等待系统资源空闲才可以被执行以及占用系统资源。在单任务操作系统中,比如DOS,CPU就只能简单的傻傻的等待浪费时间。在多任务系统中很多的进程同时在内存中保存,不论什么时候一个进程都要等待操作系统结束一个任务以后再被执行。很多等待的进程。调度其要选择哪一个进程要下一个被运行,Linux使用调度器保持系统的公平。
Linux支持很多的可执行文件格式,ELF是一个,Java是另外一个,这些都被单独管理,共同使用系统的库Library.

第二节 Linux进程
所以Linux就可以管理系统内的进程,每一个进程都代表了task_struck数据结构(任务和进程是可交换的Linux使用的术语)任务矢量是系统内每一个任务task_struct数据结构的指针数组。
这就是说进程的最大化数量取决于系统的任务矢量;默认是512登陆。当进程生成的时候,一个新的task_struct就在系统的内存中了,而且添加到任务矢量当中。为了让它便于寻找,当前的,正在运行的就用当前指针指向。
就像一般的进程种类一样,Linux支持实时处理。这些进程又非常快速的外部事件反映(因为这个术语:实时)系统调度器对它们的处理就和一般的用户进程处理不同。尽管task_struct数据结构也很大并且复杂,但是它的领域被分割成了很多的功能区域。

State 一个进程执行要根据环境改变它的状态state
Struct task_struct{
Volatile long state;

Linux有以下的状态:

Running(运行态):一个进程要不就是正在运行(当前的系统进程)或者准备运行(准备被分配CPU资源运行)

Waiting(等待态): 进程在等待一个事件结束使用资源。Linux有两种不同的等待进程:interruptible,uniterruptible。可中断进程在进程内的等待是可以被信号中断的,然而不可中断进程在等待时候是在任何环境下都不可以被中断的。

Stopped(终止态):就是说进程被中止了,一般是得到了一个信号。一个进程在调试的时候是中止状态。

Zombie(僵死态):这也是一个被中断的进程,因为某些原因,一还是有一定的task_struck任务结构在任务矢量当中。这些就像是一个死的进程。

Scheduling Info:任务调度器需要一定的信息来公平的决定哪一个进程应该让系统优先处理。

Int prio
Unsigned int time_slice;

Identifiers: 每一个系统之内的进程都有一个进程识别器。进程识别器不是一个任务矢量,而是一个简单的数字。每一个进程都有用户组识别器,这些用作控制进程进入文件和系统设施。
Pid_t pid;
Pid_t pgrp;
Pid_t tty_old_pgrp;
Pid_tsession;
Pid_t tgid;

IPC:Linux支持分类Unix Inter-Process Communication——Unix进程间通信(IPC)信号、管道、信号量还有系统V IPC的共享内存,信号量和信号队列。Linux支持第七章描述的IPC机械。

Links:在Linux系统之内,没有进程是独立于其它进程的。每一个系统之内的进程,只有首进程有父进程。新的进程不是被创造,而是复制,或者甚至是从前一个进程克隆出来的。
Struct task_struct *parent; /*parent process*/
Struct list_head children; /*list of my children */
Struct list_head sibling; /*linkage in my parent’s children list */
Struct task_strcut *group_leader;
每一个task_struct代表一个进程有指针指向父进程和它的兄弟进程(这些进程由同一个父进程)和它们的子进程也有关。你可以在Linux系统使用pstree指令中看到这些进程之间的家族关系:
Init(1)-+-crond(98)
|-emacs(387)
|-gpm(146)
|-inetd(110)
|-kerneld(18)
|-kflushd(2)
|-klogd(87)
|-kswapd(3)
|-login(160)---bash(192)---emacs(225)
|-lpd(121)
|-mingetty(161)
|-mingetty(162)
|-mingetty(163)
|-mingetty(164)
|-login(403)---bash(404)---pstree(594)
|-sendmail(134)
|-syslogd(78)
‘-update(166)

并且,系统内所有的进程都有双连接表,root是init进程task_struct数据结构。这个表允许Linux内核查找系统内的每一个进程。它需要提供指令支持,比如ps或者kill.

Times and Timers:内核监控进程和创始时间就像CPU消耗它的生命一样。每一个时钟的运行,内核升级jiffies,当前的进程都花费系统和用户态。Linux也支持进程间隔频率,进程可以在系统时钟过期的时候使用系统调用来设置时间并且发送信号给它们自己。这些时钟可以是单点的或者是阶段性时钟。
Unsigned long it_real_value, it_prof_value, it_virt_value;
Unsigned long it_real_incr, it_prof_incr, it_virt_incr;
Struct timer_list real_timer;
Struct tms times;
Struct tms group_times;
Unsigned long start_time;

File system:进程可以任意打开并且关闭文件,进程task_struct包括指针指向描述每一个打开的文件,指针有两个VFS针点。每一个VFS点都特殊的介绍了文件系统文件目录,并且提供了统一界面给重点的文件系统。在第八章中描述了文件系统是如何被支持的。第一个是进程的root(也就是它的家目录)第二个就是它的当前pwd目录。Pwd是从Unix命令的pwd而来的,打印工作目录。这两个VFS点由它们的计数领域增量,先是一个或者更多的进程在涉及到它。这就是为什么你不可一删除一个有pwd目录的进程,或者是子目录当中有pwd的进程。
/*filesystem information */
Struct fs_struct *fs;
/*open file information */
Struct files_struct *files;

Virtual memory: 很多进程都有虚拟内存(不是危害内核的或病毒)Linux内核必须知道虚拟内存在系统物理内存的哪个位置。
Struct mm_struct *mm, *active_mm;

Processor Context: 一个进程可以被想象是一个整体的系统当前状态。不管什么时候一个进程在运行的时候都在使用进程的寄存器,堆栈等等。这个就是进程的环境,当一个进程被挂起的时候,所有的CPU特殊环境都要为进程保存在task_struct中。当一个进程被调度器重启的时候,环境也从这里面恢复。
/*CPU-specific state of this task */
Struct thread_struct thread;

1. 创建进程
在了解Linux内核是如何管理进程之前,必须要了解Linux进程是如何被创建的。在Linux之内的每一个进程,除了init进程,都是系统调度fork(或者for variant)创建的。

系统调度fork()
Fork系统调度用克隆创建新的进程。新的进程就是原进程的子进程,或者说,原来的进程是新进程的父进程,就像图5.1中一样。
以下是简单的C程序,展示了使用fork新建一个进程:

图5.1

当开始运行的时候,你就得到了以下的输出:
This is the original.
This is parent. Child is 1703
Program end.
This is child.
Program end.
当程序开始的时候,首先现实的就是“此为原件”。在这个现实之后,我们有了系统调度fork().在fork之内,新的进程识别自己的创造,然后fork功能就呼叫一次,然后返回两次:第一次返回到原进程,第二次返回到新创建的进程。它们两个都在fork调度之后返回到同一个点。唯一的不同就是父进程和子进程的返回值。Fork在原进程返回有子进程的id(应该是非零的值)fork在新的进程的返回值是零。所以,父子进程执行不同的程序部分,即便它们两个是完全一样的。
原进程执行第一部分的if-condition,显示信息“此为父进程。子进程1703”当新的子进程执行另一个if-condition的时候,显示“此为子进程”两个进程都要显示“程序结束”,所以我们有两个输出。所以我们有以上的程序输出。

系统调用execve()
我们看到了通过一个fork创建一个新进程的方法,我们知道的系统调用fork创建了和自己同样的进程。那么,因为一个进程是同一的,那么一个只有系统调用fork的调用就是没有用的。如果回到最开始,那么所有的进程都是有一个祖先init进程来的。所以就是说每一个进程都和init进程一样吗?
有幸的是,我们有execve()系统调用执行另外一个程序。让我们看看以下的execve案例:
#include <stdio.h>
#include <unistd.h>

Int main(){
Char *s1[3]={“/bin/ls”,”-l”,NULL};
Char *s2[1]={NULL};

Printf( “Hello\n”);
Execve(“/bin/ls”,s1,s2);
Printf(“world\n”);
}

Execve系统调度加载了另一个程序并且执行。在以上的案例程序中,第一个execve参数就是程序文档加载的。所以它执行文档“/bin/ls”,ls命令。第二个execve参数是函数队列。第一个函数应该一直是程序名自己(在/bin/ls中),第二个函数就是“-l”。第三个参数就是变量环境队列,我们还没有在这个案例当中使用到。
你可能注意到了“world”从来没有被显示出来过。这是应为一旦execve系统调用成功执行了一个程序之后,它就不会再回来。而是,当前的进程映像完全被另一个程序所代替(在ls案例程序当中)。它也适用于数据(变量,编码等),在原始进程中有可能被新写入的程序覆盖。简单点说,execve系统调用以一个新的程序代替了当前的程序。
注意没有新的进程在execve系统调用中创造。它仅仅是代替程序。所以,新的程序有同样的进程id。意思就是从内核的角度来看,它还是有同样的进程。唯一的变化就是它进程内容的变化。
Fork和execve都能创造一个与自己不同的进程:
#include<stdio.h>

Int main(){
Int r;
Char *s1[2]={“/bin/ls”,NULL}
Char *s2[1]={NULL};
r= fork()
if(r!=0){
printf(“We have a child process %d\n”,r);
wait(NULL);
printf(“End of Execution\n”,r);
}
Else{
Execve(“/bin/ls”,s1,s2);
}
Return0
}

在这个案例当中,我们首先使用fork()来建立一个新的进程。新的进程使用execve系统调用来加载执行ls程序,当父进程是用系统调用来等待直到子进程的终结。
可以用fork和execve来创建一个arbitary进程。Init进程用了两个系统调用来创建登陆进程,登陆进程用这些来创建Shell。当你在Shell中键入你的指令的时候,Shell一般都会使用这两个系统调来创建一个新的进程并且加载一个合适的程序来执行。
3. 进程状态
如上所述,一个进程的task_struct保存了很多的关于进程的重要信息。它们其中一个就是进程状态,表现了当前进程状态:
TASK_RUNNING(运行状态) : 这个任务运行的状态表示了一个进程当前正在运行。这个名称不是总是在进程当前正在CPU执行的时候使用,简单的说,它的意思就是这个进程已经准备好运行而且可以在CPU内运行。如果有多于一个进程有TASK_RUNNING,调度起就需要决定哪一个进程要首先给CPU来执行。很容易给一个进程TASK_RUNNING状态。让我们看一看一下的程序:
#include<stdio.h>
Int main(){
While(1){}
}
以上的程序就是一个简单的忙碌循环的程序,在消耗CPU的频率。如果你编译这个程序并且运行它,那么它的进程庄它就将永远是TASK_RUNNING.如果你开始了一个多案例程序,比如3个,那么就会有三个可以运行的进程在运行,它们将要共享CPU的资源。可能很容易就确定使用“top”指令。也很容易注意到唯一的进程TASK_RUNNING状态可以放到CPU运行。其它的进程状态就不可以被运行。

TASK_INTERRUPTIBLE(可中断任务),这个状态表示一个进程当前正等待某种事件,它的等待可以被中断,比如通过一个信号中断。发呆进程等待着用户的输入,比如你的Shell,大多数时间都在TASK_INTERRUPTIBLE状态。表现这个状态,我们可以看一下以下的C程序:
#include <stdio.h>
#include <stdlib.h>
Int main(){
Sleep(1000);
}
这个程序在一定的时间处在睡眠状态然后终结。在这个程序的执行过程中,这个睡眠中的进程没有占用任何的CPU频率。直到这个进程的状态成为TASK_INTERRUPTIBLE.
TASK_UNINTERRUPTIBLE(可中断睡眠状态): 这个状态表示了一个进程当前是在等待某个时间的状态,它的等待是不可已被中断的。这一半是指那些进程等待的非常重要的事件,而且进程必须要等待这个时间的发生并且处在发呆状态。在很多情况中,不可中断状态的进程是在等待硬件事件。比如等待硬件设备的需要来完成并且给与设备响应。了解在不可中断状态下,发送给进程的任何信号都不能被执行,直到进程所等待的事件发生时非常重要的。
所以,如果一个进程在任务不可中断状态,即使使用 SIGKILL信号也不可以删除。
展示TASK_UNINTERRUPTIBLE比较困难,因为在这个状态中,一般很难被用户所注意到。一个案例就是展示设置NFS服务器,给NFS安装sync选项的时候。如果一个文件操作通过了NFS,客户就要等待操作来完成适当的响应。在等待的过程中,用户进程在TASK_UNINTERRUPTIBLE(不可中断睡眠状态),意思就是这个进程当前成在等待一些合适的NFS服务器并且等待非常重要,进程必须等待结果才可以处理其它的操作。在这段时间,等待是不可中断的,任何的发送给进程的信号都要排队等待稍后的处理。所以,如果在NFS和用户的错误操作联系之间,用户进程都需要等待,在TASK_UNINTERRUPTIBLE状态,直到再次链接。在等待当中,客户进程都不可以备任何的信号打断,也不可能被打断。

TASK_STOPPED(暂停状态)任务中断状态的意思就是执行中断一个当前的进程。一般情况下当进程调试的时候,或者进程在被Shell测试的时候(ctrl-Z键)进程在TASK_STOPPED状态。我们可以用SIGSTOP信号来降级TASK_STOPPED状态。请看一下的无线循环程序:
#include <stdio.h>
Int main(){
While(1){ };
}
这个程序简单的使用了CPU频率。如果我们执行这个程序,进程就会在TASK_RUNNING状态,我们就可以制造一个进程在TASK_STOPPED状态来发送SIGSTOP信号。比如,如果进程PID是1234,我们就可以这样发送SIGSTOP:
Kill -19 1234
这里,-19的意思就是19号信号,有sigstop信号。我们就可以确认进程现在已经被TOP命令终止。进程在我们用SIGCONT信号命令它继续执行之前就不会运行了。
Kill -18 1234
这里,-18的意思就是18号信号,就是SIGCONT信号。有这个信号,进程就会重新返回到TASK_RUNNING状态了,进程也就继续运行了。
TASK_ZOMBIE(僵死状态)一个进程的TASK_ZOMBIE状态就是进程是否终止,而不是响应它的父进程。
当一个进程结束的时候,内核就要把资源给另一个进程,比如,内存分配要回复给系统,要打开的文件必须是一个已经关闭的文件。也就是说,内核一定要通知父进程它的终止,然后回复到退出编码,处理父进程。这样内核就不能完全的摧毁一个正在终止的进程直到父进程知道子进程要被终止,并且在一切状态完好返回以后再终止。在这段期间,内核还有退出的信息,因为它已经在进程TASK_STRUCT之内。正在终止的进程就在TASK_ZOMBIE状态中。在这个状态,任何这个进程的资源都被释放了,除了进程TASK_STRUCT.进程TASK_STRUCT在附近成阅读推出状态之后会被释放。
降级TASK_ZOMBIE状态进程并不是很难。让我们看一下以下的程序:
#include <stdio.h>

Int main(){
If(fork()){
/*parent*/
Sleep(100);
Wait(NULL);
Exit(0);
}
Else {
/*child*/
Exit(0);
}
}
这个程序制造了100秒钟的TASK_ZOMBIE状态子进程。这个程序在第一眼看来可能不太容易理解,让我们看一看细节。当FORK()功能被调用的时候,FORK()就创造了一个正是和原进程一样的子进程,只有fork功能的回复值不同。原进程,就是父进程新造了一个子进程,得到了非零的回复值,但是子进程得到了零的回复值。所以,尽管子进程和父进程一样,但是fork功能回复值就决定了父子进程处理一个程序的不同部分。父进程要执行if-condition的第一部分,因为fork非零的回复值,子进程要执行if-condition的第二个部分,因为它的值恢复到了零。现在,我们有一个重点生成的结束程序。子进程什么都不用做,立即就可以退出,但是父进程变成了睡眠状态。在睡眠状态中,子进程结束,但是父进程还不知道子进程已经结束了,直到父进程使用wait()功能等待子进程退出状态。内核就把退出状态,包括父进程的睡眠状态和子进程现在已经终结状态全部保存在了task_struct当中。当父进程执行wait功能的时候,子进程的退出状态就返回了,子进程的task_struct就被释放,然后子进程彻底终结。


第三节 调度
所有的进程都部分的运行在用户态下,部分运行在系统模式下。那么这些模式是怎样被不同的重要硬件支持呢,因为一般如果从用户态转换成系统模式(反之亦然)会有一个保护设备。用户态有较少的优先权。每一次一个进程发出系统调度的时候,它都从用户态到系统态并且继续执行。在这里内核代表进程在执行任务。在Linux当中,进程不会取代当前的运行模式,它们也不能为了自己的运行而停止其它的运行的模式。每一个进程如果决定放弃一个运行当中的CPU就一定要等待一个系统事件发生才可以。比如,一个进程要等待进程从一个文件中读取数据。这种等待的发生是在系统模式中的系统调用内的;进程使用库功能打开并阅读文件,反之系统调度是从打开的文件中读取数据。在这种情况中一个等待的进程就会被其它的进程挂起,更多的进程就要准备好运行了。
进程总是会制造系统调度,所以它们总是要等待。即使这样,如果一个进程在等待了许久以后执行还是会浪费CPU的频率的话,Linux还是会做出预清空调度处理。在这个方案中,每一个进程都被允许运行至少200ms,而且,当这个时长过期的时候另一个进程被选择运行,那么原始的进程就会进入等待阶段,直到它在有机会运行。这种最小化时间的方法被称作是时间切片。
调度器必须选择所有系统待运行进程中最重要的进程运行。
1. Task_struct当中的相关信息
一个可运行的进程只是等待CPU的运行。ThizLinux使用了一个超缩放调度算法来选择当前系统内的进程,处理器分列寄存器,其它的环境也被保存在进程的task_struct数据结构中。重新储存了新的进程状态(处理器分类)运行进程的系统控制。因为调度器在系统内可运行进程中公平的分配了CPU频率,并把每一个进程的信息保存在task_struct中。
Policy:进程的调度规则。有两种Linux进程,一般的和实时的。实时进程比其它的进程有更高的优先权。如果有一个实时进程准备好了运行,它就会首先运行。实时进程一般有两种规则,round robin(SCHED_RR)还有first in first out(SCHED_FIFO)。在round robin调度中,每个可运行的实时进程在轮流运行,而在first in, first out调度中每一个可运行进程都在排队等候其它的进程运行完了再运行,没有交替。
Priority这就是调度器给一个进程的优先权。它也是一个进程允许运行的时长。你可以根据系统调度命令选择一个进程的优先权。
Rt_priority:Linux支持实时进程,这些调度比系统内其它的没有实时的进程有更高的优先权,这个领域允许调度器给实时进程相对更多的优先权。实时进程的优先权可以选择使用系统调度。
Time_slice:这就是进程允许运行的总时长。和优先权在进程第一次运行的时候启动。
2. O(1)调度器
ThizLinux内核新的O(1)调度器和当前的Linux调度器不同。它有以下几个特点:
完全的O(1)调度。Wakeup(),chedule()和计时器中断是O(1)算法。它可以在进程增多的时候局限计时器时间的增长。
它有更好的SMP延展性,因为它是用了CPU前运行队列和锁。所以两个任务在CPU内是分离的,可以完全的,没有连锁的在平行的情况下叫醒,安排并转换环境。
它有更好的SMP亲和性。以前的调度器有一定的缺点,CPU给不同的即时跳转任务以优先权。新的调度器再不用全球同步回车完全重新计算的情况下,基于每一个CPU的基础解决了干扰时间分割的问题。
新的调度器的核心就是以下的几个机理:
首先,它有两个优先权数组。有一个活动数组,还有一个到期数组。活动数组有所有的任务还有剩余的时间,而过期数组内包括的所有任务已经用完了它们的任务时长,但是这个数组还是在继续的处理。活动和过期数组不是直接访问的,通过CPU指针运行队列结构来访问。如果所有的任务都用光了我们的两个指针,那么所有的过期数组就剩了活动数组任务—活动数组提供新的过期数组集合器。
有一个64位的数组缓存。找到两个x86BSFL中最高优先权的任务。
分散数组解决是我们游乐活动和过期任务,,对时长分割的重新计算也可以马上完成,当时长分隔过期的时候。因为数组总是通过运行队列中的指针,快速转换数组。
这是一个接近round-robin调度的混合优先列表,干扰时长分割的数组转换方法。
第二,有一个任务前加载评估器
最可怕的事情就是系统加载过度。最好的不是推进交互式的任务,而是那些要占用更多CPU时长的任务。这个方法对O(1)也更加容易。
为了建立分配给系统的实际加载任务,就要用一个复杂但有精确的方法:有4个历史项,最后几秒之内的活动任务循环缓存。循环缓存是一个不需要太多准备工作的操作。项告诉调度器一个给任务精确的加载历史的方法:它是否在过去的几秒钟使用了更多的CPU时长。
任务要生成比CPU处理的重要任务更多,CPU加载有数据优先的最大化,所以即使是CPU捆绑的任务也要看其它任务的优先权,并且与其它任务共享CPU资源。
SMP加载平衡器可以延展/转换成附加计算机和高速缓存平行:NUMA调度,可以通过改变加载平衡器轻松的支持多核心CPU。

第四节 环境转换
因为调度器决定哪一个可以运行的进程下一个被执行,必定有一个方法转换到新的进程来运行当前运行着的进程而不是下一个进程直接进入CPU。我们把这个叫做环境转换,包括转变当前进程和下一个要运行的进程的环境,所以下一个进程就可以执行了。上一个进程被检测,然后下一个进程保存以后开始运行。
一下情况下需要转换环境:
当时间分割结束之后,我们需要进入其它的进程。
当一个进程决定进入一个资源,它待机并且等待,所以一个调度有选择另一个进程。
一个转换环境的方法就是基于CPU的架构。一般来说,包括保存之前进程环境并返回下一个进程的环境。
以下的编码就是做环境转换的:

这个汇编代码集合是从内核源代码子文件include/asm-i386/system.h中得到的,但是这个编码仅仅是为Intel IA-32设计的。在环境转换中,有一些重要的寄存器要先存储,比如,ESI,EDI,EBP寄存器时首先存储到钱一个进程的堆栈当中的,堆栈分类通过保存ESP寄存器到task_struct的thread.esp保存在前一个进程的task_struct当中。并且把EIP保存到新返回的点中,在lable1.$1的意思就是Lable1的地址,再Jmp指令之后。所以下一次如果一个进程要保存运行,保存地址就会在label1中运行,就是第一个popl指令。
知道当“movl%3,%%esp”执行的时候,恢复下一个进程的堆栈非常重要,下一个Pushl实时用下一个进程的堆栈。所有前3个都适用前一个进程的堆栈。所以ESI,EDI,EBP地址就在前一个进程中保存。
下一个步骤就是跳转到下一个进程的label1地址当中,要通过下一个进程的EIP完成,跳转到一个功能的项(在这个案例当中,--switch-to功能)。当功能返回之后,就使用EIP返回地址,然后间接的跳转到下一个进程的label1地址。
完全了解它怎么工作,就一定要知道调用的过程。当我们编写一个程序的时候,有很多的案例都是决定我们的功能,或者我们使用一些已存在的库的功能。让我们看看以下的功能调用程序:

这个很简单,但是可以很有效的帮助我们理解功能调用的工作原则。让我们考虑一下以下的汇编代码,与其相似的功能调度:

这是一个功能调度的汇编版本。这里调度命令用在运算元的f功能上面。图5.2显示了f()功能调度的堆栈,这个调度指令会推进返回地址,也就是在调度指令之后的指令地址,然后跳转到执行f功能上。当这个执行结束以后,功能就使用返回地址返回前一个储存在堆栈当中的地址,所以“World”就显示出来了。(我们要更简单的理解寄存器分段)
在了解发生了什么之后我们就可以确实的用一对推进和条约指令代替调度指令:
#include<stdio.h>
Int f() {
Printf(“Hello\n”)

这个程序和前一个程序作的是同样的调度指令,只是我们自己返回了地址。我们推进了label1($1f)到第一个堆栈,然后跳转到功能f的地址并且执行f。当功能f返回的时候,label1地址就在寻找并且跳转回来label1执行。所以,我们就得到了前一个程序的结果。
我们知道返回的地址在功能调度堆栈。现在我们把label1移动到不同的位置,我们就可以让功能返回到不同的地址。

这个程序没有显示“World”,因为当f功能返回的时候,就返回到了显示的“world”地址。所以,使用这个设备来跳转到希望到达的地址是有可能的。
现在我们用所有的办法返回到内核中转换代码的环境:


“保存EIP”这一行执行了“movl$1”,复制了label1($1f)到prev-?thread.eip(%1).所以指令就保存在—switch-to到前一个进程的task_struct当中了,所以当调度器决定转换回之前的进程的时候,执行就会连接到这个点。
下两行就保存了EIP,推进前一个保存了的EIP,跳转到执行功能。这个正是我们在前一个案例当中讨论过的。一次功能__switch_到回转,跳转到下一个进程的label1,开始执行下一个进程。现在检测之前的进程,下一个进程就准备运行。

第五节 练习
公平共享调度:公平共享调度的规则达到了和3个Linux调度规则之内的不同的目标。它们在所有的系统用户之间公平的分享CPU的资源。相反,SHED_OTHER规则也公平的在进程之间共享CPU资源,SCHED_FIFO和CHED_RR规则调度都是根据进程固定的优先权来决定的。
比如,如果有两个用户:A/B.如果它们两个都运行1进程并且假设没有其它的进程在运行,那么每一个进程都享受50%的系统CPU。然而,如果A运行了两个进程,B运行了两个进程,还是每个人得到50%。但是每一个进程都只有25%的资源,每个用户得到50%。
你需要依据以下的规定设计新的调度规则:
添加两个新的系统调度,添加/删除一个用户的从fair-share组分别的调用。当前进程的用户身份储存在uid领域的task_struct当前的点位。你还需要处理一些基本的错误,比如自己添加或者删除一个或一个以上的用户,或者一次性添加10个以上的用户等等。
修改调度,依据以下的属性:
Fair-share among users假设有4个用户ABCD在公平共享组当中。所以每一个用户所得到的CPU分配就是25%。
Fair-share among processes belonging to the same user假设有两个用户A,B在一个共享组中。A有两个进程,B有3个进程。每一个A的进程得到25%的CPU,每个B
Treatment of non-runnable processes:在之前的项目中,A1得到了50%的CPU,A2使的进程得到16.67%。用A1的CPU,所以A得到了50%而不是25%。如果两个任务进程都不可以运行,剩余的用户B就将得到所有的CPU资源。每一个B的进程都会得到33.33%的CPU.
实现FAIR-SHAIR调度规则,你就需要计算整个CPU在分给每一个共享组中用户的量子,然后成比例的重新分配这些量子给它们。在以上的案例中,根据在几个共享组进程来设定CPU的百分比。你就不应该修改非共享组的进程调度。

第六章 内存管理

内存管理制系统是操作系统最重要的一个部分之一。在计算机的初期,就有了需要比物理内存更大容量的内存需求。所以人们要想出种种策略发展来突破这一局限,最成功的应该就是虚拟内存。虚拟内存是系统拥有比实际更多的内存,给进程分享使用。
虚拟内存又不仅仅是让计算机内存扩展。内存管理制系统提供了:
大地址空间:操作系统是自己看起来拥有比实际上更大的内存。系统的虚拟内存就可以是物理内存的好几倍。
保护:系统中每一个进程都有自己的虚拟地址空间。这些虚拟地址空间完全与彼此分开,所以一个运行中的进程应用程序是不会彼此互相干扰的。所以,硬件虚拟内存允许内存领域来保护起它进程写入。这就保护了编码和数据不被一些破坏性的程序覆盖写入。
公平分配物理内存:内存管理制系统允许每一个系统的运行进程都有公平的系统物理内存的使用。
虚拟内存共享:虽然虚拟内存允许进程有独立的虚拟内存空间,但是当你有需要进程来共享内存的时候。比如说可能有几个进程在你的系统内运行在bash命令shell.不是有好几个bash的拷贝,而是每个进程当中的一个虚拟地址空间,这比只有物理内存有所有进程的bash地址拷贝好得多。动态的库是另一个执行几个进程之间共享的编码的一般案例。
共享内存也是在进程间通讯(IPC)中应用,两个或者两个以上的进程通过共有的内存互换信息。Linux支持Unix系统V共享内存IPC.


第一节 Linux的内存模式
在学习Linux支持虚拟内存的方法之前,了解一个抽象的模式可能更加有用。
就像进程执行一个程序,从内存中读取指令,解码。解码指令需要抓住或者在内存中保存它的内容。进程就执行指令,移动到下一个程序指令。用这个方法,进程就总是在访问内存或者从储存数据中抓取指令。
在虚拟内存系统当中所有这些地址都是虚拟地址,不是物理的地址。这些虚拟地址通过给予操作系统内的一个信息表格进程转换成物理地址。
为使转换更加简单,虚拟和物理内存被分为页。这些页面有同样的大小,如果它们不改变,系统管理它们就会很难。Alpha AXP的Linux系统使用了8KB的页,Intel x86使用4KB。这些也免得每一页都有一个特殊的代码,就是页帧号(PFN)。
在这个页面模式,虚拟地址有两部分组成;一个偏移,和一个虚拟页帧号。如果页面大小是4KB,比特11:0,虚拟地址包括偏移和12比特,以上就是虚拟页帧号。每次进程遇到一个虚拟地址,它都要从偏移和虚拟页帧号提取。进程必须翻译虚拟页帧号成为一个物理地址,访问它在物理页面中的准确位置。作者个操作进程使用页面表格。
图6.1显示了两个进程的虚拟地址空间,进程X和进程Y,它们每一个都有自己的页面表格。这些也表格导引每一个进程的虚拟页面到物理页面。这就显示了进程X的虚拟页帧号0在物理内存页帧号是1,所以Y的虚拟页帧号1在物理内存页帧号中就是4。每个进入理论页面表格都包括以下的信息:
有效标志。表示页面表格项有效。
物理页帧号就是项标注的。
访问控制信息。就标注了页面怎样被使用。可以写入吗?包括可执行编码吗?
页面表格是用虚拟页帧号访问。虚拟页帧号5可能会成为表格上面的6
为了转换虚拟地址到物理地址,处理器必须要首先作出虚拟地址的页帧号,然后在几个虚拟页面偏移。使大小在2就可以遮掩和移动了。再看看图6.1,设想页面是0x2000比特,在虚拟进程地址空间中是0x2194,转换成offset0x194虚拟页帧号1.
处理器在进程也面表格中使用虚拟页帧号,检索也面表格的项。如果页表格项在偏移当中是有效的,处理器有物理的页帧号。如果项无效,那么处理器就访问了一个不存在的虚拟内存区域。在这种情况,处理器不能解决这个地址并且需要传递控制到操作系统,才能修复。
刚才处理器注意到了纠正进程的操作系统是着访问一个虚拟地址,因为处理器无效的转换。然而处理器传递,这就是页面错误,而且操作系统也注意到了错误的虚拟地址也是页面错误的一个原因。
假设页面表格项有效,处理器负责物理页面页帧号,根据页面大小的到物理内存中的地质。最后,处理器就添加偏移到需要的指令或者数据。使用以上的案例,进程Y的虚拟页帧号1在物理页帧号是4,在0x8000中。根据0x194byte偏移给我们的最终物理地址是0x8194.
通过映射虚拟到物理地址,虚拟内存可以被系统物理页面以任何一个指令映射。比如图6.1进程x的虚拟页帧号0映射到物理页帧号就是1,当虚拟页帧号7映射到物理页帧号就是0,尽管它虚拟的页帧号比较大,这就是根据不同产品产生的有趣的现象;虚拟内存页不需要在物理内存中出现。

第二节 进程虚拟地址空间
一个进程的虚拟内存包括了从很多资源而来的可执行性编码和数据。首先是程序映像加载;比如ls命令。这个命令和其它的可执行性映像一样,由可执行性代码和数据组成。图表文件包括了所有的必需的加载可执行形变马赫关系着程序数据到进程的虚拟内存信息。第二,进程可以在运行的时候分配虚拟内存,保存阅读的文件。这个新的分配,虚拟的,内存需要连接到进程的已经存在的虚拟内存,所以就可以使用了。第三点,Linux进程使用一般的比较好用的代码库,比如文件处理历程。不是每一个进程都有它自己的库拷贝,Linux使用共享的库,用几个同时运行的进程。从这些共享库中的编码和数据就可以连接到进程的虚拟地址空间,和共享这个库的虚拟地址空间。
在任何可用的实践阶段,进程都不会使用包括在虚拟内存之内的编码数据。它不会包括只有一些初始化的编码或者处理一个特殊的事件。可能只是使用一些共享库中的历程而已。这就浪费了加载编码和数据到物理内存。在系统中增加进程的浪费,系统的运行就会变得吃力。而Linux使用了叫做demand paging的进程虚拟内存,当一个进程要使用虚拟内存的时候把它带到物理内存。所以,不是直接加载编码和数据到物理内存,内核要选择进程的页面表格,注视虚拟内存空间存在,而不在内存当中。当进程要访问编码或者数据的时候,系统硬件就会生成一个错误的页面,并把控制交给内核来处理。所以,每一个系统需要的在进程地址空间的虚拟内存的空间,所以系统需要知道虚拟内存从哪里来的,也要知道内存如何修复这些错误的页面。
内核需要管理所有的虚拟内存空间,每一个进程的虚拟内存的内容也在mm_struct数据结构中从task_struct中指过来。进程的mm_struct.
数据结构还包括了有关加载可执行性映像和指针的进程也面表格的信息。包括了指向vm_area_struct的指针列表的数据结构,每一个都在进程之内代表了一个虚拟内存空间。

图6.2

链接列表按照虚拟内存顺序升序排列,图6.2展示了一个简单进程的虚拟内存和内核数据结构一起的管理。在这些虚拟内存的范围,从几个资源,Linux提取了有vm_area_struct虚拟内存处理历程指针的界面。这个方法,所有的进程的虚拟内存都可以连续的处理,不同的内存如何管理提供这些重要的服务。比如有一个历程在一个进程试图访问虚拟内存的时候就被调用,它不存在,这就是如何处理错误的页面。

进程的vm_area_struct组数据结构不停的被Linux内核生成的新虚拟内存区域访问,因为进程修复代表虚拟内存并不在系统物理内存中。这就是找到正确的vm_area_struct变得非常重要的系统任务,而且需要一定的时间。为了提高这个访问的速度,Linux还安排了vm_area_struct数据结构再AVL(Adelson-Velskii and Landis)目录树中。这个树使每一个vm_area_struct都有一个左/右指针指向旁边的vm_area_struct当中。作指针指向一个低阶开始的虚拟地址,右边的指针指向一个高阶的虚拟地址。为了找到正确的指针,Linux要到root目录跟踪每一个左指针和右指针,直到它找到正确的vm_area_struct.当然,这些都不是那么简单的,它需要使用更多的进程时间。
当一个进程分配虚拟内存的时候,Linux不是真的为进程预定物理内存。而是描述虚拟内存,通过一个新的vm_area_struct数据结构。这就连接到了虚拟内存的进程表格。当一个进程尝试在虚拟内存中写入虚拟地址的时候,系统就会提出报错页面。处理器就要试着解码虚拟地址,但是有些没有内存的页面表格项,所以提出页面错误,让内核进行修复。Linux要看是否虚拟地址连接当前的进程虚拟地址空间。如果Linux生成一个合适的PTEs并且分配内存物理页面给这个进程。编码或者数据就需要从文件系统或者swapdisk带进这个物理页面.进程可以重新在引起页面错误的指令重新开始,这个时候,内存就物理存在,可以继续。
1. ELF
(Executable and Linkable Format)可执行可连接格式目标是文件格式,由Unix系统库设计,现在成为Linux系统最常用的一个格式。当比较目标文件格式ECOFF和a.out格式的时候,ELF就会非常灵活。ELF可执行性文件包括了可执行性编码,有时候代表了文本,或者数据。在可执行性映像内的图表描述了一个程序怎样放在进程的虚拟内存之内。就是说连接映像,或者连接编辑,到一个单独图表,包括需要运行这个映像的所有的编码和数据。这个映像还分类了映像的内存和图表第一个执行编码的地址。
图6.3显示了数据链接ELF可执行性映像的过程。
这是一个简单的C程序显示“Hello World”然后退出。标题代表了ELF图表有两个物理标题(e_phnum是2)从52比特(ephoff)在文件映像开始。第一个物理标题代表了映像可执行性编码。到达虚拟内存0x8048000有65532bytes.这是因为数据连接映像包括printf()库代码,调用“Hello World”.这个映像的点项,程序的第一个指令不是开始图表而是虚拟地址0x8048090(e_entry).这个编码在第二个物理标题之后立即开始。这个物理标题代表了成需要加载到虚拟内存地址0x8059BB8的数据。这个数据是可读可写的。你可以注意到数据在文件前2200比特(p_filesz)的大小,在内存中是4248比特。这是因为前2200比特包括了初始化之前的数据,尔后的包括了将要执行初始化编码的数据。
当Linux加载ELF可执行性映像到进程的你内存地址空间的时候,它不是真的加载映像。
它设置虚拟内存数据结构,进程vm_area_struct树和它的页表格。当程序执行错误也面的时候就会引起程序编码和数据到物理内存。没有用过的程序部分将不会加载到内存当中。当ELF binary格式加载器觉得映像ELF为有效可执行性映像的时候,就会中断进程的当前可执行性映像。这个进程是一个克隆的图(每个进程都是)以前的图表示程序父进程曾执行的,比如命令编译bash shell.就会刷新旧的可执行性映像,不管旧的虚拟内存数据结构和重置进程的错误表格。而且清空任何靠近要打开文件的信号处理器。在最后刷新新的可执行性映像的进程。不管是什么各式的可执行性映像,一样的信息都设置在进程的mm_struct当中。指针指向开始和结束的图表代码和数据。这些值在ELF可执行性映像物理标题当中都会读到,程序的一部分也会映射到进程的虚拟内存空间。这也是当vm_area_struct数据结构摄制的时候进程的页面表格修复。Mm_struct数据结构也包括了指向传入程序的参数,和这个进程的环境。
ELF共享库
一个积极的链接图,也是没有包括准备运行的所有编码和数据。其中有一些在共享库中,连接到运行时间的映像。ELF共享库的表格也在“活动链接者”(dynamic linker)当共享库连接到运行时间的时候。Linux是用了几种dynamic linkers,ld.so.1and ld-linux.so,1,所有都可以在/lib文件夹中找到。库包括了一般使用的编码,比如语言支历程。没有活动链接,所有的程序都需要自己从库中拷贝,就会占用更多的磁盘空间和虚拟内存。在活动链接中,信息也包括在ELF映像列表给每一个库历程。信息指向活动链接者,怎样把库历程连接到程序地址空间。

第三节 分页
因为物理内存比虚拟内存要少得多,操作系统必须要小心使用物理内存。一个方法就是把当前处理运行的程序占用的物理内存里面的内容加载保存到虚拟页面。比如,一个数据库程序可能运行一个数据库队列。在这种情况下,不是所有的数据库都需要加载入内存,只有需要查找纪录的数据就够了。如果数据库队列是一个查找队列那么就没有必要从数据库程序加载编码到内存当中了,因为它们访问已知的页面。
当一个进程要访问一个当前不再内存的虚拟地址的时候,处理器不能给虚拟页面找到页面表格项。在这点上,处理器报告给操作系统页面错误
如果错误虚拟内存有效,那就是说进程试着访问一个虚拟地址。可能应用程序有地方有问题,比如说写入即时地址内存。这样操作系统就结束了,保护系统内其它进程。
如果错误虚拟地址有效但是页面当前不再内存之内,操作系统就必须把相应的页面带到内存内。访问硬盘需要一段时间,相对来说,进程也要等页面找到以后才可以操作。如果其它的进程可以运行,那么操作系统会选择它们其中的一个来运行。找到的页面写在空闲的物理页面,虚拟页面项数字也加入了进程也面表格。进程就在内存错误发生的地方重新启动了。这次虚拟内存访问,处理器就能做虚拟内存到物理地址的转换,所以进程继续运行。
Linux使用需要页面加载可执行图表到进程虚拟内存中。不管命令在哪里执行,文件内容打开了,并且映射到进程虚拟内存中。这是通过修改描述这个进程的数据结构,这就叫做内存映射。然而,仅仅第一部分的映像被带入了物理内存。其它映像还在硬盘上。在映像执行的时候,生成了错误页面,Linux使用这些进程内存映射,来决定哪个部分的映像被带入内存作执行。

图6.4

1. Linux页面表格
Linux假设有3个层次的页面表格。每一个页表格都包括下一个层次也面表格要访问的页面框架数字。
图6.4展示了虚拟内存地址是如何分成几个领域的;每一个领域提供了偏移到特殊的页面表格。把虚拟地址转换成物理地址,处理器必须把每一个层次的领域内容都转换成偏移物理页面,包括下一个层面的也面表格。这就重复三次直到页面边框数字物理页面包括虚拟地址。现在最后一个领域在虚拟地址中,比特偏移用来寻找页面当中的数据。
每一个Linux运行的平台都在提供宏允许内核转换,页面表格到特殊的进程转换。这种方法,内核就不需要知道页面表格项的格式或者它们怎样安排的。
Linux使用一样的页面表格增长编码给Alpha处理器,有各业面表格层次,Intel x86处理器,由两个层面的页面表格。

2. 页面分配和回收
有很多系统内物理页面的需要。比如,当一个图标加载到内存,操作系统就需要分配页面。这些奖杯当图表结束之行并且卸载之后释放。另一个物理页面的使用就是控制内核数据结构,像页面表格一样。机械和数据结构给页面分配和回收使用,在保持虚拟内存支系统有效性是最值得批评的。

所有的系统物理页面都在mem_map数据结构中,这是一个mem_map_t数据结构列表,就是最初启动时候的初始化。每个mem_map_t描述了一个系统单页面。重要部分是:
Count就是这个页面的用户数。当页面在几个进程之间共享的时候,技术就比一个页面要强大。
Age这个领域描述了页面的年龄,用来决定是否一个页面合适作为丢弃或者交换。
Map_nr这是一个mem_map_t描述的物理页面框数字。
Free_area矢量被页面分配编码用来查找并释放页面。机械编码支持总缓存管理,处理器使用页面大小和物理页面。
每一个free_area原件包括数据块页面的相关信息。第一个元件在数组中描述单独页面,下一个数据块两个页面,下一个数据块4个等等,以2个递增。原件列表被用作队列首,再mem_map队列中有指针到页面数据结构。释放数据块页面在这里排列。map是一个指针,指向分配的页面组。Bitmap的Nbit用来设置是否释放Nth数据块页面。
图6.5体现了free_area结构。原件0有一个空闲页面,原件2有4个页面中的两个页面的空闲数据块第一就是开始页面数字4,第二步56。
Page Allocation Linux使用Buddy算法有效分配和回收数据块页面。页面分配代码试图分配一个或者更多的物理页面。所以只要有足够的空闲空间页面在系统之内,就可以达成它的要求(nr_free_pages>min_free_pages)分配编码会为页面大小的要求查找free_area。每一个free_area的元件都有分配的和空闲的数据块页面约束数据块。比如,原件2输列有一个内存映射描写每4个页面长度的空闲分配数据块。
分配算法首先查找需要大小的页面的数据块。它跟随空闲页面在free_area数据结构的原件列表队列。如果没有空闲的核实大小的页面,就寻找达一倍的数据块。这个进程要持续到找到free_area的页面数据块位置。如果一个页面数据块比它需要的大,那么就把它切割成理想的大小。因为太大的数据块再运行进程中很容易中断,所以还是切开比较好。空闲的数据块在适当的队列中排列分配给页面并且返回调度器。
比如,图6.5需要一个两个页面的数据块,第一个数据块时4个页面的,那么就要把它切成两个数据块。

页面卸载 分配页面数据块就要分块内存,把大的页面切成小的,页面卸载编码重新把页面组合成大的数据块,,为了更容易的组合。
无论何时一个页面数据块被释放的时候,比邻的或者buddy数据块就有同样的大小,看看它是否空闲。如果有,就和新的数据块在下一个页面组合起来。每次两个数据块都组合成一个大的空闲页面数据块,,也面卸载编码就是者重新组合数据块在下一次成为一个更大的。这样数据块就像内存一样,如果用法允许的话。
比如在图6.5中,如果页面1空闲,那么就和其它的空闲页面0组和排列到援建1的free_area到空闲数据块2个页面大小。
3. 需要页面
当一个可执行的图表被内存映射到进程虚拟内存的时候就开始执行。只有在图表开始无力进入内存的时候,就马上开始访问虚拟内存,就不再虚拟内存当中了。当一个进程访问虚拟内存地址的时候它不是有效的页面表格项,处理器会报告Linux页面错误.
页面错误描述这种内存访问引起虚拟地址错误。
Linux必须先找到vm_area_struct代表内存。当通过vm_area_struct数据结构查找页面错误的时候,这些连接都在AVL(Adelson-Velskii and Landis)结构树。如果没有vm_area_struct数据结构给这个错误虚拟地址,这个进程就进入了非法的虚拟地址。Linux要发送信号给进程,发送SIGSEGV信号,如果进程没有处理这个信号,就会终止进程。
Linux之后就检查发生的页面错误,允许这个区域的虚拟内存。如果一个进程访问内存以非法的方式,比如写入一个制度的文件,那么就会有内存错误信号。现在Linux决定页面错误合法,就要处理它。
Linux必须分清页面交换缓冲区文件和硬盘内的可执行性图标。使用页面表格项认证错误虚拟地址。
如果一个页面表格项是非有效的但是不是空的,那么当前的页面错误就是交换缓冲区文件。因为Alpha AXP页面表格项,这些项不是有效的,而是非零的PFN值。在这个情况下,PFN领域有关于交换缓冲区页面的错误信息。页面怎么能在交换缓冲区文件中处理,这一章之后我们会作出描述。
现在所有的vm_area_struct数据结构有一套虚拟内存操作,这些不会有nopage操作。这是因为默认的Linux会修理分配新的物理页面,Linux将会使用。
基本的Linux nopage操作在用来内存映射可执行性图标,使用安装业面来带来要求的图标页面。
然而需要的页面带到了物理内存,进程页面表格升级。对硬件行为来说非常有必要升级这些特殊的项,因为处理器是用高速缓存内的转换。现在页面错误被处理了,进程也就重新在访问错误虚拟内存活重新启动。
4. 安装Linux页面
Linux页面安装会提速访问硬盘上的文件。阅读内存映射文件的时候这些页面存储到页面安装。

图6.6显示页面安装包括page_hash_table, 指针mem_map_t数据结构的矢量。
每一个Linux的文件都通过VFS点认证数据结构(在第八章内有介绍)每个VFS电视一个独特的完全描述一个的文件。在页面表格的索引是从文件VFS点发出的,在这个文件内偏移。
不论什么时候从内存映射文件中读取一个页面,比如当它在需要分页时候需要回到内存,通过安装业面读取。如果页面在安装之内,指针指向mem_map_t数据结构代表了返回的页面错误处理编码。Linux分配一个物理页面并且从磁盘文件阅读。
如果可能的话,Linux就会初始化从文件下一个页面读取,。这个单页面读取意味着如果进程通过序列访问文件,下一个页面就会在内存中等待进程。
页面安装成为图标,读取并执行。页面将会在不需要的时候从安装中删除,就是当一个图标不被任何进程继续使用的时候。Linux是用内存来开始运行物理页面。这种情况Linux会减少页面安装的大小。
5. 转出和删除页面
当物理内存成为Linux内存管理的珍贵,支系统就必须试着释放物理页面。这个内核执行的任务转换成daemon恶魔(kswapd).
内核转恶魔是一个进程的特殊类型,一个内核的线索。内核线索就是进程没有虚拟的内存,它们在内和模式的物理地址空间运行。内核转恶魔所做得比转出文件页面要多。它的角色就是确认有足够的空余页面在系统中有效的管理系统操作。
内核转换(kswapd)由内核的init进程开始,在开始时间等待内核转变周期性无效的计时器。
每次计时器过期的时候,swap daemon要看是否系统中空闲页面数量足够多。它使用两个变量,free_pages_high和free_pages_low确定是否释放一些页面。因为空闲页面多于free_pages_high,内核转变就不能做任何事;就会进入睡眠状态直到计时器下一次过期。因为检查内和转换daemon需要很多当前页面写入swap文件。它保存在nr_async_pages;这个每次在页面排队等候写入swap文件增加,当写入swap设备完成的时候减少。Free_pages_low和free_pages_high是系统启动时间而且关联到一些系统页面。如果一些系统中的空闲页面的数量在free_pages_high 以下,或者到达了free_pages_low,内核swap daemon就要3个方法来降低系统使用的物理内存页面:
降低缓存和页面安装的大小
转出系统V共享内存页面,
转出并删除页面。
如果系统中的空闲页面少于free_pages_low,内核swap daemon就要在下次运行前释放6页。否则就要释放3页。每个以上的方法都要依次的试过,直到足够的页面被释放。内核转daemon提醒了方法要在最后释放页面的时候使用。每次运行的时候要释放页面,直到最后成功。
在有了足够的页面之后,swap daemon再次沉睡直到计时器过期。如果内核swap daemon释放页面的数量低于了free_pages_low,那么沉睡的时间就是一般的一半时间。当时方页面的数量多于了free_pages_low,内核swapdaemon就在检查中间的时间回到睡眠状态。

第四节安装
如果你要实现系统使用以上的理论模式,那么问题就会解决,但是效率不是很高。操作系统和处理其设计尽量从系统得到更多的操作。除了制造更快的处理器,内存等,最好就是保持安装有用的信息和数据是一些操作更加快捷。Linux使用一些关于安装的内存管理:
Buffer 安装 高速缓存安装包括了数据块设备驱动的数据缓存。
这些缓存有固定的大小(512bytes)包括信息快,或者读,或者写。
一个块设备视为一个可以被读取和写入的固定大小的数据块。所有的硬盘都是数据块设备。
缓存: 是一个通过设备识别器和数据块,用来快速寻找数据块。快设备只是用来通过缓存安装访问。如果数据可以在缓存安装中找到,就不需要从物理块设备读取了,比如硬盘,就更快速的访问。
页面缓存:用来提高访问硬盘上数据图标的速度
用来安装文件页面逻辑内容,通过在文件内偏移访问文件。当页面从硬盘读取到内存,它们就被安装到了页面安装。
交换缓冲区(swap):修复交换缓冲区文件中的页面。
当这些页面修改之后,它们就被写入swap文件,然后下一次页面换出的时候,就不需要再次写入文件了。页面也可以简单的被删除。在重交换系统中着保存了很多的不需要的而且浪费硬盘空间的文件。
硬件安装:一个一般的实现硬件安装就是在处理器内;一个也面表格项的安装。这个案例中,处理起步总是直接读取页面表格,而是安装需要的页面转换器。这些转换看起来有点像缓存,包括安装业面表格项的复件。当访问虚拟地址时,处理器就会是着找出适合TLB的项。如果找到了,就直接转换虚拟内存到物理内存并且执行正确的数据操作。如果处理器不能找到一个合适TLB的项,就必须得到操作系统的帮助。通过发送给操作系统缺少TLB的信号。系统就会使用传输到操作系统编码中将问题解决。操作系统给地址映射生成新的TLB项。当如果的条件都被排除以后,处理器就会让另一个转换成虚拟地址。这一次就会成功了,因为现在TLB项对地址有效了。
使用安装的弊病,硬件或者其它,如果保存了,Linux就会使用更多的时间和空间来维护这些安装,如果安装失败,系统就会崩溃。

第五节 练习
内核页面错误计算:修改Linux内核就像每一个进程都有一个页面错误计算器。当进程生成的时候计算器初始化为零,并且每一次页面错误增加的时候增加。添加一个系统调度返回到页面错误计算器,给一个特定的PID进程.
Asmlinkage int sys_pf_probe(pid_t pid, unsigned long *pf_count)

页面错误衡量工具 写一个程序定期衡量页面错误数量。这个工具要像对待子进程fork一样的特定程序。应该允许通过输入参数给这个子进程。
输入衡量工具的格式:
Asm4tool<sampling interval><program to measure>\
{arguments for the program to be measured}*
同样的输入:
Asm4tool1/bin/sleep 20
这将会运行程序sleep20,每秒钟都衡量页面错误。
计算页面错误,通过第一部分的系统调度和使用/proc文件系统衡量CPU进程时间。CPU时间应该是用户时间和系统时间给这个进程的和。衡量应该定期执行。
衡量应该输出位stdout纯文本元组:
<CPU Time in Jiffies, Number of Page Fault>
同样的输出:
5 18
105 234
205 234
305 4535
.
.
.
写一个用户程序生成页面错误控制比例(比如一秒钟一页;你可以忽略程序初始化的页面错误)。使用这个程序来有效话衡量工具。

第七章 进程间通信

内核和进程通信来相互配合彼此的活动。Linux支持一定数量的进程间通信Inter-Process Communication(IPC).信号和管道是其中的两个,但是Linux也支持基于UNIX的系统V IPC。
有很多种类的进程间通信。它们在很多地方不同,包括它们的时效性。对一个自然数在两个进程之间的传输会受到影响。这些支持资源的共享,同步,无链接和链接的数据交换或者混合。同步机械用来排除种类的条件。
无链接和链接的数据交换不同于前两个,因为它们有不同的语义模型。在这些模型中,进程发送信息到一个进程或者一个特殊的进程组。
在链接数据交换中,连接通信的两个组必须要在通信开始之前设置一个廉洁。在无链接数据交换中,一个进程必须要至少发送一个数据包,提供一个目的地址或者一个信息类型,留给产业来传输它们。
表7.1给了你一个图表,展示Linux的进程间通信支持.

第一节 内核内同步
因为内核管理系统的资源,进程访问这些资源就必须要同步。一般来说,一个进程只要它执行一个系统调度的情况下不会被一个调度器中断。 这只在它locks或者调用schedule()的时候,允许其它进程的执行的情况下。然而,内核内的进程可以被中断处理进程中段:这将会在种类方面引起结果,如果进程不是在执行任何可以锁定文件的功能。
在多进程系统中,这个状况甚至更加复杂,几个进程可以在不同的处理器上同时执行。种类条件还会在不同的运行进程中发生。
表格7.1

1. Spin lock旋转锁
在多进程系统内的同步的基础也用在其它操作系统上,这叫做Spin lock.这些locks执行内核内进程的相互排斥。这个重要的一段之内被spin lock进程执行。这个概念也叫做mutex.在特殊的计算机架构上实现。一个x86多进程spin lock系统通过C数据类型spinlock_t定义.
Typedef struct{ volatile unsigned int lock; } spinlock_t
在默认的状态,不同的lock有值1。进程想要阻止spin lock设置lock为零,再lock非零的设置情况下。
Spin lock是Linux 内核自动同步。而且还有spin lock的优点,它们可以被应用在中断处理历程中。单进程系统不需要spin lock,所以适当的操作被设置为默认。
2. Read-write lock读写锁
读写锁是和旋转锁一样的可选项。它在一个进程要读或者写的时候设立。进程只能从资源中读取,不排除别的进程。然而写入排除其它所有的进程。在Linux内核,x86读写锁多进程系统通过rwlock_t代表数据结构:
Typedef struct {
Volatile unsigned int lock;
} rwlock_t;
在x86进程,在默认状态,锁在值RW_LOCK_BIAS(0X01000000)有所不同。写入锁试着通过从RW_LOCK_BIAS减法归零,然后等待循环,直到它成功。读取所要减去1,但结果不能是负数。在这种情况,等待在忙碌循环中执行。设置读写锁所用的时间比旋转锁用的时间要长。所以在读写的使用要比写入的使用更加频繁的时候最好用它们。
3.locking routines锁历程
Spin and read-write locks通过手优化集成历程最小化设置锁的过程操作,在设置锁成功的时候。
Spin locks的初始化是通过SPIN_LOCK_UNLOCKED宏,当RW_LOCK_UNLOCKED用作读写锁的时候。可以使用spin lock()或者spin_unlock()轻松的设置和解锁SPIN LOCKS。Spin_ock_irq()宏和spin_unlock_irq()允许终端当前进程。这些宏在中断异常的时候,在调用这些功能之前的时候就有了问题,因为终端通过spin_unlock_irq()来再次激活。这里使用spin_lock_irqsave()和spin_lock_irqstore()。它们保存本地处理器的中断状态避免中断恢复解锁。
Read_lock()和write_lock()宏都设置了读或者写的锁。解锁通过read_unlock()或者write_unlock()来进行。进行中断的变量和通过spin lock宏一样。
等待队列
4。等待队列
内核之内的进程需要等待一个特殊事件的发生时经常的事,就像块被写入硬盘一样。当前的进程应该阻拦允许其它的进程的执行。
在内核中,进程被锁住了,要在等待队列中等待特定的事件发生。进程要在等待队列中,不被中断,直到进程所在的等待队列被中断处理进程或者是其它的进程重新激活。
等待队列是一个双链接的循环指针列表,到进程表格并且与spin lock相连接。
如果内核程序员要使用等待队列,就要包括一个有wait_queue_head_t的种类区域在数据结构并且以一个init_waitqueue_head()功能初始它。在初始化之后,这个队列就是空的。内核添加一个进程(内核程序员也要调用任务)用两个步骤到等待队列:使用DECLARE_WAITQUEUE()宏声明并使用指针指向任务结构来初始化wait_queue_t数据结构。这样数据结构就被写入了数据结构通过add_wait_queue()的方法。可以决定是否一个乡已经在列表当中,通过waitqueue_active().使用remove_wait_queue()一个人物就可以被中断,历程就可以访问等待队列。
然而,使用的这些功能进程不能被锁住。Sleep_on()移动进程到TASK_UNINTERRUPTIBLE状态。在这个状态,进程不能被信号中断。功能interruptible_slppe_on()保持进程在TASK_INTERRUPTIBLE状态,所以信号就可以被进程激活。可以在一个特定的时间用sleep_on_timeout()和interruptible_sleep_on_timeout()锁住进程。
一个进程对视自己进入等待队列和从等待队列中删除负责。真实的sleep_on()功能使用宏来提高这些经常使用的历程的速度。可以写入可中断和不可中断进程到同一个等待队列。睡眠状态不能被信号中断,就会成为永久睡眠状态。然而,有一些宏可以叫醒这些进程。
Wake_up()宏叫醒等待队列中的进程,wake_up_nr()叫醒一定数量的进程,wake_up_all()叫醒等待队列中所有的进程。使用wake_up_interruptible(),wake_up_interruptible_nr(),和wake_up_interruptible_all()也可以局限叫醒等待队列中的可中断进程。
5。内核信号量
等待队列用来操作内核信号量。信号量是可以在任何时间增加的计算器,但是只可以在它们的值大于零的时候减少。如果不是这样,减少进程被阻拦,就进入了信号量等待队列。这个操作有Linux选择,比以下的稍微复杂一些:
Struct semaphore {
Atomic_t count;
Int sleepers;
Wait_queue_head_t wait;
};
可变的count是atomic_t的种类。只可能通过atomic操作访问。从内核程序员的观点来看,atomic操作就是不要领导人和种类的情况。多处理器硬件提供这些操作,使用更加复杂的cache-consistent协议。
当count支少于或者等于零的时候,Up()功能增加count并且叫醒所有沉睡的进程。如果count代表正确的信号量的值,如果count的值等于1,up()就应该叫醒沉睡的进程。然而,没有增加和测试值是1的原子操作。所以,如果进程在等待信号量、count在小于0的时候会被校正。
这就允许了功能down()简单的降低count并且在等待队列中睡觉,如果值少于0。这样,当所有这些进程在信号量等待队列中睡觉或者制造信号量的时候,down()修改sleepers。操作这个就是Sleepers成为1,如果可能,count就是-1。这样几个连贯的up()调用,只有第一个调用要叫醒一个等待队列中的进程。第一个进程醒来后,会叫醒下一个准备把count再次设为0的进程。


第二节 通过文件通信
通过文件通信其实是在程序之间交换数据的最古老的方法。程序A写数据到一个文件,程序B再从这个文件中读取数据。在一个系统当中,只有一个程序可以在一个时间内运行,这不会呈现任何问题。
在多任务系统中,两个程序都作为进程来运行,至少是彼此间类似于平行的进程。种类条件一般生成文件数据的一致性,结果就是在另一个完成修改之前,从一个程序读取数据区域,或者两个进程在同一内存区域同时修改。
这种情况调用锁。最简单的方法,当然会锁住整个文件。对于Linux,像其它Unix延展的系统一样,有一个设备区域。更加一般和更加时效性,然而,是锁住文件区域的实践。这个文件访问锁可以是托管的,也可以是咨询的。咨询锁在锁住文件之后继续允许读、写文件。
然而,锁相互的排斥彼此,基于对不同种类的谨慎的判断。托管锁不允许整个区域的读写操作。有咨询锁,所有的进程访问一个文件或者读写操作都要适当的设置锁并且可以再次解锁。然而,托管所没有提供更好的保护避免进程故障:如果进程有写入文件的权利,它们就可以通过写入及索取与制造不和谐。错误程序制造的问题可以引起托管所产生严重的问题。只要这个问题进程还在运行,锁文件就不能被修改。Linux支持托管锁,但是合适的内核配置参数是默认无效的。因为以上的原因POSIX1003.1不需要托管锁,这是一个完美地接受。
如果托管锁被生成的内核支持,每个文件都支持托管锁,组执行就一定没有被设置好,set-GDI设置。托管锁没有和文件映射的mmap()和MAP_SHARED标注功能。
我们可以通过系统调度flock()或者fcntl()锁住整个文件。第二个也合适锁住文件区域。你可以读man pages获取更多详情。Flock()紧紧支持咨询锁并且和锁fcntl()基于内核同样的数据结构。

第三节 管道
一般的Linux shells允许所有的重定向。比如:
$ ls | pr | lpr
通过Ls命令输出数据列出目录的文件到标准的输入pr命令可以给它们分页。最后标准的pc命令输出就是倒入lpr命令标准输入,在默认打印机上打印出结果。管道就是间接的比特流向,从进程连接标准的输出到另一个进程的标准输入。没有进程关心这个重定向,就像一般时候地反映一样。Shell设置这些即时的在进程之间的管道。
在Linux中,管道用两个file数据结构,指向同一个VFS节点,它自己指向内存中的一个物理页。图7.1展示了每一个file数据结构包括的指针,指向不同的文件操作历程矢量;一个是写入管道的,另外一个是从管道中读出的。
这个隐藏了它和一般系统调用读写一般文件重要的不同。写入进程写到管道,比特从共享数据页复制出来。Linux必须同步访问到管道。就要确定管道的读写在步骤当中,要读些就要使用锁,等待队列和信号。
当需要写入管道的时候,使用标准写入库功能。所有这些都传输文件描述表明进程的文件数据结构设置,每一个都代表一个打开的文件或者一个打开的管道。Linux系统调度使用文件数据结构描述管道指向的写进程。写进程使用VFS节点信息代表管道管理的写需求。
如果有足够的空间写出所有的比特到管道,只要管道没有被读者锁住,Linux为写锁住并且复制比特从另一个进程的地址空间到一个共享的数据页。如果管道用读者锁住了,或者不够空间写入数据,所以当前的进程就在管道节点等待队列转成睡眠状态,调度企业被调用,所以另外一个进程就可以运行。如果可以中断,所以就可以接收信号,并被另一个读者在有足够空间写入数据或者管道解锁的时候叫醒。当数据被写入之后,管道的VFS节点被解锁,任何写的读者在节点的等待队列睡觉,自己醒来。从管道读取数据和写入数据是非常相似的进程。
进程允许作出非拦截阅读(基于它们打开的文件或者管道的模式),这样,如果没有可读取的数据或者管道被锁住,就会反馈一个错误信息。这就是说进程可以继续运行。可以选择在管道节点等待队列等待直到写入进程结束。当进程结束了管道,管到节点也和共享数据页一起被删除。
Linux还支持命名的管道,就是FIFOs,因为管道操作是建立在“先进先出”的原则。第一个写入管道的数据就是第一个从管道中读取的数据。和管道不同FIFOs不是暂时的目标,它们是文件系统的整体,可以使用mkfifo命令新建。进程可以使用FIFO所以它们就可以有权利访问它。FIFO打开的方法和管道有所不同。管道(两个文件数据结构,它的VFS节点和共享数据)也被新建在一个已经存在并且打开,由用户关闭的。Linux必须处理作者在打开之前打开它,就像在任何作者写入之前读取一样。除了这一点,FIFOs被大多数同样的管道所处理,就像管道使用相同的数据结构和操作。
1.案例
有很多种方法使用Linux管道。第一种方法就是使用特殊种类的叫做FIFO的文件或者命名管道。命名了的管道(FIFOs)一般用命令“mkfifo”新建。
Mkfifo mypipe
通过mknod系统调度新建,在一个文件系统之内新建一个节点代表管道。任何进程都可以使用一般文件操作(打开,阅读,写入,关闭等等)写入数据到这个管道中,或者从这个管道中读取数据。
Ls –l | less
这个ls标准输出通过less管道标准输入连接。在ls和less之间有一个管到节点被新建,但是和命名管道不同,这个管道没有名字。因为命名过的管道是用mknod系统调度来新建的,这个种类的管道是用管道系统调用新建的。以下的就是是用管道系统调用的例子:

这个图代表了以上的案例C程序显示在图7.2的管道。使用管道需要几个解释。首先,管道系统调用必须在fork()系统调用之前调用,确定只有但管道被新建,两个院管道和新进程可以使用在相同的管道上。管道系统调度反馈一对文件描述,一个是读取,另外一个是写入。所以一个进程就可以在一个管道之内读写。然而,很多情况下,我们需要一个进程从管道中读取另一个进程写入,所以我们要手动关闭一个不使用的管道文件描述。我们用ls进程写入管道,所以我们关闭阅读描述器用以下的调度:

Close(fd[0])
相似的,wc进程只从管道中读取数据,我们用写入描写关闭:
Close(fd[1]);
下一步是重新定向wc进程的stdin,还有ls进程的stdout,从管道读写。这个通过使用dup2系统调用来完成,扶植一个文件描述器。我们复制读取描述器用stdin:
Dup2(fd[0]);
复制写描述器stdout
Dup2(fd[1],1);
在复制这些描述器之后,读/写就会在来/去到新建的管道。在dup2,我们就有了两个不同的文件描述器引用同一个管道的同一部分。所以我们关闭不需要的文件描述器用fd[0]和fd[1]在dup2调用之后。所以,在dup2调用之后还要关闭其它多余的调用。
最后,我们使用execve系统调用加载并执行ls程序,wc程序。因为我们已经重新定向ls的stdout到管道,ls写到管道而不是原始的stdout.相似,wc从管道中读出,而不是原始的stdin.我们成功的设置了一个管道,在ls和wc的程序之间,作出进程间通信。
图7.3


第四节 系统V IPC
Linux支持3各种类的进程间通信首先显示在Unix系统V(1983).这些信息队列,信号量和共享内存。这些系统V IPC共向所有的正版方法。进程仅仅可以通过传到一个特殊的代表识别器到内核,通过系统调用访问这些资源。访问这些系统 V IPC目标就是检查使用访问允许,就像检查访问文件。到系统V IPC目标的访问权限用系统调用生成。目标的引用识别器用每个机械向资源表格的索引一样。不是直接转到索引,而是需要一些索引生成的处理。
所有的Linux数据结构代表系统V IPC目标,包括ipc_perm结构,包含用户的和新建的进程使用和组识别。这个目标的访问模式(用户,组,和其它)IPC目标键。关键用来分配系统V IPC目标引用识别器。支持两个密码设置:公共和私有。如果公共密码,系统内所有进程,用来权限检查,都可以为系统V IPC目标找到引用识别器。系统V IPC目标从来不备密码引用,只有它们引用的识别器。
1.信息队列
信息队列允许一个或者更多的进程写入信息,被一个或者更多的进程读取。Linux保持一个信息队列的列表,msgque矢量;每一个点的元件到msqid_ds数据结构都完整地描述了信息队列。当信息队列新建msqid_ds数据结构生成,系统内存分配插入矢量。
每一个msqid_ds数据结构包括了ipc_perm数据结构和指针到这个队列的信息。也就是,Linux保持队列修改次数,和上一次这个队列写入的一样。Msqid_ds还包括两个等待队列;一个队列作者,一个是信息队列读者。
每一次一个进程要写信息到一个写队列的时候,它的有效拥护和组识别器都和ipc_perm数据结构队列模式相比。如果进程可以写到队列,那么信息就从进程的地址空间复制到msg数据结构,并且放在信息队列的最后。每个信息都用一个特别种类的应用程序标记,在合作的进程之间。然而,可能没有空间给Linux信息来约束数字和可以写入的信息长度。这样进程就会添加到这个信息队列等待队列,调度器也会被调用来选择一个新得进程来运行。当一个或者更多的信息要从这个信息中读出的时候就会醒来。
从队列中阅读是一个相似的进程。还有检查进程访问等待队列权限。阅读进程可以选择得到队列的第一个信息,不管它的种类,或者选择一个特别种类的信息。如果没有信息适合这个标准,读取的进程就会被添加到信息队列的阅读等待队列,调度器运行。当新的信息写入队列,这个进程就会醒来并且再次运行。
2.使用系统V信息队列的案例
这个描写使用系统v信息队列的段落是以用户层面的应用程序。注意所有的错误查询编码都用来保持最短。永远反馈错误查询值。我们首先要创建一个信息队列,用msgget功能:


以上的C程序使用了msgget来打开一个系统V信息队列。第一个msgget参数是一个密码。这个模拟命名文件打开文件。在几个进程当中访问同一个信息队列,每个进程都要有同样的密码。第二个参数使msgget的标注。我们用666允许打开信息队列(标注666),新建一个新的信息队列,如果队列没有相应的密码(IPC_CREAT).
在打开信息队列之后,我们就可以发送信息到信息队列。这样,我们要首先宣布信息缓存器struct msgbuf.第一个struct msgbuf区域必须是一个long mtype,第二个区域可以是一个字符数组。在我们的案例中,第二个区域是char mtext[msgsize],宣布最大信息长度使MSGSIZE(值为100)。第一个领域决定了信息的种类,任何正整数的是有效的。用户可以使用这个种类的区域来选择接受某一特定种类的信息。下一个区域就是信息的内容,可以是一个数据,任意长度。在我们的案例中,我们的信息是第一种类,信息内容是“Hello World”.
我们可以使用msgsnd来发送信息,信息将会保存在信息队列中,直到从队列中读取信息。来使用msgsnd我们就要传输信息队列识别(反馈msgget值),struct msgbuf,和信息内容长度一起。在我们的案例中,信息内容长度是连接“hello world”的,加上一个额外的字符给零字符。最有一个参数就是标注,我们没有标注来传输msgsnd。如果你需要没有拦截的msgsnd,你就要传输IPC_NOWAIT为标注。
读取信息队列中的信息,我们就需要msgrcv功能:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

以上的C程序适用msgrcv从系统V信息队列中读取信息。在一次我们使用msgget来打开信息队列,我们要宣布struct msgbuf。第一个msgrcv参数就是msgget反馈的信息队列的ID,第二个参数就是内存缓存struct msgbuf存储已读信息。下一个参数定义了缓存器可以存储的最大信息内容。如果信息在信息队列中太大,不能再缓存器的struct msgbuf中保存,msgrcv就会反馈一个错误。
第四个参数就是信息阅读种类,也就是mtype决定的在struct msgbuf领域的。在我们的案例中,我们不关心系种类,我们就要读取队列中的第一个信息,不管它是什么种类,所以我们传输0作为参数来表明我们不管信息的种类。
最后一个参数就是标注。如果你需要阅读非拦截信息队列,你就需要传输IPC_NOWAIT。在调用msgrcv后,我们就可以从缓存器中阅读信息种类和信息内容了。
3.信号量
最简单的格式就是信号量在内存中,它的值可以被多于一个进程来进行检查和设置。检查和设置操作,目前是每一个进程关心的,不可中断或这是原子;当开始了以后,就没有什么可以停下它。测试和设置操作的结果就是当前信号量的值和它的设置值。可以是正的,也可以是负的。基于测试和设置操作的结果,一个进程可以睡觉,直到信号量值被另一个进程改变。信号量可以用来操作cretical regions,重要的编码值可以被一个进程在一个时间内执行。

如果你有很多的合作进程从记录中阅读一个单数据文件。你就想要文件访问严谨一些。你应该使用有初始值为1的信号量,在文件操作编码周围,放置两个信号量操作,第一个测试信号量值的减少,第二个测试它的增加。第一个进程时访问文件可以试着降低信号量的值,如果结果是-1那么就失败。直到第一个进程结束数据文件的时候这个进程可以挂起。当地一个进程结束了数据文件,它就会提高信号量的值,是它再次成为1。现在等待进程苏醒,这一次它试着增加信号量的值。
系统V IPC信号量目标每一个描述信号量数组,Linux使用semid_ds数据结构代表它。所有的semid_ds数据结构在系统中都被semary指针指向,一个指针矢量。每一个信号等数组当中都有sem_nsems,每一个都用sem_base指针指向sem数据结构。所有的进程都被允许系统V IPC信号量目标操作信号量数组可以使系统调用的操作。这个系统调度可以分类一些操作,每一个操作都被三个输入描述;信号量索引,操作值和标注设置。信号量索引是一个在信号等数组的索引,操作值是一个数字,可以添加到当前的信号量值。第一个Linux测试是否所有的操作都能成功。一个操作在操作值添加到信号量的当前值,大于零或者两个操作值和信号量的当前值是零的时候成功。如果信号量操作失败,Linux就要挂起进程,如果操作标注没有要求系统调用非拦截。如果进程被挂起,Linux就要保存信号量操作的状态,并把当前的进程放在等待队列。这样来建立sem_queue数据结构在堆栈上面,并且填满它。新的sem_queue数据结构在信号量目标等待队列的最后(使用sem_pending和sem_pending_last指针)。当前的进程放在等待队列,在sem_queue数据结构,调度器调用选择另一个进程来运行。
在所有的信号量操作之中,当前进程不被挂起就是成功,Linux应用操作在适当的信号量数组中。现在Linux必须检查等待,挂起,进程现在应用它们的信号量操作。看每一个操作等待队列(sem_pending)的成员,测试是否信号量操作者一次可以成功。如果成功就从操作等待列表中删除sem_queue数据结构,应用信号量操作到信号等数组。叫醒睡着的进程使之可以在下一次调度器运行的时候被重新启动。Linux保持查找等待列表,在没有信号量操作可以被应用,所以没有进程可以被叫醒。
有个信号量的问题,deadlocks。当一个进程变更信号量值得时候会发生,它进入一个重要的区域但是不能离开这个地方,因为它会崩溃或者被杀死。Linux通过保持调整信号量数组的列表来阻止这种事情的发生。这个主意是在这些调整被应用的时候,信号量就会还原以前的状态,在进程的信号量操作设置应用之前的状态。这些调整被保存在sem_undo数据结构队列中,和semid_ds数据结构和task_struct数据结构一起为进程使用这些信号等数组。
每一个独立的信号量操作需要调整被保存。Linux要保证每个进程的信号量数组有最多一个sem_undo数据结构。如果需要的进程没有,那么就要在需要的时候新建一个。新的sem_undo数据结构在进程的task_struct数据结构和信号等数组的semid_ds数据结构上排队。如同操作是应用在信号量在信号等数组,那么操作值的附属酒家到信号量项,在进程sem_undo数据结构的调整数组中。所以,如果操作值是2,那么-2就被添加到信号量调整项。
当进程被删除,推出Linux工作通过sem_undo数据结构设置应用到调整信号量数组。如果信号量设置被删除,sem_undu数据结构就排队在进程的task_struct但是信号量数组识别器就成为无效的。这样信号量就清除代码简单的删除sem_undo数据结构。
4.使用系统V信号量的案例
这一段就描述了使用系统V信号量在用户层面的应用。注意所有的错误查询代码都用来保持案例尽量的短。错误查询反馈值总是完成。我们就可以首先新建一个有semget和用semctl调用初始化它的信号量设置:

以上的C程序使用semget打开信号量设置。第一个semget参数就是信号量密码。这个是在文件打开状态给文件名。在几个进程中使用同一个信号量设置,每一个进程都要给出同一个信号量密码。
第二个参数就是信号量设置的大小。在我们的案例当中,信号量设置有2个成员。最后一个参数就是semget的标注。我们得到有666允许的信号量设置,并且新建一个新的信号量设置,在信号量不存在的密码之中(IPC_CREAT)。
下一步就是初始化信号量设置的值。我们要初始化2个成员,使用SETALL命令给semctl:
Unsigned short initval[]={2,2};

Semun.array=initval;
Semctl(sem,0,SETALL,semun);
根据semctl的主业,我们要确定放置初始化值得数组集合。我们初始化两个成员的值为2。在初始化之后,我们就可以在一个信号等上面操作了。
让我们看看进程可以在信号量上面操作的不同:

让我们命名这个程序为prog1.以上的程序要得到(-2,0)操作在2个成员在信号量设置。本来,信号量有(2,2)的值。在运行prog1之后,值变成了(0,2)。以下的另一个程序和prog1类似,但是得到(0,-2)操作。我们把它命名为prog2。

在运行了prog1和prog2之后,信号量设置成为(0,0)。如果我们再次运行prog1或者prog2,程序就会被拦截,等待。这是因为不管是prog1还是prog都要等到其它的进程释放信号量之后。(信号量可以通过正整数在sem_op释放)。该操作的见解可以在图7.5中看到。
删除信号量设置(虚拟删除一个文件),我们可以使用命令“ipcrm”:
Ipcrm –S 1234
这个命令删除有1234的信号量设置。


5.共享内存
共享内存允许一个或者以上的进程通过内存交流,在所有的虚拟地址空间之内显示的进程。虚拟内存页被页表格项引用到每一个共享进程页表格。它不需要在所有进程的虚拟内存的同一个地址。和所有的系统V IPC目标一样,访问共享内存空间通过密码控制,访问权限检查。当内存被共享的时候,不用检查进程如何使用它。它们必须依赖其它的机械,比如系统V信号量,同步访问内存。
每一个新建的共享内存区域都用shmid_ds数据结构代表。这些保存在shm_segs矢量当中。

Shmid_ds数据结构描述了共享内存的空间,有多少进程在使用它,和共享内存如何映射到地址空间。是共享内存的创造者,控制共享内存的访问权限,是否这个密码是公共的或者私有的。如果它有足够的访问权限,就可以锁住共享内存到物理内存。
每一个进程都希望通过系统调度分享内存。这新建了一个新的vm_area_struct数据结构描述进程的共享内存。这个进程可以选择它的虚拟地址空间共享内存在哪里,或者是否让Linux选择一个足够大的空闲区域。新的vm_area_struct结构被放在vm_area_struct表格,被shmid_ds指针指向。Vm_next_shared和vm_prev_shared指针用来连接它们。虚拟内存不能再过程中新建;当地一个进程试着访问它的时候发生。
第一次进程访问一个共享虚拟内存页,一个页面错误就会发生。当Linux调整了页面错误就会发现vm_area_struct数据结构描述它。它包括处理历程的指针,给这个种类的共享虚拟内存。这个共享内存页面错误处理编码再页面表格列表项,给这个shmid_ds看一个存在在这个页面的共享虚拟内存。如果没有存在,它就分配一个物理页并且新建一个也表格项。就想到当前进程的也表格一样,这个项保存在shmid_ds当中。这就意味着当下一个进程访问这个内存的时候,得到一个叶面错误,共享内存错误处理代码会使用这个新的物理也给这个进程。所以第一个进程访问一个共享内存的页面就导致了它被创建,并被其它的进程访问,使页面被添加到虚拟地址空间。
当进程不再分享虚拟内存,它们就从中分离。所以其它进程还使用内存,所以撤销的进程只对当前进程有所影响。它的vm_area_struct从shmid_ds数据结构中删除,并且被回收。当前进程的也表格被升级到有效的虚拟内存区域。当最后一个进程共享内存并从中撤销的时候,在物理内存之内的当前共享内存项就从中释放,像共享内存中的shmid_ds数据结构。
更多的复杂情况在共享虚拟内存没有锁在物理内存的时候产生。在这种情况下,共享内存可以交换到系统地交换磁盘在高内存使用的期间。
6.使用V共享内存的案例
这一段描述了用户层面使用系统V共享内存的应用。注意所有的错误查询代码都保持最短。错误查询反馈值完成。我们先新建一个共享内存,用shmget功能:

Return 0;
}
以上的C程序使用了shmget打开一个系统V共享内存。第一个shmget参数是密码。这个模拟给一个打开的文件命名。在几个进程中访问同一个共享内存,每一个进程都要有同样的密码。第二个参数是共享内存用比特代表的长度。在我们的案例中,我们打开一个100比特的共享内存。最后的参数使shmget的标注。我们打开一个共享内存用666标注,并新建一个共享内存如果共享内存有合适的没有存在的密码(IPC_CREAT)。
在打开一个共享内存之后,我们要联结一个共享内存用shmat功能,所以我们就访问共享内存通过一个普通的指针。第一个shmat参数是shmget的共享内存ID反馈,第二个参数需要内存第一的添加。因为我们不管内存地址,我们就用NULL。最后一个参数使shmat的标注。我们没有更多的标注所以在这里给出0。
在田家内存之后,我们可以访问共享内存,用同样的访问进程内存的方法。在我们的案例当中,我们使用strcpy复制一个连接到地址,共享内存添加的。
在使用共享内存之后,我们要使用shmdt分离共享内存,分离功能告诉系统共享内存不再被这个进程需要了,调整共享内存的引用计数。
这里,我们给出另一个相似的程序访问共享内存,用同样的方法,除了度和现实共享内存的内容。


这个程序用shmget和shmat同样的方法,使用printf显示共享内存中的内容。如果你运行第一个程序复制“Hello World!!!”连接到共享内存,你就能看到链接的“Hello World!!!”出现。

第五节IPC和sockets
现在,我们只要看看进程间通信的格式支持进程在一台计算机中的通信。Socket边城界面提供了通过网络的通信,和本地在一台单机上的通信。这个界面的优点就在于它允许网络应用程序被编程,使用文件描述的long-established Unix概念。一个特别的好的案例就是它的INET魔鬼。魔鬼等待输入的网络服务需求然后调用相应的服务程序和socket描述,为标准的输入输出。每一个简单的服务,程序调用都不需要包含单线的网络相关代码。
第六节 练习
在Linux下,大多数用户间相互作用和计算机都通过用户层面命令翻译器,也就是叫做shell的媒体(csh,ksh),隐藏一些用户/操作系统界面的细节。现在我们要写入一些可选的Shell,一个用户程序和使用系统调用复杂。
Shell中你写的命令很简单。命令行格式和其它的shell不同。格式就是:
<line>::=[inputfile]”>”[<command>][“>”<command>]*”>”[outputfile]”\n”
<command>::=command_name[<arg>]*|””(empty string)
<inputfule>::=”<string with no blanks, tabs,or>”
<outputfule>::=”string with no blanks,tabs,or>”
<command_name>::=”string with no blanks, tabs,or>”
<arg>::=”string with no blanks,tabs,or>”
以上的定义可以被翻译为汉语:一个行可以时空的,或者有命令被“>”分开,表示重新定向。注意有至少两个“>”必须代表每一行所以你就可以知道那个字符串是inputfile,哪个是outputfile.第一个“〉”在一个文件名前面,意思是重定向第一个命令到inputfule的stin;如果没有特定的文件名,使用标准输入。跟踪的“>”就被一个文件名跟踪,意思是重新定向stdout最后一个命令到outputfile;如果没有outputfule,output就成为标准的输出。在这一行,每一个“>”表示重新定向通过一个管道(所以你需要学习管道和其它相关的系统调度)。
一个命令的名称可以用可选的参数代表。简单来说,你不需要检查“*”这种字符。空格和/或者表格键这种分开文件名称的命令,args,和”>”如果一个命令名称是“〉〉”shell操作如果命令cat出现。
这里有一些合法的命令行的案例,还有它们相对应的标准Linux shell:
我们的Shell 标准Linux Shell
〉〉 Cat
>>> Cat or cat|cat
Ifile>wc>ofile Wc<ifile>ofile
>ls –l> Ls –l
>foo arg1 arg2 >pr>lpr Foo arg1 arg2|pr|lpr
查找命令:简化这个Shell的一部分,命令必须在3个地方的一个:当前的目录(.),/bin, or /usr/bin。两个进入这个部分的方法。简单的进入就是执行命令在这些目录顺序中,如果失败,就试下一个目录等等。还有很多又取得进入来查找命令名称,在这些目录中,使这执行命令查找。(你可能需要了解环境变量在C和Linux)
I/O重定向:设置标准输入和标准输出文件,用一个需要执行的命令,shell要改变自己的stdin和stdout,新建另一个进程,然后设置回状态到original.有用的进程系统调用就是dup(),dup2(),open()
输入重定向和输出通过命令管道线以非常相似的方法处理。不是表现调度open()得到重定向数据,相应的调用是pipe(),返回两个文件描述,一个打开阅读另一个写入。数据写到可写入文件描述可以被从可读管道中读取(其它的文件描述是通过pipe()返回)。通过设置shell的stdin和stdout,并且独立执行命令通过命令行,管道行就被设置好,和每一个命令被下一个命令的管道行的output。
进程相关的:在Linux,新进程的建立有两个步骤,使用系统调用fork()和execve().由execve()的变量;很多合适的操作execve(),但是不是需要的。
系统调用execve()用来执行一个命令。不论什么时候进程使用execve()系统调度,基本上是在毁灭自己,用另一个进程的命令执行。如果调用到execve()失败(比如命令不能在目录中找到),调用进程继续跟踪execve()。你也需要使用exit()和wait()系统调用。
历史:一个有用的shell特点就是它可以记录之前的输入名。比如,一些shell保存以上N条输入命令。我们用这个特点在我们简单得shell当中。以下的就是一些使用历史的命令:
% h // should display up to N previous input commands
% hj//should re-execute the jth input command. The
Syntax should be similar to the conventional shell like(!j).
你可以允许历史共享两个Shell.Conventional shell只记录之前自己调用的的输入命令。 。就是在中断,一个shell值记录历史特别的中断而不是别的。在我们的案例中,我们要我们自己的简单shell来记录所有的输入命令历史,即使它们在其它的shell.这样,你可以开始两个shells,S1和S2,系统就会保存N1+N2的输入命令,发现这个特点,我们就是用共享内存特点给进程间通信,就像信号量特点,以之避免种类的不同。


saltlightlove
saltlightlove
Latest page update: made by saltlightlove , Sep 6 2006, 3:41 AM EDT (about this update About This Update saltlightlove Edited by saltlightlove

1227 words added

view changes

- complete history)
Keyword tags: None
More Info: links to this page

Anonymous  (Get credit for your thread)


There are no threads for this page.  Be the first to start a new thread.