操作系统是一个软件,功能主要为控制计算机硬件资源,提供程序运行环境。
通常将这种软件称为内核(kernel),相对较小。
内核的接口称为系统调用
公用函数库构建在系统调用接口上,
应用程序即可使用公用函数库,也可使用系统调用。
shell是一个特殊的应用程序,为运行其他应用程序提供了一个接口。
广义上来说,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特性。其他软件包括:系统实用程序,应用程序,shell以及公用函数库等。
用户登录UNIX系统时,输入登录名和密码。
系统在口令文件(通常是/etc/passwd文件)中查看登录名。
口令文件中记录了登录名、加密口令、数字用户ID、数字组ID、注释字段、起始目录以及shell程序
用户登录后,系统通常先显示一些系统信息,然后用户就可以向shell程序键入命令。
某些系统在登录时,启动一个视图交互程序,但最后总会有一个shell程序运行在该程序中。
shell是一个命令行解释器,读取用户输入,然后执行命令
shell的用户输入通常来自于终端(交互式shell),有时则来自于文件(称为shell脚本)
系统从口令文件中相应用户登录项的最后一个字段中获取到shell程序目录
UNIX文件系统是目录和文件的一种层次结构,所有文件和目录的起点称为根目录(root),这个目录的名称是一个字符“/”
一个包含目录项的文件。
目录中的各个名字称为文件名。只有斜线(/)和空字符这两个字符不能出现在文件名中。斜线用来分割构成路径名的各文件名。空字符用来终止一个路径名。
为了可移植性,POSIX.1推荐将文件名限制在以下字符集中:字母 数字 点 短横线 下划线。
由斜线分隔的一个或多个文件名组成的序列,构成路径名。由斜线开头的路径名称为绝对路径名,否则称为相对路径名。
创建新目录时会自动创建两个文件:. 和 ..,分别指向当前目录和父目录,在最高层次的根目录中,.. 和 . 相同。
每个进程都有一个工作目录,有时称其为当前工作目录。
所有相对路径名都从工作目录开始解释。
进程可以用chdir函数更改工作目录。
登录时,工作目录设置为起始目录,该起始目录从口令文件中相应用户的登录项中取得。
非负整数(通常较小),内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,都会返回一个文件描述符。在读写文件时,可以使用这个文件描述符。
每当运行一个新程序时,所有的shell都为其打开3个文件描述符,即标准输入 标准输出 标准错误
如果不做特殊处理,则这3个描述符都链接向终端。大多数shell都提供一种方法,使其中任何一个或所有者3个描述符都能重新定向到某个文件,例如:
ls > file.txt
执行ls命令,其标准输出重新定向到file.txt文件。
函数open read write lseek 以及 close 提供了不带缓冲的I/O。这些函数都使用文件描述符。
例:
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");
}
头文件及两个常量STDIN_FILENO STDOUT_FILENO是POSIX标准的一部分。头文件包含了很多UNIX系统服务的函数原型,如read write。
STDIN_FILENO STDOUT_FILENO定义在中,指定了标准输入和标准输出的文件描述符。在POSIX标准中,他们的值分别是0和1.
该代码从标准输入读取数据并输出到标准输出
可使用重定向功能将标准输入和标准输出重定向到文件,即可实现文件复制功能。
./a.out < infile > outfile
标准I/O函数为哪些不带缓冲的I/O函数提供了一个带缓冲的接口
使用标准I/O函数无需担心如何选取最佳的缓冲区大小
标准IO函数库提供了使我们能够控制该哭所使用的缓冲风格的函数。
常用的标准I/O函数如:printf,在调用printf的程序中,总是包含
程序是一个存储在磁盘上某个目录中的可执行文件。
内核使用exec函数将程序读入内存,并执行程序。
程序的执行实例被称为进程。
UINX系统确保每个进程都有一个唯一的数字标识符,称为进程ID。进程ID总是一个非负整数
获取进程ID:
getpid();
该接口返回当前进程ID,返回数据类型为pid_t,最大大小为长整型。因此一般转换为long。
有三个用于进程控制的主要函数
fork exec 和 waitpid (exec函数有7中变体,一般统称为exec函数)
例:
if ((pid = fork()) < 0) {
err_sys("fork error");
exit(-1);
}
if (pid == 0) //fork返回值为0,表示当前进程为新创建的子进程
{
execlp(cmd, cmd, (char*)0);
exit(0);
}
//fork返回非0,表示当前进程为主进程,返回值pid为子进程pid,以下为主进程等待子进程结束
if ((pid = waitpid(pid, &status, 0)) < 0) {
err_sys("waitpid error");
}
exit(0);
上述例子实现一个简单的创建新进程的方式:fork新进程,并在新进程中调用exec函数执行需要运行的程序。
通常,一个进程只有一个控制线程:某一时刻执行的一组机器指令。
对于某些问题,如果有多个控制线程分别作用于他的不同部分,那么解决起来旧容易得多。另外,多个控制线程也可以充分利用多处理器系统的并行能力。
一个进程内的所有线程共享同一地址空间、文件描述符、栈、以及进程相关的属性。
所以各线程在访问共享数据时,需要采取同步措施以避免不一致性。
线程也用ID标识,线程ID只在它所属的进程内起作用。
UNIX系统函数出错时,通常返回一个负值。而有些函数对于出错则使用另一种约定而不是返回负值。录入大多数返回指针的函数,在出错时会返回一个null指针。
文件
POSIX和ISO C将errno定义为一个符号,扩展成为一个可修改的整型左值。可以使一个包含出错编号的整数,也可以是一个返回出错编号指针的函数。
以前使用的定义是:
extern int errno;
但是在支持线程的环境中,多个线程共享进程地址空间,每个线程都有属于他自己的局部errno以避免一个线程干扰另一个线程。例如,Linux支持多线程存取errno,将其定义为:
extern int *__errno_location(void);
#define errno (*__errno_location())
C标准定义了两个函数,用于打印出错信息。
char* strerror(int errnum); //string.h
void perror(const char* msg); //stdio.h
strerror返回errnum对应的出错消息字符串
perror首先输出msg,然后是冒号 空格,接着输出errno值的出错信息,最后是一个换行符
当进程发生了某种情况时,系统将对应信号发送给进程
进程有3中方式处理信号
1 忽略信号 (不推荐)
2 按系统默认方式处理。
3 提供一个函数,信号发生时调用该函数,称为捕捉该信号。
很多情况下都会产生信号。终端键盘上有两种产生信号的方法
中断键:Delete键或Ctrl+C
退出键:Ctrl+\
此两个键用于中断当前运行的进程。
另一种产生信号的方法是调用kill函数。可以在一个进程中调用kill函数项另一个进程发送信号。这种方式有限制:当向一个进程发送信号时,必须是哪个进程的所有者或者超级用户。
signal函数:例:
static void sig_int(int);
if (signal(SIGINT, sig_int) == SIG_ERR) {
err_sys("signal error");
}
添加上述代码,则在产生SIGINT信号时,sig_int函数会被调用
日历时间:从1970年1月1日00:00:00这个特定时间以来所经过的秒数。系统基本数据类型time_t用于保存这种时间值。
进程时间:也被称为CPU时间,用以度量进程使用的中央处理器资源。基本数据类型clock_t保存这种时间值。
UNIX系统为一个进程维护3个进程时间值:
时钟时间: 进程运行的时间总量
用户CPU时间:执行用户执行所用的时间量。
系统CPU时间:执行内核程序所经历的时间
C语言定义的直接进入内核的入口点
从实现者角度来看,系统调用和库函数之间有根本的区别;但从用户角度来看,其区别并不重要。
例:
malloc为存储空间分配函数,UNIX系统中处理存储空间分配的系统调用是sbrk,sbrk仅按指定字节数增加或减少进程地址空间。如何管理该地址空间取决于进程。
malloc即实现一种对该地址空间的特定分配。如果对标准的malloc不满足,则可以定义自己的malloc,malloc中很可能将使用sbrk系统调用。
即:malloc库函数和sbrk系统调用的职责不同:内核中的系统调用分配一块空间给进程,而库函数malloc则在用户层次管理这一空间。
另一差别是:系统调用通常提供一种最小的接口,而库函数通常提供比较复杂的功能。