[单刷APUE系列]第一章——Unix基础知识[1]

山河永寂

目录

[单刷APUE系列]第一章——Unix基础知识[1]
[单刷APUE系列]第一章——Unix基础知识[2]
[单刷APUE系列]第二章——Unix标准及实现
[单刷APUE系列]第三章——文件I/O
[单刷APUE系列]第四章——文件和目录[1]
[单刷APUE系列]第四章——文件和目录[2]
[单刷APUE系列]第五章——标准I/O库
[单刷APUE系列]第六章——系统数据文件和信息
[单刷APUE系列]第七章——进程环境
[单刷APUE系列]第八章——进程控制[1]
[单刷APUE系列]第八章——进程控制[2]
[单刷APUE系列]第九章——进程关系
[单刷APUE系列]第十章——信号[1]
[单刷APUE系列]第十章——信号[2]
[单刷APUE系列]第十一章——线程[1]
[单刷APUE系列]第十一章——线程[2]
[单刷APUE系列]第十二章——线程控制
[单刷APUE系列]第十三章——守护进程
[单刷APUE系列]第十四章——高级I/O
[单刷APUE系列]第十五章——进程间通信
[单刷APUE系列]第十六章——网络IPC:套接字
[单刷APUE系列]第十七章——高级进程间通信

声明

由于目录可能过长,所以以后只在第一章,也就是这章中提供目录索引,请各位见谅。

文章系列原因

Unix系统标准作为目前开发最重要的系统标准,应当是必须懂得的,无论是服务端C++开发还是安卓、iOS开发,实际上都和Unix环境开发密切相关,一个Unix环境开发者在入手安卓和iOS开发,是从来不会去询问很基础的一些问题,比如动态库静态库搜索路径、编译选项、环境变量的问题。目前笔者认为《Unix环境高级编程》ver3是最好的一部Unix环境开发入手著作,但是由于不少朋友在学习的时候都会碰到不少问题,所以笔者将自己的学习记录重新整理成博客。

Unix简介

操作系统的狭义定义,是将操作系统定义为一种控制计算机资源,提供程序运行环境的软件,通常我们称之为内核,内核提供接口供上层应用调用,也叫做System Call(系统调用)。同时,为了方便应用程序使用内核,通常都会有公用函数库,应用程序既可以使用系统调用,也可以使用公用函数库。系统调用和公用函数库实际上并不是同一个东西,但是对于开发者来说,可以当作同一个层,都可以使用C函数来调用。再向上,就是shell终端,作为人机交互部分,最外层则是应用程序。
而从广义上来说,操作系统就是一个包含了内核和必备系统软件的集合,这些软件是支持一个系统正常运转使用、人机交互的最小要求。
目前来说,已经不存在真正的Unix系统了,因为自从AT&T公司封闭Unix源代码,Unix的变种分支就出现了很多,其中学院派BSD,商业SystemV,和开源的Linux最重走到了最后。其中,BSD原先是基于AT&T开放代码构建,后来Unix实验室被卖给了Novell,Novell授权BSD开发Unix,但是去除了源自AT&T的源代码,最终形成了BSD-4.4 Lite,也是目前很多类Unix操作系统的基石。商业SystemV则是AT&T联合许多公司,用于解决Unix混乱的商业版本,所以后来的很多商业Unix都是基于SystemV release4版本,而后Unix被卖给Novell,最终到了X/OPEN Consortium,即后来的Open Group。
也就是说,只要实现了Unix环境的标准,就是一个Unix系统,笔者就是在Mac OS X系统下进行操作。

Unix文件和目录

Unix有一个哲学——一切皆是文件,文件在Unix环境中是非常重要的东西,Unix文件系统就是一个虚拟层次结构,所有目录都挂载于/根目录,文件夹也可以被认为是一种文件,设备也是一种文件,重要的Socket套接字也是一种文件,APUE第一个实例就是写一个类似ls命令的实现。

#include "apue.h"
#include <dirent.h>

