《APUE》与 Stevens
Stevens 的书单可以在很多人的推荐学习书籍中发现,这似乎变成了一件十分正常的事。当我自己买到这些书的英文版时,我才发现那么多人推荐的这些书在内容上便极其庞大,这仅仅是个最初的印象。
我仍旧在学习这些书,从大学开始阅读这些书。时有终止,却总能再次拿起。垂头大嚼,其中的喜悦与难受可见一斑。以我的经历来谈,阅读这些书完全不像推荐给别人那样轻松,读不下去也很常见,即便看看书的厚度也会心有余悸。
《APUE》 我读完了 80% ,即便如此每次重新重读仍旧能够找到新的知识点,这一度让我怀疑自己的理解。有人说,这本书适合在用到某个 api 时再去翻阅,这一观点我并不完全赞成,但是我自己有时也在这样做着。如果以这种观点去看待 《APUE》 这本书,那么其实完全不需要花费太多的时间。更进一步讲其实完全不需要这样的一本书,unix-like 系统中自带的帮助文档就能轻松解决你的问题,当然,如果你在开发跨平台程序时,这本书可能是最好的参考书了。
如果单单以该书庞大的内容量来考虑,那么对于大部分人来说完全可以使用上述方式来进行学习,简单讲就是用到什么再去学相关的知识。这样的方式可以将你从大量的阅读与练习中解放出来,但长远来看,这也让你难以窥其全貌,难以从系统角度去思考你遇到的问题。
上述问题并不局限于此,只是对它的讨论暂且停止。
诚如其名,《APUE》 不只讲解 unix-like 系统中的系统调用 api,它也花了大量的笔墨来讲解系统调用背后的环境。这个环境既包含每种系统调用的使用环境,也包含了其背后隐藏的 unix-like 系统的运行环境。
《APUE》 的全称为 Advanced programming in the unix environment,这里有两点需要注意。第一在于对 Advanced 的理解,第二在于对 unix environment 的理解,这是我们能从这本书中学习到的两方面的知识,也是我们需要探究的两个问题。
- 什么是 advanced programming?
上一周,我无意中发现一本书—— 《Writing Bug-Free C Code》。在这本书的某一章中提到了 strncpy 函数存在的两个问题 ,看到这里的时候我比较惊讶。在很多书中都一直在强调 strcpy 中存在的问题,及鼓励 c 语言开发者使用更安全的 strncpy 来代替 strcpy,确实没看到哪本书说 strncpy 函数存在的问题。心里面有点不以为然,就自己写代码去验证该书中提及的第一个问题——目标字符串没有足够空间时字符串的结尾 ‘\0’ 缺失的问题,结果发现确实如此,颇有点世界观崩塌的意味。在此之后,我检查系统中的帮助文档,发现有如下描述:
The strncpy() function is similar, except that at most n bytes of src are copied. Warning: If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated. If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that a total of n bytes are written.
函数原型如下:
char *strncpy(char *dest, const char *src, size_t n);
glibc 中 strncpy 函数的实现如下:
char *
strncpy (s1, s2, n)
char *s1;
const char *s2;
size_t n;
{
reg_char c;
char *s = s1;
--s1;
if (n >= 4)
{
size_t n4 = n >> 2;
for (;;) {
c = *s2++;
*++s1 = c;
if (c == '\0')
break;
c = *s2++;
*++s1 = c;
if (c == '\0')
break;
c = *s2++;
*++s1 = c;
if (c == '\0')
break;
c = *s2++;
*++s1 = c;
if (c == '\0')
break;
if (--n4 == 0)
goto last_chars;
}
n = n - (s1 - s) - 1;
if (n == 0)
return s;
goto zero_fill;
}
last_chars:
n &= 3;
if (n == 0)
return s;
do
{
c = *s2++;
*++s1 = c;
if (--n == 0)
return s;
}
while (c != '\0');
zero_fill:
do
*++s1 = '\0';
while (--n > 0);
return s;
}
源码面前,毫无秘密。这又跟上面的问题有什么联系呢?使用 strcpy 可以看做普通编程,以更安全的 strncpy 替代 strcpy 看做较高级编程,避免了 strncpy 潜在问题的编程应该可以称为相对的高级编程了吧!
另外一个具体的例子与 setjmp、longjmp 函数(宏)相关。setjmp 与 longjmp 是 C 语言中用来实现非局部跳转的手段,setjmp 通过保存必要的栈信息来设置跳转点,在嵌套程序处理中发生错误时则调用 longjmp 以设定的返回值跳转到 setjmp 设置的跳转点处继续执行。
这里存在的问题在于 POSIX 标准并没有指定 setjmp 保存 signal mask 信息,这在 UNIX 系统编程中常常会造成问题。为了避免该问题,引入了新的函数—— sigsetjmp 与 siglongjmp。使用新的函数就能避免 signal mask 不能得到保存的问题。
上面两个例子旨在说明高级编程的概念,其本身并不足以展现高级编程的全貌,但却能够给我们一个好的视角去深入的理解高级编程所要达到的目标。我们所要掌握的远不止如何使用,更要明白种种限制与种种风险的避免措施,理解具体的过程以及该过程对整个系统的作用,这一点至关重要!
ii. 什么是 unix environment?
环境是一个相对大范围的概念,unix 环境对该概念的作用范围进行了限定,尽管如此这个概念涉及的东西仍然十分庞大。从内核到系统调用到 shell 与系统库,这些都是环境的一部分。应用程序依赖于这些环境来完成创建、执行、销毁的全过程,这一过程的每一部分都相当复杂,具体的细节可以根据个人兴趣进行深入研究,但总体的架构却需要尽早的建立。
登入 unix 系统时,系统通过读取系统数据文件 /etc/passwd (密码文件)来为我们准备登入的环境,通过校验 /etc/shadow 中存储的加密密码,来验证用户的身份。校验成功后,结合 /etc/group 文件来创建必要的进程提供服务,最后我们将看到一个图形界面或者 shell 命令行,这是控制系统的主要途径。
unix 环境中,磁盘中存储的每一个目录或文件都由三组不同权限控制。同时每一个目录或文件拥有自己的属主及属组。属主是文件的拥有者,属组则控制了相同组中其它组员对共享资源的操作。 unix 环境提供了对文件目录权限的严格控制,确保只有用户自己或同组中的其它成员能够操作属于自己的文件(root 用户与特殊权限设定的文件除外)。
文件是一个用户拥有的系统资源之一,另外一个重要的资源是用户所创建的进程或线程。下图是我当前系统中的部分进程树,括号中表示进程归属的用户。
├─2*[dbus-daemon(longyu)]
├─dbus-launch(longyu)
├─dconf-service(longyu)─┬─{gdbus}
│ └─{gmain}
├─dnsmasq(dnsmasq)
├─evinced(longyu)───{gdbus}
├─evolution-calen(longyu)─┬─{dconf worker}
│ ├─{evolution-calen}
│ ├─{gdbus}
│ ├─{gmain}
│ └─{pool}
├─evolution-sourc(longyu)─┬─{gdbus}
│ └─{gmain}
同大多数现代操作系统相同,UNIX 提供了进程执行的环境,以及相关系统调用来以更小的粒度控制进程的执行。一个进程有执行的入口,对于 C 语言来说,这个入口一般是 main 函数。不过这并非不可改变,可以通过编译器提供的选项来指定其它的入口函数,这常见于嵌入式开发中。
argv 中保存着命令行参数,environ 中保存着环境变量键值对。这两者并非是必须的内容,可以不进行设置。
进程在自己的地址空间中运行,各个进程的地址空间相互独立。为了在多个进程之间进行协作,内核提供了多种进程间通信的手段,如命名管道、匿名管道、消息队列、共享内存、信号量、unix domain socket 等等。这些任务间通信机制仅仅能够在单个主机中的不同任务间进行,如果需要与不同主机上的任务进行通信,可以使用 socket 来完成。
暂且停笔吧!