linux内核以及GUN工具组成了linux系统(还有窗口管理软件、应用软件)
内核主要负责以下四种功能:
- 系统内存管理(内核通过硬盘上的存储空间来实现虚拟内存,这块区域称为交换空间。内核不断地交换空间和实际的物理内存之间反复交换虚拟内存中的内容。这使得系统以为它拥有比物理内存更多的可用内存。内核会维护一个内存页面表,指明哪些页面位于物理内存内,哪些页面被交换到了磁盘上。运行中的Linux系统,总是在交换空间和物理内存间进行着交换,并且自动把一段时间未访问的内存页面复制到交换空间区域)
- 软件程序管理(内核控制着linux系统如何管理运行在系统上的所有进程。内核创建了第一个进程【init进程】来启动系统上所有其他进程。内核在启动任何其他进程时,都会在虚拟内存中给新进程分配一个专有区域来存储进程用到的数据和代码)(Linux通常使用一个表来管理在系统开机时要自启动的进程,这个表通常位于etc/inittab文件,另外一些系统(比如流行的Ubuntu Linux)则采用/etc/init.d目录,将开机时启动或停止某个应用的脚本放在这个目录下。这些脚本通过/etc/rcX.d目录下的入口(entry)启动,这里的X代表运行级。linux操作系统有5个启动运行级,不同的运行级,自启动的进程不同)(Linux系统可以通过调整启动运行级来控制整个系统的功能)(守护进程有些就是自启动进程)
- 硬件设备管理(任何linux系统需要与之通信的设备,都需要在内核代码中加入其驱动程序代码。驱动程序代码,允许内核和硬件之间交换数据)(Linux系统将硬件设备当做特殊的文件,称为设备文件。有3种分类:字符设备文件、块设备文件、网络设备文件)
- 文件系统管理(Linux内核支持通过不同类型的文件系统从硬盘中读写数据,当然类型是有规定的。Linux内核采用虚拟文件系统(Virtual File System)作为和每个文件系统交互的接口)
GUN coreutils软件包由三部分构成:
- 用以处理文件的工具
- 用以操作文本的工具
- 用以管理进程的工具
shell的核心时命令行提示符。命令行提示符是shell负责交互的部分。它允许你输入文本命令,然后解释命令,并在内核中执行。shell也允许你在命令行提示符中输入程序的名称,它会将程序名传递给内核以启动它
所有Linux发行版默认的shell都是bash shell由GUN项目开发
Linux桌面环境(有两个基本要素决定了视频环境:显卡和显示器,要在电脑上显示绚丽的画面,Linux软件就得知道如何与这两者互通)
- X Window软件:直接和PC上的显卡及显示器打交道的底层程序,可以产生图形化显示环境
- KDE桌面:类似于Microsoft Windows
- GNOME桌面:许多Linux发行版默认的桌面环境
- Unity桌面:Ubuntu Linux发行版桌面
- 其他桌面:低内存开销的图形化桌面应用(图形化桌面环境的弊端在于它们要占用相当一部分的系统资源来保证正常运行)
完整的Linux系统包称为发行版:
- Linux LiveCD:可以通过CD来启动PC,并且无需在硬盘安装任何东西就能运行Linux发行版(无法向CD写入数据,对Linux系统作的任何修改都会在重启后失效)
***
linux应用程序表现为两种特殊类型的文件:可执行文件和脚本文件。可执行文件是计算机可以直接运行的程序,他们相当于windows中的.exe文件。脚本文件是一组指令的集合,这些指令将由另一个程序(即解释器)来执行,它们相当于windows中的.bat文件、.cmd文件或解释执行的BASIC程序
linux并不要求可执行文件或者脚本具有特殊的文件名或拓展名,文件系统属性要来指明一个文件是否为可执行的程序
在Linux中,你可以用编译过的程序代替脚本(反之亦然)而不会影响其他程序或调用者
当登录进linux系统时,你与一个shell程序(通常是bash)进行交互,它像windows中的命令提示窗口一样运行程序。它在一组指定的目录路径下(存储在shell的变量path里)按照你给出的程序名搜索与之同名的文件
shell的path里(Linux向Unix一样,使用冒号(:)分隔PATH变量里的条目),通常包含如下一些存储系统程序的标准路径:
- /bin:二进制文件目录,用于存放在启动系统时用到的程序
- /usr/bin:用户二进制文件目录,用于存放用户使用的标准程序
- /usr/local/bin:本地二进制文件目录,用于存放软件安装的程序
- 可选的操作系统组件和第三方应用程序可能被安装在/opt目录下,安装程序可以通过用户安装脚本将路径添加到path环境变量中
- 系统管理员(例如root用户)登录后使用的path变量可能还包含存放系统管理程序的目录,如/sbin和/usr/sbin
可以指定搜索文件的路径,比如./hello,它特别指示shell去执行当前目录下给定名称的程序
还可以在命令行上直接输入命令PATH=$PATH:.或编辑你的.bash_profile文件,将刚才这条命令添加到文件的末尾,然后退出登录后再重新登录进来(不应该用这种方法来修改超级用户的PATH变量)
如果忘记用-o name选项告诉编译器可执行程序的名字,编译器就会把程序放在一个名为a.out的文件里(a.out的含义是assembler output,即汇编输出)
***
- /usr/bin:系统为正常使用提供的程序,包括用于程序开发的工具
- /usr/local/bin或/opt:系统管理员为某个特定的主机或本地网络添加的程序
- /usr/include目录及其子目录:头文件通常所在位置。在调用C语言编译器时,可以使用-I标志来包含保存在子目录或非标准位置中的头文件。例如:$ gcc -I/usr/openwin/liclude/ fred.c,它指示编译器不仅在标准位置,也在/usr/openwin/liclude目录中查找程序fred.c包含的头文件
- /lib和/usr/lib目录:系统标准库文件(在默认情况下,C语言编译器【或更确切地说是链接程序】,它只搜索标准C语言库。这是从那个计算机速度还很慢而且CPU运行周期还很昂贵的时代遗留下来的问题【仅把库文件放在标准目录中,就希望编译器能够找到它是不够的,库文件必须遵循特定的命名规范并且需要在命令行中明确指定(指定路径或具体文件)】)
库是一组预先编译好的函数的集合,这些函数都是按照可重用的原则编写的
库文件的名字总是以lib开头,随后的部分指明这是什么库(例如,c代表C语言库,m代表数学库),文件名的最后部分以 . 开始,然后给出库文件类型。.a代表传统的静态函数库,.so代表共享库函数
可以通过给出完整的库文件路径名或用-l标志来告诉编译器要搜索的库文件。例如:$gcc -o fred fred.c /usr/bin/libm.a等价于$gcc -o fred fred.c -lm(-lm是简写方法,它代表的是标准库目录中名为libm.a的函数库)(-lm标志的一个好处是如果有共享库,编译器会自动选择共享库)。这条命令要求编译器编译文件fred.c,将编译产生的程序文件命名为fred,并且除了搜索标准得C语言函数库外,还搜索数学库以解决函数引用问题
可以使用-L标志为编译器增加库的搜索路径。例如:$gcc -o xllfred -L/usr/openwin/lib xllfred.c -lXll。这条命令用/usr/openwin/lib目录中的libXll库版本来编译和链接程序xllfred
函数库通常同时以静态库和共享库两种格式存在。Linux下,共享库的位置与静态库是一样的
函数库最简单的形式是一组处于“准备好使用”状态的目标文件。当程序需要使用函数库中的某个函数时,它包含一个声明该函数的头文件。(用C语言及其他语言进行程序设计时,需要用头文件来提供对常量的定义和对系统函数及库函数调用的声明)编译器和链接器负责将程序代码和函数库结合在一起以组成一个单独的可执行文件
静态库,也称作归档文件,按惯例它们的文件名都以.a结尾。只要使用gcc -c命令和ar(即archive,即建立归档文件)程序对函数分别进行编译,就可以创建自己的静态库
之所以称为ar,是因为它将若干单独的文件归并到一个大的文件中以创建归档文件或集合
静态库的一个缺点是,当你同时运行许多应用程序并且它们都来自同一个函数库的函数时,内存中就会有同一函数的多份副本,而且在程序文件自身中也有多样同样的副本。这将消耗大量宝贵的内存和磁盘空间
在一个典型的linux系统中,当一个程序使用共享库时,它的链接方式是这样的:程序本身不再包含函数代码,而是引用运行时可访问的共享代码。当编译好的程序被装载到内存中执行时,函数引用被解析并产生对共享库的调用,如果有必要,共享库才被加载到内存中
共享库优点:
- 系统可以只保留共享库的一份副本供许多应用程序同时使用,并且在磁盘上也仅保存一份
- 另一个好处是共享库的更新可以独立于依赖它的应用程序
对linux系统来说,负责装载共享库并解析客户程序函数引用的程序(动态装载器)是ld.so,也可能是ld-linux.so.2、ld-lsb.so.2或ld-lsb.so.3
用于搜索共享库的额外位置可以在文件/etc/ld.so.conf中配置,如果修改了这个文件,你需要执行命令ldconfig来处理它
***
Linux具备自动文件类型处理功能,所以使用这些Linux工具的用户一般不必了解它们是哪种语言编写的
linux让用户最满意的原因之一就是它提供了各种各样的优秀工具。如果你编写了一个小巧而简单的工具,其他人就可以将它作为一根链条上的某个环节来构成一条命令
通常你可以将许多小巧的脚本程序组合起来以创建一个庞大而复杂的程序。例如:ls -al | more
linux是高度模块化的系统。在linux中安装多个shell是完全可行的,可以挑选一种自己喜欢的shell来使用(在创建linux用户时,可以设置这个用户要使用的shell。创建用户之后,也可以通过修改用户信息来完成)(运行中,想要切换到另一个shell【例如,bash不是你系统中的默认shell】,你只需直接执行需要的shell程序【例如,/bin/bash】就可以运行新的shell并且改变命令提示符了)(linux默认的shell都是bash shell,被当作Unix shell--Bourne shell的替代品)
shell表面上和windows的命令提示符相似,但是它具备更强大的功能。你不仅可以通过它执行命令、调用Linux工具,还可以自己编写程序。shell执行shell程序,这些程序通常被称为脚本,它们是在运行时解释执行的。这使得调试工作比较容易进行,因为可以逐行地执行指令,而且节省了重新编译的时间。然而,这也使得shell不适合用来完成时间紧迫型和处理器忙碌型的任务
shell是一个作为用户与linux系统间接口的程序,它允许用户向操作系统输入需要执行的命令。linux shell比windows的命令行提示符功能强大,我们可以使用 < 和 > 对输入输出进行重定向,使用 | 在同时执行的程序之间实现数据的管道传递,使用$(...) 获取子进程的输出(反引号``也可以)
> 操作符把标准输出重定向到一个文件。默认情况下,如果该文件已经存在,它的内容将被覆盖。可以使用命令set -0 noclobber(或set -C)命令设置noclobber选项,从而阻止重定向操作对一个已有文件的覆盖
如果想对标准错误输出(也是输出,即 >)进行重定向,你需要把想要重定向的文件描述符编号加在 > 操作符前面(因为标准错误输出的文件描述符编号是2,所以此时的操作符为 2> )(使用>或者>>
对输出进行重定向,符号的左边表示文件描述符,如果没有的话表示1,也就是标准输出,符号的右边可以是一个文件,也可以是一个输出设备)(使用<
对输入做重定向,如果符号左边没有写值,那么默认就是0)
在linux下,通过管道连接的进程可以同时运行(允许连接的进程数目是没有限制的),并且随着数据流在它们之间的传递可以自动地进行协调(这里有一点需要引起注意:如果你有一系列命令需要执行,相应的输出文件是在这一组命令被创建的同时立刻被创建或写入的,所以决不要在命令流中重复使用相同的文件名。例如:cat mydata.txt | sort | uniq > mydata.txt,一开始mydata.txt文件内容就被覆盖为空了)
大于号:将一条命令执行结果(标准输出,或者错误输出,本来都要打印到屏幕上面的)重定向其它输出设备(文件,打开文件操作符,或打印机等等)
小于号:命令默认从键盘获得的输入,改成从文件,或者其它打开文件以及设备输入
>> 是追加内容
> 是覆盖原有内容
***
编写shell脚本程序有两种。可以输入一系列命令让shell交互地执行它们,也可以把这些命令保存到一个文件中,然后将该文件作为一个程序来调用(在命令行上执行的任何命令都可以放进一个shell脚本中作为一组命令执行)
shell提供了通配符扩展
#!字符告诉系统同一行上紧跟在它后面的那个参数是用来执行本文件的程序。#!后面使用的是绝对路径。考虑到向后兼容性,这个路径按惯例最好不要超过32个字符,因为一些老版本的UNIX在使用#!时只能使用这个限制之内的字符数,虽然linux通常不存在这样的限制
因为脚本程序本质上被看作是shell的标准输入,所以它可以包含任何能够通过你的PATH环境变量引用到的linux命令
linux和UNIX很少利用文件扩展名来决定文件的类型。可以为脚本使用.sh或者其他扩展名,但shell并不关心这一点。检查这些文件是否是脚本程序的最好方法是使用file命令,例如,file first或file /bin/bash
运行脚本文件的两种方法:
- 调用shell,并把脚本文件名当成一个参数。如:$ /bin/sh first
- 只输入脚本程序的名字就可以调用它(需要使用chmod命令来改变这个文件的模式,使得这个文件可以被执行)。如:$ first
在确信脚本程序能够正确执行后,可以把它从当前目录移到一个更合适的地方。比如,可以在自己的家目录中创建一个bin目录,并将该目录添加到你自己的PATH变量中。如果你想其他人也能够执行这个脚本程序,你可以将/usr/local/bin或其他系统目录作为添加新程序的适当位置,为了防止其他用户修改脚本程序,应该去掉脚本程序的写权限
在linux系统中,如果你拥有包含某个文件的目录的写权限,就可以删除这个文件
***shell语法***
- 变量:字符串、数字、环境和参数
- 条件:shell中的布尔值
- 程序控制:if、elif、for、while、until、case
- 命令列表
- 函数
- shell内置命令
- 获取命令的执行结果
- here文档
变量不需要事先为它们做出声明
在默认情况下,所有变量都被看作字符串并以字符串来存储,即使它们被赋值为数值时也是如此
shell中的所有算术运算都是按照整数来进行计算的
访问变量时,需要在它前面加一个$字符,为变量赋值时,只需要使用变量名即可
read命令将用户的输入赋值给一个变量。这个命令需要一个参数,即准备读入用户输入数据的变量名,然后它会等待用户输入数据
使用单引号和反斜线不会进行变量的替换(eg.\$myvar、'$myvar')
字符串里包含空格,必须用引号把它们括起来。此外,等号两边不能有空格
当一个shell脚本程序(交互式shell也算)开始执行时,一些变量(环境变量)会根据环境设置中的值来进行初始化。这些变量通常都是大写字母做名字,用户在脚本程序里的变量按惯例都是用小写字母做名字
参数变量:
shell的布尔判断命令[ ]或test。必须在[符号和被检查的条件之间留出空格,test命令也一样
test命令可以使用的条件类型可以归为3类:字符串比较、算术比较和与文件有关的条件测试
要想查看系统中是否有一个指定名称的外部命令,可以尝试使用which test这样的命令来检查执行的是哪一个test命令,或者使用./test这种执行方式以确保你执行的是当前目录下的脚本程序
set-uid位授予了程序其拥有者的访问权限而不是其使用者的访问权限,而set-gid位授予了程序其所在组的访问权限。这两个特殊位是通过chmod命令的选项s和g设置的
set-gid和set-uid标志对shell脚本程序不起作用,它们只对可执行的二进制文件有用
shell脚本程序中所有的变量扩展都是在脚本程序被执行时而不是在编写它时完成的。所以,变量声明中的语法错误只有在执行时才会被发现
知道循环次数用for,不知道循环次数使用while
每个模式行都已双分号(;;)结尾
如果想在某些只允许使用单个语句的地方(比如在AND或OR列表中)使用多条语句,可以把它们括在花括号{ }中来构造一个语句块
***
当编写大型的shell脚本程序时,作为一种选择,可以把一个大型的脚本程序分成许多小一点的脚本程序,让每个脚本完成一个小任务。缺点是:在一个脚本程序中执行另一个脚本程序要比执行一个函数慢的多
通常,当一个脚本执行一条外部命令或脚本程序时,它会创建一个新的环境(一个子shell),命令将在这个新环境中执行,在命令执行完毕后,这个环境被丢弃,留下退出码返回父shell。但是source命令和.命令在执行脚本程序中列出的命令时,使用的是调用该脚本的同一个shell,允许执行的脚本程序改变当前环境。相当于php的include,可以使用它俩将变量和函数结合进脚本程序
所有的脚本程序都是从顶部开始执行,所以只要把所有函数定义都放在任何一个函数调用之前,就可以保证所有的函数在被调用之前就被定义了
脚本程序从自己的顶部开始执行,当它遇见foo(){结构时,它知道脚本正在定义一个名为foo的函数。它会记住foo代表一个函数并从}字符之后的位置继续执行。当执行到单独的行foo时,shell就知道应该去执行刚才定义的函数了。当这个函数执行完毕后,执行过程会返回到调用foo函数的那条语句的后面继续执行
当一个函数被调用时,脚本程序的位置参数($*、$@、$#、$1、$2等)会被替换为函数的参数。这也是读取传递给函数的参数的办法。当函数执行完毕后,这些参数会恢复为它们先前的值(一些老版本的shell在函数执行后可能不会恢复位置参数的值)
- 可以通过return命令让函数返回数字值
- 让函数返回字符串值得常用方法是让函数保存在一个变量中,该变量可以在函数结束之后被使用
- 还可以echo一个字符串并捕获其结果
- 如果函数里没有return命令指定一个返回值,函数返回的就是执行的最后一条命令的退出码
在shell脚本编程中,退出码0表示成功。退出码1~125是脚本程序可以使用的错误代码。其余数字具有保留含义
shell脚本程序内部执行两类命令:内置命令和外置命令。内置命令是在shell内部实现,它们不能作为外部程序被调用。然而,大多数的内部命令同时也提供了独立运行的程序版本(POSIX规范的一部分)。通常情况下,命令是内部还是外部的不重要,只是内部命令的执行效率更高
:命令是一个空命令,相当于true的别名。由于是内置命令,所以它运行的比true快
eval命令:允许对参数进行求值。是shell内置命令,通常不会以单独命令的形式存在。eval命令有点像一个额外的$,它给出一个变量的值的值
expr命令:将参数当作一个表示式来求值
exec命令:有两种不同的用法。典型用法是将当前shell替换为一个不同的程序。例如:exec wall "Thanks for all the fish"这个命令会用wall命令替换当前shell。脚本程序中exec命令后面的代码都不会执行,因为执行这个脚本的shell已经不存在了;第二种用法是修改当前文件描述符
export命令:把自己的参数创建为一个环境变量,而这个环境变量可以被当前程序调用的任何脚本和程序看见。从更技术的角度来说,被导出的变量构成从该shell衍生的任何子进程的环境变量
source命令和.命令:
set命令:为shell设置参数变量。许多命令的输出结果是以空格分隔的值,如果需要使用输出结果中的某个域,这个命令就非常有用(一般将set命令和$(...)结构相结合来执行)
shift命令:把所有参数变量左移一个变量,使$2变成$1,$3变成$2,以此类推。原来$1的值将被丢弃,而$0将保持不变。如果调用shift命令指定了一个数值参数,则表示所有的参数将左移指定的次数。$*、$@和$#等其他变量也将根据参数变量的新安排做相应的变动
trap命令:指定在接收到信号后要采取的行动。trap命令有两个参数,第一个参数是接收到指定信号时将要采取的行动,第二个参数是要处理的信号名。如果要重置某个信号的处理方式到默认值,只需将command设置为-。如果要忽略某个信号,就把command设置为空字符串''。历史上,shell总是用数字来代表信号,但新的脚本程序应该使用信号的名字,它们定义在signal.h(信号是指那些被异步发送到一个程序的事件。在默认情况下,它们通常会终止一个程序的运行)
unset命令:从环境中删除变量或函数(不仅仅是赋值为空)
***
linux机器挂载(使用SAMBA)了一大块windows机器的文件系统
【中括号表示可选】
为了确保pattern传递给find命令而不是由shell来处理,pattern必须总是用引号括起(单引号的作用是防止变量扩展)
圆括号对shell来说有特殊的含义,所以你必须使用反斜线来引用圆括号
正则表达式:
***
在脚本程序里执行命令,比较老的语法形式是使用反引号(` `),新脚本是用的是$(...)。使用反引号执行命令时,如果需要在反引号中包含$、`、\等字符时,处理起来很麻烦
$(command)的结果就是其中命令的输出,一般将命令的执行结果赋值到当前脚本的变量中
算术扩展:通过expr命令可以处理一些简单的算术命令,但这个命令执行起来相当慢,因为它需要调用一个新的shell来处理expr命令。一种更好的办法使用$((...))
$((...))与$(...)命令不同,两对圆括号用于算术运算,一对圆括号用于命令的执行和获取输出
参数扩展:${i}_tmp
在shell脚本程序中向一条命令传递输入的一种特殊方法是使用here文档。它允许一条命令在获得输入数据时就好像是在读取一个文件或键盘一样,而实际上是从脚本程序中得到输入数据
here文档以两个连续的小于号<<开始,紧跟着一个特殊的字符序列,该序列将在文档的结尾处再次出现。<<是shell的标签重定向符,在这里,它强制命令的输入是一个here文档
脚本程序是解释执行的,所以在脚本程序的修改和重试过程中没有编译方面的额外开支
我们在here文档中用\字符来防止$字符被shell扩展。\字符的作用是对$进行转义,让shell知道不要尝试把$s/is/was扩展为它的值
***
在linux中,一切(或几乎一切)都是文件。这就意味着,通常程序完全可以像使用文件那样使用磁盘文件、串行口、打印机和其他设备
文件,除了本身包含的内容以外,文件的属性被保存在文件的inode(节点)中,它是文件系统中一个特殊的数据块,它同时还包含文件的长度和文件在磁盘上的存放位置
目录是用于保存其他文件的节点号和名字的文件
可以通过ln命令在不同的目录中创建指向同一个文件的链接。如果指向某个文件的链接数(即ls -l命令的输出中跟在访问权限后面的那个数字)变为零,就表示该节点以及其指向的数据不再被使用,磁盘上的相应位置就会被标记为可用空间
目录文件中的每个数据项都是指向某个文件节点的链接,删除一个文件时,实质上是删除了该文件对应的目录项,同时指向该文件的链接数减1
如果一个文件的链接数减少到零,并且没有进程打开它,这个文件就会被删除。事实上,目录项总是被立刻删除,但文件所占用的空间要等到最后一个进程(如果有的话)关闭它之后才会被系统回收(创建临时文件的技巧:先用open创建一个文件,然后对其调用unlink。这些文件只有在被打开的时候才能被程序使用,当程序退出并且文件关闭的时候它们就会被自动删除掉)
***
代表物理设备(设备分为字符设备和块设备以及网络设备)并为这些设备提供接口的文件按照惯例会被放在/dev子目录中
Unix和linux中比较重要的设备文件有3个:/dev/console、/dev/tty和/dev/null
- /dev/console:这个设备代表的是系统控制台。错误信息和诊断信息通常会发送到这个设备
- /dev/tty:如果一个进程有控制终端,那么这个文件就是控制终端(键盘或显示屏,或键盘和窗口)的别名(逻辑设备)。例如,由系统自动运行的进程和脚本就没有控制终端,所以它们不能打开/dev/tty
- /dev/null:空设备,所有写向这个设备的输出都将被丢弃
- /dev/console设备只有一个,但通过/dev/tty却能够访问许多不同的设备
普通用户不能通过编写程序来直接访问如硬盘这样的底层设备(root可以)
操作系统的核心部分,即内核,是一组设备驱动程序。它们是一组对系统硬件进行控制的底层接口。例如,磁带机有一个与之对应的设备驱动程序,它知道如何启动磁带、如何对它前后回绕、如何对它进行读写等
为了向用户提供一个一致的接口,设备驱动程序封装了所有与硬件相关的特性。硬件的特有功能通常可通过ioctl(用于I/O控制)系统调用来提供
/dev目录中的设备文件的用法都是相同的,它们都可以被打开、读、写和关闭。例如,用来访问普通文件的open调用同样可以用来访问用户终端、打印机和磁带机
库函数在满足要求时再安排执行底层系统调用,这就极大的降低了系统调用的开销
库函数往往有一个与之对应的标准头文件
总结如图:
***
每个运行的程序被称为进程,它有一些与之关联的文件描述符。这是一些小值整数,可以通过它们访问打开的文件和设备
open建立了一条到文件或设备的访问路径。如果调用成功,它将返回一个可以被read、write和其他系统调用使用的文件描述符
如果两个程序同时打开同一个文件,它们会分别得到两个不同的文件描述符。如果它们都对文件进行写操作,那么它们会各写各的,它们分别接着上次离开的位置继续往下写。它们的数据不会交织在一起,而是彼此互相覆盖
任何一个运行中的程序能够同时打开的文件数是有限制的。这个限制通常是由limits.h头文件中的常量OPEN_MAX定义的,它的值随系统的不同而不同,但POSIX要求它至少为16
当一个程序开始运行时,它一般会有3个已经打开的文件描述符
- 0:标准输入
- 1:标准输出
- 2:标准错误
- 可以通过系统调用open把其他文件描述符与文件和设备相关联
有几个因素会对文件的访问权限产生影响。首先,指定的访问权限只有在创建文件时才会使用;其次,用户掩码(由shell的umask命令设定)会影响到被创建文件的访问权限
open和creat调用中的标志实际上是发出设置文件权限的请求,所请求的权限是否会被设置取决于当时的umask的值(在mode参数中被设置的位如果在umask值中也被设置了,那么它就会从文件的访问权限中删除。因此,用户完全可以设置自己的环境)
umask是一个系统变量,它的作用是:当文件被创建时,为文件的访问权限设定一个掩码。执行umask命令可以修改这个变量的值
检查close系统调用的返回结果非常重要(因为有的文件系统,特别是网络文件系统,可能不会在关闭系统文件之前报告错误)
dup系统调用提供了一种复制文件描述符的方法,使我们能够通过两个或者更多个不同的描述符来访问同一个文件。可以用于在文件的不同位置对数据进行读写(dup2系统调用则是通过明确指定目标描述符来把一个文件描述符复制为另一个)
如果我们首先关闭文件描述符0然后调用dup,那么新的文件描述符就将是数字0(dup返回的新的文件描述符是当前可用文件描述符中最小数值)。此时标准输入会和传递给dup函数的文件描述符指向同一个文件或管道
***
标准I/O库及其头文件stdio.h为底层I/O系统调用提供了一个通用的接口。这个库现在已经成为ANSI标准C的一部分,系统调用却还不是
在很多方面,你使用标准I/O库的方式和使用底层文件描述符一样。需要先打开一个文件以建立一个访问路径。这个操作的返回值将作为其他I/O库函数的参数
在标准I/O库中,与底层文件描述符对应的是流,它被实现为指向结构FILE的指针。在启动程序时,有3个文件流是自动打开的。它们是stdin、stdout和stderr(可以作为文件流操作函数的参数)。它们都是在stdio.h头文件里定义的,分别代表着标准输入、标准输出和标准错误输出,与底层文件描述符0、1和2相对应
如果需要对设备进行明确的控制,那最好使用底层系统调用,因为这可以避免用库函数带来的一些潜在问题
Unix和linux并不像MS-DOS那样区分文本文件和二进制文件。Unix和linux把所有文件都看作二进制文件
fclose库函数关闭指定的文件流stream,使所有尚未写出的数据都写出。因为stdio库会对数据进行缓冲(只有在缓冲区满时,才进行底层系统调用),所以使用fclose是很重要的。如果程序需要确保数据已经全部写出,就应该调用fclose函数(还可以检查fclose报告的错误)
fflush库函数的作用是把所有未写出数据立刻写出。使用这个函数还可以确保在程序继续执行之前重要的数据都已经被写到磁盘上(调用fclose函数隐含执行了一次flush操作,所以你不必在调用fclose之前调用fflush)
许多安全问题都可以追溯到在程序中使用了可能造成各种缓存区溢出的函数。比如:gets对传输的个数并没有限制,所以他可能会溢出自己的传输缓冲区
一般都是简单地向一个文件流输出数据或者从一个文件流读取数据。但是printf和scanf系列函数可以格式化数据
printf系列函数能够对各种不同类型的参数进行格式编排和输出(转换控制符规定了其余的参数应该以何种方式被输出到何种地方)
- printf函数把自己的输出送到标准输出
- fprintf函数把自己的输出送到一个指定的文件流
- sprintf函数把自己的输出和一个结尾空字符写到作为参数传递过来的字符串s里。这个字符串必须足够容纳所有的输出数据
- 转换控制符总是以%字符开头(要想输出%字符,需要使用%%)。让传递到printf函数里的参数数目和类型与format字符串的转换控制符相匹配是非常重要的
- 可以利用字段限定数(字段限定数是转换控制符里紧跟在%字符后面的数字,eg:%10s)对数据的输出格式做进一步的控制(常见用法是设置浮点数的小数位数或设置字符串两端的空格数)
有的编译器能够对printf语句进行检查,但并非万无一失(比如GUN编译器gcc,你可以在编译命令中添加-Wformat选项实现这一功能)
scanf系列函数是从一个文件流里读取数据,并把数据值放到以指针参数形式传递过来的地址处的变量中
scanf函数读入的值将保存到对应的变量里去,它们必须精确匹配格式字符串。否则,内存数据就可能会遭到破坏,从而使程序崩溃
格式字符串中的空格用于忽略输入数据中位于转换控制符之间的各种空白字符(空格、制表符、换页符和换行符)
这两种输入情况下,上图scanf调用会执行成功,并把1234放到变量num里
注意:如果用户在输入中应该出现一个整数的地方放的是一个非数字字符,就可能在程序里导致一个无限循环
%c控制符从输入中读取一个字符。他不会跳过起始的空白字符
%s控制符来扫描字符串,它会跳过起始的空白字符串,并且会在字符串里出现的第一个空白字符处停下来,所以,你最好使用它来读取单词而不是一般意义上的字符串
类似于printf,scanf的转换控制符里也可以加上对输入数据字段宽度的限制。长度限定符(h对应于短,l对应于长)指定接收参数的长度是否比默认情况更短或更长。此外,如果没有使用字段宽度限定符,他能够读取的字符串的长度是没有限制的,所以接收字符串必须有足够的空间来容纳输入流中可能的最长字符串(较好的选择是使用一个字段限定符,或者结合使用fgets和sscanf从输入中读入一行数据,再对它进行扫描)
使用%[]控制符读取由一个字符集合中的字符构成的字符串
以星号(*)开头的控制符表示对应位置上的输入数据将被忽略。这意味着,这个数据不会被保存,因此不需要使用一个变量来接受它
scanf函数的返回值是它成功读取的数据项个数,如果在读第一个数据项时失败了,它的返回值是零。如果在匹配第一个数据项之前就已经到达了输入的结尾,它就会返回EOF
***
许多函数都可能改变error的值。它的值只有在函数调用失败时才有意义。你必须在函数表明失败之后立刻对其进行检查。你应该总是在使用它之前将它先复制到另一个变量中,因为像fprintf这样的输出函数本身就可能改变error的值
也可以通过检查文件流的状态来确定是否发生了错误,或者是否到达了文件尾
ferror函数测试一个文件流的错误标识,如果该标识被设置就返回一个非零值,否则返回零
feof函数测试一个文件流的文件尾标识,如果该标识被设置就返回一个非零值,否则返回零
clearerr函数的作用是清除由stream指向的文件流的文件尾标识和错误标识,可以使用它从文件流的错误状态中恢复。例如:在“磁盘已满”错误解决之后,继续开始写入文件流
每个文件流都和一个底层的文件描述符相关联。可以通过调用fileno函数来确定文件流使用的是哪个底层文件描述符,如失败就返回-1(如你需要对一个已经打开的文件流进行底层访问时(例如,对它调用fstat),这个函数将很有用)
可以通过调用fdopen函数在一个已打开的文件描述符上创建一个新的文件流(实质上,这个函数的作用是为一个已打开的文件描述符提供stdio缓冲区)
可以把底层的输入输出操作和高层的文件流操作混合使用,但一般来说,这并不是一个明智的做法,因为数据缓冲的结果难以预料
link系统调用将创建一个指向已有文件path1的新链接。你可以通过symlink系统调用以类似的方式创建符号链接。注意,一个文件的符号链接并不会增加该文件的链接数,所以它不会像普通(硬)链接那样防止文件被删除
如果想通过系统调用unlink删除一个文件的目录项并减少它的链接数,你就必须拥有该文件所属目录的写和执行权限
程序可以像用户在文件系统里那样浏览目录。在shell中使用cd,程序中使用chdir系统调用来切换目录一样
getcwd函数把当前目录的名字写到给定的缓冲区buf里(如果在程序运行过程中,目录被删除(EINVAL错误)或者有关权限发生了变化(EACCESS错误),getcwd也可能会返回null)
与目录操作有关的函数在dirent.h头文件中声明。它们使用一个名为DIR的结构作为目录操作的基础。指向这个结构的被称为目录流的指针(DIR *)被用来完成各种目录操作(与用来操作普通文件的文件流(FILE *)非常相似)
opendir函数的作用是打开一个目录并建立一个目录流。如果成功,它返回一个指向DIR结构的指针,该指针用于读取目录数据项
目录流使用一个底层文件描述符来访问目录本身,所以如果打开的文件过多,opendir可能会失败
循环打开目录,如果目录的嵌套层次太深,程序执行就会失败,这是因为对允许打开的目录流数目是有限制的
readdir函数将返回一个指针,该指针指向的结构里保存着目录流dirp中下一个目录项的有关资料。如果发生错误或者到达目录尾,readdir将返回null。POSIX兼容的系统在到达目录尾时会返回null,但并不改变error的值,只有在发生错误时才会设置error
如果在readdir函数扫描目录的同时还有其他进程在该目录里创建或删除文件,readdir将不保证能够列出该目录里的所有文件(和子目录)
closedir函数关闭一个目录流并释放与之关联的资源
telldir函数的返回值记录着一个目录流里的当前位置,可以使用seekdir调用利用这个值来重置目录扫描到当前位置
seterror函数把错误代码映射为一个字符串,该字符串对发生的错误类型进行说明
perror函数也把error变量中报告的当前错误映射到一个字符串,并把它输出到标准错误输出流
***
Linux将一切事物都看作为文件,硬件设备在文件系统中也有相应的条目。我们使用底层系统调用这样一种特殊方式通过/dev目录中的文件来访问硬件
控制硬件的软件驱动程序通常可以以某种特定方式配置,或者能够报告相关信息
近年来,倾向于提供更一致的方式来访问驱动程序的信息。事实上,这种一致的方式甚至延伸到包括与Linux内核的各种元素的通信
Linux提供了一个特殊的文件系统procfs,它通常以/proc目录的形式呈现。该目录中包含了许多特殊文件用来对驱动程序和内核信息进行更高层的访问。只要应用程序有正确的访问权限,它们就可以通过读写这些文件来获得信息或设置参数
通过特定的内核函数获得更多的信息,它们位于/proc目录的子目录中。/proc目录中的有些条目不仅可以被读取,而且可以被修改。对/proc目录中的文件进行写操作需要超级用户的权限(例如:系统中所有运行的程序同时能打开的文件总数是Linux内核的一个参数,通过读取/proc/sys/fs/file-max文件得到为76593。如果正在运行一个需要同时打开很多文件的应用程序套件(例如,一个使用了很多表的数据库系统),需要增大该值,则可以通过写同一个文件来实现)
/proc目录中以数字命名的子目录用于提供正在运行的程序的信息(每个进程都有一个唯一的标识符:一个在1~32000的数字)
ls -l /proc/进程标识符:可以详细的列出该进程的相关信息(先用ps -a列出当前正在运行进程的列表)
利用fcntl系统调用,你可以对打开的文件描述符执行各种操作,包括对它们进行复制、获取和设置文件描述符、获取和设置文件状态标志,以及管理建议性文件锁等
mmap(内存映射)函数的作用是建立一段可以被两个或更多个程序读写的内存。一个程序对它所做出的修改可以被其他程序看见
mmap还可以用在文件的处理上。可以使某个磁盘文件的全部内容看起来就像是内存中的一个数组。如果文件由记录组成,而这些记录又能够用C语言中的结构来描述的话,你就可以通过访问结构数组来更新文件的内容了(mmap函数创建一个指向一段内存区域的指针,该内存区域与可以通过一个打开的文件描述符访问的文件的内容相关联
可以通过addr参数来请求使用某个特定的内存地址。如果它的取值是零,结果指针就将自动分配。这是推荐的做法,否则会降低程序的可移植性,因为不同系统上的可用地址范围是不一样的)
msync函数的作用是:把在该内存段的某个部分或整段中的修改写回到被映射的文件中(或者从被映射文件里读出)
munmap函数的作用是释放内存段
***
当一个程序在一个多任务环境中运行时,这意味着同一时间会有多个程序运行,它们共享内存、磁盘空间和cpu周期等机器资源。甚至同一程序也会有多个实例同时运行
***
shell接收用户输入的命令行,将命令行分解成单词,然后把这些单词放入argv数组(无论操作系统何时启动一个新程序,参数argc和argv都被设置并传递给main。这些参数通常是由另一个程序提供,这个程序一般是shell,它要求操作系统启动该新程序)
在C语言程序中提供命令行开关的标准编程接口:getopt函数
getopt函数将传递给程序main函数的argc和argv作为参数,同时接受一个选项指定字符串,optstring只是一个字符列表(-i,-l,-r,-f,其中-f选项后面要紧跟一个文件名参数),每个字符代表一个单字符选项。使用相同的参数,但以不同的顺序来调用命令将改变程序的行为
***
环境变量,这是一些能用来控制shell脚本和其他程序行为的变量
可以使用环境变量来配置用户环境。例如,每个用户有一个环境变量HOME,它定义了用户的家目录,即该用户会话的默认开始位置
shell的set命令可以列出所有的环境变量。C语言程序可以通过putenv和getenv函数来访问环境变量由于getenv返回的字符串是存储在getenv提供的静态空间中,所以如果想进一步使用它,就必须将它复制到另一个字符串中,以免它被后续的getenv调用所覆盖
如果putenv由于可用内存不足而不能扩展环境,它会失败并返回-1。此时,错误变量error将被设置为ENOMEM
注意:环境仅对程序本身有效。在程序里做的改变不会反映到外部环境中,这是因为变量的值不会从子进程(你的程序)传播到父进程(shell)
用户可以通过以下方式设置环境变量的值:在默认环境中设置、通过登录shell读取的.profile文件来设置、使用shell专用的启动文件(rc)或在shell命令行上对变量进行设定
shell将行首的变量赋值作为对环境变量的临时改变
***
很多情况下,程序会利用一些文件形式的临时存储手段。这些临时文件可能保存着一个计算的中间结果,也可能是关键操作前的文件备份。例如:一个数据库应用程序在删除记录时就可能使用临时文件。该文件收集需要保留的数据库条目,然后在处理结束后,这个临时文件就变成新的数据库,原来文件则被删除
使用临时文件时,必须确保应用程序为临时文件选取的文件名是唯一的。否则,因为linux是一个多任务系统,另一个程序就可能选择同样的文件名,从而导致两个程序互相干扰
如果字符串s不为空,文件名也会写入它。对tmpnam的后续调用会覆盖存放返回值的静态存储区,所以如果tmpnam要被多次调用,就有必要给它传递一个字符串参数了。这个字符串的长度至少要有L_tmpnam(通常为20)个字符。tmpnam可以被一个程序最多调用TMP_MAX次(至少为几千次),每次它都会返回一个不同的文件名
另一个程序可能会创建出一个与tmpnam返回的文件名同名的文件。tmpfile函数则完全避免了这个问题的发生
***
用户通常是在一个响应他们命令的shell中启动程序。一个用户要登录进linux系统时,他有一个用户名和密码。一但用户名和密码通过验证,用户就可以进入一个shell(每个用户都有一个唯一的用户标识符UID)。linux运行的每个程序实际上都是以某个用户的名义在运行,因此都有一个关联的UID
可以对程序进行设置,让它们的运行看上去好像是由另一个用户启动的
有些UID是系统预定义的,其他的则是系统管理员在添加新用户时创建的。一般情况下,用户的UID值都大于100
系统文件/etc/passwd包含一个用户帐号数据库。它由行组成,每行对应一个用户,包括用户名、加密口令、用户标识符(UID)、组标识符(GID)、全名、家目录和默认shell
默认示例行:
***
主机信息在许多情况下都是很有用的。比如:可能希望根据程序运行的机器在网络上的名字来定制程序的行为
***
UNIX规范通过syslog函数为所有程序产生日志信息提供了一个接口
syslog函数向系统的日志设施(facility)发送一条日志信息。每条信息都有一个priority参数,该参数是一个严重级别与一个设施值的按位或。严重级别控制日志信息的处理方式(比如:LOG_EMERG信息可能会广播给所有用户,LOG_ALERT信息可能会EMAIL给管理员,LOG_DEBUG信息可能会被忽略,而其他信息则写入日志文件),设施值记录日志信息的来源
syslog的其他参数要根据message字符串中printf风格的转换控制符而定
openlog函数会分配并打开一个文件描述符,并通过它来写日志。可以调用closelog函数来关闭它(在调用syslog之前无需调用openlog,因为syslog会根据需要自行打开日志设施)
***
linux系统上运行的程序会受到资源限制的影响。它们可能是硬件方面的物理性限制(例如内存)、系统策略的限制(例如,允许使用的CPU时间)或具体实现的限制(如整数的长度或文件名中所允许的最大字符数)
一个程序耗费的CPU时间可分为用户时间(程序执行自身的指令所耗费的时间)和系统时间( 操作系统为程序执行所耗费的时间,即执行输入输出操作的系统调用或其他系统函数所花费的时间)
每个运行的程序都有一个与之关联的优先级,优先级越高的程序将分配到更多的CPU可用时间(普通用户只能降低其程序的优先级,而不能升高)
优先级的有效范围是-20~+20,数值越高,执行的优先级越低。默认的优先级是0,正数优先级用于后台任务,它们只在没有其他更高优先级的任务准备运行时才执行
***
当一个程序在命令符中被调用时,shell负责将标准输入和标准输出流连接到你的程序
- 标准模式:所有的输入都基于行进行处理,在一个输入行完成前(通常是用户按下回车键之前),终端接口负责管理所有的键盘输入,应用程序读不到用户输入的任何字符
如果想知道标准输出是否被重定向了,只需检查底层文件描述符是否关联到一个终端即可。系统调用isatty就是用来完成这一任务。只需将有效的文件描述符传递给它,它就可判断出该描述符是否连接到一个终端
由于linux本身是多用户系统,它通常拥有多个终端(这些终端或者是直接连接的,或者是通过网络进行连接的)。我们可以通过/dev/tty这个特殊设备找到要使用的正确终端,该设备终端始终是指向当前终端或当前的登录会话
有时,程序需要更精细的终端控制能力,而不是仅通过简单的文件操作来完成对终端的一些控制。linux提供了一组编程接口用来控制终端驱动程序的行为,从而更好地控制终端的输入和输出
通过一组函数调用来控制终端,这组函数调用与用于读写数据的函数是分离的,这就使得读写数据的接口非常简洁,同时又允许用户可以对终端的行为进行更精细的控制
硬件模型:
一台UNIX机器通过串行口连接一台调制解调器,再通过电话线连接到用户端的调制解调器,该调制解调器最终连接到用户的终端
- 输入模式控制输入数据(终端驱动程序从串行口或键盘接收到的字符)在被传递给程序之前的处理方式
- 输出模式控制输出字符的处理方式。即由程序发送出去的字符在传递到串行口或屏幕之前是入如何处理的
linux提供了虚拟控制台的功能,一个linux安装将配置8个或12个虚拟控制台。虚拟控制台通过字符设备文件/dev/ttyN使用,其中N代表一个数字,从1开始(如果使用字符界面登录linux系统,在linux启动并运行后,首先会看到一个login提示符,在输入用户名和密码登录后,你所使用的终端设备就是系统中的第一个虚拟控制台,即终端设备/dev/tty1;linux系统通常在前6个虚拟控制台上运行一个getty进程,这样用户即可用同一个屏幕、键盘和鼠标在6个不同的虚拟控制台上登录)
伪终端与终端的唯一区别是没有对应的硬件设备
***
每次程序请求内存或者尝试读写它已经分配的内存时,便会由linux内核接管并决定如何处理这些需求(linux程序决不允许直接访问物理内存)
当所访问的内存在物理上并不存在时,就会产生一个页面错误并将控制权交给内核。linux内核会对访问的内存地址进行检查,如果这个地址对于程序来说是合法可用的,内核就会确定需要向程序提供哪一个物理内存页面
当应用程序耗尽所有的物理内存和交换空间,或者当最大栈长度被超过时,内核将拒绝此后的内存请求,并可能提前终止程序的运行(当只有物理内存耗尽时,内核便会开始使用所谓的交换空间)
在linux系统中,交换空间是一个在安装系统时分配的独立的磁盘区域。内核会在物理内存和交换空间之间移动数据和程序代码,使得每次读写内存时,数据看起来总像是已存在于物理内存中
linux的交换空间中没有局部堆、全局堆或可丢弃内存段等需要在代码中操心的内容--linux内核会为你完成所有的管理工作
linux将所有的内存都以页为单位进行划分,通常每一页的大小为4096字节。用专业的术语来说,linux实现了一个“按需换页的虚拟内存系统”
对用户来说看到的就是这个虚拟的内存系统(整个),用户看到的所有内存全是虚拟的,也就是说,它并不真正存在于程序使用的物理地址上
动态使用内存的程序应该总是通过free调用,来把不用的内存释放给malloc内存管理器。这样做可以将分散的内存块重新合并到一起,并由malloc函数库而不是应用程序来管理它
如果一个运行中的程序(进程)自己使用并释放内存,则这些自由内存实际上仍然处于被分配给该进程的状态。但如果一个内存页面未被使用,linux内存管理器就可以将其从物理内存置换到交换空间中,从而减轻它对资源的使用(如果程序试图访问位于已置换到交换空间中的内存页中的数据,那么linux会短暂地暂停程序,将内存页从交换空间再次置换到物理内存,然后允许程序继续运行,就像数据一直存在于内存中一样)
请记住:一旦调用free释放了一块内存,它就不再属于这个进程。它将由malloc函数库负责管理。在对一块内存调用free之后,就绝不能再对其进行读写操作了
malloc和calloc调用都无法保证能返回一个连续的内存空间(realloc函数可以用来改变先前已经分配的内存块的长度),因为不能通过重复调用,并期望第二个调用返回的内存正好接在第一个调用返回的内存之后来扩大calloc调用创建的数组
特别重要的一点:为了改变先前已分配好的内存块长度,realloc函数可能不得不移动数据。一旦内存被重新分配之后,必须使用新的指针而不是使用realloc调用前的那个指针去访问内存
***
程序经常需要共享数据,而这通常是通过文件来实现的。在多用户、多任务操作系统中文件锁定就是一个非常重要的组成部分
Linux提供了多种特性来实现文件锁定:
- 最简单的方法就是以原子操作(所谓“原子操作”就是在创建锁文件时,系统将不允许任何其他的事情发生)的方式创建锁文件(由于进程间都是协调工作,当程序创建锁文件失败时,不能通过删除文件并重新尝试的方法来解决此问题。或许这样做可以让它创建锁文件,但另一个创建锁文件的程序将无法得知它已经不再拥有对这个资源的独占式访问权了)
- 文件段锁定或文件区域锁定:使用fcntl系统调用和使用lockf调用。但是,fcntl和lockf的锁定机制不能同时工作
文件中的每个字节在任一时刻只能拥有一种类型的锁:共享锁、独占锁或解锁
程序对某个文件拥有的所有锁都将在相应的文件描述符被关闭时自动清除。在程序结束时也会自动清除各种锁 - 锁定状态下的读写操作:当对文件区域加锁以后,你必须使用底层的read和write调用来访问文件中的数据,而不要使用更高级的fread和fwrite调用,这是因为fread和fwrite会对读写的数据进行缓存(例如:使用fread调用来读取文件的头100个字节,实际会读取超过100个字节的数据,并将多余的数据在函数库中进行缓存。再次使用fread读取下100个字节的数据实际上将读取已缓冲在函数库中的数据,而不会引发一个底层的read调用来从文件中取出更多的数据)
fcntl的参数cmd为F_SETLKW时,当无法建立锁定时,此调用会一直等到锁定动作成功为止
文件锁定fcntl和lockf设置的所有锁都是建议锁,它们并不会真正地阻止你读写文件中的数据。对锁的检测是程序的责任
避免死锁:两个程序只需要使用相同的顺序来锁定它们需要的字节或锁定一个更大的区域即可
***
虽然make命令内置了很多智能机制,但光凭其自身是无法了解应该如何建立应用程序的。必须为其提供一个文件,告诉它应用程序应该如何构造,这个文件称之为makefile
make命令和makefile文件的结合提供了一个在项目管理领域十分强大的工具。它不仅常被用于控制源代码的编译,而且还用于手册页的编写以及将应用程序安装到目标目录
***
程序缺陷的几种原因:
- 功能定义错误
- 设计规划错误:在计算机键盘前坐下,切接敲入源代码,然后期望程序能一次通过,这种情况并不常见。对于程序员来说一定要多花时间思考:如何构造程序,需要什么样的数据结构,它又应该如何在程序中使用。尽量要把细节问题提前确定下来
- 代码编写错误
程序调试的5个阶段(取样法【打印、日志】、受控制执行法【断点调试】)
- 测试:找出程序中存在的缺陷和错误
- 固话:让程序的错误可重现
- 定位:确定相关的代码行
- 纠正:修改代码纠正错误
- 验证:确定修改解决了问题
有些调试手段可能会增加程序的长度,程序的长度增加20%或30%,往往不会对程序的性能造成真正的影响。只有在程序的长度提高几个数量级时,才会造成程序性能的降低
通过设置断点在任一位置停止程序的运行。这将中断程序的运行并将控制权返回给调试器
硬件断点是某些CPU提供的功能,这些处理器可以在触发某个特定条件(一般为对某个给定区域的内存访问操作)时自动停止运行
程序运行失败时,linux和UNIX系统通常会产生一个核心转储,并将它保存在core文件中。这个文件其实是程序的内存映像文件(共享内存的一种方式),它包含程序运行失败的那个时刻的全局变量的取值
***
内存块通常是由malloc函数分配给指针变量的。如果指针变量的取值发生了变化,又没有其他指针指向这块内存,这块内存就变得无法访问。这就是一种内存泄漏现象,它将导致程序的长度不断增加。如果泄漏了大量内存,系统就会越来越慢,最终耗尽内存
***
进程和信号构成了linux操作环境的基础部分
UNIX标准把进程定义为:“一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源”
正在运行的程序或进程由程序代码、数据、变量(占用着系统内存)、打开的文件(文件描述符)和环境组成
进程有自己的栈空间,用于保存函数中的局部变量和控制函数的调用和返回。进程还有自己的环境空间,包含专门为这个进程建立的环境变量。进程还必须维护自己的程序计数器,这个计数器用来记录它执行到的位置,即在执行线程中的位置
一般来说,linux系统会在进程之间共享程序代码和系统函数库,所以在任何时刻内存中都只有代码的一份副本
正常情况下,linux进程不能对用来存放程序代码的内存区域进行写操作,即程序代码是以只读方式加载到内存中
例如用户neil和rick,同时运行grep程序在不同的文件中查找不同的字符串:
并不是程序在运行时所需要的所有东西都可以被共享(比如:进程通过各自的文件描述符来访问文件、进程使用的变量就与其他进程所使用的截然不同。除此之外,进程有自己的栈空间,用于保存函数中的局部变量和控制函数的调用和返回;进程还有自己的环境空间,包含专门为这个进程建立的环境变量。以及进程还必须维护自己的程序计数器,这个计数器用来记录它执行到的位置,即在执行线程中的位置)
在目录/proc中有一些特殊的文件,这些文件的特殊之处在于它们允许你“窥视”正在运行的进程的内部情况,就好像这些进程是目录中的文件一样
linux和UNIX一样,有一个虚拟内存系统,能够把程序代码和数据以内存页面的形式放到硬件的一个区域中
PID的取值范围从2到32768的正整数(数字1一般是为特殊进程init保留的)。当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的PID
linux进程表就像一个数据结构,它把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的PID(是进程表的索引)、进程的状态、命令字符串和其他一些ps命令输出的各类信息
最新的UNIX系统,可以同时运行的进程数可能只与用于建立进程表项的内存容量有关,而没有具体的数字限制了
ps命令输出中的start一列用来表明进程的当前状态
linux系统启动时,它将运行一个名为init的进程
其他系统进程要么是由init进程启动的,要么是由被init进程启动的其他进程启动的,可以把init进程看作操作系统的进程管理器
- 用户登录的处理过程是这样的一个例子:init进程为每个用户用来登录的串行终端或拨号调制解调器启动一次getty程序。
对应的ps命令输出如下所示:
getty进程等待来自终端的操作,向用户显示熟悉的登录提示符,然后把控制移交给登录程序
fork失败的通常原因是因为父进程所拥有的子进程数目超过了规定的限制
linux内核用进程调度器来决定下一个时间片应该分配给哪个进程,判断的依据是进程优先级,优先级高的进程运行得更为频繁
操作系统根据进程的nice值来决定它的优先级,一个进程的nice值默认为0(nice值会根据这个程序的表现而不断变化:长期不间断运行的程序的优先级一般会比较低,而暂停来等待输入的程序会得到奖励,系统会增加它的优先级)。可以使用nice命令设置进程的nice值,使用renice命令调整它的值
在linux中,进程的运行时间不可能超过分配给他们的时间片,它们采用的是抢先式多任务处理,所以进程的挂起和继续运行无需彼此之间的协作
system函数远非启动其他进程的理想手段,因为它必须用一个shell来启动需要的程序。由于启动程序之前需要先启动一个shell
exec系列函数(都是通过execve实现的)可以把当前进程替换为一个新进程。在启动新进程后,原来的程序就不再运行了。函数分为两大类:
- execl、execlp和execle的参数个数是可变的,参数以一个空指针结束
- execv和execvp的第二个参数是一个字符串数组
以字母p结尾的函数通过搜索PATH环境变量来查找新程序的可执行文件的路径,如果可执行文件不在PATH定义的路径中,就需要把包括目录在内的使用绝对路径的文件名作为参数传递给函数
由exec启动的新进程继承了原进程的许多特性。特别地,在原进程中已打开的文件描述符在新进程中仍将保持打开。任何在原进程中已打开的目录流都将在新进程中被关闭
如果想让进程同时执行多个函数,可以使用线程或从原程序中创建一个完全分离的进程(exec是替换进程)
fork会在进程表中创建一个新的表项。新进程几乎与原进程一模一样,但新进程有自己的数据空间、环境和文件描述符
fork与exec函数结合在一起使用就是创建新进程所需要的一切
wait系统调用将暂停父进程直到它的子进程结束为止
子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才告结束
如果父进程异常终止,子进程将自动把PID为1的进程(即init)作为自己的父进程。子进程现在是一个不再运行的僵尸进程。僵尸进程将一直保留在进程表中直到被init进程发现并释放。进程表越大,这一过程就越慢
可以用来等待子进程结束的系统调用。如果stat_loc不是空指针,waitpid将把状态信息写到它所指向的位置;options参数可以用来改变waitpid的行为,其中最有用的一个选项是WNOHANG,它的作用是防止waitpid调用将调用者的执行挂起
waitpid失败的情况包括:没有子进程(error1设置为ECHILD)、调用被某个信号中断(EINTR)或选项参数无效(EINVAL)
信号是UNIX和linux系统响应某些条件而产生的一个事件。接收到该信号的进程会相应地采取一些行动。信号是由于某些错误条件而生成的,如内存段冲突、浮点处理器错误或非法指令等。它们由shell和终端处理器生成来引起中断
如果进程接收到这些信号,但事先没有安排捕获它,进程将会立刻终止
对于shell和终端驱动程序这样的前台程序,在键盘上输入Ctrl+C组合键就会向它们发送SIGINT信号,这将引起该程序的终止,除非它安排了事先捕获这个信号。对于后台程序来说,使用kill命令发送一个信号给进程,该命令需要有一个可选的信号代码或信号名称和一个接收信号的目标进程PID
kill只能把信号发给属于自己的程序,即发送进程必须和接收进程拥有相同的用户id
程序可以使用signal库函数来处理信号可以使用以下两个特殊值之一来代替信号处理函数
pause的作用是把程序的执行挂起直到有一个信号出现为止。当程序接受到一个信号时,预设好的信号处理函数将开始运行,程序也将恢复正常的执行
使用信号并挂起程序的执行是linux程序设计中的一个重要部分,这意味着程序不需要总是在执行着,程序不必在一个循环中无休止地检查某个事件是否已发生。这在只有一个CPU的多用户环境中尤其重要,进程共享着一个处理器,繁忙的等待将会对系统的性能造成极大的影响
在使用信号的程序中会出现各种各样的“竞态条件”。例如,如果想调用pause等待一个信号,可信号却出现在调用pause之前,就会使程序无限期地等待一个不会发生的事件
一些系统调用会因为接受到了一个信号而失败,而这种错误可能是你在添加信号处理函数之前没有考虑到的
设置信号屏蔽字可以防止前面看到的信号在它的处理函数还未运行结束时就被接受到
linux内核中,在同一时间负责处理多个设备的中断服务例程(信号处理函数的一种)就需要是可重入的
***
线程是一个进程内部的一个控制序列
当在进程中创建一个新线程时,新的执行线程将拥有自己的栈(因此也有自己的局部变量),但与它的创建者共享全局变量、文件描述符、信号处理函数和当前目录状态
除局部变量外,所有其他变量都将在一个进程中的所有线程之间共享
一个多线程的数据库服务器,这是一种明显的单进程服务多用户的情况。它会在响应一些请求的同时阻塞(停止)另外一些请求,使之等待磁盘操作,从而改善整体上的数据吞吐量
线程之间的切换需要操作系统做的工作要比进程之间的切换少得多,因此多个线程对资源的需求要远小于多个进程
可重入代码可以被多次调用而仍然正常工作,这些调用可以来自不同的线程。代码中的可重入部分通常只使用局部变量,这使得每次对该代码的调用都将获得它自己的唯一的一份数据副本
编写多线程程序时,通过定义宏_REENTRANT来告诉编译器我们需要可重入功能,这个宏的定义必须位于程序中的任何#include语句之前。它将为我们做3件事情:
- 它会对部分函数重新定义它们的可安全重入的版本
- stdio.h中原来以宏的形式实现的一些函数将变成可安全重入的函数
- 在error.h中定义的变量error现在将成为一个函数调用,它能够以一种多线程安全的方式来获取真正的error值
在程序中包含头文件pthread.h还将向我们提供一些其他的将在代码中使用到的定义和函数原型
pthread_create它的作用是创建一个新线程,类似于创建新进程的fork函数(第三个参数是一个函数指针,新进程将在这个新位置开始执行);pthread_join函数在线程中的作用等价于进程中用来收集子进程信息的wait函数
想控制任一时刻只有一个线程可以访问一些共享内存,使用互斥量就要自然得多;但在控制对一组相同对象的访问时,就更适合使用计数信号量
信号量是一个特殊类型的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作,即使在一个多线程程序中也是如此。这意味着如果一个程序中有两个(或更多)的线程试图改变一个信号量的值,系统将保证所有的操作都依次进行
信号量一般被用来保护一段代码:
- 每次只允许一个执行线程运行,就要使用二进线信号量
- 允许有限数目的线程执行一段指定的代码,就要用到计数信号量
信号量的这种“在单个函数中就能原子化地进行测试和设置”的能力使其变得非常有价值
sem_post:以原子操作的方式给信号量的值加1(所谓原子操作是指,如果两个线程企图同时给一个信号量加1,它们之间不会互相干扰,信号量的值总是会被正确地加2)、sem_wait:以原子操作的方式给信号量的值减1,但它会等待直到信号量有个非零值才会开始减法操作
互斥量它允许程序员锁住某个对象,使得每次只有一个线程访问它
互斥量的对象类型为pthread_mutex_t。用于互斥量的函数都是来操作这个对象,参数都是先前声明过的这个对象的指针
为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后再完成操作之后解锁它
取消线程:先用pthread_setcancelstate取消请求,再用pthread_setcanceltype设置取消类型
脱离线程:在主线程继续为用户提供服务的同时创建了第二个线程,即不需要第二个线程向主线程返回信息,也不想让主线程等待它的结束
多线程程序,如果要公用共享变量,可以直接传递这个参数的值
***
使用信号,传送的信息仅限于一个信号值
管道:通常是把一个进程的输出通过管道连接到另一个进程的输入
可能最简单的在两个程序之间传递数据的方法就是使用popen和pclose函数popen函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。command字符串是要运行的程序名和相应的参数。open_mode必须是“r”或者“w”。“r”调用程序就可以使用被调用程序的输出,此时调用程序就可以使用popen函数返回的FILE*文件流指针,再通过stdio库函数(如fread)来读取被调用程序的输出;“w”调用程序就可以用fwrite调用向被调用程序发送数据,而被调用程序可以在自己的标准输入上读取这些数据
在linux(以及所有的类UNIX系统)中,所有的参数扩展都是由shell完成的
pipe函数的参数是一个由两个整数类型的文件描述符(必须使用底层的read和write调用来访问数据。管道有一些内置的缓存区,它在write和read调用之间保存数据)组成的数组的指针。写到file_descriptor[1]的所有数据都可以从file_descriptor[0]读回来,数据基于FIFO进行处理,意味着把字节1,2,3写到file_descriptor[1],从file_descriptor[0]读取的也是1,2,3(栈是LIFO)
当程序用fork调用创建新进程时,原先打开的文件描述符仍将保持打开状态。我们可以在调用fork创建新进程前创建一个管道,我们即可通过管道在两个进程之间传递数据
命名管道是一种特殊类型的文件,它在文件系统中以文件名的形式存在
、
在命令行或者用mknod函数建立命名管道
因为命名管道是以命名文件的形式存在,而不是打开的文件描述符,所以在对它进行读写操作之前必须先打开它
程序不能以O_RDWR模式打开FIFO文件进行读写操作,我们通常使用FIFO只是为了单向传输数据
当一个linux进程被阻塞时,它并不消耗CPU资源
当只使用一个FIFO并允许多个不同的程序向一个FIFO读进程发送请求时,通常将每次通过FIFO传递的数据长度限制为PIPE_BUF字节是个好办法,除非只使用一个写进程和一个读进程
***
当编写的程序使用了线程时,不管它是运行在多用户系统上、多进程系统上,还是运行在多用户多进程系统上,通常会发现,程序中存在着一部分临界代码,我们需要确保只有一个进程(或一个执行线程)可以进入这个临界代码并拥有对资源独占式的访问权
信号量正式的一个定义:它是一个特殊变量,只允许对它进行等待和发送信号两种操作
sv为true表示临界区域可用
PV操作如何把守代码中的临界区域的:
程序对所有的信号量的访问都是间接的,它先提供一个键,再由系统生成一个相应的信号量标识符
只有semget函数才可以直接使用信号量键,所有其他的信号量函数都是使用semget函数返回的信号量标识符(相当于fopen返回的FILE*文件流)
如果在执行之后还留下信号量未删除,它可能会在你下次运行此程序时引发问题
大多数共享内存的具体实现,都把由不同进程之间共享的内存安排为同一段物理内存
每个进程的逻辑地址空间到可用物理内存的映射关系:
共享内存的读写权限由它的属主(共享内存的创建者)、它的访问权限和当前进程的属主决定。共享内存的访问权限类似于文件的访问权限
第一次创建共享内存段时,它不能被任何进程访问。要想启用对该共享内存的访问,必须将其连接到一个进程的地址空间中。这项工作由shmat函数来完成
当删除一个正处于连接状态的内存段时,这个已经被删除的处于连接状态的共享内存段还能继续使用,直到它从最后一个进程中分离为止
在实际编程中我们应该使用信号量或通过消息队列、生成信号的方法来提供应用程序读、写部分之间的一种更有效率的同步机制
***
首先,服务器应用程序用系统调用socket来创建一个套接字(它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他进程共享)
接下来,服务器进程会给套接字起个名字。本地套接字的名字是linux文件系统中的文件名,一般放在/tmp或/usr/tmp目录中。对于网络套接字,它的名字是与客户连接的特定网络有关的服务标识符(端口号或访问点)。这个标识符允许针对特定的端口号的连接转到正确的服务器进程
系统调用bind来给套接字命名;系统调用listen创建一个队列并将其用于存放来自客户的连接;服务器通过系统调用accept来接受客户的连接
服务器调用accept时,它会创建一个与原有的命名套接字不同的新套接字。这个新套接字只用于与这个特定的客户进行通信,而命名套接字则被保留下来继续处理其他客户的连接
客户端首先调用socket创建一个未命名套接字,然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接
套接字的创建过程与普通文件一样,它的访问权限会被当前的掩码值所修改
一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向的数据通信
用完一个套接字后,就应该把它删除掉,即使在程序因接收到一个信号而异常终止的情况下也应该这么做。这可以避免文件系统应充斥无用的文件而变得混乱
套接字的特性由3个属性确定,它们是:域(domain)、类型(type)、和协议(protocol)
域指定套接字通信中使用的网络介质。最常见的套接字域是AF_INEF,它指的是Internet网络。其(指的是domain)底层的协议--网际协议(IP)只有一个地址族,它使用一种特定的方式来指定网络中的计算机
当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址。但是一个服务器计算机上可能同时有多个服务正在运行,此时需要使用IP端口来指定这台联网机器上的某个特定服务
因特网协议提供了两种通信机制:流和数据报
流套接字(在某些方面类似于标准的输入/输出流)提供的是一个有序、可靠、双向字节流的连接。大的消息将被分片、传输、再分组。这很像一个文件流,它接受大量的数据,然后以小数据块的形式将它们写入底层磁盘
流套接字由类型SOCK_STREAM指定,它们是在AF_INET域中通过TCP/IP连接实现的(其中IP协议是针对数据包的底层协议,它提供从一台计算机到达另一台计算机的路由,TCP协议则提供排序、流控和重传,以确保大数据的传输可以完整地到达目的地或报告一个适当的错误条件)
数据报套接字由类型SOCK_DGRAM指定,不建立和维持一个连接。它对可以发送的数据报的长度有限制。数据报套接字是在AF_INET域中通过UDP/IP连接实现的,它提供一中无序的不可靠服务,但他们开销小、速度快
数据报在服务器崩溃的时候不会给客户造成不便,也不会要求客户重启,因为基于数据报的服务器通常不会保留连接信息
type指定新创建套接字的通信类型;domain决定了socket地址类型;protocol指定了协议,协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
通信所用的协议一般由套接字类型和套接字域来决定,通常不需要选择,将参数设置为0表示使用默认协议
socket系统调用返回一个描述符,在许多方面都类似于底层的文件描述符。当这个套接字连接到另一端的套接字后,我们就可以用read和write系统调用,通过这个描述符在套接字上发送和接收数据了
close系统调用用于结束套接字连接。应该总是在连接的两端都关闭套接字
要想通过socket调用创建的套接字可以被其他进程使用,服务器程序就必须给该套接字命名
listen函数将队列长度设置为backlog参数的值,在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字。再往后的连接将被拒绝,导致客户的连接请求失败
一旦服务器程序创建并命名了套接字之后,它就可以通过accept系统调用来等待客户建立对该套接字的连接
accept系统调用只有当客户程序(指的是套接字队列中排在第一个的未处理连接)试图连接到由socket参数指定的套接字上才返回,否则accept将会阻塞(程序将暂停)直到有客户建立连接为止
客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器
参数socket指定的套接字将连接到参数address指定的服务器套接字。如果连接不能立刻建立,connect调用将阻塞一段不确定的时间。一旦这个超时时间到达,连接将被放弃,connect调用失败
但如果connect调用被一个信号中断,而该信号又得到了处理,connect还是会调用失败(error被设置为EINTR),但连接不会被放弃,而是以异步方式继续建立,程序必须在此后进行检查以查看连接是否成功建立
虽然异步方式难于处理,但我们可以在套接字文件描述符上,用select调用来检查套接字是否已处于写就绪状态
文件套接字的缺点,除非程序员使用一个绝对路径名,否则套接字将创建在服务器程序的当前目录下。为了让它通用型,需要将它创建在一个服务器及客户都认可的可全局访问的目录(如/tmp目录)中。网络套接字,只需选择一个未被使用的端口号(端口号及它们提供的服务通常都在系统文件/etc/services中,请注意总是选择没有列在该配置文件中的端口号)即可。我们不能使用小于1024端口号,它们都是系统使用保留的
回路网络中只包含一台计算机,传统上它被称为localhost,它有一个标准的IP地址127.0.0.1
每个与计算机进行通信的网络都有一个与之关联的硬件接口
通过套接字接口传递的端口号和地址都是二进制数字,不同计算机使用不同的字节序来表示整数。如果保存整数的内存只是以逐个字节的方式来复制,两个不同的计算机得到的整数值就会不一样。所以客户和服务器程序必须在传输之前,将它们的内部整数表示方式转换为网络字节序
UNIX通常以超级服务器的方式来提供多项网络服务。超级服务器程序(因特网守护进程xinetd或inetd)同时监听许多端口地址上的连接。当有客户连接到某项服务时,守护进程就运行相应的服务器
xinetd的配置文件通常是/etc/xinetd.conf和/etc/xinetd.d目录中的文件。每一个由xinetd提供的服务都在/etc/xinetd.d目录中有一个对应的配置文件。xinetd将在其启动时或被要求的情况下读取所有这些配置文件
服务器程序在接受来自客户的一个新连接时,会创建出一个新的套接字,而原先的监听套接字将被保留以继续监听以后的连接。这一事实给我们提供了一种同时服务多个客户的办法。如果服务器调用fork为自己创建第二份副本,打开的套接字就将被新的子进程所继承。新的子进程可以和连接的客户进行通信,而主服务器进程可以继续接受以后的客户连接
select系统调用允许程序同时在多个底层文件描述符上等待输入的到达(或输出的完成)
用于测试文件描述符集合中,是否有一个文件描述符已处于可读状态或可写状态或错误状态,它将阻塞以等待某个文件描述符进入上述这些状态。如果这3种情况都没有发生,select将在timeout指定的超时时间经过后返回。如果timeout参数是一个空指针并且套接字上也没有任何活动,这个调用将一直阻塞下去。参数nfds指定需要测试的文件描述符数目,测试的描述符范围从0到nfds-1
select返回时,描述符集合将被修改以指示哪些描述符正处于可读、可写或有错误的状态。我们可以用FD_ISSET来对描述符进行测试,找出需要注意的描述符
服务器可以让select调用同时检查监听套接字和客户的连接套接字
当客户需要发送一个短小的查询请求给服务器,并且期望接收到一个短小的响应时,我们一般就使用由UDP提供的服务。如果服务器处理客户请求的时间足够短,服务器就可以通过一次处理一个客户请求的方式来提供服务,从而允许操作系统将客户进入的请求放入队列
因为UDP提供的是不可靠的服务,如果数据对于你来说非常重要,就需要小心编写UDP客户程序,以检查错误并在必要时重传。实际上,UDP数据报在局域网中是非常可靠的
为了访问由UDP提供的服务,需要向以前一样使用套接字和close系统调用,但需要使用两个数据报专用的系统调用sendto和recvfrom来替代原来使用在套接字上的read和write调用