《UNIX 环境高级编程》笔记0x1:UNIX基础知识

此为书中第一章的笔记

用户

系统的口令文件由7个冒号分隔的字符组成,依次是

  • 登录名
  • 加密口令
  • 数字用户ID
  • 数字组ID
  • 注释字段
  • 起始目录
  • shell程序
1
2
parallels:x:1000:1000:Parallels,,,:/home/parallels:/usr/bin/zsh
sshd:x:121:65534::/var/run/sshd:/usr/sbin/nologin
  • user ID 用于表示系统中的不同用户,0 为root用户
  • group ID 用户组表示。同一个用户组中的成员可以共享资源。信息存放在 /etc/group

以上两个值可以在程序中通过 getuidgetgid 获得。


文件与目录

创建目录时会自动创建两个文件名:.(当前目录)和 ..(上一级目录)

以下为使用一些系统库函数打印指定目录下的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <apue.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);
}

编译运行

1
2
gcc ls_test.c -LDIRECTORY $EXTERNAL_FILE_PATH/error.c
./a.out /

系统头文件 dirent.h 包含 opendirreaddir 函数原型的定义,以及结构体 dirent 的定义。


I/O

文件描述符(file descriptor):内核用以标识一个特定进程正在访问的文件。当运行一个新程序时,shell会为其打开三个文件描述符:

  • 标准输入(standard input)0
  • 标准输出(standard output)1
  • 标准错误(standard error)2

重定向

可以对这三个做重定向处理

编写如下文件

1
2
3
4
5
6
#include <apue.h>

int main(void)
{
err_msg("read error");
}

编译该程序

1
gcc ls_test.c -LDIRECTORY $EXTERNAL_FILE_PATH/error.c

运行时

1
2
3
4
5
# 将stderr重定向至一个文件
./a.out 2>output.txt

# 屏蔽 stderr 和 stdout
./a.out > /dev/null 2>&1

不带缓冲

函数 openreadwritelseek 以及 close 提供不带缓冲的I/O。使用文件描述符

下列程序将shell输入变成shell输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <apue.h>
#include <unistd.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);
}

头文件 unistd.h 包含了预定义的 STDIN_FILENOSTDOUT_FILENO 以及函数原型 readwrite

其中,预定义的两个文件描述符如下

1
2
3
#define STDIN_FILENO  0 /* Standard input.  */
#define STDOUT_FILEN 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */

非常清楚,分别为0、1、2 。

编译该程序

1
gcc ls_test.c -LDIRECTORY $EXTERNAL_FILE_PATH/error.c

可以直接从命令行中输入,也可以进行复制文件的操作,如下

1
./a.out < ls_test.c > ls_test_test.txt

带缓冲

无需自己指定缓冲区的大小,如 fgets 读取完整的一行,而 read 需要指定读取字节的数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#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
gcc ls_test.c -LDIRECTORY $EXTERNAL_FILE_PATH/error.c

可以实现和之前的函数相似的功能。其中函数原型 ferrorputcgetc 以及 EOFstdinstdout 都在头文件 stdio.h 中定义。


程序和进程

  • 程序(program) 为一个可执行文件。内核使用 exec 函数将程序读入内存并执行
  • 进程(process) 为程序的执行实例
  • 进程ID(process ID) 每个进程的唯一的数字标识符

以下程序打印当前程序的PID

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
printf("PID: %ld\n", (long)getpid());
exit(0);
}

编译运行

1
gcc ls_test.c -LDIRECTORY $EXTERNAL_FILE_PATH/error.c

每次运行的得到的PID是不一样的,类型为 pid_t ,并不知道其大小,仅知道标准会确保其能保存到一个长整型中。

下列程序会启动一个子进程,执行用户输入的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <apue.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define MAXLINE 4096 /* max line length */

int main(void)
{
char buf[MAXLINE];
pid_t pid;
int status; /* child progress exit status */

printf("%% ");
while (fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0; /* repalce 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);
} else if ((pid = waitpid(pid, &status, 0)) < 0) {/* parent */
err_sys("witpid error");
}
printf("%% ");
}
exit(0);
}

这里的 fork 函数,对于子进程返回值为0,对于父进程返回值为子进程的PID


出错处理

出错的信息都定义在 errno.h 以及 errno-base.h

string.h 中定义了两个函数用于打印错误 strerrorperror

前者传入 errno.h 定义的类型,可以打印其代表的值;后者首先打印传入的参数值,随后打印 errno 对应的值。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(int argc, char* argv[])
{
fprintf(stderr, "EACCESS: %s\n", strerror(EACCES)); /* Permission denied */

// 设置错误类型
errno = ENOENT; /* No such file or directory */
perror(argv[0]);
exit(0);
}

编译

1
gcc ls_test.c -LDIRECTORY $EXTERNAL_FILE_PATH/error.c

打印的结果如下

1
2
EACCESS: Permission denied
./a.out: No such file or directory

同时注意,有些错误是可以恢复的,比如网络暂时无法连接、共享资源暂时不可用等。


时间值

  • 日历时间:1970.1.1 00:00:00(UTC)之后所经历的秒数,用 time_t 存储
  • 进程时间(CPU时间),用 clock_t 存储
    • 时钟时间:进程运行的时间总量
    • 用户CPU时间:执行用户指令所用的时间量
    • 系统CPU时间:进程执行内核程序所经历的时间
    • CPU时间 = 用户CPU时间 + 系统CPU时间

系统调用和库函数

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

对于通用 库函数,虽然有些函数可能会调用一个或多个内核的系统调用,但是它们并不是内核的切入点。

内核中的系统调用分配一块空间给进程,而库函数malloc则在用户层次上管理这一空间。

image1