UNIX环境高级编程 学习笔记 第一章 UNIX基础知识

所有OS都为它们所运行的程序提供服务,包括打开文件、执行新程序、分配存储区等。

操作系统可定义为一种软件,它控制计算机硬件资源,提供程序运行环境。通常将这种软件称为内核,它相对较小、位于系统核心。

内核接口被称为系统调用。公用函数库构建在系统调用接口之上,应用程序既可以使用函数库,也可以使用系统调用:
在这里插入图片描述
shell是一个特殊应用程序,为运行其他应用程序提供了一个接口。

广义上操作系统包括内核、系统实用程序、shell和函数库。如Linux是GNU的内核,GNU是一个自由的操作系统,其内容软件完全以GPL(是由自由软件基金会发行的用于计算机软件的协议证书,使用该证书的软件被称为自由软件)方式发布,其操作系统内核现在使用Linux、Free BSD等。

UNIX系统的口令文件(通常为/etc/password)中存放登录名等信息,每个登录项有七个冒号分隔的字段:登录名、加密口令(已被移动到另一文件)、数字用户ID、数字组、注释字段、家目录、shell程序:
在这里插入图片描述
登录后,系统会启动shell(登录项中最后一个字段),用户可以向shell输入命令。有些系统启动视窗管理程序,每个视窗中运行一个shell。shell命令来源于终端(交互式shell)或文件(shell脚本)。

UNIX中常见shell:
在这里插入图片描述
不同的Linux系统使用不同的系统默认shell。

目录是包含目录项的文件,每个目录项都包含一个文件名、文件的属性信息(如大小、普通文件还是目录文件、所有者、权限等)。stat和fstat函数返回包含所有文件属性的一个信息结构。

斜线/(分隔路径中的各文件名)和空字符(终止一个路径名)不能出现在文件名中。如文件名中使用了某些shell的特殊字符,就要用引号机制引用文件名。

创建目录时会创建两个目录文件,文件名分别为...。根目录中,...相同。

ls命令的简要实现:

#include "apue.h"    // 包含某些标准系统头文件、常量、函数原型
#include <dirent.h>    // 包含opendir、readdir函数、dirent结构体定义

