01.《UNIX环境高级编程》学习笔记——UNIX概述

第一章、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种信号处理方式:

  1. 忽略信号;
  2. 按系统默认方式处理,比如收到SIGINT时系统默认终止程序;
  3. 提供一个信号处理函数。

很多情况都会产生信号,中断键盘上有两种产生信号的方法,中断键(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)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值