概述
操作系统的任务是为其上运行的程序服务:包括运行、打开文件、读文件、分配内存以及时间获取。
架构
定义:
In a strict sense, an operating system can be defined as the software that controls the hardware resources of the computer and provides an environment under which programs can run.
UNIX的体系结构
由内而外:
- 内核 其接口被称为系统调用(System call)。
- shell 特殊的应用程序,为其他应用程序提供接口。
- 库函数 在系统调用接口基础上的函数库。
- 应用 就是应用。。
登录
登录方式
系统根据输入的用户名去/etc/passwd
目录下检索用户名并找到对应的密码文件进行密码匹配。
密码文件长这样:
sar:x:205:105:Stephen Rago:/home/sar:/bin/ksh
上述对应各字段值类型:
登录名:加密的密码:用户ID:组ID:注释域:起始:shell程序
shell
shell是一个命令解释器,与用户进行交互。
文件与目录
- 文件系统 树状层级结构
- 文件名
- 路径名 创建目录时候自动生成当前目录
.
和父目录..
。 - 工作目录 每个进程都有一个工作目录
- 起始目录
栗子:ls简要实现
#include "apue.h"
#include <dirent.h>
int main (int argc, char *argv[]) {
DIR *dp;
struct dirent *dirp;
// 输入参数不为2, 暗示使用方法不对
if (argc != 2) {
err_quit("usage: ls directory_name");
}
// 打开指定目录失败
if ((dp = opendir(argv[1])) == NULL) {
err_sys("can't not open %s", argv[1]);
}
// 遍历dp文件夹 打印内部文件的文件名
while ((dirp = readdir(dp)) != NULL) {
printf("%s\n", dirp->d_name);
}
// 关闭文件夹句柄 && 退出程序
closedir(dp);
exit(0);
}
这里可能遇到一个apue.3e的编译问题,解决方案戳这里。
命令行编译执行结果如下:
输入与输出
文件描述符 (file descriptor)
通常是一个小的非负整数,内核用它标识一个特定进程访问的文件。
标准输入 标准输出 标准错误
一般来说,所有派系的shell都会为程序打开标准输入、标准输出、标准错误三个文件描述符。并且这三个文件描述符都能重定向到某个文件。
ls > file.list
非缓冲的I/O
非缓冲函数如:open, read, write, lseek, close
。这些函数都与文件描述符协作。
举个栗子:从标准输入读写入标准输出
#include <stdio.h>
#include <apue.h>
#define BUFFSIZE 4096
int main(void) {
int n;
char buf[BUFFSIZE];
// 头文件<unistd.h>中包含了`STDIN_FILENO`与`STDOUT_FILENO`两个常量
while ((n = (int)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);
}
命令运行结果截图:
标准I/O
标准I/O函数提供一种对不用缓冲I/O函数的带缓冲接口。 无脑说就是不用设置缓冲区大小。比如fgets函数读完一行的长度而read函数需要制定读入的字节数。
我们最熟悉得标准I/O是printf
。。。
举个栗子: 用标准I/O将标准输入复制到标准输出
#include <stdio.h>
#include <apue.h>
int main(void) {
int c;
// 头文件<unistd.h>中包含了`STDIN_FILENO`与`STDOUT_FILENO`两个常量
while ((c = getc(stdin)) != EOF) {
if (putc(c, stdout) == EOF) {
err_sys("output error");
}
if (ferror(stdin)) {
err_sys("input error");
}
}
exit(0);
}
运行结果略。。
程序和进程
程序
程序是放在磁盘上的。
进程和进程ID
程序执行的实例叫做进程。每个进程都有自己的唯一标识符PID。
栗子:打印进程ID
int main(void) {
printf("hello world from process ID %d\n", getpid());
exit(0);
}
运行结果:
进程控制
进程控制主要函数有:fork、exec、waitpid
举个栗子:
#include <stdio.h>
#include <sys/wait.h>
#include <apue.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("couldn't execute: %s", buf);
exit(127);
}
if ((pid = waitpid(pid, &status, 0)) < 0) {
err_sys("watipid error");
}
printf("%% ");
}
exit(0);
}
运行结果:
线程和线程ID
通常一个进程只有一个控制线程,同一时刻只能执行一组机器指令。
在同一个进程内得线程共享同一个地址空间、文件描述符、栈以及进程相关属性。
因为共享资源的关系,所以这里有个线程安全的概念。
与进程相同,线程也用ID标识。线程ID只在进程中起作用。出了进程就没有意义了。
出错处理
UNIX函数出错的时候通常返回一个负值,用errno
表示错误得种类。某些函数不返回负值而是使用另外一种约定,比如返回一个只想对象的指针的大多数函数,出错时候返回null指针。
使用errno的两条规则:
- 如果没有出错,则errno值不会被例行清除。只有当函数返回值错误的时候才去检查其值。
- 没有函数会将
errno
值设置为0,在<errno.h>中定义的所有常量都不为0。
标准C钟定义了下面两个方程来实现打印错误消息。
char *strerror(int errnum);
void perror(const char *msg);
第一个函数表明根据errno找到错误信息字符串指针,第二个函数表明根据指针,打印出错误信息。
举个栗子:
#include <string.h>
#include <errno.h>
int main(int argc, char *argv[]) {
fprintf(stderr, "EACCES: %s\n", strerror(EACCES));
errno = ENOENT;
perror(argv[0]);
exit(0);
}
通过将./a.out传入perror
函数,这样可以在管道中知道是哪个程序出错了。
错误恢复
在<errno.h>
的错误定义划分为两种:致命与非致命。致命错误没有恢复操作。而非致命错误相反,遇到此类错误时候可以更为妥善的处理。
资源相关的非致命错误包含EAGAIN, ENFILE, ENOBUFS, ENOLCK, ENOSPC, EWOULDBLOCK
,有些时候还有ENOMEM
。EBUSY
问题出在共享资源被占用的时候也可以视为非致命错误。
资源相关非致命错误的恢复策略一般是延迟重试。
适时的采用恢复策略可以增加应用的健壮性从而避免异常退出。
用户标识
用户ID
用户ID用来让系统区分不同的用户。
用户ID为0得用户为超级用户。操作系统的很多权限只向超级用户提供。
组ID
组ID是系统管理员在指定用户名提供的。这种机制允许组内成员共享资源。
组文件将组名映射为数字组ID,它通常是/etc/group.
举个栗子:
int main(int argc, char *argv[]) {
printf("uid = %d, gid = %d\n", getuid(), getgid());
exit(0);
}
附加组ID
大多数UNIX版本还允许一个用户属于另外一个用户组。
信号
信号是通知进程已经发生某种情况的一种技术。举个栗子,一个进程在执行除法运算操作,除数为0,则将名为SIGFPE的信号发给该进程。
栗子:
为了让程序能捕捉到信号,需要让其调用signal
函数来指定对应信号应当执行的动作(函数)。下栗是在捕获信号时候进行打印输出:
// 声明信号handler
static void sig_int(int);
// 简单handler实体 - 打印
void sig_int(int signo) {
printf("interrupt\n%% ");
}
时间值
UNIX系统有两类时间值:
- 日历时间
- 进程时间
日历时间:广义时间,用time_t
修饰。
进程时间:CPU时间,用来度量进程使用的CPU资源,用clock_t
修饰。进程时间又分为:
- 时钟时间 进程运行的时间总量。与总进程数有关。
- 用户CPU时间 CPU执行用户指令所用的时间
- 系统CPU时间 CPU执行内和程序所用的时间
用户CPU时间与系统CPU时间统称CPU时间。可以通过time
函数来获取时间值组成。
系统调用和库函数(理论)
从实现者角度看,系统调用和库函数之间有重大区别,但从用户角度,区别并不是很重要。
应用程序可以调用系统函数或者库函数,而很多库函数会调用系统调用。
系统调用通常提供最小接口,而库函数提供比较复杂的功能。
总结
UNIX概述
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。