《Linux/UNIX系统编程手册》读书笔记

2018-1-30

一、UNIX、C语言以及Linux的历史回顾

1. UNIX简史、C语言的诞生

    1969年,贝尔实验室的Ken Thompson首次实现了UNIX系统。

    1973年,C语言步入成熟期,人们以其重写了几乎整个UNIX内核。

2. UNIX两大分支:BSD、System V

    1969~1979年间,UNIX历经了多个版本,其中从第七版起,UNIX分裂为两大分支:BSD和System V。

    加州大学伯克利分校为UNIX开发了许多新特性,然后发布了属于自己的UNIX发布版——BSD。

    AT&T公司被获准销售UNIX,这催生出了另一种UNIX的变种——System V。

3. Linux简史:GNU项目、Linux内核

    GNU项目开发了一套几乎完备且可以自由分发的UNIX实现,但独缺一颗能够有效运作的内核。

    1991年,Linus Torvalds开发出Linux内核,随后许多程序员也加入到了改进内核的行列中。

4. POSIX标准:关于操作系统接口方面

 

二、一系列与Linux系统编程相关的基本概念

1. 内核:操作系统的核心

    指管理和分配计算机资源的核心层软件。即其为管理计算机的有限资源提供了相关的软件层。

    内核的职责包括:进程调度、内存管理、提供文件系统、创建和终止进程、访问设备、提供应用编程接口等。

    内核态&用户态:处理器架构允许CPU在用户态或核心态下运行。

2. shell:命令解释器

    一个具有特殊用途的程序,用于读取用户输入的命令,并执行相应的程序以响应命令。

    “login shell”指的是:用户刚登陆系统时,由系统创建,用以运行shell的进程

3. 文件描述符:指代打开的文件

    获取的方法:调用open()

    由shell启动的进程会继承3个已打开的文件描述符:0为标准输入,指代为进程提供输入的文件;1为标准输出,指代供进程写入输出的文件;2为标准错误,指代供进程写入错误信息或异常通告的文件。在交互式shell或程序中,上述三者指向终端。

4. 程序

    两种形式:源码形式、二进制机器语言指令

5. 进程

    内存布局:程序的指令、数据(程序使用的静态变量)、堆、栈

6. 内存映射

7. 静态库和目标库:一种文件,供其他应用程序调用

8. 信号:软件中断

    进程收到信号,就意味着某一事件或异常情况的发生。

    进程间通信的方式之一。

9. 线程:类似于共享同一虚拟内存及一干其他属性的进程

    执行相同的程序代码,共享同一数据区域和堆,但每个线程都拥有属于自己的(装载本地变量和函数调用链接信息)。

10. /proc文件系统:一种虚拟文件系统

    由一组目录和文件组成,以文件系统目录和文件形式,提供一个指向内核数据结构的接口。

    

2018-1-31

三、掌握系统编程的先决条件

1. 系统调用:内核入口

    借助该机制,进程可以请求内核(以自己的名义)去执行某些动作。  

    幕后步骤:进程通过调用C语言函数库中的外壳函数发起系统调用,外壳函数执行一条中断机器指令,并执行该中断向量所指向的代码。

    小结:进程向内核请求服务,与用户空间的函数调用相比,系统调用会产生显著的开销,因为系统需要临时性地切换到核心态,此外,内核还需验证系统调用的参数、用户内存和内核内存之间也有数据需要传递。

2. 库函数:组成C语言标准函数库

    部分库函数不会使用任何系统调用,其他库函数则构建于系统调用层之上。

    设计库函数是为了提供比底层系统调用更为方便的调用接口。

    小结:标准的C语言函数库提供了大量的库函数,有些库函数会利用系统调用来完成工作,而另一些库函数则完全在用户空间内执行任务。

3. GNU C语言函数库(glibc):Linux上最常用的实现

    获取glibc版本

4. 关于来自系统调用的错误

    系统调用失败(由函数的返回值来表明)后,会将全局整形变量errno设置为一个正值,以标识具体的错误。

    根据errno的值打印错误消息:perror()、strerror()

    小结:大多数系统调用和库函数都会返回一个状态值,以表明调用成功与否。

5. 常用的头文件及错误诊断函数

头文件 功能说明
stdio.h 标准I/O函数
stdlib.h 常用的库函数原型,加上常量EXIT_SUCCESS和EXIT_FAILURE
sys/types.h 许多程序使用到的类型定义
unistd.h 许多系统调用的原型
errno.h 声明errno,定义error常量
string.h 常用的字符串处理函数

    常用的错误诊断函数:errMsg、errExit、err_exit、errExitEN

6. 系统数据类型:降低不同UNIX系统间相互移植的难度

    每种类型的定义均使用C语言的typedef特性,如pid_t数据类型用以表示进程ID。

    大多数命名均以_t结尾,许多都声明于头文件<sys/types.h>中。

    应用程序应采用这些类型定义来声明其使用的变量,才能保证可移植性。如声明“pid_t mypid;”将允许应用程序在任何系统上正确表示进程ID。

    常用系统数据类型的描述见P51。

   打印系统数据类型:强制转换为某一类型。

 

