MP77的UNIX课件笔记(11)

3 进程与文件描述符

3.1 内核中的文件打开结构

我们已经在前文中介绍过有关文件描述符的概念。在OS文件系统中,为了提高文件访问的效率,在访问一个文件时,将文件的inode节点读入内核内存,在整个文件访问期间使用内存中的inode节点。由于同一个进程可以访问多个文件,多个进程可以同时访问文件,因此在内核中构建了“活动文件目录AFD”active file directory,文件描述符即该文件目录的索引。

从内核的角度来看,AFD是一个三级存储结构,它包括了如下几个部分:

1、进程PCB的user结构中有一整型数组u_ofile,记录当前进程打开的文件。所谓的文件描述符fd,实际上就是user结构中u_ofile数组的下标值。每个进程有一个u_ofile数组。当然,无论是动态分配还是静态分配,系统不可能允许这个数组无限大,这就是每个进程最多可以打开的文件个数限制。

2、u_ofile数组中的元素值,是一个整数,这个整数是file数组的一个下标。file定义在/usr/include/sys/file.h中,主要包含以下几个域:

struct file{

char f_flag; //开启文件的读写操作要求

char f_count; //引用计数

int f_inode; //内核中inode数组的下标,可找到已读入内核中的文件inode节点

offset_t f_offset; //文件读写位置指针,系统在此记录文件读写位置

}

3、内存inode表在整个系统也只有一张,是外存中inode的缓冲。在内存inode中也有一个引用计数字段,统计有多少个file结构引用它。

在具体的OS实现中,尽管可能使用了更复杂的引用类型,但整体三级结构的框架是不变的。

内存AFD三级存储结构的构建,与文件描述符的有关操作密切相关。

例如open调用,其实质是增加了活动文件目录,在三级表格中增加原始条目。在open层次上已经将设备虚拟得跟普通磁盘文件一样。

又例如管道操作pipe,网络通信socket,都是创建文件描述符,系统把它们虚拟得跟普通文件一样,随后利用read、write像访问普通磁盘文件一样访问管道,或者在网络连接上收发数据。fork导致的子进程继承也会增加条目。

而close调用实质是AFD的删除操作,直接消除u_ofile项,根据引用计数,可能会引发file,inode结构的释放。进程正常地或者异常地终止,内核都会根据进程user结构中u_file的记载,自动关闭已打开的所有文件。

3.2 文件描述符的继承和复制

3.2.1 文件描述符的继承

根据上节介绍的AFD原理,我们很容易能理解fork和exec对已打开文件的影响。这里仍有必要对此进行进一步说明。

fork创建子进程后,子进程继承父进程已打开的所有文件描述符。具体做法就是,子进程user结构中的u_ofile是父进程这个数组的复制。为了防止随后各自独立执行的父子进程close调用会带来的影响,进行了这样的复制后,所有打开文件描述符对应file结构中的f_count都加1。这样,父子进程可以独立地关闭各自的文件,而对另一个进程不产生影响。这种做法还使得父子进程共用文件的读写位置。

由于fork后父进程的文件描述符被子进程继承,因此子进程不需要再次执行open调用,就可以直接使用这些文件描述符,由于exec系统调用不会创建任何进程,执行exec系统调用也不影响文件描述符。

shell程序正是利用了文件描述符的继承机制,向子进程自动文件描述符0、1、2,分别表示标准输入、标准输出和标准错误输出,确保该进程的上述操作均关联至当前tty。

3.2.2 close-on-exec标志

在有的情况下,我们希望在执行exec时自动关闭某些文件描述符。这就需要为已经打开的文件设置close-on-exec标志。

内核为每个文件描述符记录了一个文件描述符标志字,标志字的第0比特是close-on-exec标志。默认情况下,该标志位被清除,因此如果要求文件描述符在exec时自动关闭,必须取出这个文件描述符的标志字,将第0比特设置为1,标志字的其他比特保持原值,然后重新设置文件描述符的标志字。

获取文件描述符标志字,使用系统调用函数fcntl,函数原型如下:

include

int fcntl(int fd,int cmd,…);

fcntl有很多功能,这里用到的仅仅是获取和设置文件描述符控制字的功能,后面的文件和记录锁定,以及设置无阻塞I/O时,还会用到这个函数。

flags = fcntl(fd,F_GETFD,0);

flags |= FD_CLOEXEC;

fcntl(fd,F_SETFD,flags);

上述程序演示了获取文件描述符关键字、置位close-on-exec标志,然后重新设置文件描述符控制字。F_GETFD和F_SETFD都是中定义的宏。为了程序的可读性,不直接操作第0比特,而是使用宏FD_CLOEXEC,清除该标志应当使用下面的语句:

flags &= ~FD_CLOEXEC;

3.2.3 文件描述符的复制

fork在创建新进程时复制所有文件描述符,如果只需要复制一个文件描述符,需要使用到System call为dup2。

int dup2(int fd1,int fd2);

复制文件描述符fd1到fd2,fd2可以是空闲的文件描述符,如果fd2是已打开的文件,则先关闭原先的fd2,如果fd1不是有效的描述符,则不关闭fd2,调用失败。dup2的返回值为-1时,标志调用失败。

3.3 管道操作

3.3.1 创建管道

进程使用fork创建子进程后,父子进程就有各自独立的存储空间,互不影响。两个进程之间交换数据就不可能像进程内的函数调用那样,通过传递参数或者使用全局变量实现,必须通过其它的方式。

管道是一种历史悠久的进程间通信机制,在shell中通常使用元字符|连接两个命令,就是基于管道机制而实现的。

管道创建后会在内核中生成一个管道对象,进程可以得到两个文件描述符,然后程序就像访问文件一样访问管道。write调用将数据写入管道,read调用从管道中读出写入的内容。读入的顺序和写入的顺序相同。

int pipe(int pfd[2]);

当创建管道失败时,pipe返回-1。创建管道成功后,获得两个文件描述符pfd[0]和pfd[1],分别用于读管道和写管道。这样如果进程向pfd[1]写入数据,那么就会从pfd[0]顺序读出来。

管道实现的基本思路是,当使用fork创建子进程后,文件描述符被继承,这样父进程冲pfd[1]写入的数据,子进程就可以从pfd[0]读出,从而实现父子进程之间的通信。

一般情况下,父子进程就可以关闭不再需要的文件描述符。

3.3.2 管道读写操作

对于写操作write来说,由于管道是内核中的一个缓冲区,缓冲区不可能无限大,或者说管道不可能长度无限。若管道已满,则write操作会导致进程被阻塞,直到管道另一端read将已进入管道的数据取走后,内核才把阻塞在write的写端进程唤醒。管道容量依赖于Unix系统的实现,一般至少为4096B。

管道的读操作分三种情况。

第一种情况,管道为空,则read调用会将进程阻塞,而不是返回0.进程会一直等待到管道写端向管道写入了数据,才会醒来,read调用返回。类似的情况还有终端读,以及网络通信socket读,在终端没有按键,或者网络上尚未有数据到达的时候,read一样会将进程睡眠等待,而不是返回0。

第二种情况,管道不为空,返回读取的内容,read调用的形式为:

n = read(fd,buf,m);

read的第三个参数m是最多可以读取的字节数。如果管理中实际有n个字节,那么如果m>=n,则读n个;如果m

第三种情况,管道写端已关闭,则返回0。类似的,终端文件和网络socket,终端上按ctrl+D键或者网络连接被关闭,read也是返回0。

两个独立的进程对管道的读写操作,如果未写之前,读先行一步,那么,操作系统内核在系统调用read中让读端进程睡眠,等待写端送来数据。同样,如果写端的数据太多或者写得太快,读端来不及读,管道满了之后操作系统内核就会在系统调用write中让写端进程睡眠,等待读端独奏数据。这种同步机制,在读写速度不匹配时不会丢失数据。

3.3.3 管道的关闭

只有所有进程中引用管道写端的文件描述符都关闭了,读端read调用才返回0。

关闭读端,不再有任何进程读,则导致写端write调用返回-1。errno被设为EPIPE,在写端write函数退出前进程还会收到SIGPIPE信号,默认处理是终止进程,该信号可以被捕捉。

3.3.4 管道通信应注意的问题

1、管道传输的是一个无记录边界的字节流。写端的一次write所发送的数据,读端可能需要多次read才能读取,如一次写64KB数据。也有可能写端的多次write所发送的数据,读端一次就全部读出积压在管道中的所有数据。使用TCP协议的网络socket操作也存在同样的问题。

2、父子进程需要双向通信时,应采用两个管道。父子进程只使用一个管道进行双向数据传送时会存在问题导致数据流混乱。

3、父子进程使用两个管道传递数据,安排不当就有可能产生死锁。死锁出现的原因是,如果父进程一次性将若干处理请求写至管道A,然后读管道B等待这个请求的处理结果。子进程先读管道A得到处理请求,但是每次只从管道A中读走一个请求,将处理结果写到管道B。如果因为某个处理请求的数据过大,写管道A满而导致父进程被阻塞,而子进程因要向父进程写回一个体积较大的数据而导致写管道B也被阻塞,这时死锁出现。

4、管道的缺点,管道是半双工的通信通道,数据只能在一个方向上流动,且只限于父子进程或同祖先进程间通信,而且没有保留记录边界。

3.3.5 命名管道

命名管道允许没有共同祖先的不相干进程访问一个FIFO管道。首先用命令:

mknod pipe0 p

创建一个文件,pipe0是文件名,p是文件类型标识。

这时在文件系统中就存在一个命名管道,向这个文件中写入数据,就是向管道内写数据,从这个文件中读取数据,就是从管道中读取数据。

发送者调用:

fd = open(“pipe0”,O_WRONLY);

write(fd,buf,len);

接受者调用:

fd = open(“pipe0”,O_RDONLY);

len = read(fd,buf,sizeof buf );

总的来说,管道是最早用于进程之间通信的手段,包括后来增加的命名管道。而Unix从System V开始增强了进程之间的通信机制IPC(inter-process communication),提供了消息队列message、信号量semaphore和共享内存share memory等多种通信方式,限于篇幅我们不可能一一列举,读者也可以根据需要随时查阅相关资料。

Comments

MP77的UNIX课件笔记(10)

上一章讨论了Unix进程控制机制和具体实现,在此基础上我们将进一步研究进程间通信的原理和方法。

2 信号Signal

2.1 信号的基本概念

信号是进程间通信的方法之一,它用以指示某些事件的发生。信号提供了一种异步事件处理的方法。

信号可以由系统核心程序发出,也可以由某些进程发出,但大部分时候是由核心程序发出的。

如系统核心程序在下面几种情况会向进程发信号:

1、程序有异常行为,如企图处以零。(SIGFPE)

2、系统测出一个可能出现的电源故障。(SIGPWR)

3、该进程的子进程执行终止。(SIGCHLD)

4、用户由终端对目标进程输入中断(delete或ctrl-c),退出(ctrl-)等键。

5、进程调用kill函数可以将信号发送给一个进程或进程组。

6、用户可以用kill命令将信号发送给其他进程。

同时系统中很多条件下会产生一个信号,它们分别被赋予不同的含义:

7、按键产生信号。

SIGINT:Ctrl+C(有的系统是Del键),默认情况下中止当前的进程。

SIGQUIT:Ctrl+\键,默认情况下中止当前进程,但额外生成一个记录进程的内存存储图像的core文件,调试程序可以使用core文件检查进程在终止时的状态。

SIGTSTP信号:在支持作业控制的系统中,终端上按下“挂起键”(一般是 Ctrl+Z键),会产生SIGTSTP信号,默认处理是暂停当前进程的执行,挂起(suspend)当前进程。

8、硬件异常产生信号。

SIGSEGV:内存越界或者试图写只读存储区的存储单元,CPU中的内存管理单元MMU的内存保护机制会引发一个软件中断,操作系统内核在中断服务程序中向进程发送段违例信号(segmentation violation)。

SIGFPE:CPU产生中断最终导致内核向进程发送浮点溢出信号SIGFPE通知用户态的进程。

SIGBUS:早期的RISC结构CPU要求一个4B整数的地址必须能被4整除。

SIGILL:用户状态下的CPU不允许执行硬件I/O指令和其他特权指令。如果用户程序代码中有这样的指令,或者是非法的指令编码,CPU就会产生软中断,最终内核的处理就是送达进程SIGILL信号。默认处理是进程终止。

9、事件产生的信号,当某些事件发生,内核监测到某种条件时,也会给进程发出信号。

SIGALRM:当进程设置的闹钟时间到时会收到该信号。

SIGPIPE:两进程用管道进行通信,从管道读取数据的进程关闭了管道,向管道的写操作进程收到SIGPIPE信号。

SIGTTIN:后台进程试图读终端,会导致终端向其发送SIGTTIN信号,默认处理是进程终止。

SIGHUP:发生在用户从当前终端退出登录的时候,运行在该终端上的程序,会收到SIGHUP信号。

SIGCLD:子进程终止时会产生僵尸进程,内核向父进程发送该信号,通知父进程用wait()调用来获取子进程的终止状态,并销毁僵尸进程,释放僵尸进程占用的资源。

10、其他进程发送来的信号。

用户直接使用kill命令,或者,程序中使用kill()函数,向其他进程发送信号。

发送的信号可以是任意信号类型。发送信号的进程和接收信号的进程必须是同一个用户的进程,或者,发信号的进程是超级用户。以防止不同用户间的恶意干扰。

2.2 SIGNAL定义

SIGNAL标识定义在/usr/include/sys/signal.h头文件中,给每个信号都定义了一个宏名字,这些宏名字都是以SIG开头,这些信号都被定义为正整数(信号编号)。例如delete键和ctrl-c产生的信号是SIGINT,退出(ctrl-)产生的信号是SIGQUIT等,SIGINT的值为2,SIGQUIT的值为3。

SIGNAL类型有很多,这里不再详细介绍。读者可以参考以下较常用的值:

名字 说明 缺省动作

SIGALRM 用户用alarm设置时钟,定时器到时 终止

SIGCHLD 子进程消亡,向父进程发此信号 忽略

SIGCONT 使暂停进程继续 忽略

SIGFPE 算术异常,如除以0 终止w/core

SIGILL 当硬件检查到非法指令时,发送该信号 终止w/core

SIGINT 用delete或ctrl_c 终止,发送到终端相连的所有进程 终止

SIGKILL 杀死进程,不能被捕获或忽略,发生紧急事件用 终止

SIGQUIT 用户用ctrl-\终止程序 终止w/core

SIGUSR1 用户自定义信号1 终止

SIGUSR2 用户自定义信号1 终止

SIGHUP 一个终端切断时,发送信号到该终端相连的所有进程 终止

SIGTERM 由kill命令发送的系统默认终止信号 终止

SIGPIPE 写管道错,进程向没有任何读进程的管道中写数据 终止

2.3 kill发送信号

kill命令和kill函数的功能仅仅是将一个信号送达一个进程或者进程组内的所有进程。

尽管多数的默认情况下,用户直接使用kill命令,不附带任何选项,会给进程送达一个SIGTERM信号。对于那些终端失去控制的进程无法用Ctrl+C键终止,那么,就可以从其他终端上登录,用ps命令查出进程的PID,然后用kill命令发送信号给进程,如果终端还不能恢复正常,甚至可以用kill命令发送信号给这个终端上的shell进程。

