1.1 登录
1. 用户登录名
登录Linux系统时,需先键入用户登录名,然后键入用户密码,系统通过/etc/passwd(口令文件)文件校验用户登录名和用户密码。口令文件中的登录项由7个以冒号分隔的字段组成,分别为登录名、加密口令、数字用户ID(224)、数字组ID(20)、注释字段、起始目录(/home/stevens)、以及Shell程序(/bin/sh)。
2. 登录Shell
登录后,系统先显示一些典型的系统信息,然后就可以向Shell程序键入命令。Shell是一个命令行解释器,它读取用户输入,然后执行命令,用户通常用终端,有时则通过Shell脚本文件向Shell进行输入。常用的 Shell有:
Bourne Shell, /bin/sh
C Shell, /bin/csh
Korn Shell, /bin/ksh
系统从口令文件中登录项的最后一个字段确定应该执行哪一种Shell。
Bourne Shell是其最早版本,也是最流行的,C Shell和Korn Shell是其后继开发和升级的。三种Shell语法类似,功能基本相同。
1.2 文件和目录
1. 文件系统
Linux文件系统是一种树形层次结构,包括目录和文件,目录的起点称为根(root),其名字是一个字符/。
目录(directory)是一个包含目录项的文件,在逻辑上,可以认为每个目录项都包含一个文件名,同时还包含说明该文件属性的信息。文件属性有文件类型、文件长度、文件所有者、文件的许可权、文件最后的修改时间等,可用stat和fstat函数返回一个包含所有文件属性的信息结构。
2. 文件名
目录中的各个名字称为文件名,斜线 (/)不能出现在文件名中,斜线分隔是构成路径名。当创建一个新目录时,系统自动创建了两个文件名,分别为.(称为点 )和..(称为点-点),点引用当前目录,点-点则引用父目录,在最高层次的根目录中,点-点与点相同。
3. 起始目录
登录时,工作目录设置为起始目录(home directory),该起始目录从口令文件中的登录项中取得。起始目录是创建用户时创建的,如test的起始目录为/home/test。起始目录又称为主目录,用“~”可表示该用户的主目录。
4. 路径名
0个或多个以斜线分隔的文件名序列构成路径名(pathname),以斜线开头的路径名称为绝对路径名(absolute pathname ),否则称为相对路径名(relative pathname)。如当前目录为/home/test/hello,cd /home/test/hello/why是使用绝对路径方式到达该目录,cd ./why是使用相对路径到达该目录,cd ~/hello/why是通过主目录到达该目录。
5. 工作目录
每个进程都有一个工作目录(working directory),有时称为当前工作目录(current working directory)。所有相对路径名都从工作目录开始解释,进程可以用chdir函数更改其工作目录。
登录一个用户时就有一个工作目录,用cd命令可以改变工作目录,如登录test用户后敲入cd hello,其工作目录变为/home/test/hello。
1.3 输入和输出
1. 文件描述符
文件描述符是一个非负整数,内核用来标识一个特定进程正在访问的文件。当内核打开一个现存文件或创建一个新文件时,它就返回一个文件描述符。
每个进程在Linux内核中都有一个task_struct结构体来维护进程相关的信息,称为进程描述符(Process Descriptor),又称为进程控制块(PCB,Process Control Block)。task_struct中有一个指针指向files_struct结构体,称为文件描述符表,其中每个表项包含一个指向已打开文件的指针,如下图12-1所示。
进程控制块(task_struct)指向文件指针的顺序为task_struct-> files(file_struct) ->fd数组,fd数组大小决定了进程打开的最大文件个数。
图12-1 文件描述符图
用户程序不能直接访问内核中的文件描述符表,而只能使用文件描述符表的索引(即0、1、2、3这些数字),这些索引就称为文件描述符(File Descriptor),
用int
型变量保存。当调用open
打开一个文件或创建一个新文件时,内核分配一个文件描述符并返回给用户程序,该文件描述符表项中的指针指向新打开的文件。当读写文件时,用户程序把文件描述符传给read
或write
,内核根据文件描述符找到相应的表项,再通过表项中的指针找到相应的文件。
2. 标准输入、标准输出和标准出错
每当运行一个新程序(进程,包括Shell程序产生的进程)时,进程自动打开三个文件描述符,即标准输入、标准输出以及标准出错。Shell中0表示标准输入(stdin),1表示标准输出(stdout),2表示标准错误(stderr)。
标准输入对应键盘,标准输出对应屏幕,标准错误同样对应屏幕。
Shell都提供一种方法,使任何一个或所有这三个描述符都能重新定向到某一个文件。例如:ls>file.list,就是将标准输出重新定向到名为file.list的文件上。
3. 不用缓存的I/O
函数open、read、write、lseek以及close提供了不用缓存的I/O,这些函数都是用文件描述符进行工作。不用缓存的I/O函数一次读写操作完成一次系统调用,当初级文件I/O写函数write所带的写大小参数太小时,会引起系统调用次数过多而造成系统效率低下。
4. 标准I/O
标准I/O函数读写时无需关心缓存大小,操作系统对标准I/O函数自动分配缓存、使用缓存和管理缓存。使用标准I/O可无需担心如何选取最佳的缓存长度,例如fread、fwrit、fprintf、fscanf、fgets等都是标准I/O,标准I/O是不用缓存的I/O的发展,这些函数默认都使用了缓存。
1.4程序与进程
1. 程序
可执行程序是存放在磁盘文件中的可执行文件,可使用6个exec函数中的一个由内核将程序读入内存,并使其执行。
2. 进程和进程 ID
程序的执行实例被称为进程(process)。进程空间(包括代码段空间、数据区空间、运行的堆栈空间)存在于内存中。CPU执行的是进程代码段的代码,进程执行时间就是CPU执行该进程代码的时间。程序是静态的,存放在硬盘上,是永久的;而进程是动态的,是暂时的,有其生命周期,当程序加载到内存时,操作系统为其分配好其进程空间时,从main函数开始运行时,标志着该进程生命的开始,当调用exit函数退出时标志着该进程生命的结束,随后操作系统回收进程空间和所占资源。
每个Linux进程都一定有一个唯一的数字标识符,称为进程ID(process ID)。进程ID总是一非负整数,getpid函数可得到本进程的进程ID号。
下面是getpid的函数范例,getpid.c源代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("pid=%d\n", getpid()) ;
printf("parent pid=%d\n", getppid()) ;
return 0 ;
}
编译 gcc getpid.c -o getpid。
执行./getpid,执行结果如下:
pid=6307
parent pid=5441
使用 ps -ef|grep 5441,得到如下结果:
zjkf 5441 5438 0 06:20 pts/0 00:00:00 -bash
其中getpid.c就是源程序,getpid执行码就是编译后的可执行程序,./getpid执行码执行的过程叫做进程。可见,不管源程序,还是编译后的可执行程序,都是静态的,而进程是动态的。getpid进程的父进程为Shell(bash)进程,Shell进程是所有在终端上执行进程的父进程。
3. 进程控制
用于进程控制的主要函数有fork、exec(exec函数有六种变体,但经常把它们统称为exec函数)和waitpid。
1.5 ANSI C
American National Standards Institute(ANSI——美国国家标准学会)是由公司、政府和其他成员组成的自愿组织。它们协商与标准有关的活动,审议美国国家标准,并努力提高美国在国际标准化组织中的地位。
由于美国在计算机早期发展中一直处于领先地位,因此ANSI的很多标准已经成为事实上的国际标准。其中常见的ANSI ASCII字符编码几乎为所有的编码方式所兼容。
标准的力量无处不在,标准是一种约定和约束,提供了技术目标说明与技术规范,标准的导向性和强制性加快了技术交流和提高了技术质量。
1. ANSI C
符合ANSI标准的C语言称为ANSI C,现在的C语言程序一般都符合ANSI C标准。
2. ANSI C函数原型
标准库函数的函数原型都在头文件中提供,程序可以用#include指令包含这些原型文件。对于用户自定义函数,程序员需要在头文件或文件头声明其原型。对于返回int型和void型且不带形参的C语言函数可以不声明其函数原型,但不鼓励这么用。
函数原型描述了函数到编译器的接口,编译程序在编译时就可以检查在调用函数时是否使用了正确的参数。
如getpid函数原型如下,其中pid_t是一个typedef定义类型,其定义为typedef int pid_t。
pid_t getpid ( void ) ;
调用带参数的getpid(如getpid(1)),则ANSI C程序将给出下列形式的出错信息:
line 8: too many arguments to function " getpid "
另外,因为编译程序知道参数的数据类型,所以如果可能,它就会将参数强制转换成所需的数据类型。
3. ANSI C类属指针
在标准ANSI中,read和write的第二个参数现在是void *类型,而早期的 Unix(Linux是Unix的后继者,站在Unix的肩膀上)系统都使用char *这种指针类型。
ANSI C使用void *作为类属指针来代替char *,这样函数原型和类属指针的组合消去了很多非ANSI C程序需要的显式类型强制转换。
例如,下面代码使用ANSI C标准,可以写成:
float data[100];
write(fd,data,sizeof(data)) ;
若使用非ANSI C编译程序,则需写成:
write( fd , (void *)data , sizeof(data)) ;
1.6 用户标识
1. 用户ID
在Linux系统,每个用户有一个唯一的用户ID号。
口令文件登录项中的用户ID(user ID)是个数值,它向系统标识各个不同的用户。系统管理员在确定一个用户登录名的同时,确定其用户ID,用户不能更改其用户ID,通常每个用户有一个唯一的用户ID。
用户ID为0的用户为根 (root)或超级用户 (superuser)。在口令文件中,通常有一个登录项,其登录名为root,root用户属于特权用户。如果一个进程具有超级用户特权,则大多数文件许可权检查都不再进行。某些操作系统功能只限于向超级用户提供,超级用户对系统有自由的支配权。
2. 组ID
在Linux系统,每个用户有一个唯一的组ID号。
口令文件登录项也包括用户的组 ID(group ID),它也是一个数值。组 ID也是由系统管理员在确定用户登录名时分配的。
组文件将组名映射为数字组 ID,它通常是/etc/group。
一个用户可以属于多个组,但只能属于一个主组,非主组又称为添加组或附属组。
getuid.c源代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("uid=%d, gid=%d\n", getuid(), getgid()) ;
return 0 ;
}
编译 gcc getuid.c -o getuid。
执行./getuid,执行结果如下:
uid=1008, gid=1003
1.7 出错处理
当Linux函数出错时,经常会给整型变量errno设置一个值,error中每个值都表示特定的含义。
文件<errno.h>中定义了变量errno以及可以赋予它的各种常数宏定义,这些常数都以E开头,如errno等于常数EACCES时,表示“此进程没有打开该文件的权限”。
对于errno有两条规则:第一条规则是如果没有出错,则error的值不会被重设;因此,仅当函数的返回值指明出错时,才检验其值。第二条是任一函数都不会将errno值设置为0,在<errno.h>中定义的所有常数都不为0。
C标准定义了两个函数,即strerror和perror,它们帮助打印出错信息,这两个函数的函数原型如下:
# include <string.h>
char *strerror(int errnum);
此函数将errnum(它通常就是errno值)映射为一个出错信息字符串,并且返回此字符串的指针。
perror函数在标准出错上产生一条出错消息(基于errno当前值),然后返回。
# include <stdio.h>
void perror( const char *msg);
它首先输出由msg指向的字符串,然后是一个冒号、一个空格、然后是对应errno值的出错信息,最后是一个换行符。
下面以两个范例来说明strerror和perror的使用。
strerror.c源代码如下,此代码打印出0到131的错误原因描述。
#include <stdio.h>
#include <string.h>
int main()
{
int i;
for(i=0;i<132;i++)
{
printf("%d : %s\n", i, strerror(i));
}
return 0 ;
}
编译 gcc strerror.c -o strerror。
执行./strerror,执行结果如下:
0 : Success
1 : Operation not permitted
2 : No such file or directory
……
perror.c源代码如下,此函数打印出错误原因信息字符串。
#include <stdio.h>
int main()
{
FILE *fp;
fp = fopen("/tmp/noexist", "r+") ;
if ( fp == NULL )
{
perror("fopen") ;
return -1 ;
}
return 0 ;
}
编译 gcc peror.c –o perror。
执行./perror,执行结果如下:
fopen: No such file or directory
摘录自《深入浅出Linux工具与编程》