1.2 UNIX体系结构
1、体系结构:
(1)内核 (2)系统调用 (3)Shell、公共函数库 (4)应用程序
1.4 文件和目录
1、例:ls(l)命令的简要实现
#include "apue.h"
#include
int main(int argc,char * argv[])
{
DIR *dp;
struct dirent *dirp;
if(argc!=2)
err_quit("Usages: ls directory_name");
if((dp=opendir(argv[1])==NULL))
err_sys("can't open %s,argv[1]");
while((dirp=readdir(dp))!=NULL)
printf("%s\n",dirp->d_name);
closedir(dp);
exit(0);
}
(1)系统头文件dirent.h,以便使用opendir和readdir的函数原型,以及dirent结构的定义。
(2)opendir()函数返回指向DIR结构的指针,我们将该指针传送给readdir来读每个目录项。当目录已无目录项可读时则返回null指针。
(3)函数exit终止程序,参数0为正常结束,参数值1~255表示出错
1.5 输入和输出
1、文件描述符(file descriptor):通常是一个小的非负整数,内核用以标识特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符,在读、写文件时,可以使用这个文件描述符。
2、每当运行一个新程序时,所有的shell都为其打开3个文件描述符,即标准输入(standard input)、标准输出(standard output)以及标准错误(standard error)。
3、不带缓冲的I/O
(1)函数open、read、write、lseek以及close提供了不带缓冲I/O。这些函数都使用文件描述符。
4、如果愿意从标准输入中读,并向标准输出写,则下列程序可用于复制任一UNIX普通文件。
//查看STDIN_FILENO和STDOUT_FILENO值
#define STDIN_FILENO 0
/* standard input file descriptor */
#define STDOUT_FILENO 1
/* standard output file descriptor */
#define STDERR_FILENO 2
/* standard error file descriptor */
#include "apue.h"
#define BUFFSIZE 4096
int main(void)
{
int n;
char buf[BUFFISZE];
while((n=read(STDIN_FILENO,buf,BUFFSIZE))>0)
if(write(STDOUT_FILENO,buf,n)!=n)
err_sys("write error");
if(n<0)
err_sys("read error");
exit(0);
}
(1)头文件unistd.h(apue.h中包含了此头文件)及两个常量STDIN_FILENO和STDOUT_FILENO是POSIX标准的一部分。头文件unistd.h包含了很多UNIX系统服务的函数原型,包括read和write。
(2)两个常量STDIN_FILENO和STDOUT_FILENO定义unistd.h头文件中,它们指定了标准输入和标准输出的文件描述符。在POSIX标准中,它们的值分别是0和1,但是考虑到可读性,我们将使用这些名字代表这些变量。
(3)read函数返回读取字节数,此之用作要写的字节数。当到达输入文件的尾端时,read返回0,程序停止执行。如果发生了一个读错误,read返回-1.出错时大多数系统函数返回-1。
5、标准I/O
(1)标准I/O库函数为那些不带缓冲的I/O函数提供了一个带缓冲的接口。使用标准I/O函数无需担心如何选取最佳的缓冲区大小。
(2)使用标准I/O函数还简化了对输入行的处理,如fgets函数读取一个完整的行,而read函数读取指定字节数。
(2)例:将标准输入复制到标准输出,也就是能复制任一UNIX普通文件。
#include "apue.h"
int main(void)
{
int c;
while((c=getc(stdin))!=EOF)
if(putc(c,stdout)==EOF)
err_sys("output error");
if(ferror(stdin))
err_sys("input error");
exit(0);
}
1.6 程序和进程
1、程序
(1)程序(program)是一个存储在磁盘上某个目录中的可执行文件。内核使用exec函数(7个exec函数之一),将程序读入内存,并执行程序。
2、进程和进程ID
(1)程序执行的实例被成为进程(process)/任务(task)。
(2)UNIX系统确保每个进程都有一个唯一的数字标识符,将进程ID(process ID)。进程ID总是一个非负整数。
例:用于打印进程的ID。
#include "apue.h"
int main(void)
{
printf("hello world from process ID %ld\n",
(long)getpid());
exit(0);
}
//使用man 2 getpid()查看getpid()函数原型
(1)此程序运行时,它调用函数getpid得到其进程ID。
(2)getpid返回一个pid_t数据类型。我们不知道它的大小,仅知道是标准会保证它在一个长整型中。
(3)虽然大多数进程ID可以用整型表示,但用长整型可以提高移植性。
3、进程控制
(1)有3个用于进程控制的主要函数:fork、exec和waitpid。(exec函数有7种变体,但经常把它们统称为exec函数。)
(2)例:该程序从标准输入读取命令,然后执行这些命令。
#include "apue.h"
#include
int main(void)
{
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
printf("%% ");
/* print prompt (printf requires %% to print %) */
while(fgets(buf,MAXLINE,stdin)!=NULL){
if(buf[strlen(buf)-1]=='\n')
buf[strlen(buf)-1]=0;
/* replace newline with null */
if ((pid==fork())<0){
err_sys("fork error");
}else if (pid==0) { /* child */
execlp(buf,buf,(char *)0);
exec_ret("could't execute: %s",buf);
}
/* parent */
if((pid=waitpid(pid,&status,0))<0)
err)sys("waitpid error");
printf("%% ");
}
exit(0);
}
1.调用fork创建一个新进程。fork对父进程返回一个新的子进程的进程ID(一个非负整数),对子进程则返回0。
2.在子进程中,调用execlp以执行从标准输入读入的命令。
3.子进程调用execlp执行新程序文件,而父进程希望等待子进程终止,这是通过调用waitpid实现的。
4.waitpid函数返回子进程的终止状态(status变量)。
4、线程和线程ID
(1)通常,一个进程只有一个控制线程(thread)——某一时刻执行的一组机器指令。
(2)一个进程内的所有线程共享同一地址空间、文件描述符、栈以及进程相关的属性。因为它们能访问同一存储区,所以各线程在访问共享数据需要采取同步措施以避免不一致性。
1.7 出错处理
1、当UNIX系统函数出错时候,通常会返回一个负值,而整型变量errno通常被设置为具有特定信息的值。
2、头文件errno.h中定义了errno以及可以赋予它的各种常量。
3、对于errno应当注意两条规则
(1)如果没有出错,其值不会被例程清楚。因此,仅当函数的返回之指明出错时,才检验其值。
(2)任何函数都不会将errno值设为0而且在errno.h中定义的所有常量都不为0。
4、C标准定义了两个函数,用于打印出错信息:
#include
char * strerror(int errnum);
//strerror函数将errnum(通常为errno值)映射为一个出错信息字符串,并返回此字符串的指针。
#include
void perror(const char *msg);
//perror函数基于errno的当前值,在标准错误上产生一条出错信息,并返回。首先输出由msg指向的字符串,然后是一个:,一个空格,接着对应errno值的出错信息,最后是一个换行符。
5、例:程序显示两个出错函数的使用方法
#include "apue.h"
#include
int main(int argc,char * argv[])
{
fprintf(stderr,"EACCES: %s\n",stderror(EACCES));
errno=ENOENT;
perror(argv[0]);
exit(0);
}
//man errno查看错误的类型
6、出错恢复
(1)致命性错误:无法执行恢复动作,最多就打印出一条错误信息或将错误信息写进日志文件。
(2)非致命性错误可妥善进行处理。
1.8 用户标识
1、例:用于打印用户ID组ID
#include "apue.h"
int main(void)
{
printf("uid = %d, gid = %d\n",getuid(),getgid());
exit(0);
}
//man getuid 查看getuid函数原型
//man getgid 查看getgid函数原型
1.9 信号
1、信号(signal)用于通知进程发生了某种情况。
2、进程有以下3种处理信号的方式:
(1)忽略信号
(2)按系统默认方式处理
(3)提供一个函数,信号发生时调用该函数,这被成为捕捉该信号
3、终端上的键盘有两重产生信号的方法,分别成为中断键(interrupt key,通常是Delete键或Ctrl+C)和退出键(quit key,通常是Ctrl+),它们被用于中断当前进程。
4、另一种产生信号的方法是调用kill函数。在一个进程中调用此函数就可向另一个进程发送信号。
5、限制:当向一个进程发送信号时,我们必须是进程的user或者root用户。
6、例:为了捕捉SIGINT信号,程序需要调用signal函数,其中指定了当产生SIGINT信号时要调用的函数的名字,函数名为sig_int。
#include "apue.h"
#include
static void sig_int(int); /* out signal-caching functions */
int main(void)
{
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
if(signal(SIGINT,sig_int)==SIG_ERR)
err_sys("signal error");
printf("%% "); /* format print */
while(fgets(buf,MAXLINE,stdin)!=NULL){
if(buf[strlen(buf)-1]=='\n')
buf[strlen(buf)-1]=0;
/* replace newline with null */
if((pid=fork())<0){
err_sys("fork error");
}else if(pid==0){ /* child */
execlp(buf,buf,(char *)0);
err_ret("couldn't execute: %s",buf);
exit(127);
}
/* parent */
if((pid=waitpid(pid,&status,0))<0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
void sig_int(int signo)
{
printf("interupt\n%% ");
}
1.10 时间值
1、UNIX系统使用过两种不同时间值
(1)日历时间,该值是子协调世界时(Coordinated Universal Time,UTC)1970年1月1日 00:00:00这个特定时间以来所经过的秒数累计值。
(系统基本数据类型time_t用于保存这种时间值)
(2)进程时间。也被称为CPU时间,以度量进程使用中央处理器资源。进程时间以始终滴答计算。
(系统基本数据类型clock_t)用于保存这种时间值
2、UNIX系统为一个进程维护了3个进程时间值:
(1)时钟时间
(2)用户CPU时间
(3)系统CPU时间
3、时钟时间又成为墙上时钟时间(wall clock time),它是进程运行的时间总量,其值与系统中同时运行的进程数有关。
4、用户CPU时间是指执行用户指令所用的时间量。
5、系统CPU时间是该进程执行内核程序所经历的时间
6、用户CPU时间和系统CPU时间之和被成为CPU时间
1.11 系统调用和库函数
1、所有的操作系统都提供多种服务的入口,这些入口被称为系统调用(system call)。
2、系统调用和库函数都是以C函数的形式出现,两者都为应用程序提供服务。
3、我们可以替换库函数,但系统调用通常是不能被替换的。
4、以malloc库函数为例,UNIX系统调用处理存储分配的是sbrk(2),sbrk在内核中分配一块空间给进程,而库函数malloc则在用户层次管理这一空间。
5、系统调用和库函数之间的另一个差别:系统调用通常提供最小的接口,而库函数提供比较复杂的功能。