但是,该信号是否确实能够将进程“杀死”还要看信号的类型以及进程自身的行为,是否安排了捕捉这个信号。

2.3.1 kill命令

kill -signal PID-list

kill 1275 1277

默认信号为15(SIGTERM),一般会导致进程终止。

kill -9 1326

向进程1326发送一个信号9(SIGKILL),会导致该进程死亡。

在kill命令中,指定进程号PID时,可使用特殊的PID号0,$kill 0 或

kill -9 0

向与本进程同组的所有进程发送信号。

2.3.2 会晤组和进程组

UNIX为每个进程在其PCB中设置了两个字段,进程组号PGID(process group ID),会晤组号SID(session ID)。

进程的PGID是创建子进程的时候从父进程那里继承来的,PGID相同的所有进程构成一个“进程组”。PID号和PGID相等的进程是进程组的组长。组长进程终止后,进程组照样可以存在。

从进程的组织结构上看,一个会晤组由一个或者多个进程组构成。

进程的SID也是创建子进程时从父进程那继承来的,SID相同的所有进程构成一个“会晤组”。PID和SID相等的进程是会晤组首进程。也可能会出现没有首进程的会晤组。

ps命令的j选项(job)可以打印出进程的PGID和SID。

ps -j -u fang

查阅用户fang的所有进程,每个进程都打印出PGID和SID。

一个会晤组由多个进程组构成。进程组分两类,前台进程组和后台进程组。前台进程组最多只有一个,后台进程组可以有多个。

例如在shell中,从当前登录shell启动的所有进程都属于一个会晤组,会晤组首进程是登录shell自己。通常在一个tty中,进程的标准输出会输出到tty,tty上的按键产生的SIGINT和SIGQUIT信号,只送到前台进程组。如果后台进程企图从tty上获取输入如:scanf(),gets(),进程就会收到SIGTTIN信号,默认处理是终止进程。

2.3.2.1 SIGHUP信号

SIGHUP信号的产生有两种不同的情况:

1、如果控制终端突然断开,那么,内核负责向会晤组首进程发送SIGHUP信号。会晤组首进程终止,内核仅负责向会晤组内的前台进程组发送SIGHUP信号,但不发送到后台进程组。会晤组首进程终止还会导致原会晤组内的所有进程组都失去控制终端。

控制终端突然断开的情况发生在连接终端的调制解调器断线,或者使用网络虚拟终端时,TCP连接断开。TCP连接断开的原因会是由于网络故障,或者TELNET客户端使用TELNET自身的close命令关闭连接。

2、会晤组首进程的终止,包括自愿终止和被迫终止。在登录shell中执行exit或logout,作为会晤组首进程的shell就会终止。被迫终止:在其他终端上使用kill -9命令,或者会晤组首进程中的软件故障导致内存越权访问而收到内核发来的SIGSEGV信号而终止。

会晤组首进程终止后,残留的后台进程组就失去了控制终端,用ps命令列出的进程的TTY属性打印的是问号(?)。即使同一个用户再次从这个终端登录,也不会成为这些进程的控制终端。

失去控制终端的进程中访问终端的操作read(0,buf,nbytes)会导致read返回0,这会影响scanf,gets等函数;写操作write(1,buf,nbytes)会失败返回-1,errno为EIO,没有任何输出,这影响printf等函数。

2.3.2.2 不同shell的PGID与SID区别

会晤组,前台进程组,后台进程组,进程的这些组织关系,是由UNIX的相关系统调用实现的。

上述的这些处理方式,在C-shell, K-shell, bash中相同。

在不支持作业控制的传统的Bourne Shell中,处理就会有些不同。它作为登录shell时,在一个终端上启动的所有进程,包括像前面使用&元字符启动的进程,以及前台进程,都属于同一个进程组。登录shell进程做组长。也就是说会晤组内只有一个进程组,而且作为前台进程组。

SIGHUP信号的发送时机与会晤组,进程组,控制终端的关系,以及进程组织关系的安排,在不同的系统或者不同的shell中会有些差异。有的UNIX不支持作业控制功能,没有会晤组(session)的概念,但是都支持进程组的概念。

2.3.2.3 setpgid

进程组的最主要作用就是进程组的成员可以一起接收到相同的信号。这样便于一起管理共同协作的多个进程。

UNIX提供了系统调用函数setpgid(),可以修改进程的PGID。

include

include

int setpgid(pid_t pid, pid_t pgid);

将进程pid组号设为pgid。成功函数返回0;失败返回–1。

如果参数pid设为0,使用进程自己的PID;参数pgid设为0,使用pid指定进程的PID做组号。

为了安全起见,系统只允许进程修改它自己和它的子进程的PGID,而且,在子进程调用了exec之后,就不能再改变子进程的PGID。

如果fork之后再由父进程修改子进程的PGID就可能会出问题。如果子进程赶在父进程修改PGID之前执行了exec,那么父进程的修改就会失败。所以,fork之后,子进程修改自己的PGID后再执行exec就可以避免这样的情况发生。

2.3.2.4 setsid

一般的程序员对setpgid()调用不是很感兴趣,这常常由shell程序使用。

一般程序员更感兴趣的是系统调用函数setsid()。它设置进程的SID和PGID都为自己的PID,而且脱离控制终端。系统调用的函数原型是:

pid_t setsid(void);

调用这个函数的进程必须不是组长进程,调用才能成功。

调用结束后,事实上创建了新的会晤组和进程组,并失去控制终端。

当前进程成为了新会晤组和新进程组的惟一成员,既是会晤组首进程又是进程组组长。这样,脱离了原来的会晤组和进程组关系之后,原终端退出登录,原进程组群发信号,当前进程都不会再受到干扰。

2.3.3 kill系统调用

include

include

int kill(pid_t pid,int signo);

把信号signo发送给进程标识号为pid的相关进程。成功时间返回0,失败返回-1。

pid取值情况:

正数:将信号发送给指定的进程;

0: 将信号发送给调用进程的同组进程;

负数:向以-pid为组长的所有进程发信号sig。

2.4 信号的捕捉与处理

2.4.1 信号的捕捉

进程接收到信号后,处理的方式有三种:

1、忽略方式

进程在接收到一个被指明忽略的信号后,则将该信号清除后,立即返回,不进行其他处理。但信号SIGKILL和SIGSTOP是不能被忽略的。原因是,它们向超级用户提供一种使进程终止的方式。

signal(SIGINT,SIG_IGN);

signal的第一个参数是要忽略的信号名字,第二个参数是宏SIG_IGN。执行了这个调用后,进程就不再收到SIGINT信号。

如果进程忽略SIGCLD信号,子进程终止后,系统会自动销毁僵尸子进程。

信号被忽略,作为进程的一种属性,会被它的子进程所继承。

Unix提供一种系统命令来实现SIG_IGN的效果,即nohup。

用nohup来运行一个命令可以使得程序的执行免于SIGHUP信号的打扰,在终端注销后继续运行。

上面的例子,没有修改xyz.c中的任何程序,单独启动xyz时,可以被kill命令终止,从abc进程中启动时,又不能被kill命令终止。

如果使用类似上述方式,不需要修改命令程序,也可以做到让启动的命令进程忽略SIGHUP信号。这样,终端被挂断时,就不会终止正在运行的命令。这就是nohup命令的基本做法。

nohup 命令 命令参数

$nohup find / -name data -print>f.res 2>/dev/null &

find命令就在后台运行,终端注销时进程也不会终止。如果上述命令的输出没有重定向,nohup自动将find命令的输出重定向到nohup.out文件中。

2、默认方式

大多数信号的系统默认动作是终止该进程。

signal(SIGINT,SIG_DFL);

signal的第一个参数是信号的宏名字,第二个参数是宏SIG_DFL。

由于信号的处理属性会从父进程继承,所以,程序运行初始,信号的处理方式不见得会是一种默认方式。如果要求必须是默认处理方式,那么,就必须执行这个函数调用。

3、捕捉方式

进程在接收到该信号时,执行用户设置的信号处理函数,执行完毕,恢复现场,然后继续往下执行。

include

singal(int signo, *func)

signo是除SIGKILL和SIGSTOP以外的任何一种信号。

func定义了该信号的处理方式,它的值可以是:

SIG_IGN

SIG_DFL

当指使定函数时,我们称为捕捉此信号,对应的函数称为signal handler or signal-catching function。

signal调用成功的返回值总是进程上次对指定信号的处理方式。失败时返回-1。

例如下段程序捕捉按下Ctrl+C键时和Ctrl+\键时产生的信号。

include

void sig_handle(int sig)

{printf(“HELLO! Signal %d catched.\n”, sig);}

main(){

int i;

signal(SIGINT, sig_handle);

signal(SIGQUIT, sig_handle);

for (i = 0; ; i++) {

printf(“i=%d\n”, i);

sleep(1);}

}

UNIX中一个捕捉的信号在处理它的用户函数被调用之前,首先被内核重置到它的默认行为。因此,第一次按下Ctrl+\ 时,执行sig_handle()之前,已被置为默认行为。从此之后,只要再按Ctrl+\ 键,仍按默认行为处理,导致进程终止。

2.4.2 捕捉处理方式

当造成信号的事件发生时,将为进程产生一个信号(或向一个进程发送一个信号)在信号产生时,内核通常在进程表中设置对应于该信号的的位。

如当系统运行一个需要较长时间的程序时,我们发现有错误产生,并断定该程序最终要失败。为了节省时间,可以按ctrl-c终止该程序的运行。这一过程的实现就用到了信号。

响应键盘输入的核心部分发现了中断ctrl-c后,就向发中断字符的终端上运行的所有进程发送一个SIGINT信号。该进程接收到此信号时,就完成与SIGINT有关的工作,然后终止。该终端上的shell进程也会收到内核发来的SIGINT信号,由于它必须执行,以解释以后键入的系统命令,所以它会忽略这个信号。当然程序也可以捕获这个信号。

2.4.3 SIGCLD信号的处理

将信号处理函数第一行写为重新设置信号处理函数的语句signal,可以减少风险。

需要特别注意的是SIGCLD信号的处理。当一个子进程终止后产生了僵尸子进程,父进程会收到信号SIGCLD。信号处理函数必须在完成了wait()调用销毁了僵尸进程之后,才可以再次调用类似下面的语句来重新设定SIGCLD信号的用户处理函数。

signal(SIGCLD, …);

因为在调用signal(SIGCLD,…)时,内核将检查当前是否已经有僵尸子进程,如果现有的僵尸子进程尚未用wait()调用销毁,内核会立刻发送SIGCLD信号。再次进入信号处理函数,信号处理函数的第一行,又重复这样的操作,于是进入死循环,进程堆栈不停地增长,最终程序被终止。所以,必须在用wait()调用销毁了僵尸子进程之后才可以再次重置SIGCLD的处理函数。

如果进程不设置对SIGCLD信号的处理,而且也不在子进程终止后去调用wait(),那么,子进程终止后的僵尸进程就一直存在。

2.4.4 进程收尸

上节最后提到子进程终止后的僵尸进程将一直存在,那么我们需要试图为其“收尸”以释放系统资源。

ps命令可以看到子进程是僵尸(defunct)进程,kill -9命令无法销毁它。

kill将其父进程终止后,僵尸进程变成孤儿进程,由操作系统的1进程领养,新的父进程负责销毁僵尸进程。于是,最后的ps命令发现僵尸进程已经被销毁。如果用户进程是个长期运行的进程,一直作为僵尸进程的父进程,那么,僵尸进程就一直存在。

程序中应当捕捉SIGCLD,在信号处理函数中执行wait调用销毁僵尸进程。如果进程对它的子进程终止状态毫无兴趣,在System V中,可以直接使用:

signal(SIGCLD, SIG_IGN);

忽略SIGCLD信号,这样,子进程产生的僵尸就被系统自动销毁了,不会再有僵尸子进程出现。

.2.5 全局跳转longjmp

经常需要一个类似于SIGINT这样的信号来只终止当前的活动,而不是整个进程。这时,当进程捕捉信号后必须跳到主循环,或者在某个地方恢复执行。例如如下程序:

main(){

int c;

for (;;) {

printf(“Input a command:”);

c = getchar();

switch © {

case ’Q’: return(0);

case ’A’: func_a(); break;

case ’B’: func_b(); break;

… }

}

}

这段程序先是提示用户输入一条命令,然后根据命令的不同,执行不同的内容。

例如用户输入A,就执行函数func_a(),假设该函数的处理非常复杂,需要完成共10个阶段非常复杂的计算,大约总共需要5~10min。

有时,用户在输入了命令A后又反悔了,希望中止func_a()对命令A的处理,而要重新选择另一条命令。这就要求中止func_a()的执行而返回到前面的printf语句去执行。下面程序改成:

include

void main_control(int sig){

int c;

signal(sig, main_control);

for(;;) {

printf(“Input a command:”);

c = getchar();

switch © {

case ‘Q’: return 0;

case ‘A’: func_a(); break;

case ‘B’: func_b(); break;

M

} }}

int main(void){

main_control(SIGINT);}

这样,在进行func_a()处理期间,用户按中断键Ctrl+C,就会终止func_a()处理,再次出现Input a command:的提示,可以重新输入命令。但是,这有严重的缺陷。

(1)每次敲击中断键,程序都停留在信号捕捉函数中,且嵌套得越来越深,这样就会有越来越多的内容压在堆栈中,可能在一段时间内工作得还行,但占用的栈空间越来越多,最终可能会导致该进程用户栈空间溢出。

(2)main_control()一旦返回,进程的执行将根据堆栈中记录的状态,返回到当初被SIGINT中断的地方恢复刚才的执行,会让用户感到迷惑不解:刚才的动作已打断而且又已经开始了新的工作,可过一段时间后死灰复燃。

那么该问题的解决办法就是把堆栈恢复为第一次调用main_control()时的状态,再调用main_control()去重新执行。

include

int setjmp(jmp_buf env); / 返回值为0,或者是longjmp提供的值/

void longjmp(jmp_buf env, int val);

其中setjmp将当前栈状态保存入env中,longjmp负责将当前的执行流程转为调用setjmp的地方,同时堆栈状态也恢复至目标位置的状态。程序改进后如下:

include

include

static jmp_buf jmpbuf;

void intr_proc(int sig)

{

printf(“\n…INTERRUPT\n”)

longjmp(jmpbuf, 1);

}