int main(int argc, char *argv[]) {    // ISO C风格main函数声明
    DIR *dp;
    struct dirent *dirp;    // C中用法,声明类对象时加struct,C++中加不加都行

    if (argc != 2) {
        err_quit("usage: ls directory_name");    // 自编函数,用于处理错误
    }
    
    if ((dp = opendir(argv[1])) == NULL) {    // 第一个参数是要ls的目录,打开它,opendir返回指向DIR结构的指针
        err_sys("can not open %s", argv[1]);    // 自编函数,用于处理错误
    }

    while ((dirp = readdir(dp) != NULL) {    // 使用readdir循环读取目录中的目录项,返回指向dirent的指针
        printf("%s\n", dirp->d_name);    // 根据d_name还可调用stat函数获取文件的所有属性
    }
    
    closedir(dp);
    exit(0);    // 0正常结束,1~255表示出错
}

使用gcc编译器编译以上代码(历史上,cc是C编译器;在配置了GNU C的系统中,C编译器是gcc,其中,cc通常链接到gcc):

gcc -l stdc++ myls.cpp    # 默认生成a.out可执行文件,需使用-l选项手动链接C++库,后缀名.cpp表示C++程序,.c表示C程序

系统手册一般是电子文档形式,查看ls命令手册页:

man ls    

每个进程都有一个工作目录,也称为当前工作目录,进程可以用chdir改变其工作目录。登录后,工作目录被设为起始目录(home),可从/etc/password的条目中获得。

文件描述符是一个非负整数,内核用以标识一个进程正在访问的文件。内核打开或创建一个文件时,都会返回一个文件描述符,用以读写这个文件。

任何程序运行时,shell都会打开三个文件描述符,标准输入、标准输出和标准错误。一般它们都会链接向终端。可以将其中一个或几个重定向到某个文件:

ls > file.list

上例将命令ls的标准输出的内容重定向到文件。

函数open、read、write、lseek、close提供了不带缓冲的IO,它们使用文件描述符。

无缓存IO操作数据流向路径:数据—内核缓存区—磁盘。
标准IO操作数据流向路径:数据—流缓存区—内核缓存区—磁盘。

不带缓存的I/O对文件描述符操作,带缓存的I/O是针对流的。

将标准输入复制到标准输出:

#include "apue.h"    // 包含头文件<unistd.h>,其中包含很多UNIX系统服务的函数原型

#define BUFFSIZE 4096

int main() {
    int n;
    char buf[BUFFSIZE];

    while ((n = read(STDIN_FILENO, buf, BUFFSIZE) > 0) {    // read返回读取的字节数,如发生读错误,大多数系统返回-1;STDIN_FILENO所在头文件为unistd.h,类型为int
        if (write(STDOUT_FILENO, buf, n) != n) {    // 将n用作要写入的字节数
            err_sys("write error");
        }
    }
    if (n < 0) {
        err_sys("read error");
    }
    
    exit(0);
}

STDIN_FILENO和STDOUT_FILENO是POSIX标准的一部分,它们指向了标准输入和标准输出的文件描述符。

编译后执行文件:

./a.out > data    # 将标准输出重定向到文件data,输入时用文件结束符(通常为Ctrl+D)终止本次复制
./a.out < infile > outfile    # 会将infile中的内容复制到名为outfile的文件中

标准IO为不带缓冲的IO函数提供了一个带缓冲的接口。标准IO无需担心如何选取最佳缓冲区大小,即上例的BUFFSIZE常量大小。标准IO函数还简化了对输入行的处理,可用fgets函数读取完整的行,而read函数读取的是指定字节数。

C中的标准IO函数原型包含在头文件stdio.h中。

使用标准IO改写上例程序:

#include "apue.h"    // 包含头文件stdio.h

int main() {
    int c;
    while ((c = getc(stdin)) != EOF) {    // getc一次读取一个字符,读到输入的最后一个字节时,返回常量EOF,该常量定义在头文件stdio.h中
        if (putc(c, stdout) == EOF) {    // putc一次将一个字符写到标准输出,成功返回写入字符的ASCII值,失败返回EOF,返回值类型为int
            err_sys("output error");
        }
    }
    
    if (ferror(stdin)) {    // 输入输出函数(如putc、getc、fread、fwrite等)出错时,ferror函数返回非0错误值,每次调用都产生一个新错误值,会覆盖旧错误值
        err_sys("input error");
    }
    
    exit(0);
}

stdin和stdout在头文件stdio.h中定义,分别表示标准输入和标准输出。

程序是存储在磁盘上的可执行文件。内核使用exec函数(7个exec函数之一)将程序读入内存,并执行程序。

程序的执行实例被称为进程,UNIX确保每个进程都有唯一的数字标识符,称为进程ID,它是一个非负整数。

打印进程ID:

#include "apue.h"    // 包含unistd.h头文件

int main() {
    printf("hello world from process ID %ld\n", (long)getpid());    // getpid函数位于头文件unistd.h,获取进程ID,返回类型为pid_t,标准保证它能保存在长整型中;%ld表示long int
    exit(0);
}

主要的控制进程的函数:fork、exec、waitpid,exec函数有7种变体,但统称为exec函数。

从标准输入读取命令并执行:

#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#define MAXLINE 10 

int main() {
    char buf[MAXLINE];    
    pid_t pid;    // pid_t类型所在头文件为unistd.h
    int status = 0;

    printf("%% ");    // 打印提示符
    while (fgets(buf, MAXLINE, stdin) != NULL) {    // fgets从标准输入一次读取一行,返回的每一行由换行符终止,后随一个空字符;当输入行的第一个字符为文件结束符时,返回一个NULL指针
        if (buf[strlen(buf) - 1] == '\n') {    // 将每行的最后一个字符由换行符改为空字符,实际后面还有一个空字符
                                               // strlen返回不带'\0'的串长度;这么做是由于函数execlp要求输入的参数以空字符结束
            buf[strlen(buf) - 1] = 0;    // '\0'的值为0
        }
        
        if ((pid = fork()) < 0) {    // fork创建一个新进程(子进程),fork对父进程返回新的子进程的进程ID,对子进程返回0
            printf("fork error");
        } else if (pid == 0) {    // 在子进程中
            execlp(buf, buf, (char *)0);    // 执行从标准输入读入的命令,这就用新的程序文件替换了子进程原先执行的程序文件,如execlp执行成功,则子进程在此终止
                                            // execlp的第一个参数是要执行的程序,第二个参数是参数0(一般是程序名)
                                            // fork+exec就是某些系统所称的spawn(spawn是进入expect环境后才可以执行的expect内部命令,能够代替我们实现与终端的交互,我们不必再守候在电脑旁边输入密码,或是根据系统的输出再运行相应的命令)一个新进程
            printf("could not execute: %s", buf);
            exit(127);
        }

        if ((pid = waitpid(pid, &status, 0)) < 0) {    // 父进程希望等待子进程执行结束,waitpid指出希望等待的进程ID,返回子进程终止状态(存入status变量),此程序中我们没有使用终止状态
            printf("waitpid error");
        }
        printf("status : %d\n", &status);    // 输出子进程终止状态
        printf("%% ");
    }
    return 0;
}

执行结果:
在这里插入图片描述
以上程序的限制是不能向执行的命令传递参数,如只能ls,而不能ls后跟指定文件夹。为传递参数,需分析输入行,找出参数和命令,传给execlp函数。

通常,一个进程只有一个线程。对于某些问题,如果有多个控制线程分别作用于问题的不同部分,解决起来就容易得多。多个控制线程也可以充分利用多处理器系统的并行能力。

一个进程内所有线程共享同一地址空间、文件描述符、栈及与进程相关的属性。由于它们能访问同一存储区, 因此访问共享数据时需要采取同步措施避免不一致性。

线程ID只在它所属的进程中生效。

UNIX系统函数出错时,常返回一个负值,整型变量errno常被设置为有特定信息的值。而有些函数通过另一种方式表示出错,如返回对象指针的函数,大多在出错时返回一个空指针。

头文件errno.h中定义了整型变量errno以及可以赋予它的各种常量,这种常量以E打头。Linux中,出错常量在errno手册页中列出:

man errno

ISO C和POSIX(可移植操作系统接口)将errno定义为一个符号,它扩展成为一个可修改左值,以前的定义是:

extern int errno;

但现在支持线程后,多个线程共享进程地址空间,每个线程都有它的局部errno以避免一个线程干扰另一个线程。linux支持多线程存取errno,将其定义为:

extern int *__errno_location(void);    // 返回一个int指针
#define errno (*__errno_location())    // errno是该函数返回的指针所指的int

如没有出错,errno值不会被例程清除,因此,仅当函数的返回值指明已经出错时,才检验其值;任何函数都不会将errno设为0,而且在errno.h中定义的所有常量都不为0。

C标准定义了两个函数用于打印出错信息:

#include <string.h>

char *strerror(int errnum);    // 接受errno值,将其映射为出错消息,并返回出错消息字符串
#include <stdio.h>

void perror(const char *msg);    // 基于当前errno的值,在标准错误上输出出错消息,消息组成为msg指向的字符串,之后是冒号空格,再接对应于errno值的出错消息,最后是换行符

使用出错函数:

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    fprintf(stderr, "EACCES: %s\n", strerror(EACCES));    // 函数fprintf在头文件stdio.h中,函数strerror在头文件string.h中
    errno = ENOENT;
    perror(argv[0]);    // 在头文件stdio.h中
    exit(0);    // 在头文件stdlib.h中
}

函数fprintf作用为格式化输出,第一个参数为FILE类型对象的指针,stderr(定义在stdio.h中)是标准错误;第二个参数是要输出的内容,可被附加参数列表中值所替换。

以上代码执行结果:
在这里插入图片描述

我们将argv[0]传给perror,这是一个标准的UNIX惯例,这样在程序作为管道一部分执行时可以知道是哪个程序输出的错误信息:

prog1 < inputfile | prog2 | prog3 > outputfile

errno.h中定义的出错分为致命性的和非致命性的。致命性错误无法执行恢复操作,最多在屏幕上打印一条出错信息或将出错消息写入日志,然后退出。非致命性出错可以处理,大多非致命性错误是暂时的,当系统活动少时不会出现,如资源短缺(EAGAIN、ENFILE、ENOBUFS、ENOLCK、ENOSPC、EWOULDBLOCK),有时ENOMEM也是非致命性出错,当EBUSY指明共享资源正在使用时,也可以被认为是非致命性错误。EINTR中断一个慢速系统调用时可将它作为非致命性出错处理。

资源相关的非致命性错误的典型恢复操作是每隔一段时间重试,如网络连接中断。一些应用使用指数补偿算法,每次迭代时等待更长时间。

应由应用的开发者决定哪些情况下应用可以从出错中恢复。

口令文件(/etc/password)登录项中用户ID是一个数值,向系统标识不同用户。用户不能更改用户ID,通常每个用户有一个唯一的用户ID。

根用户(又称超级用户、root用户)的用户ID为0,它的特权被称为超级用户特权。如一个进程有超级用户特权,则大多数文件权限检查都不再进行。某些操作只向root用户提供。

Mac OS X客户端版本禁用root,而服务器版本可以使用它。

口令文件中也有用户的组ID,它是在确定用户登录名时分配的。同组各个成员间可以共享资源(如文件)。

组文件(/etc/group)将组名映射为数值的组ID。

使用数值的用户ID和组ID管理权限是历史原因,磁盘上的每个文件都存有该文件所有者的用户ID和组ID,存储这两个值只需4字节(假设每个以双字节整型值存放),如使用ASCII登录名和组名,需要更多磁盘空间,并且在权限检验时,字符串比整型更耗时间。

但对于用户,使用名字比数值方便。ls -l命令用口令文件中的映射将用户ID映射为登录名,有时,删除用户时不用-r选项就不会删除其home目录,此时其中文件也存在,若再创建一个用户,它的用户ID与已删除的用户ID相同时,再调用ls -l会发现已删除用户的文件的所有者变成了新用户。

获取用户id和组id:

#include <iostream>
#include <sys/types.h>
using namespace std;

int main() {
    cout << "uid: " << getuid() << endl << "gid: " << getgid() << endl;    // 这两个行数定义在头文件sys/types.h中
}

用户还可以属于除了口令文件登录项中组ID之外的其他附属组。从4.2BSD开始,允许用户最多属于16个其他的组。登录时,从文件/etc/group中寻找列有该用户作为其成员的前16个记录项作为该用户的附属组。POSIX要求至少应支持8个附属组,实际上大多支持16个。

信号用于通知进程发生了某种情况,如除法时除了0,则将名为SIGFPE(浮点异常)的信号发送给进程。三种处理信号方式:
1.忽略信号,但不推荐用于处理指示硬件异常的信号,如除0或访问进程地址空间以外的存储单元,这些异常产生的后果不确定。
2.按系统默认的方式处理,对于除0,系统默认终止进程。
3.提供函数,信号发生时调用函数,这称为信号捕捉。

终端键盘上产生信号的方式:
1.中断键,Delete或Ctrl+C。
2.退出键,Ctrl+\。
它们用于中断当前运行的进程。kill函数也能产生信号,在一个进程中调用它可向另一个进程发送信号,但我们必须是另一个进程的所有者或超级用户。

调用程序时如使用中断键,则执行此程序的进程终止,这是因为对于此信号(SIGINT)的系统默认动作是终止进程,该进程没有告诉内核应如何处理此信号,所以系统按默认方式终止该进程。

捕捉信号需要signal函数:

#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#define MAXLINE 10 

static void sig_int(int signo) {
    printf("interrupt\n%% ");     // 打印一句话来处理错误
}

int main() {
    char buf[MAXLINE];    
    pid_t pid;
    int status = 0;

    if (signal(SIGINT, sig_int) == SIG_ERR) {    // 当信号是SIGINT时,调用sig_int
        printf("signal error");
    }

    printf("%% ");    // 打印提示符
    while (fgets(buf, MAXLINE, stdin) != NULL) {    // fgets从标准输入一次读取一行,返回的每一行由换行符终止,后随一个空字符;当输入行的第一个字符为文件结束符时,返回一个NULL指针
        if (buf[strlen(buf) - 1] == '\n') {    // 将每行的最后一个字符由换行符改为空字符,实际后面还有一个空字符;strlen返回不带'\0'的串长度;这么做是由于函数execlp要求输入的参数以空字符结束
            buf[strlen(buf) - 1] = 0;    // '\0'的值为0
        }
        
        if ((pid = fork()) < 0) {    // fork创建一个新进程(子进程),fork对父进程返回新的子进程的进程ID,对子进程返回0
            printf("fork error");
            exit(127);
        } else if (pid == 0) {    // 在子进程中
            execlp(buf, buf, (char *)0);    // 执行从标准输入读入的命令,这就用新的程序文件替换了子进程原先执行的程序文件;fork+exec就是某些系统所称的spawn一个新进程
            printf("could not execute: %s", buf);
            exit(127);
        }

        if ((pid = waitpid(pid, &status, 0)) < 0) {    // 父进程希望等待子进程执行结束,waitpid指出希望等待的进程ID,返回子进程终止状态(status变量),此程序中我们没有使用它
            printf("waitpid error");
        }
        printf("status : %d\n", &status);
        printf("%% ");
    }
    return 0;
}

UNIX系统使用过两种时间值:
1.日历时间,是自协调世界时1970/1/1 00:00:00以来经过的秒值,这个时间可用于记录文件最近一次的修改时间,系统基本数据类型time_t用于保存这种时间值。
2.进程时间,也称CPU时间,用来度量进程使用的CPU资源。每秒曾取为50、60或100个时钟周期值。系统基本数据类型clock_t保存它。

度量一个进程的执行时间时,UNIX为一个进程维护了3个进程时间值:
1.时钟时间,又称墙上时钟时间,它是进程运行的时间总量(包括进程在阻塞和等待状态的时间),其值与系统中同时运行的进程数有关,因为系统中进程数过多会引起资源的抢占等。
2.系统CPU时间,指为该进程执行内核程序经历的时间,如进程进入系统服务时,如read、write。
3.用户CPU时间,指用户指令执行所用时间。

以上的2、3两项统称为CPU时间。

使用time命令获取进程的执行时间:
在这里插入图片描述
time的-p选项指明以POSIX缺省的时间格式打印时间统计结果,单位为秒。time的输出格式与使用的shell有关,原因是某些shell不运行/usr/bin/time,而是使用一个内置函数测量程序运行时间。

程序可用操作系统提供的、向内核请求服务的入口点,这些入口点被称为系统调用。

UNIX是为每个系统调用在标准C库中设置一个具有同样名字的函数,用户进程用标准C调用序列来调用这些函数,之后函数又用系统要求的技术调用内核服务。

有些库函数可能会使用系统调用,如printf函数调用write系统调用输出字符串。

系统调用和库函数在使用者看来区别不大,但实现者看来区别很大。

存储空间分配函数malloc有多种方法进行存储空间分配和回收操作,对于UNIX系统,malloc底层调用的是sbrk系统调用分配空间,如果我们不喜欢malloc的空间分配方式,可以定义自己的malloc函数。malloc与sbrk关系:
在这里插入图片描述
另一个库函数和系统调用关系的例子:UNIX只提供一个返回自协调世界时以来过的秒数的系统调用,但其他库函数可以使用它进行不同的解读。

应用程序既可以调用系统调用,也可以调用库函数,很多库函数会调用系统调用:
在这里插入图片描述
系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值