2018-2-8

四、文件I/O

1. 文件描述符

    指代打开的文件,包括管道(PIPE)、FIFO、socket、终端设备和普通文件。

    针对每个进程,文件描述符都自成一套。

    大多数程序都使用的3种标准的文件描述符:0、1、2。细节:在程序运行之前,shell代表程序打开这3个文件描述符。更确切地说,程序继承了shell文件描述符的副本——在shell的日常操作中,这3个文件描述符始终是打开的。(在交互式shell中,这个文件描述符通常指向shell运行所在的终端。)如果命令行指定对输入/输出进行重定向操作,那么shell会对文件描述符做适当修改,然后再启动程序。

2. 文件I/O操作的几个主要系统调用:open()、creat()、read()、write()、close()

    fd = open(pathname, flags, mode)

    numread = read(fd, buffer, count)

    numwritten = write(fd, buffer, count)

    status = close(fd)

3. 打开一个文件:open()

    函数原型:int open(const char *pathname, int flags, mode_t mode);

    功能:open()调用既能打开一个文件,也能创建并打开一个新文件。

    返回:文件描述符;失败则返回-1,并将errno置为相应的错误标志。

    参数:flags用于指定文件的访问模式,而mode则指定了文件的访问权限。如果open()并未指定O_CREAT标志,则可以省略mode参数

    补充:flags参数值可参考P60,调用发生错误时的一些errno值的设置可参考P63。

4. 读取文件内容:read()

    函数原型:ssize_t read(int fd, void *buffer, size_t count);

    功能:从文件描述符fd所指代的打开文件中读取数据。

    返回:实际读取的字节数,遇到文件结束(EOF)则返回0;失败则返回-1。

    参数:count指定最多能读取的字节数,而buffer则提供用来存放数据的内存缓冲区地址(缓冲区至少应有count个字节)。

    补充:系统调用不会分配内存缓冲区用以返回信息给调用者,故需预先分配大小合适的缓冲区并将缓冲区指针传递给系统调用。

5. 数据写入文件:write()

    函数原型:ssize_t write(int fd, void *buffer, size_t count);

    功能:将数据写入一个已打开的文件中。

    返回:实际写入文件的字节数。

    参数:文件描述符fd指代要写入的文件,而buffer则为要写入文件中数据的内存地址,count参数为欲从buffer写入文件的数据字节数。

    补充:对磁盘文件执行I/O操作时,write()调用成功并不能保证数据已经写入磁盘。因为为了减少磁盘活动量和加快write()系统调用,内核会缓存磁盘的I/O操作。

6. 关闭文件:close()

    函数原型:int close(int fd);

    功能:关闭一个打开的文件描述符,并将其释放回调用进程,供该进程继续使用。

    返回:失败则返回-1,错误类型有:企图关闭一个未打开的文件描述符,或者两次关闭同一文件描述符等。

7. 改变文件偏移量:lseek()

    函数原型:off_t lseek(int fd, off_t offset, int whence);

    功能:依照offset和whence参数值调整该文件的偏移量。

    返回:新的文件偏移量。

    参数:参数offset的单位为字节;参数whence表明参考基点,可设置为:SEEK_SET、SEEK_CUR、SEEK_END。

 

2018-2-9

五、深入文件I/O

1. 原子操作

    文件I/O操作存在两种竞争状态,可通过正确使用open()的标志位,来保证操作的原子性。

2. 文件控制操作:fcntl()

    函数原型:int fcntl(int fd, int cmd, ...);

    功能:对(一个打开的)文件描述符执行一系列控制操作。包括修改打开文件的状态标志复制文件描述符

    参数:cmd参数所支持的操作范围很广;第三个参数以省略号来表示,意味着可以将其设置为不同的类型,或者加以省略。

3. fcntl()的用途——获取或修改一个已打开的文件的访问模式和状态标志

    方法:要获取这些设置,应将cmd参数设置为F_GETFL;而要设置文件的状态标志,则cmd应为F_SETFL。

4. 文件描述符和打开文件的关系

    文件描述符标志为进程和文件描述符所私有,对这一标志的修改不会影响同一进程或不同进程中的其他文件描述符。

5. 复制文件描述符:dup()、dup2()

    用途:通过复制文件描述符,达到实现重定向操作的目的。

6. 其他调用:pread()、pwrite()、readv()、writev()、truncate()、ftruncate()

7. 非阻塞I/O:在打开文件时指定O_NONBLOCK标志

    说明:若open调用未能立即打开文件,则返回错误,而非陷入阻塞。

8. /dev/fd目录

    对每个进程,内核都提供有一个特殊的虚拟目录/dev/fd。该目录中包含“/dev/fd/n”形式的文件名,n是与进程中的打开文件描述符相对应的编号。例如,/dev/fd/0对应于进程的标准输入。

    打开/dev/fd目录中的一个文件等同于复制相应的文件描述符。

    用途:用于shell中,将其作为文件名参数传递给shell命令。

 

六、进程之结构篇——虚拟内存的布局及内容

1. 进程的定义:由内核定义的抽象的实体,并为该实体分配用以执行程序的各项系统资源。

    关键点:可以用一个程序来创建许多进程,也即许多进程运行的可以是同一程序