main(){

int c;

setjmp(jmpbuf);

signal(SIGINT, intr_proc);

for (;;) {

printf(“Input a command:”);

c = getchar();

switch © {

case ‘Q’: return 0;

case ‘A’: func_a(); break;

case ‘B: func_b(); break;

… }

}

2.6 信号对进程执行的影响

可以捕捉一个信号,为这个信号设置一个程序员自定义的处理函数。当一个信号到达进程的时候,执行这个处理函数。有两种可能的情况:

第一种情况是进程正在执行用户态程序指令。这包括程序员自编的程序代码和库程序代码,在处理上等同看待,不做区分,也无法区分。这种情况下,信号到达时,进程正在执行的代码被暂停,执行完信号处理函数之后,除非信号处理函数中exit()终止当前进程或者longjmp跳转到别的地方,否则恢复到被信号中断的位置继续执行,这种做法跟中断服务程序类似。

第二种情况是进程正在执行系统调用。传统的UNIX内核,进程在执行内核代码时,是不可被其他进程打断的,只有在系统调用结束或者进程在核心态代码中调用sleep原语睡眠,自愿让出CPU时,进程调度才会发生。所以,只需要考虑进程睡眠时收到信号的处理,其他时间是不可被信号打断的。

进程在执行系统调用而睡眠时收到信号,有两种处理方法。对于像磁盘操作那样的快速的I/O,进程睡眠时间很短暂,这些系统调用不会被信号操作打断。

但是,对于那些慢速的I/O,如:终端读写或者从网络接收数据,wait()等待子进程结束,进程间通信的函数,这些操作,只要条件不满足,进程会睡眠很长时间。终端读入时如果终端上的用户离开终端很长时间,进程等待的时间就会很长,甚至会无限期等待。慢速I/O的系统调用导致进程睡眠时,到达的信号就会打断这个系统调用,返回到用户态程序,系统调用返回–1,标志系统调用出错,errno中记录的出错代码是EINTR。

程序员应当处理这样的错误,并且根据需要重新开始系统调用,或者完成其他的操作。

2.7 sleep,pause与alarm

2.7.1 sleep

int sleep(int seconds);

sleep不是系统调用,而是一个库函数。函数的返回值是剩余的秒数。由于信号会打断睡眠进程,所以,sleep()不见得会睡足指定的时间。

使用sleep函数应注意的问题。

cat slp.c

include

void handler(int sig){

signal(SIGINT, handler);}

int main(void){

int n;

signal(SIGINT, handler);

n = sleep(3600);

printf(“n = %d\n”, n);

}

./slp

^Cn = 3596

程序执行4s后,按Ctrl+C 键就会打印出上述信息,就执行了sleep之后的printf函数,而没有持续等待一个小时。如果进程一定要睡眠一个小时,并且有可能会到达信号,那么,程序该自己设法继续睡眠。把上述程序的最后两条语句改为:

for (n = 3600; n > 0; n = sleep(n))

printf(“Timeout will occur in %d seconds.\n”, n);

那么,每按一次Ctrl+C键,sleep调用就返回一次,程序打印出剩余的秒数。极端情况下,上述循环不见得会让程序在这个循环等待一个小时。解决这样的问题,可以采用time()获取时间坐标,决定sleep时长。这里不再给出改进后的程序。

2.7.2 pause

系统调用pause()的功能有点类似sleep函数,只是指定的秒数是无穷大∞。

pause会使得进程一直睡眠。一旦收到信号,从信号捕捉函数返回后,pause()才返回。一般用pause()无限期等待一个信号。

int pause(void);

pause函数返回值只可能是-1,errno为EINTR。

2.7.3 alarm

alarm调用用于设置进程自己的报警时钟(闹钟),函数原型为:

int alarm(int seconds);

每个进程都有一个报警时钟,存储在它内核中的PCB中。当报警时钟走完时,就向自己发送SIGALRM信号。子进程继承父进程的报警时钟值。报警时钟在exec执行期间仍保持这一设置。进程收到SIGALRM后的默认处理是终止进程。使用这种功能,程序中在fork语句后exec语句前加上alarm(n);语句,就会使得一个不对SIGALRM信号进行任何处理设置的命令的执行时间限制为n秒。

alarm参数seconds为秒数。当seconds>0时,将闹钟设置成seconds指定的秒数。当seconds=0时,关闭报警时钟。

例如,程序要求操作员在5s内输入一命令。超时了使用默认命令CMDA。

include

static char cmd[256];

void default_cmd(int sig){

strcpy(cmd, “CMDA”);}

int main(void)

{

signal(SIGALRM, default_cmd);

printf(“Input command : ”);

alarm(5);

scanf(“%s”, cmd);

alarm(0);

printf(“cmd=[%s]\n”, cmd);

}

由于库函数scanf会调用read系统调用从当前终端读取数据,进程睡眠时收到SIGALRM信号系统调用就会返回。这个程序的一个最糟糕情况是,用户输入了命令,但是在执行语句alarm(0)之前正好闹钟到期,输入无效。

Linux默认情况下,从终端读的系统调用不可被SIGALRM中断,上述程序在Linux中成功运行,还需要在程序中增加下面的语句,以允许SIGALRM信号可以打断对终端的read()系统调用:

siginterrupt(SIGALRM, 1);

第二个参数1表明允许SIGALRM信号打断系统调用;如果为0,表明不许打断系统调用。

在连载11中,我们将进一步对进程间通信进行讨论,并且介绍利用中间介质进行进程间通信的原理和基本方法。

Comments

MP77的UNIX课件笔记(9)

本章和下一章讨论UNIX进程控制和进程间通信机制和相关System call。从“操作系统原理”中我们了解到,进程是OS进行系统资源管理的基本单位。其具体的内容则不属于本章讨论范围,但我们假设读者已经阅读过有关操作系统进程管理方面的文献。

1 进程控制

进程是如何创建的?我们以日常工作的shell平台为起点展开讨论。

1.1 用程序运行程序

简单地说,shell是用于管理程序的系统工具,其实质也是一个程序。

用程序运行程序的思想来实现一个简单shell,我们想到被称为exec的System call。exec的特点是:

1、exec创建执行其它程序的进程,而不必返回。

2、这些例程简单的把新程序覆盖在旧程序上,并以调用者提供的参数去执行这个新代码。

下面是一段使用exec运行ls的程序示例:

main()

{

/*the definition below can also be made like this:

char args[4]={“ls”,“-l”,“/usr/bin”,“(char)0”}*/

char *args[4];

args[0] = “ls”; / build the arglist /

args[1] = “-l”;

args[2] = “/usr/bin”;

args[3] = (char ) 0; / terminate with NULL */

printf(“about to execute ls -l /usr/bin\n” );

execvp( “ls” , args );

printf( “that was ls, cool, eh?\n”);

}

exec包含六种形式,除了建立在exec基础上之外:

l与v,指定命令行参数的两种方式,l代表的形式在函数参数中列举出命令行参数,v要实现组织成一个数组。函数参数中的命令行参数表,参数数目可变,所以最后一个要以0为结束标识。

e,用envp数组的内容初始化环境参数,否则使用与当前相同的环境参数environ。

p,函数第一个参数,是程序文件名,在环境变量PATH指定的多个查找目录中查询可执行文件,否则不按PATH指定路径自动搜索。

由于exec覆盖源进程直接运行目标程序,因此上段C代码中的最后一行输出that was ls,cool,eh?并不会显示在标准输出。

很明显,仅仅使用exec无法满足我们的需求,这种需求主要表现在:

1、为了保护旧程序,要把它分割成两个副本:其中一个被覆盖,而另一个则等待新的覆盖进程的结束。

2、调用成功不返回;失败返回。

为了实现以上功能,需要建立新的进程供exec调用。

1.2 进程创建

Unix涉及进程创建的基本System call是int fork()。fork()调用成功内核建立一个新进程。新进程是调用fork()的进程的副本。即新创建的进程继承了其父进程的环境。

新进程(子进程)运行与创建者(父进程)一样的程序,其中的变量与创建进程的变量相同.

父进程与子进程并发执行,哪一个进程能够占用CPU由进程调度程序决定。它们都是从fork()调用后的那条语句开始执行。例如程序段:

main()

{int pid;

printf(“for example”);

pid=fork();

printf(“fok system calll”);

……

}

标准输出中至少会出现fok system call * 2的显示,且表象上根本无法分别“父”与“子”。简单地说,父子进程间互不影响其数据堆栈的情况。

注意到fork的返回值为整型,实际上就是子进程的process id。也就是说,fork会同时返回两个分别指示父子进程的pid。其中:

1、父进程的返回值为子进程的PID;

2、子进程的返回值为0;

3、出错返回值为-1,并将errno置为相应值。

include

main(){

int ret_from_fork, my_pid;

my_pid = getpid();

printf(“Hi, my pid is %d\n”, my_pid );

ret_from_fork = fork();

sleep(1);

printf(“after fork(). It returned %d and my pid is %d\n”,

ret_from_fork, getpid());

}

本段程序演示了fork父子进程的pid状态。至于父子进程标识谁最先显示,完全由CPU决定。

1.3 空闲进程

现在回过头来再看shell,似乎心中有数了许多。紧接着问题来了,我们现在知道shell程序通过创建子进程运行新程序,那么此时父进程处于什么地位?子程序运行结束后又是如何回退至父进程的?

显然父进程需要做的是在子进程运行过程中等待,直到子进程结束。这就需要用到/usr/include/sys/wait.h中的System call了。

pid_t wait(int *status);

其中pid_t定义在/usr/include/sys/types.h中,返回值代表已终止的子进程的pid号。status的作用是返回一个整型值,其代表了子进程终止的原因。子进程的终止一般有两种情况:“自杀”和“被杀”,“自杀”即自愿终止,在程序中调用函数exit()或主函数return均可。“被杀”是指由其它进程或者操作系统内核向进程发送信号将其杀死。第一种情况可以获得进程正常终止的返回码,第二种情况可以获得进程被杀死的信号值,“信号值”会在下一篇文章讨论。

在/usr/include/sys/wait.h中定义了几个宏支持返回status的调用:

WIFEXITED(status),如果进程正常终止,则为真。调用WEXITSTATUS(status)可以返回子进程的返回码。

WIFSIGNALED(status),如果进程异常终止,则为真。调用WTERMSIG(status)获得子进程被杀的信号值。

严格说来,status的低8位反映了子进程的终止状态:0表示子进程正常结束,非0表示出现了各种各样的问题。status高8位带回了exit的返回值。低7位记录信号序号,bit 7 用来指明发生了错误并产生了内核映像(core dump)。

1.4 简单shell的实现

至此,我们完全有能力制作一个简单的仿真shell程序了,尽管尚无法实现有关信号、文本编辑器以及一些其它特殊功能的调用。

从本篇开始,我们将构建一个shell及其基本系统命令的C语言程序,此项工作会持续到本系列连载的结束。

/ example of simple shell /

include

include

define MAXARGS 20 / cmdline args /

define ARGLEN 100 / token length /

main()

{char arglist[MAXARGS+1]; / an array of ptrs */

int numargs; / index into array /

char argbuf[ARGLEN]; / read stuff here /

char makestring(); / malloc etc */

numargs = 0;

while ( numargs < MAXARGS )

{ printf(“Arg[%d]? ”, numargs);

if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != ‘\n’ )

arglist[numargs++] = makestring(argbuf);

else

{ if ( numargs > 0 ) / any args? /

{ arglist[numargs]=NULL; / close list /

execute( arglist ); / do it /

numargs = 0; / and reset /

}

}}

return 0;}

execute( char *arglist[] )

{ int pid, exitstatus; / of child /

pid = fork(); / make new process /

switch( pid ){

case -1:

perror(“fork failed”);

exit(1);

case 0:

execvp(arglist[0], arglist); / do it /

perror(“execvp failed”);

exit(1);

default:

while( wait(&exitstatus) != pid );

printf(“child exited with status %d,%d\n”,

exitstatus>>8, exitstatus&0377);

}

}

char makestring( char buf )

/*

  • trim off newline and create storage for the string

*/

{

char cp, malloc();

buf[strlen(buf)-1] = ‘\0’; / trim newline /

cp = malloc( strlen(buf)+1 ); / get memory /

if ( cp == NULL ){ / or die /

fprintf(stderr,“no memory\n”);

exit(1);

}

strcpy(cp, buf); / copy chars /

return cp; / return ptr /

}

/ end of example /

1.5 字符串解析的C语言细节处理

通过上述程序我们可以获得一种对输入字符串进行格式解析的方法,事实上C语言库函数中即给出了一些极为方便的函数调用。

char strtok(char string, char *tokens);

1、给定字符串string以及“单词”分界符的集合tokens,从字符串中分析出一个“单词”。忽略字符串中连续多个的单词分隔符。

2、函数返回值是指向单词的首字符的指针。

3、函数的副作用是,会修改原先给定的字符串存储空间中的内容。

以下是strtok运行原理解析:

首先键盘输入 who am i后按enter,应当注意who之前有一个空格和一个制表符(tab),who和am之间有两个空格。

下面是用fgets(s, sizeof s, stdin)读入到内存中的字符串s的存储结构,每个字节用十六进制列出。

\t w h o a m i \n \0

s 20 09 77 68 6f 20 20 61 6d 20 69 0a 00

第一次执行p=strtok(s,“ \t\n”)之后的状态:

\t w h o a m i \n \0

s 20 09 77 68 6f 00 20 61 6d 20 69 0a 00

p

第二次执行p=strtok(NULL,“ \t\n”)后的状态:

\t w h o a m i \n \0

s 20 09 77 68 6f 00 20 61 6d 00 69 0a 00

p

第三次执行p=strtok(NULL,“ \t\n”)后的状态:

\t w h o a m i \n \0

s 20 09 77 68 6f 00 20 61 6d 00 69 00 00

p

这时,strtok扫描结束的位置指向了字符串尾部的’\0’,第四次再执行strtok就会返回NULL,表示找不到新的单词。

1.6 另类的进程创建方法

vfork()创建一个进程,而新进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但它并不把父进程的地址空间完全复制到子进程,因为子进程会立即调用exec(或exit),于是也就不访问该地址空间,不过它在exec(或exit)调用之前,它在父进程空间运行。

vfork()保证子进程先运行,在它调用exec(或exit)之后父进程才运行。

这里需要明确两个终止操作的区别exit和exit,它们均定义在/usr/include/unistd.h中,不同的是前者属于C语言库函数的范畴,而exit属于System call,也就是说,在具体实现中,exit最终仍需要调用_exit。

但exit终止进程时比系统调用exit多做一些操作。这些操作主要是自动关闭用fopen等函数创建的缓冲I/O文件,将缓冲区里的数据写到磁盘上。exit释放缓冲文件的文件缓冲区和缓冲文件所需要的数据结构,因为库函数相关的这些数据都存放在进程的数据区内,这会影响子进程借用的父进程的数据段,所以,当采用vfork()创建进程时,必须直接使用系统调用exit。

例如下段程序:

cat vf.c

int main(void){

int a = 1;

if (vfork() == 0) {

a++;

_exit(0);

} else {

a++;

}

printf(“a=%d\n”, a);

}

cc vf.c -o vf

./vf

a=3

显然子进程使用了父进程的数据区进行操作。

1.7 另类的程序运行方法

在C语言标准库中还定义了这样的函数,其本质上使用了fork、exec、wait等System call,用于执行某一行命令。

int system(char *cmd)

其中cmd是可执行程序的名称。该系统调用的功能是执行参数cmd所指定的命令。正确返回0,否则返回错误代码(非0值)。它的执行与命令行键入的命令具有同样的效果。

现在考虑如下实际问题:

编程时希望在程序中获得系统当前的IP转发路由表,一时又找不到编程接口,但是,执行netstat -rn命令后可以获得这个表格。选项r指的是路由表,选项n指的是输出IP地址时用数字形式,而不是自动将地址转换为主机名。

将命令输出重定向到一个临时文件中,然后从程序中读这个文件的内容,获取所希望的信息。最后,把文件删除。具体实现如下:

cat cmd.c

include

main(){

char fname[256], cmd[256], buf[256];

FILE *f;

tmpnam(fname);

sprintf(cmd, “netstat -rn > %s”, fname);

printf(“Execute \”%s\“\n”, cmd);

system(cmd);

f = fopen(fname, “r”);

while(fgets(buf, sizeof buf, f))

printf(“%s”, buf);

fclose(f);

printf(“Remove file \”%s\“\n”, fname);

unlink(fname);

}

唯一需要解释的是char * tmpnam(char *str);

其作用是获得一个临时文件的文件名,库函数会保证这个文件名与已有文件名不冲突。

库函数sprintf的用法和printf类似,只是打印的字符存到了字符串而不是显示在终端输出。sprintf使用和printf一样的格式控制字符串,程序员还经常用它将二进制数字转换成ASCII码串。

程序把system(cmd)得到的文件打印出来。需要时,程序应该解读这个文件,获得所需要的信息。

1.8 进程信息获取命令

ps(process status)命令列出系统中进程的当前状态,实际上就是将进程系统数据段中的部分进程属性有选择地打印出来。不同的UNIX系统,ps命令的使用也有些差别。

-a (all)列出所有当前终端上启动的进程;

-t (tty)指定一个终端名字,列出这个终端上启动的所有进程,如ps -t pts/1;

-u (User)指定一个用户名,列出这个用户的所有进程,如ps -u jiang;

-e (everything)列出系统中所有的进程;

-f (full)以full格式列出进程;

-l (long)以long格式列出进程。

下一节将介绍进程间通信的相关原理和方法。

Comments

MP77的UNIX课件笔记(8)

本章主要介绍目录和文件的状态信息,这些是大量系统功能应用的基础。后续文章会针对前次连载的文件系统、System call等内容进行部分常用程序的设计以及分析工作。

1 文件状态信息

1.1 获取文件的状态信息

该部分系统调用定义在/usr/include/sys/stat.h中,并且需要types.h的支持。

int stat(char path, struct stat statbuf); / return 0 on success or -1 on error /

int fstat(int fd,struct stat statbuf); / returns 0 n success or -1 on error */

stat和fstat的原理均是从inode节点中获取文件状态信息。值得注意的是,如果是利用C语言缓冲I/O方式打开文件,将FILE *f转换为相应文件描述符的方法是,使用宏调用fileno(f)即可返回discriptor。stat由于是提供了路径,因此可能需要逐级深入,导致效率普遍不如fstat,但考虑到fstat仅适用于已打开的文件,那么两种call就需要灵活使用了。

注意到参数struct stat *statbuf定义了存储文件状态信息的结构体,其在types.h中定义,其基本成员如下:

struct stat {

dev_t st_dev; /device of inode/

ino_t st_ino; / inode number/

mode_t st_mode; / mode bits /

nlink_t st_nlink; / number of links to file /

uid_t st_uid; / owener’s userid /

dev_t st_gid; / owener’s group id /

dev_t st_rdev; / for special files /

off_t st_size; /file size in characters /

time_t st_atime; /time file last read/

time_t st_mtime; /time file last written or created/

time_t st_ctime; /time file or inode last changed/

}

尽管stat成员类型大部分均为整型,但基于可移植性的需要,通常不直接定义为C语言的内部数据类型如short、int或long等等。可以发现,此类类型定义都是以_t结尾的使用typedef重定义后的类型声明符。

1.1.1 st_mode转换成字符

st_mode字段长度为16位,包括4位的类型标识,suid、sgid和sticky字段以及9位权限标识位。

类型标识位我们在连载五中已经有所涉及:

http://www.hanyi.name/blog/?p=169

suid和sgid涉及到系统安全,它们分别指Set User ID和Set Group ID,其作用类似。对一个suid被置位的程序来说,其权限设置示例如下:

rwsr–r–

这里需要解释一下effective uid和effective gid,它们是记录在内核中相对于用户uid和gid的标识,其主要目的是为了分配给其相应的权限。一般情况下,如果suid和sgid未被置位,那么进程显示的euid应是正在执行操作的用户uid。但是在suid被置位的情况下,euid会显示为程序拥有者的uid,并且该进程享受拥有者权限。

简单举例/usr/bin/passwd命令,功能是修改用户密码,其实质是对/etc/passwd和/etc/shadow进行更新操作。我们知道/etc/.一般对普通用户不具备wx权限。借助于/usr/bin/passwd,该程序的默认权限为:

rws–x–x

那么普通用户就可以以root身份对上述两个文件进行w操作了。

sgid的作用与suid类似,创建该位的主要目的是当涉及到root权限时,如非必要应置sgid而非suid以避免造成安全缺陷。

sticky粘贴位,该位一般只在目录文件定义时有效。sticky置位的目录下所有用户都可以在该目录下创建文件,但除root外的用户只能删除自己创建的文件。

这里可能需要解释一下Unix的文件删除机制。严格说来删除权限和源文件的mode字段无关,而是与存放该文件的目录文件权限有关,如果允许相应用户拥有删除权限,那么用户必须在该目录下拥有写权限。

但是/usr/include/sys/stat.h中对标识的宏定义为八进制,且无法与st_mode中的bit直接对应,为此要借助于掩码运算计算出相应的字段定义,计算方法是st_mode & 相应的标识掩码(其中&为位运算中的“与”)。例如文件类型宏:

define S_IFMT 0170000 /type of file/

define S_IFREG 0100000 / Regular file /

define S_IFDIR 0040000 / Directory /

define S_IFCHR 0020000 / Character device /

define S_IFBLK 0060000 / Block device /

define S_IFIFO 0010000 / FIFO/

define S_IFLNK 0120000 / Symbolic link /

define S_IFSOCK 0140000 / SOCKET/

S_IFMT即为文件类型的掩码,运算时只需st_mode & S_IFMT,判断其相应的文件类型宏编码即可。下面仍列出一些/usr/include/sys/stat.h宏定义编码:

define S_ISUID 0004000 /set user id on execution/

define S_ISGID 0002000 /set group id on execution/

define S_IREAD 0000400 /read permission /

define S_IWRITE 0000200 /write permission /

define S_IEXEC 0000100 /execute/search permission,owner/

在编写程序的时候,可能会利用如下代码:

if ((info.st_mode & S_IFMT) == S_IFDIR)

printf(“this is a directory”);

/usr/include/sys/stat.h同样给出了此类转换的宏定义,例如:

define S_ISDIR(mode) (((mode)&S_IFMT)==S_IFDIR)

define S_ISREG(mode) (((mode)&S_IFMT)==S_IFREG

define S_ISBLK(mode) (((mode)&S_IFMT)==S_IFBLK

define S_ISCHR(mode) (((mode)&S_IFMT)==S_IFCHR

那么就可以实现本节题目所提出的问题了:

void mode_to_letters( int mode, char str[] ){

strcpy( str, “———-” ); / default=no perms /

if ( S_ISDIR(mode) ) str[0] = ’d’; / directory? /

if ( S_ISCHR(mode) ) str[0] = ‘c’; / char devices /

if ( S_ISBLK(mode) ) str[0] = ‘b’; / block device /

if ( mode & S_IRUSR ) str[1] = ‘r’; / 3 bits for user /

if ( mode & S_IWUSR ) str[2] = ‘w’;

if ( mode & S_IXUSR ) str[3] = ‘x’;

if ( mode & S_IRGRP ) str[4] = ‘r’; / 3 bits for group /

if ( mode & S_IWGRP ) str[5] = ‘w’;

if ( mode & S_IXGRP ) str[6] = ‘x’;

if ( mode & S_IROTH ) str[7] = ‘r’; / 3 bits for other /

if ( mode & S_IWOTH ) str[8] = ‘w’;

if ( mode & S_IXOTH ) str[9] = ‘x’;

}

1.1.2 时间字段转换成字符

1.1中已经给出了文件状态信息中的st_atime(read)、st_mtime(write or create)和st_ctime(change)时间字段的定义,为了将其转换为标准系统时间,需要使用/usr/include/time.h中的System call函数。其基本格式为:

char ctime(const time_t clock);

注意到stat中的时间字段均为time_t型,也是Unix中系统定义的基本类型。ctime的目的是将time_t转换为标准时间格式,并以字符串形式回传。其最小单位时间为秒,采用UTC(Coordinated Universal Time )时间,其绝对值00:00:00代表1970年1月1日00:00:00,返回的格式示例如下:

Thu Nov 24 18:22:48 1986

值得注意的是,当年份字段长度小于4个字节时需要用前导0补齐,当年份大于4个字节时,显示会成为:

Thu Nov 24 18:22:48 81986

在年份字段之前会出现5个空格,这是为了防止在一些老式软件中将26个字节的输出判断为超出时间范围而显示出错。

showtime( time_t timeval )

{

char ctime(); / convert long to ascii */

char cp; / to hold address of time */

cp = ctime( &timeval ); / convert time to string /

printf(“%12.12s”, cp+4 ); / pick 12 chars from pos 4 /

}

以上是一段输出time_t类型时间的程序示例。

1.1.3 st_uid & st_gid 转换成字符

st_uid和st_gid是在stat字段中记录文件拥有者ID和拥有者所在用户组ID的字段。前文已经提到用户信息和用户组信息分别存储在/etc/passwd和/etc/group中,因而要获取详细的用户或组信息就必须实现读取并分析操作,系统给出了此方面的System call。不同类型的Unix发行版其passwd结构也略有不同,这就造成了其相应结构体字段的不同。这里用OpenBSD的/usr/include/pwd.h定义加以说明,其它系统的设定较之并不全面:

struct passwd {

char pw_name; / user name */

char pw_passwd; / encrypted password */

uid_t pw_uid; / user uid /

gid_t pw_gid; / user gid /

time_t pw_change; / password change time /

char pw_class; / user access class */

char pw_gecos; / login info */

char pw_dir; / home directory */

char pw_shell; / default shell */

time_t pw_expire; / account expiration / };

}

其调用函数为struct passwd *getpwuid(uid_t uid);

st_gid定义在/usr/include/grp.h中,其结构体字段如下:

struct group {

char gr_name; / group name */

char gr_passwd; / group password */

gid_t gr_gid; / group id /

char gr_mem; / group members /

};

其调用函数为struct passwd *getgrgid(uid_t uid);

2 设备文件

2.1 一些概念

我们在连载5中已经介绍过特殊文件类型,它是Unix一种独特的文件类型。特殊文件类型总的说来包含物理设备文件和逻辑设备文件,Unix的设备管理将设备组织得看起来同磁盘文件一样,可以通过文件系统里的文件名进行访问。

UNIX的设备分为块设备和字符设备:

1、块设备面向磁盘等可以随机访问的存储设备

2、mkfs,mount等使用块设备文件名,映射块设备。

3、终端、打印机等为字符设备,也叫做“原始(raw)”设备。

所有的设备在文件系统中都有一文件名,在文件的inode节点中记录了文件类型是块设备文件或者字符设备文件。

每个设备都对应一个主设备号和次设备号,也记录在inode节点中。按照惯例,设备文件都放在/dev目录下。

我们通过ls -l查看一些设备文件时得到如下信息:

ls -l /dev/hdc1 /dev/fd[01] /dev/tty1

brw-rw—- 1 root floppy 2, 0 Jan 30 2003 /dev/fd0

brw-rw—- 1 root floppy 2, 1 Jan 30 2003 /dev/fd1

brw-rw—- 1 root disk 22,1 Jan 30 2003 /dev/hdc1

crw——- 1 root root 4, 1 Jun 2 20:32 /dev/tty1

其中主设备号和次设备号的作用列出如下:

1、主设备号对应物理设备的一类。

2、设备能够正常工作,需要提供设备驱动程序。

3、设备驱动程序中有多个入口函数,设备驱动程序安装时注册这些函数,安装完成之后,会得到一个主设备号。

4、主设备号与驱动程序相关联,根据主设备号,可以检索到已经登记了的这个设备的设备驱动程序的入口函数。

5、主设备号用来定位所使用的设备驱动程序,次设备号用来区分这一类物理设备的哪一个。

要创建设备文件,需要使用mknod命令。

mknod /dev/ttyE0 c 16 0

/dev/ttyE0是要创建的设备文件名,c指的是字符设备,16是主设备号,0是次设备号。

2.2 随机数生成器/dev/random

我们在连载5详细介绍了逻辑设备文件/dev/null和/dev/zero,逻辑设备文件也被称作虚拟设备文件,设备驱动程序不操作任何物理硬件,它像普通设备驱动程序一样提供设备驱动程序入口,通过这些入口,可以和内核交互作用,得到某些数据。这些设备叫做“虚拟设备”。

如设备文件/dev/mem和/dev/kmem可以通过某些操作获取内核中的一些数据表格内容,常常被随操作系统软件带来的一些运行在用户态的工具使用。

这里再引入一个常用的虚拟设备文件——/dev/ranodm。

在Linux中,虚拟设备文件/dev/random是一个随机数生成器,根据系统键盘和鼠标等外设的活动生成二进制的随机数数列。可以在用户自己编写的程序中使用这个设备文件。

命令od -x /dev/random可以观察这些随机数序列。在系统外设都不活动时,这个设备暂停供应随机数序列。

另一个虚拟设备文件/dev/urandom,无论外设是否活动,只要你的程序读取该设备,都会源源不断地供应随机数序列。命令od -x /dev/urandom可以观察到这些随机数序列。

2.3 终端设备文件

每个终端设备,无论是网络虚拟终端设备,还是一个真正的终端设备,都对应一个名字。可以按名字像普通文件那样使用终端,但是不方便的是无法决定当前终端到底是哪个终端。

特殊的设备文件/dev/tty就是当前终端,而无论实际使用的终端到底是哪个。尽管scanf和printf等函数可以从当前终端获得输入和从当前终端输出,但是标准输入和标准输出的重定向功能,会使得程序不再从当前终端输入和输出数据。

对于程序员来说,当程序在这种情况下仍然需要和用户交互时,/dev/tty文件特别有用。程序可以打开/dev/tty文件输入和输出数据,在重定向环境中,仍然保持从当前终端输入和输出数据。

3 文件和目录权限

尽管前部分内容很多涉及到了文件和目录权限的问题,但并没有详细描述文件系统的权限控制方法和掩码运算原理。

3.1 权限控制

UNIX的每个文件和目录,都有对应的一组权限存放在它的inode节点中,用于控制用户对它的访问。每个文件都有惟一的属主和组号,记录在inode节点中。

UNIX有命令chown和chgrp可以修改文件的属主和组,也有相应的系统调用函数chown() 。在inode节点中记录的权限有9b,描述3个级别,分别是文件主、同组用户、其他用户。每个级别的权限都包括3部分:读权限、写权限、执行权限。

1、普通文件的权限

用户对普通文件文件具有读权限,用户可以读取文件。

用户对文件具有写权限,可以修改文件的内容。

用户对文件具有可执行权限,可以执行这个文件。

2、目录的权限

目录文件存储了多个由“文件名-i节点号”组成的目录项。目录的读写权限,就是对这个目录表文件的读写权限。

目录无读权限,目录文件不许读,ls操作会失败。

目录无写权限,目录文件修改的操作都被禁止。创建文件、删除文件、文件改名、复制文件到该目录、创建子目录、删除子目录等这些操作需要在目录表中增加、删除或者修改条目。

对于上面所叙述的权限控制,现总结概括如下:

1、目录不可写,不可以保护目录下的所有的文件不可写

在一个只允许读的目录下修改一个已经存在的文件,不需要修改目录表中的任何数据。

inode节点中的数据和文件内容要发生变化,只要文件自身有可写属性,这些操作就能正常完成。

2、文件的只读权限可保护文件不被误写,不能保护文件被误删

一个文件有只读属性,文件所在目录有写权限,就可删除这个文件。删除文件,不需要打开文件,只需修改文件所在目录,只要目录可写就可以了。rm命令会在删除一个只读文件时给出一个善意的提示,但是删除照样可以完成。

3、目录有执行权限指可以检索该目录

cat /a/b/c要求/,/a,/a/b三目录有x权限,c有读权限。

cd../stud4628要求.目录、..目录和stud4628目录有x权限。

3.2 访问合法性验证的顺序

用户操作一个文件时,系统根据登录用户名、组名及文件i节点中存储的文件主和组,判断该使用3级权限的哪一级。

若文件主与登录用户相同,则只使用文件主权限,不再查组和其他用户的权限。

若文件主与登录用户不同,但文件i节点中记录的组号与登录用户的组号相同,则只使用组权限,不使用关于其他用户的权限。

若文件主与登录用户不同,文件i节点中记录的组号与登录用户的组号也不同,则使用文件关于其他用户的权限。

超级用户root不受任何权限的制约。

3.3 修改权限

ls -l

可以查当前目录下所有文件和子目录的权限

ls -1d .

列出当前目录自身的权限

如果要修改已有文件和目录的权限,只有root用户和文件拥有者才能执行此类操作。其命令为chmod。

使用chmod时有两种体现形式,分别是字母形式和数字形式:

1、字母形式

chmod [ugoa][+-=][rwx] file-list

u (user) 文件主的权限

g (group) 同组用户的权限

o (other) 其他用户权限

a (all) 所有上述三级权限

要执行的操作的符号:

  • 给指定用户增加存取权限

  • 取消指定用户的存取权限

= 给指定用户设置存取权限,其他权限都取消

定义存储权限的字母:

r read

w write

x execute

chmod u+rw *

chmod go-rwx *.[ch]

chmod a+x batch

chmod u=rx try2

2、数字形式

chmod mode file-list

使用3个八进制数字,分别描述文件主、同组用户、其他用户的权限。

八进制: 6 4 0

二进制: 110 100 000

权限: rw- r– —

chmod 640 .[ch] makefile .log

3.4 权限修改示例

1、文件写权限

who am i

jiang pts/2 Jun 06 08:34

who > mydata

ls -l mydata

-rw-r–r– 1 jiang usr 58 Jun 06 09:04 mydata

chmod u-w mydata

who >> mydata (只读文件不许写)

mydata: The file access permissions do not allow the specified action.

rm mydata (只读文件可以被删除)

rm: Remove mydata? y

ls -l mydata

ls: 0653-341 The file mydata does not exist.

2、文件的读权限

who > mydata

chmod u-rw mydata

cat mydata (无法读取不允许读的文件中内容)

cat: 0652-050 Cannot open mydata.

chmod 644 mydata

3、目录写权限

chmod u-w . (当前目录不许写)

who > mydata2 (不能创建新文件)

mydata2: The file access permissions do not allow the specified action.

who>> mydata (可以修改已有的文件,追加一部分数据)

rm mydata (不能删除文件)

rm: 0653-609 Cannot remove mydata.

The file access permissions don’tallow the specified action.

cp /etc/passwd mydata (可以覆盖旧文件)

cp /etc/passwd mydata2 (不能创建新文件)

cp: mydata2: The file access permissions do not allow the specified action.

mv mydata MyData (文件不许改名)

mv: 0653-401 Cannot rename mydata to MyData:

The file access permissions do not allow the specified action.

mkdir Test (不可创建子目录)

mkdir: 0653-357 Cannot access directory ..

.: The file access permissions do not allow the specified action.

4、目录读权限

设pwd为: /usr/jiang

chmod u-r .

ls (不可读的目录无法列出其中文件)

ls: .:The file access permissions do not allow the specified action.

chmod 000 . (取消当前目录所有权限)

ls

ls: 0653-345 .: Permission denied.

chmod 755 . chmod: .:The file access permissions do not allow the specified action.

chmod 755 /usr/jiang (这种访问不需要当前目录的权限,可恢复当前目录权限)

5、子目录ttt没有读写权限,但是保留了x权限

chmod u=x ttt

cat ttt/ccp.c

main(int argc, char **argv)

{

}

rm ttt/arg.c (子目录没有写权限,不能删除其中的文件)

rm: 0653-609 Cannot remove ttt/arg.c.

The file access permissions do not allow the specified action.

ls ttt (子目录没有读权限,不能列出其中的文件)

ls: ttt: The file access permissions do not allow the specified action.

6、子目录有读写权限,但没有x权限

chmod u=rw ttt

ls ttt

BUGS.report arg.c ccp.c chap.h mydata

arg auth.c chap.c disk.img

cat ttt/arg.c

cat: 0652-050 Cannot open ttt/arg.c.

7、试图设置其他用户的文件或目录的权限

chmod 777 /

chmod: /: Operation not permitted.

#

3.5 文件掩码umask

3.5.1 概念

umask指设置文件和目录的初始权限。掩码值的含义是在新创建的文件和目录中,不含有掩码值中列出的权限。

umask (打印当前的umask值)

umask 022 (将umask值设置为八进制的022)

umask是shell内部命令,是进程属性的一部分,每个进程都对应一umask值。umask命令用于修改shell进程自身的umask属性。

常将umask命令放到自动执行批处理文件中,如用csh作登录时,可将umask命令加入到.login文件或.cshrc文件中;用sh作登录shell时,可以将umask命令加入到.profile文件中。

umask 022 (将umask值设置为八进制的022)

掩码值: 022

二进制: 000010010

掩掉的权限:—-w–w-

即文件和目录的初始权限,不含同组用户和其他用户的写权限。

umask 077 (将umask值设置为八进制的077)

掩码值: 077

二进制: 000111111

掩掉的权限:—rwxrwx

除文件主外,不许其他用户访问

3.5.2 System call在umask的应用

int umask(int new_mask);

new_mask为新掩码值,函数返回值为原先的掩码值。为了读出进程的掩码值,需要调用umask两次。

新文件的初始权限由umask和open中的权限参数共同确定。

设umask为077

fd = open(filename, O_CREAT | O_WRONLY, 0666);

文件的权限为0666,屏蔽掉077后为0600,即rw——-。

初始创建的文件的权限受open调用的规定值和进程自身属性的umask值影响。

文件的权限只有在初始创建时受上述因素影响,一个已存在的文件的权限,不会受open和umask的影响。

系统调用chmod可修改文件的权限,它不受umask的影响。

int chmod(char *path, mode_t mode);

int fchmod(int fd, mode_t mode);

Comments

MP77的UNIX课件笔记(7)

1 UNIX系统调用

1.1 System Call

System call是OS提供的应用程序和OS交互的惟一手段,应用程序只能通过系统调用访问硬件资源。

在 Windows 中,OS 很少直接将System call提供给程序员使用,它提供类似缓冲文件操作那样既有用户态代码又有System call的函数库, 如 Windows API。

同样的,在C语言中同时提供了一些标准库函数,从程序员的角度来说,这些与 System call 并不等同,其主要原因是:

1) System call 后立刻产生一个软中断,系统从用户态进入到内核状态,函数的功能由操作系统的内核代码实现。

2)普通函数同程序员编写的程序一样,通过函数调用,在用户态完成函数内部的处理后返回。有些库函数如缓冲文件fopen等,在函数里完成一些必要处理之后再引用系统调用。