int main(int argc, char * argv[])
{
    DIR *dp;
    struct dirent *dirp;
 
    if (argc != 2)
        err_quit("usage: 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);
}

然后运行

> cc myls.c

结果报错:

example.c:1:10: fatal error: 'apue.h' file not found
#include "apue.h"
         ^
1 error generated.

不少朋友就非常郁闷,我就是按照上面的打的代码啊,怎么错了?其实,apue.h头文件是这本书里自行编写的头文件,目的在于减少#include的代码量,让读者更加专注于实际代码。读者可以从附录B 其他源代码这一章中找到apue.h头文件。或者从官方网站直接下载源代码,复制出apue.h文件。
我们把头文件放到myls.c同一目录下,再次编译

Undefined symbols for architecture x86_64:
  "_err_quit", referenced from:
      _main in example-c42025.o
  "_err_sys", referenced from:
      _main in example-c42025.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

好像错误更多了啊,但是仔细查看错误信息,里面提示缺少err_quiterr_sys的二进制代码。众所周知,编译器编译成为二进制代码分为四个步骤:预处理、编译、汇编和链接
预处理过程只是展开宏定义,处理条件编译,展开include指令来插入实际代码和一些杂项工作。编译就是将预处理完成的代码进行词法分析、语义分析和代码优化,最终生成相应的汇编代码文件,然后就是汇编过程,将其翻译为二进制代码,然后就是链接工作,使用链接器将各个目标文件组装到一起,解决符号依赖等问题,最终形成一个二进制文件,其中还分为链接动态库和静态库的不同。这些阶段都可以指定编译参数来让编译器只执行到某一阶段,比如:

> cc -E xxx.c #只执行预处理阶段
> cc -S xxx.c #只执行到编译阶段
> cc -c xxx.c #只执行到汇编阶段

根据不同到编译器参数可能有一点点不同,但是都可以通过man命令来查看具体的编译器参数,比如Linux下就是gcc,FreeBSD和Mac OS X就是使用clang作为编译器。
实际上上述问题就是因为缺少库文件,而附录B里面就有关于错误处理的代码,我们可以直接从书的源代码里面找出来,然后使用

cc -c error.c
cc -c errorlog.c

将其编译为二进制文件,然后使用ar -r xxx.o xxx.o将其打包为静态库文件,把liberror.a放到lib文件夹下,把apue.h放到include文件夹下,然后我们把头文件命令改为#include "include/apue.h",使用编译命令cc -L./lib -lerror myls.c就能够正确的编译出二进制文件了。
最终,编译出的二进制文件就能方便的显示目录下的所有文件。

《Unix程序员手册》是Unix系统必备的工具书,里面使用节(section)来组织内容的,包含了许许多多的内容例如我们使用man ls的时候,可以看到里面有ls(1)这就是代表这个内容是存放在第一节里,一般来说,Unix系统参考手册内部是如下组织

  1. User Commands and Utilities

  2. System Calls

  3. C Library Functions

  4. File formats

  5. Headers,tables and macros

  6. Games and demos

  7. Device and Network Interfaces

  8. Maintance and Accounting commands

  9. Device driver interfaces

我们可以使用man -s [section] [content]的形式来查找内容,而且Man Page是我们在做开发的时候的得力助手,Mac OS X的内部手册是BSD的用户手册。

输入和输出

文件描述符在C语言内部是一个非负整数,内核用其来标示一个进程访问的文件,每个进程都维护自己的文件描述符,按照标准规定,当一个进程运行时,都默认打开三个文件描述符,即标准输入、标准输出和标准错误,正常情况下,这三个文件都指向终端输出,但是在终端可以使用重定向的方式将这三个文件描述符指向不同的地方。我们可以查看一下系统头文件

> vim /usr/include/unistd.h

我们可以找到

#define  STDIN_FILENO   0       /* standard input file descriptor */
#define STDOUT_FILENO   1       /* standard output file descriptor */
#define STDERR_FILENO   2       /* standard error file descriptor */

实际上0、1、2就默认已经被使用了,如果我们在此基础上新打开一个文件,实际上是增加在3的位置,而且每个进程都有0、1、2的文件描述符。

程序和进程

程序是一段放置于磁盘上的二进制代码,内核使用exec函数族来讲进程读入内存,并且执行程序,在内存中运行的程序实例被称为进程,Unix标准要求每个进程都有唯一表示符(process ID即pid),pid是一个非负整数。

#include "include/apue.h"

int main(int argc, char *argv[])
{
    printf("hello world from process ID %lu\n", (unsigned long)getpid());
    exit(0);
}

最终程序会输出运行进程的pid,我们可以通过一下命令

man 2 getpid

得到关于此系统调用的函数原型

pid_t getpid(void);

pid_t是标示进程id的数据类型,我们可以通过查看/usr/include/unistd.h文件,发现它里面有#include <sys/_types/_pid_t.h#include <_types.h>两条命令,然后我们再打开/usr/include/sys/_types/_pid_t.h/usr/include/_types.h,其中,_pid_t.h里有typedef __darwin_pid_t pid_t;,而/usr/include/_types.h里有一条语句#include <sys/_types.h>,我们再次打开/usr/include/sys/_types.h,里面有typedef __uint32_t __darwin_id_t;,那么非常简单,实际上pid_t就是__uint32_t,可以看出,系统使用了32位无符号整形存储了pid_t类型,实际上,标准并没有规定其大小,而是保证它能保存在一个长整形中。所以我们将其转换为它可能用到的最大数据类型。

进程控制有主要三个函数:fork、exec和waitpid。(exec函数有7种变体,但是一般统称exec函数)

#include "include/apue.h"
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    char buf[MAXLINE];
    pid_t pid;

    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, NULL);
            err_ret("couldn't excute: %s", buf);
            exit(127);
        }
        if ((pid = waitpid(pid, NULL, 0)) < 0)
            err_sys("waitpid error");
        printf("%% ");
    }
    exit(0);
}
  • 在这个程序里使用了标准I/O函数fgets从标准输入读取一行,当输入文件结束符Ctrl+D时候,fgets返回一个null指针,然后就会直接执行exit(0);让进程退出

  • fgets每次读取的一行都以换行符终止,所以buf最后两个字符就是'\n'和'\0',但是execlp函数要求参数必须以'\0'结尾,不需要'\n'换行符,所以我们使用'\0'字符先替换了'\n',让execlp函数能顺利执行

  • 调用fork函数创建一个新进程,新进程是父进程的副本,fork对父进程返回子进程的pid,对子进程则返回整数0,并且子进程是完全复制父进程的当前内存空间,所以子进程一开始执行的代码就是父进程正在执行的代码,所以说fork函数被调用一次(在父进程调用),但返回两次(父进程和子进程都得到返回值)

  • 根据fork函数的返回值判断当前进程是子进程还是父进程,在子进程中,调用execlp执行命令,使用新的程序文件替换了原先子进程的程序文件。而父进程则使用waitpid等待子进程的终止,当一切执行完毕,则打印出新的提示符%

通常情况下,一个进程只有一个线程运行,但是Unix实际上存在线程模型,能够让我们创建管理线程,从而更好的共享进程的内存,并且充分利用多处理器系统的并行能力。
一个进程内的所有线程共享同一内存地址、文件描述符、栈和进程属性,从而线程创建是非常轻量化的,开销很少,但是由于共享资源,所以各个线程在访问资源的时候必须采取同步措施防止出现资源同时访问等问题。
与进程相似,线程可以说是一个轻量化的进程,它也有自己的线程ID,但是线程ID只在自己的进程内起作用。

阅读 10.9k

静雅斋
码代码|Minecraft|Node.jser&amp;PHPer|iOS移动端开发者|Web前端码农
2.4k 声望
156 粉丝
0 条评论
2.4k 声望
156 粉丝
文章目录
宣传栏