2. 进程号

    数据类型:pid_t

    补充:低数值的进程号为系统进程和守护进程所长期占用。

    父进程:每个进程都由其父进程创建,父进程终止后,子进程就会变为“孤儿”,随即由init进程收养。

3. 进程内存布局:进程分配到的内存的布局

    每个进程分配到的内存的组成部分:文本段、数据段、栈、堆

    文本段:包含了进程运行的程序机器语言指令,其是只读、可共享的。因为多个进程可同时运行同一程序。

    初始化数据段:包含显式初始化的全局变量和静态变量。

    未初始化数据段:包含了未进行显式初始化的全局变量和静态变量。

    栈:一个动态增长和收缩的段,由栈帧组成。系统会为每个当前调用的函数分配一个栈帧,栈帧中存储了函数的局部变量、实参和返回值。

    堆:可在运行时(为变量)动态进行内存分配的一块区域。

 

2018-2-10

4. 虚拟内存管理:进程内存布局存在于虚拟内存

    虚拟内存:利用程序的访问局部性这一特性,使得程序仅有部分地址空间存在于RAM中。

    实现:将每个程序使用的内存切割成小型的、固定大小的“页”单元,相应地将RAM划分成一系列与虚存页尺寸相同的页帧。任一时刻,每个程序仅有部分页驻留在物理内存页帧中,程序未使用的页拷贝保存在交换区(磁盘空间中的保留区域)内,作为RAM的补充——仅在需要时才会载入物理内存。

    页表:内核为每个进程维护一张页表,其描述了每页在进程的虚拟地址空间中的位置。

    进程的虚拟地址空间:进程驻留在RAM中的部分 + 进程保存在交换区的部分

    优点:因为需要驻留在内存中的仅是程序的一部分,所以程序的加载和运行都很快,而且一个进程所占用的内存(即虚拟内存大小)能够超过RAM容量。同时,每个进程使用的RAM减少了,RAM中同时可以容纳的进程数量就增多了。

5. 环境:每个进程都有的称之为环境列表的字符串数组,每个字符串都以名称=值形式定义

    环境:“名称-值”的成对集合,可存储任何信息。而将列表中的名称称为环境变量。

    从程序中访问环境:通过全局变量environ

    修改环境的系统调用:getenv()、putenv()、setenv()、unsetenv()、clearenv()

6. 非局部跳转:setjmp()、longjmp()

    能够从甲函数跳转到乙函数,但这将是程序难于阅读和维护,应尽量避免使用。

 

七、内存分配——在堆或堆栈上分配内存

1. 在堆上分配内存:进程可通过增加堆的大小来分配内存

    堆:一段长度可变的连续虚拟内存,始于进程的未初始化数据段末尾,随着内存的分配和释放而增减。

    program break:堆的当前内存边界,最初其正好位于未初始化数据段末尾之后。

    调整program break的系统调用:brk()和sbrk()

2. 在堆上进行内存分配的系统调用:malloc()、free()

    函数原型:void *malloc(size_t size);

    参数:size为分配的字节数;返回类型void*表示可以将返回值赋给任意类型的C指针。

    函数原型:void free(void *ptr);

    功能:释放ptr所指向的内存块,将这块内存填加到空闲内存列表中。

    补充:进程终止时,其占用的所有内存都会返还给操作系统,包括在堆内存中由malloc函数包内一系列函数所分配的内存。

3. 在堆上分配内存的其他方法:calloc()、realloc()

    函数原型:void *calloc(size_t numitems, size_t size);

    功能:给一组相同对象分配内存,并将已分配的内存初始化为0(而malloc()则不会进行初始化)。

    参数:参数numitems指定分配对象的数量;size指定每个对象的大小。

    函数原型:void realloc(void *ptr, size_t size);

    功能:调整一块内存的大小,此块内存应是之前由malloc包中函数所分配的。

4. 在堆栈上分配内存:alloca()

    通过增加栈帧的大小从堆栈上分配,该类内存会在调用alloca()的函数返回时自动释放。

 

八、用户和组

1. 用于定义用户和组的系统文件

    密码文件:/etc/passwd

    shadow密码文件:/etc/shadow

    组文件:/etc/group

2. 用于从这些系统文件中获取信息的库函数

    这些库函数的功能包括:从密码文件、shadow文件和组文件中获取单条记录,扫描上述各个文件中的所有记录

    库函数:getpwnam()、getpuid()、getgrnam()、getgrgid()等

3. crypt()函数:用于加密和认证登录密码

    描述:UNIX系统采用单向加密算法对密码进行加密,故验证候选密码的唯一方法是使用同一算法对其进行加密,并将加密结果与存储于/etc/shadow中的密码进行匹配。

    加密算法:封装于crypt()函数之中

    函数原型:char *crypt(const char *key, const char *salt);

    好处:对需要认证用户的程序来说极为有用。

 

 

 

    

 

 

 

 

    

 

    

    

 

 

    

 

    

    

 

posted @ 2018-01-31 00:09  GGBeng  阅读(2538)  评论(0编辑  收藏  举报