第一章、UNIX基础知识
1.1 引言
1.2 UNIX体系结构
操作系统 :也是一种软件,控制计算机资源、提供程序运行环境。通常称为内核。
系统调用: 内核的接口称为系统调用
1.3 登录
- 登录名 :登录UNIX系统时需要 登录名和密码,保存在 /etc/passwd 中 passwd中的口令由7个冒号分割的字段组成,依次为:登录名、加密口令、用户ID、组ID、注释字段、起始目录、shell程序。其中shell字段指示了系统应该为用户执行哪一个shell。
- shell :命令解释器,从终端(用户输入)或文件(脚本)中获取命令并执行。
目前测试程序使用的阿里云服务器上root用户默认shell为 /bin/bash (Bourne-again shell),开发使用的嵌入式设备root用户默认shell则为 /bin/sh (Bourne shell)。
1.4 文件和目录
- 文件系统(filesystem): UNIX文件系统是目录和文件的一种层次结构。起点为 根(root) 目录,即“/”;
- 目录(directory):包含目录项的文件。每个目录项包含一个文件名和该文件的文件属性信息。文件属性指文件类型、文件大小、文件所有者、文件权限和修改时间等。使用 stat 和 fstat 函数可以获取文件的文件属性。
- 文件名(filename):目录中的各个名字。不可以使用 “/” 和 " “(空字符),”/"用来分隔构成路径名的个文件名,空字符用来终止一个文件名。创建新目录会自动创建两个文件:. 和 … ,表示当前目录和上级目录,根目录中 . 和 … 相同。
- 路径名(pathname): 由 ‘/’ 分隔的由一个或多个文件名组成的序列。以 ‘/’ 开头的表示绝对路径,否则为相对路径。
- 工作目录(working directory):每个进程都有一个工作目录,也叫当前工作目录。进程可以使用chdir()函数更改其工作目录。
- 起始目录(home directory):登录时工作目录设置为起始目录,用户的起始目录从口令文件(/etc/passwd) 中获得
>> 代码1-4: "ls"命令的简单实现
#include <stdio.h>
#include <dirent.h>
int main(int argc, char *argv[ ])
{
DIR *dp;
struct dirent *dirp;
if (argc != 2)
printf("usage : ls directory\n");
if ((dp = opendir(argv[1])) == NULL)
printf("can not open %s \n", argv[1]);
while ((dirp = readdir(dp)) != NULL)
printf("%s\t", dirp->d_name);
printf("\n");
closedir(dp);
return 0;
}
编译、执行
root@MRS:unix_program# gcc -Wall program_1_4_ls.c
root@MRS:unix_program# ./a.out .
.. a.out program_1_4_ls.c program_1_5_stdIO.c .
1.5 输入和输出
-
文件描述符: 文件描述符通常是一个比较小的非负数,内核用以标识一个特定进程访问的文件。内核打开或创建文件时会返回一个文件描述符。可以用来读写文件。
-
标准输入、标准输出和标准错误:运行一个新程序,shell会为其打开3个文件描述符,标准输入(standard input)、标准输出(standard output)和标准错误(standard error),这三个描述符默认执行终端。
-
不带缓冲的I/O:open、read、write、lseek、close提供了不带缓冲的 I/O ,这些函数使用文件描述符。
-
标准I/O: 标准I/O为不带缓冲的I/O函数提供了带缓冲的接口,使用标准I/O不需要考虑如何选取最佳的缓冲区大小,并且简化了输入操作,如: fgets 函数可以读取一行,而 read 函数需要指定读取的大小。
>> 代码1-5-1: 标准输入复制到标准输出(不带缓冲)
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#define BUFF_SIZE 4096
int main(int argc, char *argv[ ])
{
int n = 0;
char buf[BUFF_SIZE] = {0};
while ((n = read(STDIN_FILENO, buf, BUFF_SIZE)) > 0)
if (write(STDOUT_FILENO, buf, n) != n)
printf("write error:%s\n", strerror(errno));
if (n < 0)
printf("errno failed:%s\n", strerror(errno));
return 0;
}
编译运行
root@MRS:unix_program# gcc -Wall program_1_5_I2O.c
root@MRS:unix_program# ./a.out > outfile
hello world
^C
root@MRS:unix_program# cat outfile
hello world
root@MRS:unix_program# cat infile
unix programming
root@MRS:unix_program# ./a.out < infile > outfile
root@MRS:unix_program# cat outfile
unix programming
root@MRS:unix_program#
>> 代码1-5-2: 标准输入复制到标准输出(标准I/O)
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(int argc, char *argv[ ])
{
char c;
while ((c = getc(stdin)) != EOF)
if (putc(c, stdout) == EOF)
printf("output error:%s\n", strerror(errno));
if (ferror(stdin))
printf("input error:%s\n", strerror(errno));
return 0;
}
编译运行
root@MRS:unix_program# gcc -Wall program_1_5_stdIO.c
root@MRS:unix_program# ./a.out
hello
hello
world
world
^C
以上两个程序都可以实现标准输入到标准输出,不同的是:使用 read() 、write() 这类不带缓冲的函数时需要指定读写的大小,而标准I/O可以自己处理读写的大小,比如使用 EOF 、\0 来限制。
1.6 程序和进程
-
程序: 程序是一个存储在磁盘上某个目录下的可执行文件。内核使用exec将程序读入内存并执行程序
-
进程和进程ID: 程序的执行实例称为进程,某些系统中也叫 任务(如:freeRTOS),UNIX系统中确保每个进程都有一个唯一的数字标识符(非负整数),称为进程ID(process ID)。可以使用 getpid() 函数获取当前进程的进程ID。
-
进程控制: 进程控制的函数主要有3个:fork、exec、waitpid。
>> 代码1-6: 模拟shell的基本实施
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAXLINE 4096
int main(int argc, char *argv[ ])
{
char buf[MAXLINE] = {0};
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) {
printf("fork failed:%s\n", strerror(errno));
return -1;
} else if (pid == 0) {
execlp(buf, buf, (char*)0);
printf("can not exec %s :%s\n", buf,strerror(errno));
return -1;
}
if ((pid = waitpid(pid, &status, 0)) < 0)
printf("waitpid failed:%s\n", strerror(errno));
printf("%%");
}
return 0;
}
编译、运行
root@MRS:unix_program# ./a.out
% ls
a.out program_00_fork.c program_01_04_ls.c program_01_05_stdIO.c program_01_06_process_control.c
Makefile program_00_fork.o program_01_05_I2O.c program_01_06_getpid.c
%ps
PID TTY TIME CMD
15462 pts/0 00:00:00 bash
16771 pts/0 00:00:00 a.out
16773 pts/0 00:00:00 ps
%^C
-
线程和线程ID:通常一个进程只有一个线程——某一时刻执行的一组机器指令。多线程程序便于处理需要多个任务同时运行的事务,同时也可以充分利用多处理器系统的并行能力。
一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性。因此,多线程程序中要注意采取同步措施。线程也有ID标识,但线程ID只在它所属的进程内起作用。
1.7 出错处理
-
errno:UNIX系统出错时,通常会返回一个负值,而且整型常量 errno 通常被设置为具有特定信息的值。文件 <errno.h> 中定义了errno以及可赋予它的常量,表示不同的错误类型。多线程环境中,每个线程都有自己的局部 errno,不会相互干扰。errno有两条规则:一、不出错时,errno的值不会被清除;二、任何函数都不会将errno设置为0。C标准定义了两个函数,用于打印错误信息
char *strerror(int errno); // 返回errno对应的错误信息的字符串指针 void perror(const char *msg); // 先输出msg,再输出errno代表的错误信息 // 输出为: msg: [err info]
>> 代码1-7:输出错误信息
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(int argc, char *argv[ ])
{
fprintf(stderr, "EACCES:%s\n", strerror(EACCES));
errno = ENOENT;
perror(argv[0]);
return 0;
}
编译、运行
root@MRS:unix_program# gcc -Wall program_01_07_error.c
root@MRS:unix_program# ./a.out
EACCES:Permission denied
./a.out: No such file or directory
- 出错恢复:可将<errno.h>中定义的错误信息分为两类:致命性错误和非致命性错误。对于致命性错误,无法恢复,一般打印一条错误信息后退出;对于非致命性错误(如:EAGAIN、ENFILE、ENOBUFS等),可以采用适当的方式(如:延迟重试)进行恢复。
1.8 用户标识
UNIX系统中的文件系统一般会保存磁盘上文件的用户ID和组ID。相较于保存用户名和组名,用户ID和组ID占用更少的空间,便于校验权限。但对于用户而言,名字比使用数字ID方便,因此,口令文件包含了用户ID到用户名的映射关系,组文件包含了组ID到组名的映射关系。使用 getuid() 和 getgid() 可以获取用户ID和组ID。
- 用户ID:UNIX系统使用一个数值标识不同的用户,即用户ID(user ID),保存在口令文件中。用户ID为0的用户为根用户(root)或超级用户(superuser),根用户(超级用户)对系统有自由的支配权。
- 组ID:口令文件中也包括用户的组ID(group ID),它也是一个数值。由系统管理员分配,用于将若干用户集合到项目或部门中,相同组的用户可以共享资源。组文件将组名映射为数值的组ID,通常为 /etc/group。
- 附属组ID:除口令文件中对一个用户指定一个组外,大多数UNIX系统允许用户属于另外的一些组,读 /etc/group,可以得到一个用户的附属组(supplementary group ID)。
1.9 信号
信号 用于通知进程发生了某种情况。进程通常有3种信号处理方式:
- 忽略信号;
- 按系统默认方式处理,比如收到SIGINT时系统默认终止程序;
- 提供一个信号处理函数。
很多情况都会产生信号,中断键盘上有两种产生信号的方法,中断键(interrupt key,一般为 Delete 键 或 Ctrl+c)和 退出键(quit key,通常为 Ctrl+\),用于中断当前运行的进程;另一种产生信号的方式是使用kill命令,但必须是要中断的进程的所有者或超级用户。
>> 代码1-9:信号处理
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAXLINE 4096
static void sig_proc(int signal)
{
if (SIGINT == signal)
printf("Recive signal SIGINT");
}
int main(int argc, char *argv[ ])
{
char buf[MAXLINE] = {0};
pid_t pid;
int status;
if (signal(SIGINT, sig_proc) == SIG_ERR)
printf("signal failed:%s\n", strerror(errno));
printf("%% ");
while (fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) -1] = 0;
if ((pid = fork()) < 0) {
printf("fork failed:%s\n", strerror(errno));
return -1;
} else if (pid == 0) {
execlp(buf, buf, (char*)0);
printf("can not exec %s :%s\n", buf,strerror(errno));
return -1;
}
if ((pid = waitpid(pid, &status, 0)) < 0)
printf("waitpid failed:%s\n", strerror(errno));
printf("%%");
}
return 0;
}
编译、运行
root@MRS:unix_program# gcc -Wall program_01_09_signal_proc.c
root@MRS:unix_program# ./a.out
% pwd
/home/songliuyang/program/unix_program
%^CRecive signal SIGINT
can not exec :No such file or directory
%^\Quit
程序根据 代码1-6 修改而来,执行 ./a.out ,程序可以执行终端输入的命令,但是输入 Ctrl+c 却不能中断程序,而是打印 “Recive signal SIGINT”,说明当前进程使用 signal_proc 处理了SIGINT 信号,同时,使用 Ctrl+\ 依然可以中断进程。
1.10 时间值
历史上,UNIX系统使用过两种不同的时间值。
日历时间:自协调时间(Coordinated Universal Time, UTC),表示 1970年1月1日00:00:00 以来经历的秒数。这些值可以用于记录文件最近一次的修改时间。系统基本数据类型 time_t 用于存储这种时间值。
进程时间:也称为 CPU时间,用以度量进程使用的中央处理器资源,进程时间以时钟滴答计算。系统基本数据类型 clock_t 保存这种时间值。度量一个进程的执行时间时,UNIX系统维护了3个进程时间值:
- 时钟时间:也称为 墙上时钟时间(wall clock time),是进程运行的时间总量,与系统中同时运行的进程数有关;
- 用户CPU时间:用户CPU时间是指执行用户指令所用的时间量;
- 系统CPU时间:系统CPU时间是指 执行该进程过程中执行内核程序所用的时间,即内核态的时间。
用 户 C P U 时 间 + 系 统 C P U 时 间 = C P U 时 间 用户CPU时间 + 系统CPU时间 = CPU时间 用户CPU时间+系统CPU时间=CPU时间
1.11 系统调用和库函数
UNIX系统提供了定义良好、数量有限、直接进入内核的入口点,这些入口点称为系统调用(system call),一般使用C语言定义。
UNIX系统使用的技术是为每个系统调用在标准C库中设置一个具有相同名字的函数。这些库函数可能会调用一个或多个内核的系统调用,但它们并不是内核的入口点。
系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能。
一些系统调用也可以被用户应用程序直接调用,比如进程控制系统调用(fork、exec、wait)。