为便于不同UNIX系统之间C语言源程序的移植,UNIX的系统调用函数的名字,参数排列顺序,参数类型定义,返回值的类型,以及实现的功能,都属于POSIX标准来规范的内容。

Windows也支持大部分的POSIX函数标准,所以许多UNIX的系统调用函数在Windows系统下也可以使用,Windows提供了功能等价的函数。

1.2 系统调用的返回值

UNIX的System call一般都返回一个整数值,例如:

1)errno.h中定义了许多宏名代替整数值。如EACCESS、EIO、ENOMEM等。每个System call的在线手册中都列出可能的出错情况和对应的错误代码的宏名。

2)char *strerror(int errno)可将整数错误代码转换成一个字符串。

3)void perror(const char *string);它会在标准错误输出设备上产生一条消息,描述在系统调用或使用库函数(库函数中使用了系统调用)期间所遇到的最后一个错误。先输出字符串string,再输出一个 冒号 和 空格 ,然后再打印出这条消息和换行符。

void perror(const char *string)定义在中。

2 文件和目录的访问

2.1 文件存取

在UNIX中,由于文件系统的支持,应用程序根据文件名访问文件,不用操心数据怎样存放在外存上。

首先引入文件描述符的概念。

在通常的认识里,处理整数比字符串更有效,索引整数叫“文件描述符”(file descriptor)。

系统根据它定位文件当前读写位置和文件inode节点信息。每个进程有自己的文件描述符,文件描述符从0开始编号,它是PCB里的一个数组的下标。

值得注意的是,文件操作一般使用“文件描述符”作参数而不是文件名,文件访问结束后,“关闭”操作释放占用的存储空间。

首先介绍,它是位于/usr/include/sys下的文件控制库文件引用,其实际地址为/usr/include/fcntl.h,该文件包含了了几乎所有与文件控制操作有关的系统调用定义。

2.1.1 打开文件

int open(char *filename, int flags);

int open(char *filename, int flags, mode_t mode);

open函数有两种定义方式,返回值为文件描述符。char* filename传入目标文件所处的路径(absolute or relative),int flags指定此次操作的限制范围,例如只读/读写,或是否创建新文件。flags通常为整形常量,但便于使用期间fcntl.h中还为其设置了宏定义描述。open的返回值通常为源文件的文件描述符,如果文件打开失败,则返回值为-1。

O_RDONLY 只读,Open for reading only;

O_WRONLY 只写,Open for writing only.;

O_RDWR 读/写,Open for reading and writing;

O_CREAT 不存在时创建,Create file if it does not exist.;

O_TRUNC 清空源文件,Truncate size to 0;

O_APPEND 在末尾添加,Append on each write;

mode定义了文件保护模式(该项在创建时有效),其值需要与系统掩码值综合后产生最终结果,在权限一节会详细介绍。

2.1.2 文件读写

首先介绍头文件unistd.h,其实际地址为/usr/include/unistd.h。unistd意为Unix standard header,即Unix标准库文件,作用类似于C语言的stdlib.h(standard library header,包含了标准函数以及NULL等宏定义内容)。Unix中与file I/O有关的的System call均包含在此文件内。

int write(int fd, void *buf, int n);

write()尝试将buf缓冲区中的内容输出至文件描述符所对应的磁盘数据区中,n指期望写入的字节数,其返回值为真正写入的字节数,如果该系统调用返回值为-1,则说明系统调用失败。

int read(int fd, void *buf, int n);

read() 尝试从文件描述符对应的磁盘数据区中输入文件内容至内存缓冲区中,n指期望读的字节数。返回值为实际读的字节数,如果该系统调用返回值为-1,则说明系统调用失败。

值得注意的是,read/write操作类似于文件的 顺序访问 机制,这是因为:

1、在内核中为每个打开的文件保留一个记录当前操作位置的指针;

2、无论是read还是write,都从文件的当前位置读取或写,操作后,文件当前指针自动向后移动;

3、这非常适合于按顺序访问文件中的数据;

这就可能出现类似下面的情况,在多进程同时对一个文件进行操作时,当read返回值为0时,此时已读至文件尾部,但如果文件被外部进程进行写操作时,会默认写至文件末尾,再次调用read会返回新的可能值。

System call还支持 随机访问 机制,这要利用如下函数(同样包含在unistd.h中):

off_t lseek(int fd,off_t offset,int whence);

off_t offset定义强制移动当前文件读写指针的字节偏移量,int whence指offset的实际参考基准,以上两个参数在/usr/include/sys/types.h中定义。其中关于whence的宏定义如下:

SEEK_SET,读写指针移动到offset指定的位置(绝对值);

SEEK_CUR,读写指针移动到当前读写指针加offset后的位置;

SEEK_END,读写指针移动到文件尾加offset后的位置,也就是文件中字节数加offset后的位置。

在后两种方式中,允许offset为负值。

利用lseek时仍然需要注意系统在文件管理机制上的问题。一般来说,如果lseek被指定为超过文件末尾之后的位置,那么对应用程序来说文件大小的确是目前“实际的文件大小”,但是对系统来说,在块管理机制的情况下仅仅会给多余的非空内容分配出相应的存储块(这就可能存在占用1-9块和100块而中间块不占空间的情况)。此时当文件被写入时会导致系统分配真正的磁盘存储块,就造成了大量的“空块”浪费的情况,绝大多数情况下这是毫无意义的,因而应尽量避免出现此类情况。

System call还允许用户截断一个文件:

int ftruncate(int fd,off_t length);

length定义了截取的文件字节长度,当length为0则文件长度即变为0。

2.1.3 文件关闭

int close(int fd);

首先需要注意:

1、每个程序允许同时打开的文件数是有限的,整个系统允许打开的文件总数,也是有限的。

2、程序无论正常还是异常退出,所有由程序打开但尚未关闭的文件描述符,系统都会自动关闭。

3、文件访问的另一种方式是使用C语言的标准函数fopen,fprintf,fscanf,fgets,fread,fwrite,…,close。这一组函数,是在系统调用open,read,write,…,close基础上构建的库函数。

因而尽管程序终止时文件描述符总会被强制关闭,但在一些后台程序中,close的作用对资源的合理分配是不可忽视的。

2.2 目录访问

2.2.1 与普通数据文件相同的方式打开目录文件

目录和普通文件一样,占用文件存储区中的空间。目录中的数据是“文件名-inode节点”对。

早期的UNIX每个目录项16B,包括2B的inode节点号和14B文件名。inode节点号为0标志文件已删,因此inode节点1开始编号。用open以只读方式打开一个目录文件,read得到16B目录项。程序自己来分析inode节点号和文件名。

使用长文件名后,目录存储结构发生了变化,应用程序必须了解目录文件的存储结构,这很不方便。因此系统提供了库函数,用于操作目录文件。可将目录文件和普通文件一样打开读,但内核保留写目录的权利,任何用户态程序,就算是超级用户,也不允许改写目录内容,这样,保证其中数据的结构正确和有效。

当以处理普通磁盘文件的方式打开目录读取目录文件数据时,由于需要根据目录表的存储结构在二进制数据中判断文件名、inode节点号,这对普通用户来说是极不方便的。

2.2.3 目录有关的System call

首先介绍/usr/include/dirent.h,即directory entry目录项头文件,定义了有关Unix目录操作的System call和宏定义。

DIR opendir(char dirname);

char *dirname指定目标目录路径(absolute or relative),该调用返回该目录的标志句柄,DIR在dirent.h中定义。

struct dirent readdir(DIR dir);

