UNIX环境高级编程(1):UNIX基础知识(1)

本系列文章是学习被誉为UNIX编程圣经的《UNIX环境高级编程》的读书笔记。《UNIX环境高级编程》的英文全称为《Advanced Programming in the UNIX Environment》,简称《APUE》,其做者是UNIX和网络技术领域的知名专家W.Richard Stevens。html

本书描述了UNIX系统的程序设计接口:系统调用接口和标准C库提供的不少函数。与大多数操做系统同样,UNIX为程序运行提供了大量的服务--打开文件,读文件,启动一个新程序,分配存储区,获取当前时间等等。这些服务被称为系统调用接口(system call interface)。另外标准C库提供了大量普遍应用于C程序中的函数。linux

本书共分为6个部分:shell

(1)对UNIX程序设计基本概念和术语的简要描述,以及对各类UNIX标准化工做和不一样UNIX实现的讨论;编程

(2)I/O:不带缓冲的I/O,文件和目录,标准I/O库和标准系统数据文件;bash

(3)进程:UNIX进程的环境,进程控制,进程之间的关系和信号;网络

(4)更多的I/0:终端I/O,高级I/O和守护进程;函数

(5)IPC:进程间通讯;学习

(6)实例;spa

接下来就正式进入《UNIX环境高级编程》的学习了。本章将从程序设计人员的角度快速浏览UNIX,对书中引用的一些术语和概念进行简要的说明,后续文章再对这些概念做更详细的说明。操作系统

UNIX体系结构:

在严格意义上,可将操做系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。通常而言,咱们称这种软件为内核(kernel)。 内核的接口被称为系统调用(system call)。公用函数库构建在系统调用接口之上。应用软件既可使用公用函数库,也可使用系统调用。

Shell是一种特殊的应用程序,它为运行其余的应用程序提供了一个接口。

在广义上,操做系统包括内核和一些其它软件,这些软件使得计算机可以发挥做用,这些软件包括系统实用程序(system utilities),应用软件,shell以及公用函数库等。

例如,Linux是GNU操做系统使用的内核,仅仅是GNU操做系统的关键组件之一,GNU操做系统还包括不少其它的自由软件,例如bash,Tex,GNU C库等等。因此严格意义上这些操做系统的发行版应该称为GNU/Linux,可是不少人将其简称为Linux。虽然这种表达方法不正确,可是“操做系统”本省就具备双重含义,因此这也是能够理解的。关于GNU和Linux的关系,能够参考:http://www.gnu.org/gnu/linux-and-gnu.en.html

登陆:

登陆名:用户在登陆UNIX系统时,须要输入登陆名及相应的口令。系统在其口令文件(一般是/etc/passwd文件)中查看登陆名。

shell:

shell:shell是一个命令行解释器,它读取用户输入,而后执行命令。用户一般经过终端(交互式shell),有时则经过文件(shell script)向shell进行输入。用户在登陆后,系统从口令文件中相应用户登陆项的最后一个字段中了解到应该为该登陆用户执行哪个shell。

文件和目录:

文件系统:UNIX文件系统是目录和文件组成的一种层次结构,目录的起点称为根(root),写为"/"。目录是一个包含许多目录项的文件,逻辑上,能够认为每一个目录项都包含一个文件名,同时还包含说明该文件属性的信息。

文件名:目录中的各个名字称为文件名,不能出如今文件名中的字符只有斜线("/")和空字符(null)。斜线用来分隔构成路径名的各文件名,空字符则用来终止一个路径名。

在建立新目录时,会自动建立两个文件名:.(称为点)和..(称为点-点)。点指当前目录,点点指父目录。在根目录中,点和点点相同。

现现在,几乎全部商品化的UNIX系统都支持至少255个字符的文件名。

路径名:一个或多个斜线分隔的文件名序列(也能够斜线开头)构成路径名。以斜线开头的路径名称为绝对路径,不然称为相对路径。相对路径引用至关于当前目录的文件。

下列程序是ls(1)命令的简要实现,用于列出某个目录下的全部文件名。

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>

int
main(int argc, char *argv[])
{
	if (argc != 2) {
		printf("usage: ls directory_name\n");
		exit(1);
	}

	DIR *dp;
	struct dirent *dirp;
	
	if ( (dp = opendir(argv[1])) == NULL) {
		printf("can't open %s\n", argv[1]);
		exit(2);
	}
	while ( (dirp = readdir(dp)) != NULL) {
		printf("%s\n", dirp -> d_name);
	}
	
	closedir(dp);
	exit(0);
}

ls(1)这种表示方法是UNIX系统的惯用方法,表示引用UNIX手册集中的某个特定项。ls(1)表示引用第一部分中的ls项。各部分一般用数字1-8表示,每一个部分中的各项则按字母顺序排列。在个人CentOS上,这八个部分分别为:


目前,大多数手册都以电子文档的形式提供。因此若是使用的是联机手册,可使用以下命令查看ls命令手册页:man 1 ls 或 man -s1 ls。

因为不一样的UNIX系统的目录项的格式是不一样的,所以上个程序采用opendir,readdir,closedir函数来对该目录进行处理。关于该程序的更多细节将在后续文章进一步介绍。

工做目录:每一个进程都有一个工做目录,有时将其称为当前工做目录。全部相对路径名都从工做目录开始解释。

