作者andylin02学习章节第1章 UNIX 基础知识关键词 Unix体系结构、系统调用、登录流程、文件描述符、程序与进程、错误处理、errno、用户标识、进程ID、信号、日历时间、进程时间一、第1章导读Unix编程世界的“入门地图”第1章《UNIX基础知识》是整本书的总纲和入门基石。它不像后续章节那样深入某个具体的系统调用而是为读者描绘了一幅Unix系统的全景图操作系统是如何组织的用户如何登录程序如何执行文件如何读写进程是什么错误如何记录这一章的学习目标是建立Unix环境编程的“世界观”。虽然大部分内容看似“常识”但书中揭示的许多细节如系统调用与库函数的区别、内核数据结构、错误处理函数errno的用法等是后续章节频繁使用的基础概念。掌握本章你将获得一张清晰的Unix知识地图为深入学习系统编程打下坚实基础。二、Unix体系结构从用户到硬件的四层模型Unix系统采用严格的分层架构从内向外依次是内核Kernel、系统调用System Call、Shell与公用函数库Library Routines、应用软件Application Software。层次组件说明举例第1层最内内核Kernel直接管理硬件提供进程、内存、文件系统、网络等服务是整个操作系统的核心。在严格意义上操作系统可以定义为控制计算机硬件资源并为程序提供运行环境的软件我们通常称这个软件为内核Linux内核、FreeBSD内核第2层系统调用System Call内核暴露给应用程序的接口是进入内核的唯一入口。应用程序不能直接访问内核空间必须通过系统调用“陷入”内核。不同Unix变体提供的系统调用数量不同Linux 3.2.0提供约380个FreeBSD 8.0提供超过450个open,read,write,fork,exec第3层Shell与公用函数库Shell是一个特殊的应用程序为运行其他应用程序提供了一个接口。公用函数库构建在系统调用接口之上可能封装系统调用也可能纯计算printf(调用write),strlen(无系统调用),malloc(调用sbrk)第4层最外应用软件用户直接使用的程序既可以使用公用函数库也可直接使用系统调用Shell、编辑器、数据库、浏览器2.2 系统调用与库函数的区别系统调用是操作系统内核提供的服务入口而库函数是用户空间提供的功能封装。两者都是C函数的形式但从实现角度看有根本区别对比维度系统调用库函数提供者操作系统内核标准C库如glibc执行空间内核态需要特权级别切换用户态CPU不切换特权级是否能被替换通常不能被替换可以自定义实现例如自定义malloc举例sbrk扩展进程堆、write、readmalloc内存分配、printf格式化输出三、登录过程从启动到Shell的完整旅程3.1 终端登录Terminal LoginsUnix系统的登录流程是理解进程关系的重要起点。整个登录过程由init进程PID为1驱动以下是BSD风格的终端登录流程3.2 登录过程的详细步骤系统启动后内核创建init进程PID为1。init进程负责引导系统启动它读取配置文件/etc/ttys并为每一个登录终端fork一个进程。getty程序运行由initfork创建的进程调用exec执行getty程序。getty对终端设备调用open函数以读、写方式将终端打开。一旦设备被打开文件描述符0、1、2就被设置到该设备。然后getty输出login:之类的信息并等待用户键入用户名。用户输入用户名用户键入用户名后getty的工作就完成了它通过调用exec执行login程序并将用户名作为参数传递。login验证login程序根据输入的用户名调用getpwnam获取对应的密码然后调用getpass打印Password:提示等待用户输入密码。login对输入的密码进行加密并与从/etc/shadow中获取的加密口令进行对比。密码错误处理如果密码不同login程序调用exit退出init进程得到进程终止状态后会再次执行fork进行登录重试。密码正确login程序完成以下工作切换到用户主目录chdir改变终端设备的所有权和权限设置组IDsetgid和initgroups初始化环境变量改变用户IDsetuid以激活登录Shell。Shell启动登录Shell启动后会读取启动文件.profile或.bash_profile这些文件用于设置系统环境变量和全局配置。四、文件和目录Unix的“一切皆文件”4.1 文件系统基础Unix文件系统是目录和文件的一种层次结构所有东西的起点都是称为根目录的/。目录是包含目录项的文件每个目录项都包含一个文件名和文件属性等信息。文件名规则文件名中不能出现的两个字符斜线/和空字符\0POSIX.1推荐使用字符集字母a-z、A-Z、数字0-9、句点.、短横线-和下划线_每个目录自动包含两个特殊文件名.当前目录和..父目录路径名类型绝对路径名Absolute Pathname以斜线/开头的路径名从根目录开始定位文件相对路径名Relative Pathname不以斜线开头的路径名从当前工作目录开始解释4.2 文件描述符文件描述符是内核用于标识一个特定进程正在访问的文件的小的非负整数。每当运行一个新程序时所有的shell都为其打开三个文件描述符文件描述符名称常量默认设备0标准输入stdinSTDIN_FILENO键盘1标准输出stdoutSTDOUT_FILENO终端2标准错误stderrSTDERR_FILENO终端这三个文件描述符都可以通过shell的、、2等操作符重定向到文件或管道。4.3 口令文件/etc/passwd登录项由7个冒号分隔的字段组成依次是登录名:加密口令:用户ID:组ID:注释字段:起始目录:shell程序。由于安全原因现代系统已将加密口令移至/etc/shadow文件中。五、输入与输出两种I/O方式5.1 不带缓冲的I/OUnbuffered I/O不带缓冲的I/O直接调用内核提供的系统调用包括open、read、write、lseek、close等函数这些函数都使用文件描述符。示例程序将标准输入复制到标准输出copy_stdio.c#include apue.h #define BUFFSIZE 4096 int main(void) { int n; char buf[BUFFSIZE]; 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); }程序说明使用read从标准输入读取数据使用write将数据写入标准输出。程序读取BUFFSIZE字节的数据块但实际读取的字节数n可能小于请求的字节数例如当读到文件末尾或从管道/终端读取时。5.2 标准I/OStandard I/O标准I/O为不带缓存的I/O函数提供了一个带缓冲的接口使用标准I/O时无需担心如何选择最佳缓冲区大小。示例程序将标准输入复制到标准输出copy_stdio2.c#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); }程序说明使用getc一次读取一个字符使用putc一次写入一个字符。这种方法虽然每次只处理一个字符但由于标准I/O库内部使用缓冲实际性能并不差。六、程序与进程6.1 程序与进程的区别概念定义存储位置特点程序Program存储在磁盘上的可执行文件磁盘静态、被动内核使用exec函数将其读入内存并执行进程Process程序的执行实例内存动态、有生命周期每个进程都有一个唯一的数字标识符称为进程ID非负整数6.2 获取进程ID示例程序打印当前进程的进程IDgetpid.c#include apue.h int main(void) { printf(hello world from process ID %ld\n, (long)getpid()); exit(0); }运行输出示例hello world from process ID 189106.3 进程控制的主要函数Unix提供了三个主要的进程控制函数fork、exec系列和waitpid。fork用于创建新进程exec用于执行新程序waitpid用于父进程等待子进程结束。示例程序简单的Shell实现shell.c#include apue.h #include sys/wait.h int main(void) { char buf[MAXLINE]; pid_t pid; int status; printf(%% ); /* 打印提示符 */ while (fgets(buf, MAXLINE, stdin) ! NULL) { if (buf[strlen(buf) - 1] \n) buf[strlen(buf) - 1] 0; /* 替换换行符 */ if ((pid fork()) 0) { err_sys(fork error); } else if (pid 0) { /* 子进程 */ execlp(buf, buf, (char *)0); err_ret(could not execute: %s, buf); exit(127); } /* 父进程 */ if ((pid waitpid(pid, status, 0)) 0) err_sys(waitpid error); printf(%% ); } exit(0); }程序说明该程序实现了一个简单的Shell不断读取用户输入的命令行使用fork创建子进程子进程调用execlp执行命令父进程使用waitpid等待子进程结束。七、出错处理7.1 errno变量当Unix函数发生错误时通常会返回一个负值并且全局整型变量errno被设置为一个特定的错误码定义在errno.h。处理errno时必须注意两条规则立即检查仅当函数出错时errno的值才有意义必须在调用失败后立即检查因为后续函数可能覆盖它不会自动清零任何函数都不会将errno的值置为0调用成功时errno保持不变7.2 错误处理函数函数头文件功能char *strerror(int errnum);string.h将错误码映射为出错消息字符串并返回该字符串的指针void perror(const char *msg);stdio.h基于errno的当前值在标准错误上产生一条出错消息输出格式为“msg: 错误描述”7.3 错误处理示例示例程序演示strerror和perror的用法error_demo.c#include apue.h #include errno.h int main(int argc, char *argv[]) { /* 演示 strerror */ fprintf(stderr, EACCES: %s\n, strerror(EACCES)); /* 演示 perror */ errno ENOENT; perror(argv[0]); return 0; }运行输出示例EACCES: Permission denied ./a.out: No such file or directory此示例首先使用strerror将错误码EACCES转换为描述字符串“Permission denied”。然后将errno设置为ENOENT调用perror输出“./a.out: No such file or directory”。八、用户标识8.1 用户ID和组ID标识说明用户IDUser ID, UID在口令文件登录项中用于向系统标识各个不同用户的数值。用户ID为0的用户是根用户超级用户拥有系统最高权限组IDGroup ID, GID向系统标识各个不同用户组的数值同组的各个成员可以共享资源相关函数#include unistd.h uid_t getuid(void); /* 获取实际用户ID */ uid_t geteuid(void); /* 获取有效用户ID用于权限检查 */ gid_t getgid(void); /* 获取实际组ID */ gid_t getegid(void); /* 获取有效组ID */8.2 相关文件组文件/etc/group将组名映射为数值的组ID包含4个字段组名称:组密码:组ID:该组用户列表以逗号分隔使用id命令可以显示当前用户的有效和实际UID及GID九、信号9.1 信号的基本概念信号Signal是一种软件中断用于通知进程发生了某个事件如用户按CtrlC、除零错误、定时器超时等。进程对信号的处理方式有三种忽略信号SIGKILL和SIGSTOP不可忽略执行系统默认动作通常是终止进程捕获信号执行用户自定义的信号处理函数9.2 如何产生信号触发方式说明终端按键如CtrlC产生SIGINTCtrl\产生SIGQUIT硬件异常如除零错误产生SIGFPE无效内存引用产生SIGSEGV函数调用kill(2)或raise(3)函数可以发送信号软件条件如定时器到期产生SIGALRM子进程结束产生SIGCHLD十、时间值10.1 两种时间值类型说明数据类型获取函数日历时间Calendar Time自协调世界时UTC1970年1月1日00:00:00以来所经过的秒数累积值可用于记录文件最近一次的修改时间等time_ttime()进程时间Process TimeCPU时间用以度量进程使用的CPU资源以时钟滴答计算Linux 3.2.0每秒100个滴答clock_ttimes(),clock()10.2 三个进程时间值Unix系统为一个进程维护3个进程时间值时间值说明公式时钟时间Clock Time墙上时钟时间进程运行的时间总量包括阻塞和就绪时间与系统中同时运行的进程数有关时钟时间 用户CPU时间 系统CPU时间 阻塞时间 就绪时间用户CPU时间User CPU Time执行用户指令所用的时间量—系统CPU时间System CPU Time为该进程执行内核程序所经历的时间—10.3 使用time命令测量时间可以使用time(1)命令测量命令执行的时间例如测量grep命令的耗时$ cd /usr/include/ $ time -p grep -R _POSIX_SOURCE /dev/null real 0.07 user 0.03 sys 0.03十一、第1章知识点速查表知识点核心内容Unix体系结构内核 → 系统调用 → 公用函数库 → 应用软件系统调用 vs 库函数系统调用进入内核库函数可能在用户空间执行malloc可替换但sbrk不可替换登录过程init → getty → login → shell文件描述符0stdin, 1stdout, 2stderr可通过重定向改变程序 vs 进程静态文件 vs 动态执行实例进程有唯一PID进程控制函数fork,exec系列,waitpid错误处理errnoperror()/strerror()用户标识UID, GID超级用户UID0信号软件中断三种处理方式时间值日历时间time_t与进程时间clock_t十二、动手实践建议12.1 环境配置下载APUE源码从官网 http://www.apuebook.com/code3e.html 下载src.3e.tar.gz安装依赖库sudo apt-get install libbsd-dev编译cd apue.3e make安装头文件和库将include/apue.h复制到/usr/include/将lib/libapue.a复制到/usr/lib12.2 验证系统限制#include stdio.h #include unistd.h #include errno.h int main() { printf(Maximum number of open files: %ld\n, sysconf(_SC_OPEN_MAX)); printf(Maximum number of clock ticks per second: %ld\n, sysconf(_SC_CLK_TCK)); return 0; }12.3 观察进程信息# 查看当前shell的PID echo $$ # 查看进程详细信息 ps -p $$ -o pid,ppid,uid,stat,command # 查看所有进程 ps aux十三、常见易混淆点Q1系统调用和库函数有什么区别A系统调用是内核提供的服务入口需要陷入内核态执行通常不能被替换库函数是用户空间的封装可能调用系统调用也可能不调用如strlen完全不涉及系统调用且库函数可以被自定义实现替换。Q2errno何时被设置A仅当系统调用或某些库函数失败时设置。成功时不会清零因此必须在调用失败后立即检查。Q3getpid()返回的进程ID类型是什么A返回pid_t类型在POSIX标准中是有符号整数类型使用时建议转换为long类型以避免格式符匹配问题。Q4如何确保程序的时间测量准确A使用clock()测量CPU时间使用gettimeofday()测量墙上时钟时间。测量时应多次运行取平均值并尽量减少其他系统负载的干扰。十四、学习心得第1章是全书的总纲内容虽浅却为后续章节奠定了概念基础。重点掌握以下几点系统调用是Unix编程的核心任何涉及资源操作文件、进程、网络最终都要通过系统调用错误处理必须养成习惯生产级代码中每个系统调用都应检查返回值并处理错误区分真实ID与有效ID这是理解权限提升如setuid程序的关键信号是异步的编写信号处理函数时必须小心可重入问题第10章会深入掌握本章的知识后你已经获得了Unix环境编程的“入门地图”。下一章将进入第2章“Unix标准化及实现”探讨不同Unix系统之间的差异以及如何编写可移植代码。十五、下一篇预告下一篇将进入第2章“Unix标准化及实现”内容包括ISO C、POSIX、Single UNIX Specification三大标准的演进历程不同Unix实现Linux、macOS、Solaris、FreeBSD的差异系统限制的运行时获取方式sysconf、pathconf、fpathconf编译时限制与运行时限制的区别基本系统数据类型pid_t、off_t、size_t等的可移植性意义敬请期待本文为个人学习笔记仅用于知识分享。如有错误欢迎指正。点赞 收藏 分享让更多开发者看到这篇深度解析❤️ 如果觉得有用请给个赞支持一下作者