readdir取第一个目录项的内容,并返回dirent型结构体指针,dirent定义了目录项包括的具体属性,常用的有d_name(char *)、d_ino(int)等,且当前句柄指针跳至下一目录项等待读取。readdir返回NULL,标志目录表已经读到尾。

2.2.4 目录操作的其它System call

这里不再详述其使用方法,事实上这些都是显而易见的。

int closedir(DIR *dir); //关闭目录表访问句柄

int unlink(char *pathname); //删除文件

int link(char oldpath,char newpath); //建立硬连接

int symlink(char oldpath,char newpath); //建立符号连接

int rename(char oldpath,char newpath); //文件改名

int chdir(char *path); //修改当前工作目录

int getcwd(char *buf,int size); //当前工作目录名

int mkdir(char *pathname,mode_t mode); //创建目录

int rmdir(char *pathname); //删除目录

后续文章中我们将利用以上System call进行实际应用示例研究。

Comments

mp77的UNIX课件笔记(6)

1 一些概念

1.1 基本文件系统root file system

UNIX的基本文件系统类型分为两大类,一是根文件系统(注意与本节标题的区别),顾名思义,此类文件系统无法被卸载umount。 二是子文件系统,子文件系统以基本文件系统中某一子目录的身份出现:硬盘、软盘、USB盘、CD-ROM、网络文件系统NFS等,不像DOS那样使用逻辑盘的概念。根文件系统和子文件系统都有自己独立的一套存储结构和目录结构,甚至文件系统的格式都不一样。

1.2 UNIX文件系统操作

要创建一个文件系统,需要使用mkfs命令。如:

mkfs /dev/fd0135ds18

其中/dev/fd0135ds18表示一个3.5英寸容量为1.44MB的软盘。一般来说子文件系统需要用户自己加载,我们使用mount命令完成:

mount /dev/fd0135ds18 /mnt

当mount命令不带参数时意即列出当前本机所有的文件系统。如果要卸载某些文件系统,可以用umount(注意不是un)。

1.3 NFS

NFS即Network File System网络文件系统,目前是UNIX系统中的一个重要服务组件。它允许主机向外界共享目录和文件。 现有一案例,主机C和主机S上均已安装好了NFS软件包,主机C期望共享主机S上的文件目录/usr/jiang。 首先应在主机S的文件/etc/exports中添加行/usr/jiang。然后在主机C上执行下面的命令:

mount -f NFS 203.123.54.189:/usr/jiang /xbg

其中203.123.54.189是主机S的地址,/xbg是事先在主机C上已建好的空目录。此时主机C就能通过/xbg访问主机S的/usr/jiang中的目录或文件了。

1.4 引用Windows格式分区举例

Windows系统的分区格式一般为FAT、FAT32或NTFS,当机器安装了Linux+Windows双系统时无法直接看到对方的分区(一些较新的发行版已支持此类功能)。如果要在Linux中查看windows分区信息,则需要使用加载的方法解决。 首先使用fdisk –l查看本机硬盘以及分区情况,系统一般会自动识别分区格式信息。然后在Linux系统中建立相应的空文件目录,使用mount载入即可。 值得注意的是,mount的载入源必须是建立在块设备文件类型基础上的文件(也称设备)。也就是说例如其它字符设备文件无法通过mount形式加载。

2 UNIX文件系统

2.1 逻辑结构

UNIX的文件系统是由一系列大小相同的块构成的,其类型为:引导boot块、超级块、Inodes以及数据块。boot block标记为0号块,用于启动系统时存放引导程序;superblock标记为1号块,存储与整个文件系统相关的配置信息;Inodes存放每个文件的inode节点信息;data blocks是由大小为512bytes的块构成的数据存储部分。 值得注意的是,目录文件在UNIX中跟普通文件一样,存于“文件存储区”中,有自己的i节点。其基本组成单位是“目录项”,每个目录项由一个“文件名-i节点号”对构成,提高了目录检索的效率。

2.2 索引结构

文件名和对应的节点编号存储在目录文件中,从下面的inode结构信息可以看到,其主要目的是为了存储文件的全部属性,包括具体的块地址。 inode为系统自定义的一种数据结构,其存放位置即相关定义的部分内容如下: // struct stat { …… dev_t st_dev; /device of inode/ ino_t st_ino; / inode number/ mode_t st_mode; / mode bits / nlink_t st_nlink; / number of links to file / uid_t st_uid; / owener’s userid / dev_t st_gid; / owener’s group id / dev_t st_rdev; / for special files / off_t st_size; /file size in characters / time_t st_atime; /time file last read/ time_t st_mtime; /time file last written or created/ time_t st_ctime; /time file or inode last changed/ } 通过inode中存放的磁盘文件表信息,可以直接定位至文件内容的起始位置。例如当我们需要定位/usr/ast/mbox时。对于根目录的inode来说,/usr的inode号为6,察看磁盘文件表知/usr实际在132块的位置,以同样的方法递归察看即可找到/usr/ast/mbox的起始块号。 现在详细说明inode节点和物理块号之间的联系。通常inode节点有四个独立的属性,分别为直接索引地址、一级索引地址、二级索引地址和三级索引地址。其中直接索引地址事实上即10个具体的数据块号。此后为间接索引地址,其实质为在每一个地址指定的块号上再次指定一个磁盘文件表地址,实现嵌套存储。

2.3 举例说明

现用一个经典案例进一步解释文件系统的索引结构:一个inode节点有10个用于数据块的访问地址,以及一级索引、二级索引、三级索引块的地址。如果每个块有256个磁盘地址,假设一个磁盘块是1K,那么一个文件最大有多大? 通过题设可知,inode除了直接索引的10个数据块外,还使用一级索引1256个块、二级索引1256256个块、三级索引1256256256个块,因此该系统中的一个文件大小最多有10+256+256256+256256*256= 16843018K。事实上该系统最大支持16TB的单独文件。

3 与文件系统有关的应用

3.1 察看磁盘使用信息

df

df即disk free space,它可以列出每个子文件系统的设备文件名,mount安装的路径,文件存储区和inode节点区的总长度以及空闲空间和百分比。 -v: 选项使得系统显示更多的信息; -i: 显示与i节点有关的信息;

du

du即disk usage,显示所有下属子目录在内的某一目录中文件使用的块数。

3.2 硬连接和符号连接

每个目录项指定的文件名和inode节点号的映射关系,叫做硬连接hard link。同一inode节点被目录项引用的次数即为link数。建立硬连接使用ln命令进行,以下的示例能基本说明硬连接的作用。

ls –l chapt0

-rw-r–r– 1 kc kermit 54332 Jun 01 12:28 chapt0

ln chapt0 intro

ls -l intro chapt0

-rw-r–r– 2 kc kermit 54332 Jun 01 12:28 chapt0 -rw-r–r– 2 kc kermit 54332 Jun 01 12:28 intro

ls -i intro chapt0

88077 chapt0 88077 intro 现在用vi对intro进行编辑,完成存盘退出。

ls -li intro chapt0

88077 -rw-r–r– 2 kc kermit 54140 Jun 01 12:30 chapt0 88077 -rw-r–r– 2 kc kermit 54140 Jun 01 12:30 intro

rm chapt0

ls -l intro

-rw-r–r– 1 kc kermit 54140 Jun 01 12:30 intro 用ln建立硬连接时,只限于文件,两个文件名的路径名也必须处于同一文件系统中。不允许对目录用ln命令建立硬连接。 目录表也使用了硬连接的方法,但是对于任意目录来说,由于.和..文件被系统自动创建,使得其link数不低于2。例如对于/home、/home/bin、/home/jiang、/home/liu组成的/home目录表来说,由于/home/.、/home/bin/..、/home/jiang/..、/home/liu/..、/home均为/home的硬连接,因此其link数实际为5。 符号连接symbolic 也叫软连接。 符号连接的思想被广泛应用到用名字进行管理的信息系统中,符号连接允许给一个对象取多个符号名字,可以通过不同的路径名共享同一个信息对象。在UNIX中,用特殊文件“符号连接文件”来实现符号连接,符号连接文件中仅包括一个描述路径名的字符串。

ln –s

例如:

who > users_on

ln –s users_on active_users

ls –l active_users

lrwxrwxrwx 1 fang kermit 8 Jul26 16:57 active_users->users_on 以上内容透露出的信息是,active_users文件的i节点记录了文件类型为符号连接文件,用ls -l列出时,类型域为l。文件内容是users_on字符串,->后为符号连接文件的内容。符号连接文件的最后一次修改时间以后不再变化。 当用户选择active_users进行操作时,系统实际打开users_on。将active_users展开成该符号连接文件存储的字符串users_on的操作,不是由shell完成,而是由OS的文件管理模块完成。文件管理模块处理符号连接的方法是,根据提供的路径名。在逐个翻译路径名中用斜线分割开的路径分量时,若系统发现符号连接,就把符号连接的内容加到路径名的剩余部分的前面,翻译这个名字产生结果路径名。若符号连接包含绝对路径名,使用绝对路径名。 最后注意,系统在展开符号连接时,由于文件内容可能是绝对路径信息,也可能是相对路径信息,故须注意其中的区别。

3.3 连接类型的区别和应用分析

符号连接的特点是,对连接文件的访问实际上是访问源文件,当删除源文件后再访问连接文件将出现出错信息。又如对目录进行符号连接操作,则当cd 连接文件时,此时的目录环境与连接文件完全没有任何联系,即使执行cd ..,也只是切换至源文件的上层目录。 而使用硬连接文件时,删除源文件并不影响连接文件继续对文件内容的访问。

硬连接和符号连接的区别详细如下: 1)硬连接利用文件系统的存储结构,实现信息共享。 2)符号连接依靠操文件系统的软件处理,实现信息共享。 3)硬连接只适用于文件,不允许用户通过命令对目录实现硬连接,以保障文件系统中目录的树形结构不被破坏。 4)每个文件系统有自己的一套inode节点,不同子文件系统之间,不可能用硬连接实现文件的共享。 5)符号连接完全适用于目录,也可以将符号连接文件和符号连接引用的文件安排在不同的文件系统中,比硬连接更灵活。

连接的主要用途是节约存储和变更目录。 案例1 在一台家庭主机中,如果需要共同拥有一套照片文件,则既可以为其建立硬连接或者符号连接文件,但优劣在于: 使用符号连接,删除某文件,系统会释放相应的空间,可能会导致某些引用这个文件的符号连接不再能正常打开文件; 使用硬连接,需删除所有相关目录项才能释放文件的存储空间。

案例2 /usr/adm存储一些系统软件的日志 /usr/tmp存放一些临时文件 /usr/spool存放用户未读的邮件和打印任务队列 某系统管理员希望将这些目录重新规划至/var中,为了不破坏原系统的功能调用,建立符号连接至/usr中就可以实现这些功能。

ln -s /var/adm /usr/adm

ln -s /var/tmp /usr/tmp

ln -s /var/spool /usr/spool

事实上读者可能会发现,符号连接酷似于Windows中的快捷方式。

Comments

mp77的UNIX课件笔记(5)

原定本章介绍Unix的两个重要文本编辑程序——Vi和Emacs,但由于本系列篇幅限制不可能对这两种软件(尤其是后者)进行足够详细的讨论。事实上如果不能深入研究Emacs的话那么对我们来说是没有任何意义的,因此该部分内容被移至系列的最后进行讨论。 本章开始我们将进入UNIX文件系统进行深入研究。

1 一些基本问题

在UNIX系统中,几乎所有的数据、信息和计算机指令等等都以文件形式存储在外存secondary storage(also called auxiliary storage,vis a vis main memory)中,所有文件都是以目录为单位进行收集和组织的。在多用户系统(Multi-User)中,每一位用户都拥有自己独立的主目录。UNIX的用户和系统管理员按照等级树管理相应的文件或目录。 当一名用户登录系统时,默认目录为用户的主目录,如果要显示当前工作目录,可以输入:

pwd

即打印当前工作目录(acronym for Print Work Directory)。 UNIX是对大小写敏感的操作系统,而Windows并无这方面的要求。

1.1 UNIX的文件类型

UNIX的文件类型可以分为三大类别,分别是普通类型、目录类型以及特殊文件类型。 普通类型包括ASCII文本、可执行程序(executable)、图像(jpeg)文件或html文件等等。 目录类型,例如用户主目录~/.。 特殊文件类型,UNIX中一种独特的文件类型。一类是硬件设备,例如/dev/lp0,一般接打印机等设备。另一类是逻辑设备,这里介绍两种UNIX常用的逻辑设备: /dev/null,它有一个形象的名字“黑洞”,即可以接受任何数据,而这些数据将不会在其它I/O操作中显示。例如:

netstat –an &> /dev/null

这时标准输出中不会显示任何信息,>是重定向标示符,我们可能在msdos中遇到过command > filename的情况,事实上其原理是一致的。另外还有一个相对应的>>意在filename末尾添加信息。&表示标准输出和标准错误信息,如果仅仅想对标准输出信息进行操作可用1替换&,如果只想屏蔽标准错误可输入2。 /dev/zero,这是一种与/dev/null似乎完全相反的文件,它代替了一种标准输入,且此类输入仅仅包含二进制0,其ASCII字符为null。现在来看一种应用示例:

dd if=/dev/zero of=swap bs=1024 count=40

dd是UNIX命令,其作用是自定义一个输入流和一个输出流,并借此实现数据I/O。if指定输入流,此处来源/dev/zero。of指定输出流,此处定义名为swap的文件,bs即blocksize定义块大小,count定义块个数。执行此命令后,系统会建立一个名为swap的文件,此文件是由40个大小为1024字节且内容全部为null的空文件。 以上两种文件类型在UNIX系统中起到了至关重要的作用。/dev/null主要应用于shell脚本,也几乎是shell脚本编程中最常使用的重定向输出之一。/dev/zero的用途更为广泛,系统管理员可以使用该功能为系统建立临时的交换文件(或称交换分区),因为其良好的结构恰恰对应与内存管理,因此可以明显提升某些数据服务器的工作效率。

1.2 目录和文件的表示方法

UNIX的目录管理是典型的树形结构,这里不再赘述。其命名规则并不很严格,大多支持200字符以上的长文件名,一个特点是除斜线slash(不包括backslash)以外的字符几乎都是合法被接受的(包括不可打印字符)。 在shell中表示路径,可以采用/usr/home/guest/.profile的绝对路径(absolute)表示,也可以采用../httpd/bin/apachect1的相对路径(relative)表示。其中每个目录下均包含两个目录指示符.(指当前目录)和..(指上级目录)。如果用户使用csh,还能利用~/表示用户主目录。 我们在前文提到正则表达式 (Regular expression)与通配符(wildcard)含义并不一致。这些通配符的意义是由shell程序进行分析和解释的。下面介绍几种简单的通配符: ,即任意长度字符串,唯一例外的是当表示文件名头部时无法匹配以.起始的文件,如.profile; ?,匹配任意单字符; [],匹配括号内任意字符,也可以指定其范围。但[A-Z]*并不和正则表达式中的本式等同,这点需要注意。 仍需补充的是,shell在处理含通配符或者多个项的命令时,一般先将目标目录下的文件名按字典顺序排序,然后逐一查找,因此输出的最终结果也是按字典序排序的。