起始目录:登陆时,工做目录设置为起始目录,起始目录从口令文件中的相应用户的登陆项中取得。

输入与输出:

文件描述符:文件描述符一般是一个小的非负整数,内核用它表示一个特定进程正在访问的文件。当内核打一个已有文件或建立一个新文件时,它返回一个文件描述符。以后读写文件时,就可使用该文件描述符。

标准输入、标准输出、标准出错:按照惯例,每当运行一个新程序时,shell就为其打开三个文件描述符:标准输入、标准输出、标准出错。若是程序中没有作什么特殊处理,则这三个文件描述符都链向终端。大多数shell也提供相应的方法,让这三个文件描述符重定向到某个文件。

不带缓冲的I/O:函数open,read,write,lseek以及close都提供了不用缓冲的I/O,这些函数都使用文件描述符

下列程序将标准输入复制到标准输出:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define BUF_SIZE 4096

int
main(int argc, char *argv[])
{
	int n;
	char buffer[BUF_SIZE];

	while ( (n = read(STDIN_FILENO, buffer, BUF_SIZE)) > 0) {
		if (write(STDOUT_FILENO, buffer, n) != n) {
			printf("write error\n");
			exit(1); 
		}
	}

	if (n < 0) {
		printf("read error\n");
		exit(2); 
	}

	exit(0);
}

头文件<unistd.h>以及两个常量STDIN_FILENO以及STDOUT_FILENO都是POSIX标准的一部分。该头文件包含了许多UNIX系统服务的函数原型。STDIN_FILENO以及STDOUT_FILENO分别指定了标准输入和标准输出的文件描述符。它们的典型值是0和1,可是考虑到可移植性,最好仍是使用标识符。

在shell中运行该程序时,经过重定向,能够用于复制任何一个UNIX普通文件。

标准I/O:标准I/O函数提供一种对不带缓冲I/O函数的带缓冲的接口。使用标准I/O函数能够无需担忧如何选取最佳的缓冲区大小,并且简化了对输入行的处理。

下列程序用标准I/O函数实现了将标准输入复制到标准输出:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>
#include <stdlib.h>


int
main(int argc, char *argv[])
{
	int c;

	while ( (c = getc(stdin)) != EOF) {
		if (putc(c, stdout) == EOF) {
			printf("put char error\n");
			exit(1);
		}
	}

	if(ferror(stdin)) {
		printf("get char error\n");
		exit(2);
	}

	exit(0);
}

getc一次读取一个字符,putc将该字符写到标准输出。程序中的标准输入常量 stdin,标准输出常量stdout,以及文件结束符EOF,都定义在<stdio.h>中。

程序和进程

程序:程序是存在磁盘上、处于某个目录中的可执行文件。使用6个exec函数中的一个由内核将该程序读入存储器,并使其执行。

进程和进程ID:程序的执行实例被称为进程。UNIX系统确保每一个进程都有一个惟一的数字标识符,称为进程ID。进程ID老是一个非负整数。

下列程序经过调用getpid函数来获取本身的进程ID:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>


int main(void)
{
	printf("hello world from process ID : %d\n", getpid());
	exit(0);
}

进程控制:有三个用于进程控制的主要函数:fork,exec,waitpid。(exec函数有6种变体,但把它们统称为exec函数)。

下列程序展现UNIX系统的进程控制功能,该程序从标准输入中读取命令,而后执行这些命令,是一个相似于shell的简单实现:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>


#define MAX_LINE 32

int main(void)
{
	char buf[MAX_LINE];
	pid_t pid;
	int status;

	printf("%% ");
	while (fgets(buf, MAX_LINE, stdin) != NULL) {
		if (buf[strlen(buf) - 1] == '\n') {
			buf[strlen(buf) - 1] = '\0';
		}

		if ( (pid = fork()) < 0) {
			printf("fork error");
			exit(1);
		} else if (pid == 0) {
			/* child process */
			execlp(buf, buf, (char*)0);
			printf("can't execute %s\n", buf);
			exit(2);
		} 

		if ( (pid = waitpid(pid, &status, 0)) < 0) {
			printf("waitpid error\n");
			exit(3);
		}
		printf("%% ");
	}

	exit(0);
}

该程序最主要的部分就是调用fork函数建立一个新进程。新进程是调用进程的复制品,所以调用进程称为父进程,新建立的进程为子进程。fork函数向父进程返回子进程的进程ID(非负),向子进程返回0。因此fork函数是调用一次,返回两次(分别在父进程和子进程中)。

在子进程中,调用execlp函数执行从标准输入读入的命令。这就用新的程序文件代替了子进程原先执行的程序文件。而父进程调用waitpid函数等待子进程终止。

关于该程序的更多细节将在后续文章进一步学习。

线程和线程ID:

一般,一个进程只有一个控制线程。可是对于某些问题,若是不一样部分各使用一个控制线程,那么解决问题会更加容易,并且多个控制线程也能充分地利用多处理器系统的并行性。

在一个进程内全部线程共享同一地址空间,文件描述符,以及相关的进程属性。由于全部线程都能访问同一存储区,因此各线程在访问共享数据时须要采起同步措施以免不一致性。

线程也用ID标识,可是线程ID只在它所属的进程内起做用。一个进程中的线程ID在另外一个进程中并没有意义。

在进程模型创建好久以后,线程模型才被引入UNIX系统中,这两个模型间存在复杂的相互做用。