第一周:http://user.qzone.qq.com/631467229/main
第二周至第八周:http://blog.csdn.net/sinat_34144680
第一周:计算机是如何工作的
- //在64位环境下编译成32位的汇编
- gcc -S -o ccode32.s ccode.c -m32
- //链接时会缺少构建32 位可执行程序缺少的包,使用以下指令安装:
- sudo apt-get install libc6-dev-i386
- //编译链接成32位的可执行文件
- gcc -o ccode32 ccode.c -m32
第二周:操作系统是如何工作的
计算机是如何工作的?(总结)——三个法宝
-
存储程序计算机工作模型,计算机系统最最基础性的逻辑结构;
-
函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;
-
enter
-
pushl %ebp
-
movl %esp,%ebp
-
-
leave
-
movl %ebp,%esp
-
popl %ebp
-
-
函数参数传递机制和局部变量存储
-
-
中断,多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。
打开shell
- cd LinuxKernel/linux-3.9.4
- qemu -kernel arch/x86/boot/bzImage
-
打开shell
- cd LinuxKernel/
- qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
-
使用自己的Linux系统环境搭建MenuOS的过程
- # 下载内核源代码编译内核
- cd ~/LinuxKernel/
- wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
- xz -d linux-3.18.6.tar.xz
- tar -xvf linux-3.18.6.tar
- cd linux-3.18.6
- make i386_defconfig
- make # 一般要编译很长时间,少则20分钟多则数小时
- # 制作根文件系统
- cd ~/LinuxKernel/
- mkdir rootfs
- git clone https://github.com/mengning/menu.git # 如果被墙,可以使用附件menu.zip
- cd menu
- gcc -o init linktable.c menu.c test.c -m32 -static –lpthread
- cd ../rootfs
- cp ../menu/init ./
- find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
- # 启动MenuOS系统
- cd ~/LinuxKernel/
- qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
-
重新配置编译Linux使之携带调试信息
-
在原来配置的基础上,make menuconfig选中如下选项重新配置Linux,使之携带调试信息
-
- kernel hacking—>
- [*] compile the kernel with debug info
-
make重新编译(时间较长)
-
使用gdb跟踪调试内核
- qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
- # -S freeze CPU at startup (use ’c’ to start execution)
- # -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
另开一个shell窗口
- gdb
- (gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
- (gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
- (gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后
Linux内核启动过程相关的参考资料
-
计算机的启动过程概述
-
x86 CPU启动的第一个动作CS:EIP=FFFF:0000H(换算为物理地址为000FFFF0H,因为16位CPU有20根地址线),即BIOS程序的位置。http://wenku.baidu.com/view/4e5c49eb172ded630b1cb699.html
-
BIOS例行程序检测完硬件并完成相应的初始化之后就会寻找可引导介质,找到后把引导程序加载到指定内存区域后,就把控制权交给了引导程序。这里一般是把硬盘的第一个扇区MBR和活动分区的引导程序加载到内存(即加载BootLoader),加载完整后把控制权交给BootLoader。
-
引导程序BootLoader开始负责操作系统初始化,然后起动操作系统。启动操作系统时一般会指定kernel、initrd和root所在的分区和目录,比如root (hd0,0),kernel (hd0,0)/bzImage root=/dev/ram init=/bin/ash,initrd (hd0,0)/myinitrd4M.img
-
内核启动过程包括start_kernel之前和之后,之前全部是做初始化的汇编指令,之后开始C代码的操作系统初始化,最后执行第一个用户态进程init。
-
一般分两阶段启动,先是利用initrd的内存文件系统,然后切换到硬盘文件系统继续启动。initrd文件的功能主要有两个:1、提供开机必需的但kernel文件(即vmlinuz)没有提供的驱动模块(modules) 2、负责加载硬盘上的根文件系统并执行其中的/sbin/init程序进而将开机过程持续下去。
-
一、用户态、内核态和中段
1.1 用户态、内核态和中段处理过程
(1)系统调用:库函数把系统调用封装起来。
(2)用户态:低级别执行条件下,执行语句。(3级)cs,eip只能0x-0xbffff的地址空间.。
(3)内核态:高的执行条件下,代码执行特权指令,访问任意物理地址,CPU的执行级别对相内核态。(0级)访问cs,eip的值,任意的地址(逻辑地址),0xc以上的只能内核访问。
(4)为什么有级别划分:为了让系统更稳定,用户态不会让系统崩溃。
(5)中段处理:是从用户态进入到内核态的主要方式。系统调用知识一种特殊的中断。中段发生后的第一件事就是保存现场。中段处理结束前的最后一件事是恢复现场。
(6)从用户态到内核态的转变过程:
用户态栈顶地址、当前文字、当时cs:eip的值保存。
内核态栈顶地址、当前文字、当时cs:eip的值激活。
(7)保存现场就是进入中断程序,保存需要保存的寄存器数据。恢复现场就是退出中断程序,恢复保存寄存器的数据。
(8)中段处理的过程:
首先保存cs:eip的值;保存当前堆栈段、栈顶、标志寄存器;加载系统调用或者系统调用的中段服务历程入口。
之后执行内核代码、完成中断服务、发生进程调度。
二、系统调用概述
2.1 系统调用概述和系统调用的三层皮
(1)系统调用意义
由操作系统管理硬件,防止用户态进程把系统搞崩溃。
用户程序和具体的硬件接口替代,不会和硬件有太多的关系。
(2)操作系统提供的API和系统调用的关系
把系统调用封装成API,API只是一个函数,
Libc发布系统调用。定义一些API引用封装例程。
(3)一个单独的API可能调用几个系统调用,不同的API可能调用一个系统调用。
(4)系统调用的三层皮:xyz(应用程序封装接口API)、system_call(内核代码中断向量对应的入口起点)和sys_xyz(内核态)。
(5)系统调用号:使用eax寄存器进行传递;系统调用参数传递:每个参数长度不超过寄存器长度,参数长度不超过6个,超过6个把某一个寄存器设为指针作为内存进行传递。
三、使用库函数API和C代码中嵌入汇编代码触发同一个系统调用
3.1使用库函数调用系统的时间
第五周:扒开操作系统的三层皮(下)
①给MenuOS增加time和time-asm命令
rm menu -rf //强制删除当前menu
git clone http://github.com/mengning/menu.git //重新克隆新版本的menu
cd menu
ls
make rootfs
vi test.c //进入test.c文件
MenuConfig("getpid","Show Pid",Getpid);
MenuConfig("getpid_asm","Show Pid(asm)",GetpidAsm); //在main函数中增加MenuConfig()
int Getpid(int argc,char *argv[]);
int GetpidAsm(int argc,char *argv[]); //增加对应的Getpid和GetpidAsm两个函数
make rootfs //编译
②使用gdb跟踪系统调用内核函数sys_time
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
gdb
(gdb)file linux-3.18.6/vmlinux
(gdb)target remote:1234 //连接到需要调试的MenuOS
(gdb)b start_kernel //设置断点
(gdb)c //执行,可见程序在start_kernel处停下
list //可查看start_kernel的代码
(gdb)b sys_time //sys_time是13号系统调用对应的内核处理函数,在该函数处设置断点
(gdb)c
③系统调用在内核代码中的工作机制和初始化
系统调用在内核代码中的工作机制和初始化
简化后便于理解的system_call伪代码
简单浏览system_call和iret之间的主要代码
第六周:进程的描述和进程的创建
进程控制块PCB——task_struct
为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。
-
struct task_struct数据结构很庞大
-
Linux进程的状态与操作系统原理中的描述的进程状态似乎有所不同,比如就绪状态和运行状态都是TASK_RUNNING,为什么呢?
-
进程的标示pid
-
所有进程链表struct list_head tasks;
-
程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系
-
Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info和进程的内核堆栈
-
进程处于内核态时使用, 不同于用户态堆栈,即PCB中指定了内核栈,那为什么PCB中没有用户态堆栈?用户态堆栈是怎么设定的?
-
内核控制路径所用的堆栈 很少,因此对栈和Thread_info 来说,8KB足够了
-
fork一个子进程的代码
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- int main(int argc, char * argv[])
- {
- int pid;
- /* fork another process */
- pid = fork();
- if (pid < 0)
- {
- /* error occurred */
- fprintf(stderr,"Fork Failed!");
- exit(-1);
- }
- else if (pid == 0)
- {
- /* child process */
- printf("This is Child Process!\n");
- }
- else
- {
- /* parent process */
- printf("This is Parent Process!\n");
- /* parent will wait for the child to complete*/
- wait(NULL);
- printf("Child Complete!\n");
- }
- }
创建一个新进程在内核中的执行过程
-
fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;
-
Linux通过复制父进程来创建一个新进程,那么这就给我们理解这一个过程提供一个想象的框架:
-
复制一个PCB——task_struct
- err = arch_dup_task_struct(tsk, orig);
-
要给新进程分配一个新的内核堆栈
- ti = alloc_thread_info_node(tsk, node);
- tsk->stack = ti;
- setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈
-
要修改复制过来的进程数据,比如pid、进程链表等等都要改改吧,见copy_process内部。
-
-
从用户态的代码看fork();函数返回了两次,即在父子进程中各返回一次,父进程从系统调用中返回比较容易理解,子进程从系统调用中返回,那它在系统调用处理过程中的哪里开始执行的呢?这就涉及子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题,这是在哪里设定的?copy_thread in copy_process
- *childregs = *current_pt_regs(); //复制内核堆栈
- childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因!
- p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
- p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址
可执行文件的创建——预处理、编译和链接
- shiyanlou:~/ $ cd Code
- shiyanlou:Code/ $ vi hello.c
- shiyanlou:Code/ $ gcc -E -o hello.cpp hello.c -m32
- shiyanlou:Code/ $ vi hello.cpp
- shiyanlou:Code/ $ gcc -x cpp-output -S -o hello.s hello.cpp -m32
- shiyanlou:Code/ $ vi hello.s
- shiyanlou:Code/ $ gcc -x assembler -c hello.s -o hello.o -m32
- shiyanlou:Code/ $ vi hello.o
- shiyanlou:Code/ $ gcc -o hello hello.o -m32
- shiyanlou:Code/ $ vi hello
- shiyanlou:Code/ $ gcc -o hello.static hello.o -m32 -static
- shiyanlou:Code/ $ ls -l
- -rwxrwxr-x 1 shiyanlou shiyanlou 7292 3\u6708 23 09:39 hello
- -rw-rw-r-- 1 shiyanlou shiyanlou 64 3\u6708 23 09:30 hello.c
- -rw-rw-r-- 1 shiyanlou shiyanlou 17302 3\u6708 23 09:35 hello.cpp
- -rw-rw-r-- 1 shiyanlou shiyanlou 1020 3\u6708 23 09:38 hello.o
- -rw-rw-r-- 1 shiyanlou shiyanlou 470 3\u6708 23 09:35 hello.s
- -rwxrwxr-x 1 shiyanlou shiyanlou 733254 3\u6708 23 09:41 hello.static
-
- shiyanlou:Code/ $ readelf -h hello
-
查看该ELF文件依赖的共享库
- shiyanlou:sharelib/ $ ldd main
- linux-gate.so.1 => (0xf774e000) # 这个是vdso - virtual DSO:dynamically shared object,并不存在这个共享库文件,它是内核的一部分,为了解决libc与新版本内核的系统调用不同步的问题,linux-gate.so.1里封装的系统调用与内核支持的系统调用完全匹配,因为它就是内核的一部分嘛。而libc里封装的系统调用与内核并不完全一致,因为它们各自都在版本更新。
- libshlibexample.so => /home/shiyanlou/LinuxKernel/sharelib/libshlibexample.so (0xf7749000)
- libdl.so.2 => /lib32/libdl.so.2 (0xf7734000)
- libc.so.6 => /lib32/libc.so.6 (0xf7588000)
- /lib/ld-linux.so.2 (0xf774f000)
- shiyanlou:sharelib/ $ ldd /lib32/libc.so.6
- /lib/ld-linux.so.2 (0xf779e000)
- linux-gate.so.1 => (0xf779d000)
- # readelf -d 也可以看依赖的so文件
- shiyanlou:sharelib/ $ readelf -d main
- Dynamic section at offset 0xf04 contains 26 entries:
- 0x00000001 (NEEDED) 共享库:[libshlibexample.so]
- 0x00000001 (NEEDED) 共享库:[libdl.so.2]
- 0x00000001 (NEEDED) 共享库:[libc.so.6]
- 0x0000000c (INIT) 0x80484f0
- 0x0000000d (FINI) 0x8048804
- 0x00000019 (INIT_ARRAY) 0x8049ef8
可执行程序的执行环境
-
命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
-
$ ls -l /usr/bin 列出/usr/bin下的目录信息
-
Shell本身不限制命令行参数的个数, 命令行参数的个数受限于命令自身
-
例如,int main(int argc, char *argv[])
-
又如, int main(int argc, char *argv[], char *envp[])
-
-
Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
-
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
-
库函数exec*都是execve的封装例程
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- int main(int argc, char * argv[])
- {
- int pid;
- /* fork another process */
- pid = fork();
- if (pid<0)
- {
- /* error occurred */
- fprintf(stderr,"Fork Failed!");
- exit(-1);
- }
- else if (pid==0)
- {
- /* child process */
- execlp("/bin/ls","ls",NULL);
- }
- else
- {
- /* parent process */
- /* parent will wait for the child to complete*/
- wait(NULL);
- printf("Child Complete!");
- exit(0);
- }
- }
-
-
-
命令行参数和环境串都放在用户态堆栈中
-
装载时动态链接和运行时动态链接应用举例
动态链接分为可执行程序装载时动态链接和运行时动态链接,如下代码演示了这两种动态链接。
-
准备.so文件
shlibexample.h (1.3 KB) - Interface of Shared Lib Example
shlibexample.c (1.2 KB) - Implement of Shared Lib Example
编译成libshlibexample.so文件
- $ gcc -shared shlibexample.c -o libshlibexample.so -m32
dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example
dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example
编译成libdllibexample.so文件
- $ gcc -shared dllibexample.c -o libdllibexample.so -m32
-
分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件
main.c (1.9 KB) - Main program
编译main,注意这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl
- $ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32
- $ export LD_LIBRARY_PATH=$PWD #将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。
- $ ./main
- This is a Main program!
- Calling SharedLibApi() function of libshlibexample.so!
- This is a shared libary!
- Calling DynamicalLoadingLibApi() function of libdllibexample.so!
- This is a Dynamical Loading libary!
可执行程序的装载
-
命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
-
Shell本身不限制命令行参数的个数, 命令行参数的个数受限于命令自身
-
例如,int main(int argc, char *argv[])
-
又如, int main(int argc, char *argv[], char *envp[])
-
-
Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
-
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
-
库函数exec*都是execve的封装例程
-
-
-
sys_execve内部会解析可执行文件格式
-
do_execve -> do_execve_common -> exec_binprm
-
search_binary_handler符合寻找文件格式对应的解析模块,如下:
- 1369 list_for_each_entry(fmt, &formats, lh) {
- 1370 if (!try_module_get(fmt->module))
- 1371 continue;
- 1372 read_unlock(&binfmt_lock);
- 1373 bprm->recursion_depth++;
- 1374 retval = fmt->load_binary(bprm);
- 1375 read_lock(&binfmt_lock);
-
对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读
-
Linux内核是如何支持多种不同的可执行文件格式的?
- 82static struct linux_binfmt elf_format = {
- 83 .module = THIS_MODULE,
- 84 .load_binary = load_elf_binary,
- 85 .load_shlib = load_elf_library,
- 86 .core_dump = elf_core_dump,
- 87 .min_coredump = ELF_EXEC_PAGESIZE,
- 88};
- 2198static int __init init_elf_binfmt(void)
- 2199{
- 2200 register_binfmt(&elf_format);
- 2201 return 0;
- 2202}
-
-
修改int 0x80压入内核堆栈的EIP
进程的调度时机与进程的切换
操作系统原理中介绍了大量进程调度算法,这些算法从实现的角度看仅仅是从运行队列中选择一个新进程,选择的过程中运用了不同的策略而已。
对于理解操作系统的工作机制,反而是进程的调度时机与进程的切换机制更为关键。
进程调度的时机
-
中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
-
内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
-
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
进程的切换
-
为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换;
-
挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行;
-
进程上下文包含了进程执行需要的所有信息
-
用户地址空间: 包括程序代码,数据,用户堆栈等
-
控制信息 :进程描述符,内核堆栈等
-
硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)
-
-
schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换
-
next = pick_next_task(rq, prev);//进程调度算法都封装这个函数内部
-
context_switch(rq, prev, next);//进程上下文切换
-
switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程
- 31#define switch_to(prev, next, last) \
- 32do { \
- 33 /* \
- 34 * Context-switching clobbers all registers, so we clobber \
- 35 * them explicitly, via unused output variables. \
- 36 * (EAX and EBP is not listed because EBP is saved/restored \
- 37 * explicitly for wchan access and EAX is the return value of \
- 38 * __switch_to()) \
- 39 */ \
- 40 unsigned long ebx, ecx, edx, esi, edi; \
- 41 \
- 42 asm volatile("pushfl\n\t" /* save flags */ \
- 43 "pushl %%ebp\n\t" /* save EBP */\
- 44 "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \
- 45 "movl %[next_sp],%%esp\n\t" /* restore ESP */ \
- 46 "movl $1f,%[prev_ip]\n\t" /* save EIP */\
- 47 "pushl %[next_ip]\n\t" /* restore EIP */ \
- 48 __switch_canary \
- 49 "jmp __switch_to\n" /* regparm call */\
- 50 "1:\t" \
- 51 "popl %%ebp\n\t" /* restore EBP */ \
- 52 "popfl\n" /* restore flags */ \
- 53 \
- 54 /* output parameters */ \
- 55 : [prev_sp] "=m" (prev->thread.sp), \
- 56 [prev_ip] "=m" (prev->thread.ip), \
- 57 "=a" (last), \
- 58 \
- 59 /* clobbered output registers: */ \
- 60 "=b" (ebx), "=c" (ecx), "=d" (edx), \
- 61 "=S" (esi), "=D" (edi) \
- 62 \
- 63 __switch_canary_oparam \
- 64 \
- 65 /* input parameters: */ \
- 66 : [next_sp] "m" (next->thread.sp), \
- 67 [next_ip] "m" (next->thread.ip), \
- 68 \
- 69 /* regparm parameters for __switch_to(): */ \
- 70 [prev] "a" (prev), \
- 71 [next] "d" (next) \
- 72 \
- 73 __switch_canary_iparam \
- 74 \
- 75 : /* reloaded segment registers */ \
- 76 "memory"); \
- 77} while (0)
-
Linux系统的一般执行过程
最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程
-
正在运行的用户态进程X
-
发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).
-
SAVE_ALL //保存现场
-
中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
-
标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
-
restore_all //恢复现场
-
iret - pop cs:eip/ss:esp/eflags from kernel stack
-
继续运行用户态进程Y
几种特殊情况
-
通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
-
内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
-
创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;
-
加载一个新的可执行程序后返回到用户态的情况,如execve;
安装和登录命令:login、shutdown、halt、reboot、mount、umount、chsh
文件处理命令:file、mkdir、grep、dd、find、mv、ls、diff、cat、ln
系统安全相关命令:passwd、su、umask、chgrp、chmod、chown、chattr、sudo、pswho
一定要养成在命令行下工作的习惯,要知道X-window只是运行在命令行模式下的一个应用程序。在命令行下学习虽然一开始进度较慢,但是熟悉后,您未来的学习之路将是以指数增加的方式增长的。从网管员来说,命令行实际上就是规则,它总是有效的,同时也是灵活的。即使是通过一条缓慢的调制解调器线路,它也能操纵几千公里以外的远程系统。 要增加自己Linux的技能,只有通过实践来实现了。对Linux命令熟悉后,可以开始搭建一个小的Linux网络,这是最好的实践方法。Linux是网络的代名词,Linux网络服务功能非常强大,不论是邮件服务器、Web服务器、DNS服务器等都非常完善。