2 UNIX文件系统命令

2.1 查看目录信息ls

ls

ls即list,列出当前目录的内容,同一个目录下即可能有文件,也可能有子目录,然而当单独使用ls时并不能对此类信息作出区分。 ls有很多选项,且大多数都经常会用到,因此掌握ls是UNIX系统管理员的基本功之一。下面列出一些非常实用的ls选项: -a,列出所有本目录下文件,并包括所有文件名以.起始的文件。在UNIX中此类文件即是隐藏的,也就是说在不带-a的情况下ls无法查看这些隐藏文件。常见的隐藏文件如.profile、.bashrc等都是默认于用户主目录下,存储了该用户的自配置信息。 -l,以长格式显示UNIX的文件类型、权限、硬连接数(hard links,后文介绍)、拥有者、拥有者所属组、大小、最后一次修改时间以及文件名等等。 -F,在文件的尾部添加特殊字符以代表其文件类型,例如*表示可执行文件,/表示目录。 -R,同时列出所有的子目录信息。 -d,列出目录内容的同时显示符号连接或目录的信息,包括了连接的目标并列出目录的内容。 -I,列出所有文件的i-node号。 现在我们给出执行ls –la命令后的结果示例: -rw——- 1 gambill cls 10868 Jan 14 08:44 vm_mail drwxr-xr-x 2 gambill stdt 96 Jun 13 1999 wvannot 给出信息的第一列记录了文件类型和相关权限。其第一位字符通常表示文件类型,当文件类型为-时表示文本文件、可执行文件或硬连接文件;d表示目录;另外还有一些其它的文件类型标识,例如b块设备文件、l符号连接文件等等。 第一列剩余的9位字符记录了该文件的许可权限,其中每三位分别记录r读、w写和x可执行三类权限,并相应于文件所有者、该文件所属的组、以及其他任何人的权限。第三列是指当前文件的连接数,gambill是文件的所有者、其后即gambill所在的组。然后是文件的大小(以bytes为单位)、最后一次修改时间以及其文件名。

2.2 文件复制cp

cp option file1 file2

cp命令首先创建file2,然后将file1的数据拷贝至file2中。如果file2已存在,则覆盖源文件。选项i即当目标文件已存在时首先提示用户是否覆盖源文件,选项r支持拷贝目录及其全部内容至目标目录。

2.3 文件转移mv

mv option file1 file2

mv可以将file1移至file2,同时删除原有file1文件。mv一个常用场合是在修改文件名的时候。使用选项i可在当file2已存在时询问用户是否覆盖源文件。

2.4 文件删除rm

rm option files

rm命令删除files中列出的所有文件。选项i依然是在删除前询问用户是否删除该文件,选项r删除一个目录下的文件,并且删除其子目录下的所有文件,还有一个常用选项f即强制删除该文件且不用提示用户是否确认。 当用户确认要删除一个目录时,最好的办法是使用rm –rf来实现。 前面提到UNIX的文件名除字符slash外均可使用,假设pwd下某文件名为-i,那么按原方法应执行rm –i,鉴于i是rm的选项之一,因此这条command显然无法满足要求。因此大部分时间我们习惯将路径用.标识符标识:

rm ./-i

以上操作是没有任何问题的,但当我们使用grep搜索字符串-regulation时类似问题再次出现,且无法利用上述办法解决。为此UNIX的大部分命令约定–为选项域结束标志,那么这之后的-XXX就不会再被列入选项列表了,例如:

rm – -i

2.5 文件查找find

find的功能是在类UNIX平台上通过目录查找目标文件,它可以根据指定的目录信息,以及用户定制的规则(Criteria,plurality for Criterion)查找相对应的文件。不仅如此,find还允许用户定制对目标文件进行操作。

find verl.d ver2.d -name ‘*.c’ -print

以上是一个find应用示例,其选项含义如下: -name,文件名的匹配,允许使用文件名通配符 *、?和[ ],文件名通配符描述串传递到find程序中,因此,应当用引号括起来,以免被shell展开。 -type,类型,其中f:普通文件,d:目录,l:符号连接文件,c:字符设备文件,b:块设备文件,p:管道文件,如:-type d。 -size,±n[c],文件大小,正号表示大于,负号表示小于,无符号表示等于,其他与数量有关的选项,也采用这样的方式。 -size +100 表示长度大于100块; -size -100 长度小于100块; -size +100000c 表示长度大于100000字节; -size -100000c 表示长度小于100000字节; “操作”选项一般有以下几种常用内容: -print,打印查找到的符合条件的文件的路径名。 -exec,-ok,对查找到的符合条件的文件执行某个命令。

2.6 当前工作目录pwd

pwd即print working directory。如果要选择进入不同目录,需要使用cd dname。

2.7 创建目录mkdir

mkdir dname

在当前目录下创建新目录dname,相应还有一种删除目录rmdir,但由于rmdir只能够删除内容仅含..的目录,因此实际用途并不如rm -rf广泛。

2.8 打包压缩工具tar和compress

tar我们在连载三已有详细介绍,读者可前往查看: http://www.hanyi.name/blog/?p=161 compress是早期UNIX的文件压缩工具,其相应的解压程序为uncompress,但随着gnuzip等更强大软件的出现此类工具已逐渐被淘汰,事实上近年来的发行版中均未附带compress工具。

下一章我们将讨论UNIX文件系统的有关内容。

Comments

mp77的UNIX课件笔记(4.b)

二、常用UNIX系统工具

UNIX已拥有许多功能强大的文本编辑器的支持,但对于系统管理人员来说,在shell级直接发送指令并准确获得自己所需的数据——这不失为一种提高效率的好办法。而UNIX附带的文本文件处理程序几乎就能完全实现这些功能,总的来说这些程序有如下特点:

1、默认从标准输入stdin获得数据; 2、当指定文件名时,从文件中获取数据,可以同时指定多个文件; 3、默认在标准输出stdout显示; 下面将对这些直接针对文本进行操作(也可以通过管道重定向自其它位置,后文介绍)的程序逐一进行说明。

1)基本文件操作

more

UNIX最基本的逐屏显示程序,当显示满一屏后最后一行显示为——more——,按任意键后跳至下一屏内容。Linux中还附带了一个功能更为强大的逐屏显示程序:

less

less不仅提供了more独特的逐屏显示功能,还支持键盘的方向键,或者j,k,类似vi的光标定位键以及PageUp、PageDown、Ctrl+F/B、Home和End键。 UNIX中还有一个古老的逐屏显示程序pg,其显示方式与more接近,不过每屏显示后需要输入子命令来选择上滚或下翻操作,功能上与less相近,近年来的发行版中都已经用more/less代替了pg。

cat

cat类似于dos中的type命令,显示文本文件中的内容,其独特之处在于可以利用>运算符将标准输入重定向至文件,直到Ctrl+D结束输入(亦或者是直接指定输入源文件),这是创建新文件一种较为简捷的方法。

echo

将命令行参数输出至标准输出,echo有很多备选选项,可以自由格式化输出结果。例如:

echo abcdABCD | od –x

od即octal dump,八进制输出。也就是说以字节为单位输出相关信息,上例中把echo输出的字符串重定向至od输入串,按字节分析后转换为十六进制打印结果如下: 0000000 6162 6364 4142 4344 0a00

head -linecount

tail -linecount

head和tail分别是查看文件头部/尾部的指定行数,这对程序主要用于查看一些严格按照格式书写的文件——例如公文、邮件等一些相关的设置信息。同时也是为避免查阅超大文件时效率低下的问题(但要真正实现随心所欲查看似乎仍然需要一些技巧)。例如:

netstat –s –p tcp | head –5

netstat指network statistics,是一种统计网络出入、路由表以及当前网络接口状态信息的程序。-s选项指定按协议输出每一条项目的详细数据,-p tcp只显示基于tcp协议的网络连接状态。head -5只显示前五行,即输出头部部分的全局信息。 tail还有一个实用的选项-f(forever),意即实时显示文件新增加的内容,也就是说程序在首次输出完成后并不退出,而是对目标文件继续监听并输出新增内容,除非用户Ctrl+C强行中断程序运行。

wc

列出文件中一共有多少行,多少个单词、字符,当指定的文件数大于1时,最后还列出一个合计。也可以根据选项指定输出信息:-1文件行数;-c文件字符数;-w文件单词数。 当同时处理多个文件时,wc会在最后一行列出每一项的totoal值。

sort

对文件内容进行排序,这种排序对大多数文件来说并没有太大意义,不过对于phonebook或者是checklist来说还算是比较方便的了(尽管现在恐怕已经很少有人再把这样的信息明文写在ASCII文本里了)。主要使用默认选项(按ASCII字符串比较方式)和-n(numberic)按数值均为从小到大排序。

tee

tee其实是英文字母T的单词形式,其形象描述了本程序的功能——将程序输出信息结果保存至指定文件。一般要和管道连用:

who | tee whois.log

2)高级文件操作

接下来的问题涉及到模式搜索的范畴,其最便于使用的工具无非是正则表达式(Regular expression)。但是正则表达式正面临这样一种尴尬:入起门来十分容易,一旦深入研究就变得异常艰难,UNIX就是采用了正则表达式进行文本文件模式匹配的操作系统之一,尽管其起源于编译原理,但又有多少编译大拿真正把正则表达式融会贯通呢? 幸好那些深入的工作离我们尚有不小的距离,能够熟练掌握如今这种被越来越多高级语言推崇的模式匹配工具,就已经足以使我们的技术水平更上数层台阶了。 正则表达式中含有以下六个固定的元字符 . * [ \ ^ $ 元字符代表特殊的含义,如$,只在出现至表达式最尾部时才表示行尾标志,否则与其自身匹配; ^只有出现在表达式最首部时才有特殊意义,否则与其自身匹配; *匹配其前面的单字符出现0次或多次; 句点.匹配任意单个字符(不能为空); \(backslash)即转义字符,可以使元字符直接与自身匹配; [和]搭配使用时定义了一个集合,其可以匹配集合中描述的任意一个字符。 其它符号与自身匹配(在扩展正则表达式和一些其它语言定义的正则表达式中,还包括一些额外的元字符)。需要注意的是,我们知道

ls *.[ch]

是指显示当前目录下后缀为.c和.h文件列表。正则表达式与此处的shell通配符意义并不相同,请读者稍加注意。

现在我们介绍一些建立在正则表达式基础上的文本文件处理程序。

grep pattern file-list

global regular expression print,文本过滤程序,按正则表达式规则,筛选出含有指定模式字符串的文本行。当指定的文件数>1,查找到指定字符串时,整个行,连同文件名一起显示。指定的文件数≤1,只列出含有指定模式的整个行的内容,不显示文件名。 grep在默认情况下是不识别正则表达式的,例如:

grep O_RDWR /usr/include/*.h

显示O_RDWR定义的库文件位置。如果要使用正则表达式,需要在pattern项处用’regular expression’注明。 egrep指扩充的(extended)正则表达式文本过滤程序,本程序比grep多出几个元字符: (),即一个字符串集合,这将更改原有*的定义; +,表示其前面的单字符或串集合出现一次或多次; ?,表示0次或1次出现,比句点.多出对空字符的匹配。 在处理较多批次文件时应当考虑到egrep的算法时间复杂度问题。 fgrep和前两个程序-f选项的功能基本一致,即不带正则表达式的字符串匹配,这是性能最好的一种过滤程序,同时也能快速匹配一些grep/egrep由于自身原因无法匹配的字符串。 以上三个程序都共享一些常用的选项,例如-n每行显示行号;-i忽略字母的大小写;-v显示所有不包含模式的行。

当我们利用grep来建立某些大规模文本过滤规则的时候,有时会要求一些具备精确结果的报表,而这是grep无法实现的功能。但此类功能对于管理员来说并不罕见,因而促使一些功能强大的同类工具的崛起。

awk ‘Program’ file-list

awk是以创建者Aho、Weinberger、Kernighan三人的姓名首字母命名的,其强大之处在于Program部分支持类似C语言、并吸收了许多同期语言优秀的语法规则,且同样识别’regular expression’。另外内置了多个全局变量: NR,No. of Record当前记录的记录号; $0,当前记录; $1,$2,…,当前记录中的域,通常以[{witespace}\t]等为token界限。 FILENAME,当前输入的文件名。 awk甚至还支持C语言中所有的关系运算符。下面给出一些awk程序示例:

date | awk ‘{print $4}’

未指定file-list,未指定条件,输出所有记录中第四个域(即当前系统时间)的内容。

who | awk ‘/^ *zhang/ {printf(“%s ”,$2)}’

未指定file-list,未指定条件,使用正则表达式,输出所有记录中zhang姓用户登录的终端标识。

ls -s | awk ‘$1>2000 {print $2}’

未指定file-list,指定条件为记录中第一域数据大于2000,输出所有记录中相应第二域的内容。 另外,awk提供了-F选项供用户自定义域分隔符,例如:

awk –F : ‘$2 ==〝〞’ /etc/passwd

awk –F : ‘$2 ~/^$/’ /etc/passwd

awk –F : ‘$2 !~/./’ /etc/passwd

awk –F : ‘length($2) == 0’ /etc/passwd

指定file-list为/etc/passwd,指定条件,其中~表示与正则表达式匹配,!~表示与正则表达式不匹配,模式的域分隔符由默认的[{witespace}\t]被用户指定为冒号:,如此以来就可以处理诸如/etc/passwd中的文本结构了。 更令人惊讶的是,awk的-f支持用户载入自己定义的规则和报表格式文件,例如:

cat list.awk

BEGIN { printf(“=====================================\n”) printf(“FILENAME %s\n”, FILENAME) printf(“————————————-\n”)} END { printf(“====================================\n”) } { printf(“%3d: %s\n”, NR, $0) } 其中BEGIN和END分别定义了报表头部和尾部的输出规则,最后是报表内容的输出规则。

awk –f list.awk md5.c


FILENAME md5.c

1: 2: #include “md5.h” 3: 4: / forward declaration / 5: static void Transform (); 6: 7: / F, G, H and I are basic MD5 functions / 8: #define F(x, y, z) (((x) & (y)) | ((~x) & (z))) …… 298: buf[2] += c; 299: buf[3] += d; 300: }

301:

如今awk在开发者的不断发展下功能已经日趋多样化,甚至还拥有了自身独有的程序设计语言——awk语言。 有了awk作为强有力的文本过滤工具,就不能缺少其在shell脚本方面的王牌搭档——sed。 sed全称stream editor,很简单即流编辑器,但其功能绝非简单而已。sed的两种命令方法都与awk类似,且完全可以参考后者:

sed ‘Program’ file-list

sed –f filename file-list

这里只解析sed的一条语句。

tail –f pppd.log | sed ’s/145.37.123.26/QiaoXi/g’

上面这条命令的目的是开启一个pppd连接日志的实时监控窗口,并将其中的IP地址转换为QiaoXi输出。其中s命令是“替换(substitute)”,正slash分割正则表达式,后半部分是替换字符串QiaoXi,g指global flag,使得s命令在一行中遇到多个模式描述的字符串时,都替换为QiaoXi,否则,一行仅替换一次。 事实上awk和sed作为UNIX脚本编程的利器已经雄霸了多年,直到最近二十年,各种继承先哲的OO scripting languages如Perl、Ruby以及Python等异军突起,迅速征服了大量技术人员,后来人们还发现这些脚本语言能很好地用于软件工程快速原型设计。关于awk+sed的脚本编程读者可以参考经典书籍,我们今后也会有shell编程方面的详细讨论。

最后,我们来看两条令人轻松的程序。

tr string1 string2

单纯地将在字符串string1中出现的输入字符被替换为字符串string2中的对应字符。

cat telnos | tr UVX uvx

cat report | tr ‘[a-z]’ ‘[A-Z]’

cat file1 | tr % ‘\012’ //将%改为换行符

cat myap.c | tr ‘\015’ ‘ ’ > myap1.c //将文件中多余的回车改为空格,回车的ASCII码是八进制的015

仍然要注意的是不要漏掉必需的单引号。

cmp file1 file2

顾名思义,cmp就是比较两个任意类型的文件,并将结果输出在标准输出中。默认情况下,当两个文件完全一致时不会返回任何信息,否则返回不一致的行数和首字符位置信息。还有一种基于两个文件不一致性处理的程序diff。

diff file1 file2

11a12,13 diff不仅有对比两个任意文件(包括文本文件、二进制文件等)的功能,而且可以指定对其中不一致的地方进行增删改操作。如以上命令行在file1中的11行之后append文件2中的12、13行,类似的还有c(change)即将file1中的某行替换为file2中的某行,以及d(delete)即删除file1中的指定内容并加入file2中的指定内容。

Comments

《南京!南京!》会成为传世经典?

 首先原谅我依然是躲在家里看dygod刚出炉的dvd……大部分是因为最近一段时间连看一部电影都会让人产生极度奢侈的罪恶感。

 “南京”对于近代以来的中国人来说始终有说不尽的话题。导演起初筹拍影片的时候我还在上高二,当初就对片名中的两个叹号有些感兴趣,甚至还写了一篇同名文章(恕我已经忘却大半内容)记述自己浅显的理解。

 如果我没有记错的话,多年来国人对南京浩劫的理解始终处于中学历史教科书的阶段。且不论教科书式的历史仅仅是为了记录历史事实,或是被东瀛一些民族极端人士称为又一个民族极端产物,我们对南京浩劫的印象恐怕就只有两种形态:

 1、数十万同胞们可怜,他们是侵略战争最直接和最残酷的受害者,也是让后代们久久不能释怀的首要原因;

 2、侵略军的可恨,他们是战争进行过程中的杀人机器,同时也是毫无人性地残害普通贫民的刽子手。

 如此一来教育达到了两个目的:千万不要忘记那些死难者,他们是我们的同胞;侵略者曾对我们犯下了滔天罪行,这是任何挽救都永远无法弥补的。然而很少有人去提及历史的警世作用,我们也并不希望观众们仅仅是怀着一种单纯的愤慨与哀思夹杂的感情去观赏该片,事实上它是一部深刻的教育片——与以往不同的是,影片所展现出的教育主题几乎超过了多年来国产电影长期形成的一种束缚。这种束缚是一种对人天性向往和平的回避,以及抛开政治、文化之外人性共通的否认。

  我认为《南京!南京》和稍早前的《鬼子来了!》表达了相类似的主题,尤其是对比观看之后这种特征就愈发明显。但相较于《南京!南京!》,《鬼子来了!》是建立在一个完全虚构的小说的基础上,剧情甚至有些荒诞(但又让人无法找出批驳的理由来),前者的史实基础是有目共睹的,相信导演三年筹拍的艰辛并没有白费。我们看到,已经有许多评论者认为影片达到甚至超越了世界级《辛德勒的名单》的水平。

 就影片本身而言,观影之中我就感觉到一些差距,最后仍然坚定了这种信念。这种差距并非关于经济、演员或者是导演,而是我认为影片中最重要的因素——剧本。据说该片大热后引起了一些历史原型相关者的不满,一些纪念馆的负责人认为影片并没有完全依据历史事实——在我看来是完全可以接受的,因为若要考虑到导演一心向往传世之作的心情,就不难理解部分情节适当的艺术化了。

 但颇令人遗憾的要属影片对人物的描绘。首先看侵略方主角角川正雄:一个在家乡接受了良好教育的年轻士兵,第一次开枪“误杀”了贫民,这使他开始感到惶恐——没什么问题。关于百合子的分支,无论是由于长期离乡而怀有深深的孤独感,亦或是别的什么原因,男主角居然就顿生情愫,声称要和她“结婚”,实在令人难以想得通,况且后来证明百合子对这位“初次”的士兵并无太多印象,这点的重要性直接关系到后来百合子病逝,男主角认为自己失去了挚爱的“妻子”而更加陷入了深深的矛盾与痛苦之中。

 再来看中方男主角唐先生,影片中此人并未被描写成英雄,他只是一个深爱着自己身边每一位亲人的男人,为了妻女他甚至可以向侵略者委屈求全,面对侵略者始终是低声下气,后来甚至说服Mr.Rabe携自己一家离开南京——当然这一切都在最后一刻戏剧性地变化,唐先生自愿把唯一的离开名额让给了化妆成仆从的中国军官,自己重回险境——有人说他是在此完成了最后一次的自我救赎,并没有不合理之处,但考虑到唐先生早先失去了自己的亲生女儿——这恐怕是对一名父亲来说足以使其丧失生活勇气的了,更何况临死前他还自豪地告诉侵略者说她妻子又怀上了他的孩子。问题在于唐先生确定自己一定会死,侵略者军官事先也暗示他若回来一定会死——但依然让人难以理解,为什么一个看起来似乎还帮助侵略者清剿伤兵的“朋友”,会很快不明不白地被执行枪决,他的死不会令人意外,只是这种“死法”的确让人“意外”了一把。

 最后来看侵略者军官,他似乎已经是影片中最为“残暴”的人,然而还是始终摆脱不了男主角心中的那种“矛盾”,包括他最终枪杀的那个痴迷越剧《梁祝》的闺门小旦“小妹”,理由竟是她太漂亮,死了要比这样活着更好。片尾还特意给出了他洗澡的镜头,和片尾发生的故事同处一个背景,人物并不一定想着不同的事。在这一点上,就又很难让人理解他的暴行到底来源于何处,真是“杀红了眼所以丧心病狂”?这也太小看人类的EQ和IQ了吧。

 尽管在剧本上影片依然给人带来了些许遗憾,但从选材、场景、故事上本片都足以达到世界级。庆幸的是本片包含了一个关键的侵略者视角,使得众人可以较为清楚的看看,到底战争的本质为何,到底人性的本质为何,到底导致这场灾难的源头为何,到底片中民族个性的差距为何。这恐怕就是影片所要体现出的教育意义吧。

 但若想成为传世经典?最起码的,这段尘封七十年之久的历史需得形成全人类的共识再说了。

Comments

mp77的UNIX课件笔记(4.a)

了解UNIX的文件系统是必要的,就像对于windows我们所了解到的。但二者最大的区别就在于熟知前者会对进一步的工作有决定性的推动作用;后者呢?最终还是无奈地回归平台。原定于本节详细介绍文件系统,但为了提升实用效果我们决定先插入一章全面介绍UNIX基本应用程序的内容(由于本节内容过于冗长,分两部分加以说明)。

 

一、 常用UNIX系统命令

 

1、 man,即mannual,可以通过此命令查看UNIX在线文档并查找关于本系统的命令、工具、开发函数的详细介绍、选项及使用方法。甚至对于man本身,可以输入:

# man man

即可查阅man命令有关的系统手册信息。

事实上还有一些更为常用的查看帮助方法,这在执行一些特定工作时能大大提高查找效率。例如:

# command –help

–help本身是查看命令帮助的选项,少数命令还支持-h选项。–help的好处是只列出命令的输入格式以及全部选项说明,也就是最常用的一种帮助查看方式。

# info command

info是GNU下的一个文本工具,Linux中涵盖了大量的系统开发信息,包含几乎所有man和–help中的内容,一般只有专业人员才会用到。

# which command

显示命令的具体目录。

 

2、 显示系统信息。基本系统信息包括了系统时间、在线用户以及当前连接的终端设备。

# date

查看系统日期,其强大之处在于可以定制输出格式,自身甚至可以进行大量实用运算。

例如输入:

# date ”+%Y.%m.%d %H:%M:%S Day %j”

2009.5.20 17:51:05 Day 140

“%j”记录已经历当年的天数,更多参数可在–help中查询。

# cal

查看某月日历,空参数则直接返回当月日历,另外可以用

# cal month year

指定查看日历。

# who

查看在线用户,显示用户ID、登录终端以及登录时间,如果用户通过网络登录,还会附带显示登录主机名称。

这里需要解释一下“登录终端”功能。Linux采用了虚拟终端技术,这种技术可以使得用户在虚拟shell对远程主机进行操作,即Virtual Terminal。具体来讲,就是通过键盘Alt+F1-F6键在6个不同的shell间进行切换,每个shell的登录用户是任意的(甚至是可以相同的)。平时没事的时候,我们可以在不同的终端登录不同用户,并互相进行一些用户间操作(显得有些浪费时间……)。关于Virtual Terminal的设置在/etc/inittab中,如下内容:

# Run gettys in standard runlevels

1:2345:respawn:/sbin/mingetty tty1

2:2345:respawn:/sbin/mingetty tty2

3:2345:respawn:/sbin/mingetty tty3

4:2345:respawn:/sbin/mingetty tty4

5:2345:respawn:/sbin/mingetty tty5

6:2345:respawn:/sbin/mingetty tty6

 

ttyX指定相关终端的名称,minigetty将终端与login进行连接,实现验证登录用户的功能。另外如果系统安装了X-window,那么该系统启动后占用下一个未被占用的终端(如果/etc/inittab中x值为5默认启动X-window模式的话,则该系统占用tty1终端)。另外如果在X-window系统中使用ctrl+alt+F1-F6依然可以进行文本、X-window模式间的切换(理论上是可行的,但笔者在Virtual Machine 6.5挂载redhat 9.0中似乎无法实现此功能,或者是因为切换成本太高,有时甚至会导致X-window崩溃5.23备注:家里的ubuntu8.1完全是可行的)。

# who am i

# whoami

以上两条命令显示当前主机登录用户情况。

# w

显示当前系统的详细状态,包括运行时间、在线用户数量、IDLE(该终端空闲时间)、JCPU(该终端的所有作业占用的CPU时间)、PCPU(该终端的前台作业占用的CPU时间)以及what(显示该终端当前正在运行的命令或程序)。

# uptime

仅仅显示w命令的表头内容,如系统时间、在线用户数量、平均CPU占用时间等等。

# tty

显示当前终端的设备文件名。

 

3、 更新用户信息。

# usermod

该命令只能被root用户使用,可以更改指定用户的用户名、密码等信息。普通用户使用时会显示command not found错误。

# passwd

全用户使用,简易更新当前用户的登录密码,并且有评价当前密码复杂度的功能。

 

4、 与其他用户通信。

# write mp77

Hello world!

可以向正在登录的用户发送消息,消息可以换行,Ctrl+D结束发送,可以使用<重定向文档中的内容。

# talk mp77

如果接收方允许接收对话,则屏幕分成上下两部分,两用户可以以键盘会话。终止talk的执行按Ctrl+C键或Ctrl+D键。

如果对方此时没有登录系统或者虽已登录但拒绝消息,通信失败。与其他UNIX系统上的用户会话的talk命令中应有对方主机名,如cdc.xynet.edu.cn主机上的用户wang通信,执行下列命令:

# talk wang@cdc.xynet.edu.cn

设置本机是否允许接收消息,则输入以下命令:

# mesg

显示当前mesg状态。

# mesg y

将mesg状态置为yes,允许接收消息。

# mesg n

拒绝接收消息,但任何时候都无法拒绝root向用户发送的消息。

# wall

即write to all,广播方式向所有已登录用户发送消息,该命令无法被屏蔽,默认使用权限为所有用户,因此很多系统管理员需设置禁止普通用户使用该命令。

 

5、 mail

mail是一种系统应用程序,从理论上说,mail也是用户间通信的一种方式(但不限于此功能),鉴于其功能强大且比较复杂,移至本段说明。

# mail -s ”title” username@hostname.address

向username发送一封邮件,可以通过重定向输入文件,邮件内容按Ctrl+D结束输入。

紧接着会出现Cc:即Carbon copy,建立副本并抄送往其他用户。如果要发送带附件的邮件,建议安装更为强大的electronic mail agent,例如mutt(Redhat 9.0附带此软件包):

# mutt -a [appendix] -s [title]

接着按提示操作即可,注意mutt编辑邮件内容时会自动调用vi,完成后需要保存退出并自动退回mutt界面,按y完成发送。

如果用户接收到新邮件,则每次登录时系统会提示You have new mail。输入mail即可。

此时会进入&的命令提示符状态,并且只会解释mail中定义的一些命令或选项,查看帮助输入?即可。

所有未读邮件会在提示符上方列表中显示,顶行的箭头指示当前操作对应于某邮件,事实上我们也可以使用后面的ID直接指定操作对象,现说明几种常用功能:

# q

阅读当前邮件,如果删除则输入d,所有未读邮件保存在/var/spool/mail/username中。

对邮件操作结束后有两种退出模式:

# x //不存档退出,即还原之前的所有操作,已读邮件仍被列为未读。

# q //存档退出,已读邮件会从/var/spool/mail/username中删除,并存入该用户主目录下的mbox中。

 

6、 远程数据交流,包括远程登录和远程文件传输两种功能。

远程登录有rlogin、telnet等命令。

# rlogin hostname

# telnet localhost

前文提到rlogin因为缺乏安全性支持,已经不被推荐使用,因而主流依然是telnet。

连接成功后会提示输入login name以及password,验证成功后会直接进入Virtual terminal模式进行远程控制。期间输入^]可以挂起远程terminal进入telnet>命令提示符,输入help可以查看命令帮助,z挂起telnet回到VT模式,close关闭当前连接,quit直接退出telnet。

# rcp username@hostname:directory dstdirectory

rcp即remote file copy远程文件复制,本功能类似windows中的共享功能,且仅能在UNIX主机间使用。需要注意的是,rcp需要提前预知远程主机用户名,以及拥有远程复制的权限。此外还可以-x开启传输加密功能(增加主机负担)。

更为强大灵活的工具要依赖ftp,其基本格式类似telnet,但内部命令完全不同。

# ftp hostname

ftp登录远程主机,连接成功后提示输入用户名和密码,并进入ftp>命令提示符。也可以在提示符内输入open hostname连接目标主机。help可以查看ftp命令集,常用的有get(下载)、put(上传)、delete(删除)、bye(断开)等。

这里值得注意的是ftp的ascii和binary两种传输模式。

ftp默认使用ascii传输文件,这是因为在不同系统环境中传输数据时,有可能会遇到双方主机文本格式不同的问题,因此需要通过ftp对源文本文件进行转译以适应这种变化。但当传输的内容是直接建立在二进制基础上时,这种转译就有可能破坏所传输文件。这时切换至binary模式可以解决此类问题。

对ftp感兴趣的读者可以参考RFC 959,空闲时间按标准自己做个ftp客户端玩玩(强调是空闲时间…)。

 

Comments