================
第一课开发环境
================
TIOBE(世界计算机语言排名)
1 - C
2 - Java
3 - Objective C
4 - C++
C/C++/数据结构和算法- 平台无关,算法逻辑
UC/Win32/Android/iOS - 平台相关,系统调用
嵌入式/驱动程序/移植- 硬件相关,硬件接口
一、课程内容
------------
开发环境- 1 天-+
内存管理- 1 天|
文件系统- 2 天|
进程管理- 1 天|
信号处理- 1 天+- 共10 天
进程通信- 1 天|
网络通信- 1 天|
线程管理- 1 天|
线程同步- 1 天-+
二、Unix 操作系统
----------------
1. 简介
~~~~~~~
美国AT&T 公司贝尔实验室,
1971 年,
肯.汤普逊、丹尼斯.里奇。
PDP-11,多用户、多任务、支持多种处理器架构。
高安全性、高可靠性,高稳定性。
既可构建大型关键业务系统的商业服务器应用,
也可构建面向移动终端、手持设备等的嵌入式应用。
图示:pdp-11.jpg
2. 三大派生版本
~~~~~~~~~~~~~~~
1) System V
AIX: IBM,银行
Solaris: SUN->Oracle,电信
HP-UX
IRIX
2) Berkley
FreeBSD
NetBSD
OpenBSD
Mac OS X
3) Hybrid
Minix: 迷你版的类Unix 操作系统。
Linux: GPL,免费开源,商用服务器(RedHat)、
桌面(Ubuntu)、嵌入式(Android)。
3. Unix 族谱
~~~~~~~~~~~
图示:unix_history.png
三、Linux 操作系统
-----------------
1. 简介
~~~~~~~
类Unix 操作系统,免费开源。
不同发行版本使用相同内核。
手机、平板电脑、路由器、视频游戏控制台、台式计算机、
大型计算机、超级计算机。
严格意义上的Linux 仅指操作系统内核。
隶属于GNU 工程。
发明人Linus Torvalds。
图示:linus.jpg
2. 标志
~~~~~~~
Tux (Tuxedo,一只企鹅)
图示:tux.png
3. 相关知识
~~~~~~~~~~~
1) Minix 操作系统
荷兰阿姆斯特丹Vrije 大学,
数学与计算机科学系,
Andrew S. Tanenbaum,
ACM 和IEEE 的资深会员。
2) GNU 工程
Richard Stallman 发起于1984 年,
由自由软件基金会(FSF)提供支持。
GNU 的基本原则就是共享,
其主旨在于发展一个有别于一切商业Unix 系统的,
免费且完整的类Unix 系统――GNU Not Unix。
3) POSIX 标准
Portable Operating System Interface for
Computing Systems,
统一的系统编程接口规范。
由IEEE 和ISO/IEC 开发。
保证应用程序源代码级的可移植性。
Linux 完全遵循POSIX 标准。
4) GPL
通用公共许可证。
允许对某成果及其派生成果的重用、修改和复制,
对所有人都是自由的,但不能声明做了原始工作,
或声明由他人所做。
4. 版本
~~~~~~~
1) 早期版本:0.01, 0.02, ..., 0.99, 1.0
2) 旧计划:介于1.0 和2.6 之间,A.B.C
A: 主版本号,内核大幅更新。
B: 次版本号,内核重大修改,奇数测试版,偶数稳定版。
C: 补丁序号,内核轻微修订。
3) 2003 年12 月发布2.6.0 以后:缩短发布周期,A.B.C-D.E
D: 构建次数,反映极微小的更新。
E: 描述信息:
rc/r - 候选版本,其后的数字表示第几个候选版本,
越大越接近正式版本。
smp - 对称多处理器。
pp - Red Hat Linux 的测试版本。
EL - Red Hat Linux 的企业版本。
mm - 测试新技术或新功能。
fc - Red Hat Linux 的Fedora Core 版本。
如:
# cat /proc/version
Linux version 3.6.11-4.fc16.i686
# cat /proc/version
Linux version 3.2.0-39-generic-pae
5. 特点
~~~~~~~
1) 遵循GNU/GPL
2) 开放性
3) 多用户
4) 多任务
5) 设备独立性
6) 丰富的网络功能
7) 可靠的系统安全
8) 良好的可移植性
6. 发行版本
~~~~~~~~~~~
1) 大众的Ubuntu
2) 优雅的Linux Mint
3) 锐意的Fedora
4) 华丽的openSUSE
5) 自由的Debian
6) 简洁的Slackware
7) 老牌的RedHat
四、GNU 编译工具GCC
------------------
1. 支持多种编程语言
~~~~~~~~~~~~~~~~~~~
C、C++、Objective-C、Java、Fortran、Pascal、Ada
2. 支持多种平台
~~~~~~~~~~~~~~~
Unix、Linux、Windows。
3. 构建(Build)过程
~~~~~~~~~~~~~~~~~~
编辑-> 预编译-> 编译-> 汇编-> 链接
1) 编辑: vi hello.c -> hello.c
2) 预编译:gcc -E hello.c -o hello.i -> hello.i -+
3) 编译: gcc -S hello.i -> hello.s | GCC
4) 汇编: gcc -c hello.s -> hello.o | 工具链
5) 链接: gcc hello.o -o hello -> hello -+
范例:hello.c
4. 查看版本
~~~~~~~~~~~
gcc -v
5. 文件后缀
~~~~~~~~~~~
.h - C 语言源代码头文件
.c - 预处理前的C 语言源代码文件
.i - 预处理后的C 语言源代码文件
.s - 汇编语言文件
.o - 目标文件
.a - 静态库文件
.so - 共享库(动态库)文件
6. 编译单个源程序
~~~~~~~~~~~~~~~~~
gcc [选项参数] 文件
-c - 只编译不链接。
-o - 指定输出文件。
-E - 预编译。
-S - 产生汇编文件。
-pedantic - 对不符合ANSI/ISO C 语言标准的
扩展语法产生警告。
-Wall - 产生尽可能多的警告。
范例:gcc -Wall wall.c
-Werror - 将警告作为错误处理。
范例:gcc -Werror werror.c
-x - 指定源代码的语言。
范例:gcc -x c++ cpp.c -lstdc++
-g - 生成调试信息。
-O1/O2/O3 - 优化等级。
7. 编译多个源程序
~~~~~~~~~~~~~~~~~
gcc [选项参数] 文件1 文件2 ...
思考:头文件的作用是什么?
1) 声明外部变量、函数和类。
2) 定义宏、类型别名和自定义类型。
3) 包含其它头文件。
4) 借助头文件卫士,防止因同一个头文件被多次包含,
而引发重定义错。
包含头文件时需要注意:
1) gcc 的-I 选项
指定头文件附加搜索路径。
2) #include <...>
先找-I 指定的目录,再找系统目录。
3) #include "..."
先找-I 指定的目录,再找当前目录,最后找系统目录。
4) 头文件的系统目录
/usr/include
/usr/local/include
/usr/lib/gcc/i686-linux-gnu/4.6.3/include
/usr/include/c++/4.6.3 (C++编译器优先查找此目录)
范例:calc.h、calc.c、math.c
math.c 中不包含calc.h,输出0.000000。
参数和返回值均按int 处理。
math.c 中包含calc.h,输出30.000000。
参数和返回值均按double 处理。
8. 预处理指令
~~~~~~~~~~~~~
#include // 将指定文件的内容插至此指令处
#include_next // 与#include 一样,
// 但从当前目录之后的目录查找,极少用
#define // 定义宏
#undef // 删除宏
#if // 判定
#ifdef // 判定宏是否已定义
#ifndef // 判定宏是否未定义
#else // 与#if、#ifdef、#ifndef 结合使用
#elif // else if 多选分支
#endif // 结束判定
## // 连接宏内两个连续的字符串
# // 将宏参数扩展成字符串字面值
#error // 产生错误,结束预处理
#warning // 产生警告
范例:error.c
# gcc error.c -DVERSION=2
error.c:4:3: error: #error "版本太低!"
# gcc error.c -DVERSION=4
error.c:6:3: warning: #warning "版本太高!" [-Wcpp]
# gcc error.c -DVERSION=3
# ./a.out
版本:3
#line // 指定行号
范例:line.c
#pragma // 提供额外信息的标准方法,可用于指定平台
#pragma GCC dependency <文件> // 若<文件>比此文件新
// 则产生警告
#pragma GCC poison <标识> // 若出现<标识>
// 则产生错误
#pragma pack(1/2/4/8) // 按1/2/4/8 字节
// 对齐补齐
范例:pragma.c
9. 预定义宏
~~~~~~~~~~~
__BASE_FILE__ // 正在编译的源文件名
__FILE__ // 所在文件名
__LINE__ // 行号
__FUNCTION__ // 函数名
__func__ // 同__FUNCTION__
__DATE__ // 日期
__TIME__ // 时间
__INCLUDE_LEVEL__ // 包含层数,从0 开始
__cplusplus // C++编译器将其定义为1,
// C 编译器不定义该宏
范例:print.h、predef.h、predef.c
# gcc predef.c
__BASE_FILE__ : predef.c
__FILE__ : print.h
__LINE__ : 9
__FUNCTION__ : print
__func__ : print
__DATE__ : May 25 2013
__TIME__ : 07:31:39
__INCLUDE_LEVEL__ : 2
# g++ predef.c
__BASE_FILE__ : predef.c
__FILE__ : print.h
__LINE__ : 9
__FUNCTION__ : print
__func__ : print
__DATE__ : May 25 2013
__TIME__ : 07:32:33
__INCLUDE_LEVEL__ : 2
__cplusplus : 1
10. 环境变量
~~~~~~~~~~~~
C_INCLUDE_PATH - C 头文件的附加搜索路径,
相当于gcc 的-I 选项。
CPATH - 同C_INCLUDE_PATH。
CPLUS_INCLUDE_PATH - C++头文件的附加搜索路径。
LIBRARY_PATH - 链接时查找静态库/共享库的路径。
LD_LIBRARY_PATH - 运行时查找共享库的路径。
范例:calc.h、calc.c、cpath.c
# gcc calc.c cpath.c
cpath.c:2:17: fatal error: calc.h: No such file or directory
通过gcc 的-I 选项指定C/C++头文件的附加搜索路径:
# gcc calc.c cpath.c -I.
将当前目录作为C 头文件附加搜索路径,
添加到CPATH 环境变量中:
# export CPATH=$CPATH:. // export 保证当前shell 的
// 子进程继承此环境变量
# echo $CPATH
# env | grep CPATH
也可以在~/.bashrc 或~/.bash_profile
配置文件中写环境变量,持久有效:
export CPATH=$CPATH:.
执行
# source ~/.bashrc
或
# source ~/.bash_profile
生效。以后每次登录自动生效。
头文件的三种定位方式:
1) #include "目录/xxx.h" - 头文件路径发生变化,
需要修改源程序。
2) C_INCLUDE_PATH/CPATH=目录- 同时构建多个工程,
可能引发冲突。
3) gcc -I 目录- 既不用改程序,
也不会有冲突。
五、库
------
1. 合久必分――增量编译――易于维护。
分久必合――库――易于使用。
2. 链接静态库是将库中的被调用代码复制到调用模块中,
而链接共享库则只是在调用模块中,
嵌入被调用代码在库中的(相对)地址。
3. 静态库占用空间非常大,不易修改但执行效率高。
共享库占用空间小,易于修改但执行效率略低。
4. 静态库的缺省扩展名是.a,共享库的缺省扩展名是.so。
六、静态库
----------
1. 创建静态库
~~~~~~~~~~~~~
1) 编辑源程序:.c/.h
2) 编译成目标文件:gcc -c xxx.c -> xxx.o
3) 打包成静态库文件:ar -r libxxx.a xxx.o ...
# gcc -c calc.c
# gcc -c show.c
# ar -r libmath.a calc.o show.o
ar 指令:ar [选项] 静态库文件名目标文件列表
-r - 将目标文件插入到静态库中,已存在则更新。
-q - 将目标文件追加到静态库尾。
-d - 从静态库中删除目标文件。
-t - 列表显示静态库中的目标文件。
-x - 将静态库展开为目标文件。
注意:提供静态库的同时也需要提供头文件。
2. 调用静态库
~~~~~~~~~~~~~
# gcc main.c libmath.a (直接法)
或通过LIBRARY_PATH 环境变量指定库路径:
# export LIBRARY_PATH=$LIBRARY_PATH:.
# gcc main.c -lmath (环境法)
或通过gcc 的-L 选项指定库路径:
# unset LIBRARY_PATH
# gcc main.c -lmath -L. (参数法)
一般化的方法:gcc .c/.o -l<库名> -L<库路径>
3. 运行
~~~~~~~
# ./a.out
在可执行程序的链接阶段,已将所调用的函数的二进制代码,
复制到可执行程序中,因此运行时不需要依赖静态库。
范例:static/
七、共享库
----------
1. 创建共享库
~~~~~~~~~~~~~
1) 编辑源程序:.c/.h
2) 编译成目标文件:gcc -c -fpic xxx.c -> xxx.o
3) 链接成共享库文件:gcc -shared xxx.o ... -o libxxx.so
# gcc -c -fpic calc.c
# gcc -c -fpic show.c
# gcc -shared calc.o show.o -o libmath.so
或一次完成编译和链接:
# gcc -shared -fpic calc.c show.c -o libmath.so
PIC (Position Independent Code):位置无关代码。
可执行程序加载它们时,可将其映射到其地址空间的
任何位置。
-fPIC - 大模式,生成代码比较大,运行速度比较慢,
所有平台都支持。
-fpic - 小模式,生成代码比较小,运行速度比较快,
仅部分平台支持。
注意:提供共享库的同时也需要提供头文件。
2. 调用共享库
~~~~~~~~~~~~~
# gcc main.c libmath.so (直接法)
或通过LIBRARY_PATH 环境变量指定库路径:
# export LIBRARY_PATH=$LIBRARY_PATH:.
# gcc main.c -lmath (环境法)
或通过gcc 的-L 选项指定库路径:
# unset LIBRARY_PATH
# gcc main.c -lmath -L. (参数法)
一般化的方法:gcc .c/.o -l<库名> -L<库路径>
3. 运行
~~~~~~~
运行时需要保证LD_LIBRARY_PATH
环境变量中包含共享库所在的路径:
# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
# ./a.out
在可执行程序的链接阶段,
并不将所调用函数的二进制代码复制到可执行程序中,
而只是将该函数在共享库中的地址嵌入到可执行程序中,
因此运行时需要依赖共享库。
范例:shared/
gcc 缺省链接共享库,可通过-static 选项强制链接静态库。
如:gcc -static hello.c
八、动态加载共享库
------------------
#include <dlfcn.h>
1. 加载共享库
~~~~~~~~~~~~~
void* dlopen (
const char* filename, // 共享库路径,
// 若只给文件名,
// 则根据LD_LIBRARY_PATH
// 环境变量搜索
int flag // 加载方式
);
成功返回共享库句柄,失败返回NULL。
flag 取值:
RTLD_LAZY - 延迟加载,使用共享库中的符号
(如调用函数)时才加载。
RTLD_NOW - 立即加载。
2. 获取函数地址
~~~~~~~~~~~~~~~
void* dlsym (
void* handle, // 共享库句柄
const char* symbol // 函数名
);
成功返回函数地址,失败返回NULL。
3. 卸载共享库
~~~~~~~~~~~~~
int dlclose (
void* handle // 共享库句柄
);
成功返回0,失败返回非零。
4. 获取错误信息
~~~~~~~~~~~~~~~
char* dlerror (void);
有错误发生则返回错误信息字符串指针,否则返回NULL。
范例:load.c
注意:链接时不再需要-lmath,但需要-ldl。
九、辅助工具
------------
nm: 查看目标文件、可执行文件、静态库、
共享库中的符号列表。
ldd: 查看可执行文件和共享库的动态依赖。
ldconfig: 共享库管理。
事先将共享库的路径信息写入/etc/ld.so.conf 配置文件中,
ldconfig 根据该配置文件生成/etc/ld.so.cache 缓冲文件,
并将该缓冲文件载入内存,借以提高共享库的加载效率。
系统启动时自动执行ldconfig,但若修改了共享库配置,
则需要手动执行该程序。
strip: 减肥。去除目标文件、可执行文件、
静态库和共享库中的符号列表、调试信息等。
objdump: 显示二进制模块的反汇编信息。
# objdump -S a.out
指令地址机器指令汇编指令
-------- -------------------- ---------------------
8048514: 55 push %ebp
8048515: 89 e5 mov %esp,%ebp
8048517: 83 e4 f0 and $0xfffffff0,%esp
804851a: 83 ec 20 sub $0x20,%esp
804851d: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
作业:编写一个函数diamond(),打印一个菱形,其高度、
宽度、实心或者空心以及图案字符,均可通过参数设置。
分别封装为静态库libdiamond_static.a 和
动态库libdiamond_shared.so,并调用之。
代码:diamond/
================
第二课内存管理
================
一、错误处理
------------
1. 通过函数的返回值表示错误
~~~~~~~~~~~~~~~~~~~~~~~~~~~
1) 返回合法值表示成功,返回非法值表示失败。
范例:bad.c
2) 返回有效指针表示成功,
返回空指针(NULL/0xFFFFFFFF)表示失败。
范例:null.c
3) 返回0 表示成功,返回-1 表示失败,
不输出数据或通过指针/引用型参数输出数据。
范例:fail.c
4) 永远成功,如:printf()。
练习:实现四个函数
slen() - 求字符串的长度,若为空指针,则报错。
scpy() - 字符串拷贝,考虑缓冲区溢出,
成功返回目标缓冲区地址,
目标缓冲区无效时报错。
intmin() - 求两个整数的最小值,若二者相等,则报错。
intave() - 求两个整数的平均值,考虑求和溢出,
该函数不会失败。
代码:error.c
2. 通过errno 表示错误
~~~~~~~~~~~~~~~~~~~~
#include <errno.h>
1) 根据errno 得到错误编号。
2) 将errno 转换为有意义的字符串:
#include <string.h>
char* strerror (int errnum);
#include <stdio.h>
void perror (const char* s);
printf ("%m");
范例:errno.c
3) errno 在函数执行成功的情况下不会被修改,
因此不能以errno 非零,作为发生错误判断依据。
范例:iferr.c
4) errno 是一个全局变量,其值随时可能发生变化。
二、环境变量
------------
1. 环境表
~~~~~~~~~
1) 每个程序都会接收到一张环境表,
是一个以NULL 指针结尾的字符指针数组。
2) 全局变量environ 保存环境表的起始地址。
+---+
environ -> | * --> HOME=/root
+---+
| * --> SHELL=/bin/bash
+---+
| * --> PATH=/bin:/usr/bin:...:.
+---+
| . |
| . |
| . |
+---+
| 0 |
+---+
图示:env_list.bmp
2. 环境变量函数
~~~~~~~~~~~~~~~
#include <stdlib.h>
环境变量:name=value
getenv - 根据name 获得value。
putenv - 以name=value 的形式设置环境变量,
name 不存在就添加,存在就覆盖其value。
setenv - 根据name 设置value,注意最后一个参数表示,
若name 已存在是否覆盖其value。
unsetenv - 删除环境变量。
clearenv - 清空环境变量,environ==NULL。
范例:env.c
三、内存管理
------------
+----+--------+----------------------------+----------+
| 用| STL | 自动分配/释放内存资源| 调C++ |
| | C++ | new/delete,构造/析构| 调标C |
| 户| 标C | malloc/calloc/realloc/free | 调POSIX |
| | POSIX | brk/sbrk | 调Linux |
| 层| Linux | mmap/munmap | 调Kernel |
+----+--------+----------------------------+----------+
| 系| Kernel | kmalloc/vmalloc | 调Driver |
| 统| Driver | get_free_page | ... |
| 层| ... | ... | ... |
+----+--------+----------------------------+----------+
四、进程映像
------------
1. 程序是保存在磁盘上的可执行文件。
2. 运行程序时,需要将可执行文件加载到内存,形成进程。
3. 一个程序(文件)可以同时存在多个进程(内存)。
4. 进程在内存空间中的布局就是进程映像。
从低地址到高地址依次为:
代码区(text):可执行指令、字面值常量、
具有常属性的全局和静态局部变量。只读。
数据区(data):初始化的全局和静态局部变量。
BSS 区:未初始化的全局和静态局部变量。
进程一经加载此区即被清0。
数据区和BSS 区有时被合称为全局区或静态区。
堆区(heap):动态内存分配。从低地址向高地址扩展。
栈区(stack):非静态局部变量,
包括函数的参数和返回值。从高地址向低地址扩展。
堆区和栈区之间存在一块间隙,
一方面为堆和栈的增长预留空间,
同时共享库、共享内存等亦位于此。
命令行参数与环境区:命令行参数和环境变量。
图示:maps.bmp
范例:maps.c
比对/proc/<pid>/maps
# size a.out
text data bss dec hex filename
2628 268 28 2924 b6c a.out
| | | | |
+-------+-------+ (10) +---+---+ (16)
V ^
+-------------------+
(+)
五、虚拟内存
------------
1. 每个进程都有各自互独立的4G 字节虚拟地址空间。
2. 用户程序中使用的都是虚拟地址空间中的地址,
永远无法直接访问实际物理内存地址。
3. 虚拟内存到物理内存的映射由操作系统动态维护。
4. 虚拟内存一方面保护了操作系统的安全,
另一方面允许应用程序,
使用比实际物理内存更大的地址空间。
图示:vm.png
5. 4G 进程地址空间分成两部分:
[0, 3G)为用户空间,
如某栈变量的地址0xbfc7fba0=3,217,554,336,约3G;
[3G, 4G)为内核空间。
6. 用户空间中的代码,
不能直接访问内核空间中的代码和数据,
但可以通过系统调用进入内核态,
间接地与系统内核交互。
图示:kernel.png
7. 对内存的越权访问,
或试图访问没有映射到物理内存的虚拟内存,
将导致段错误。
8. 用户空间对应进程,进程一切换,用户空间即随之变化。
内核空间由操作系统内核管理,不会随进程切换而改变。
内核空间由内核根据独立且唯一的页表init_mm.pgd
进行内存映射,而用户空间的页表则每个进程一份。
9. 每个进程的内存空间完全独立。
不同进程之间交换虚拟内存地址是毫无意义的。
范例:vm.c
10. 标准库内部通过一个双向链表,
管理在堆中动态分配的内存。
malloc 函数分配内存时会附加若干(通常是12 个)字节,
存放控制信息。
该信息一旦被意外损坏,可能在后续操作中引发异常。
范例:crash.c
11. 虚拟内存到物理内存的映射以页(4K=4096 字节)为单位。
通过malloc 函数首次分配内存,至少映射33 页。
即使通过free 函数释放掉全部内存,
最初的33 页仍然保留。
图示:address_space.png
#include <unistd.h>
int getpagesize (void);
返回内存页的字节数。
范例:page.c
char* pc = malloc (sizeof (char));
|
v<--------------- 33 页--------------->|
------+-------+----------+-------------------+------
| 1 字节| 控制信息| |
------+-------+----------+-------------------+------
^ ^ ^ ^ ^
段错误OK 后续错误不稳定段错误
六、内存管理APIs
----------------
1. 增量方式分配虚拟内存
~~~~~~~~~~~~~~~~~~~~~~~
#include <unistd.h>
void* sbrk (
intptr_t increment // 内存增量(以字节为单位)
);
返回上次调用brk/sbrk 后的末尾地址,失败返回-1。
increment 取值:
0 - 获取末尾地址。
>0 - 增加内存空间。
<0 - 释放内存空间。
内部维护一个指针,
指向当前堆内存最后一个字节的下一个位置。
sbrk 函数根据增量参数调整该指针的位置,
同时返回该指针原来的位置。
若发现页耗尽或空闲,则自动追加或取消页映射。
void* p=sbrk(4); p=sbrk(0);
^ ^
| |
返回*-- increment ->* 返回
| |
v v
--+---+---+---+---+---+---+--
| B | B | B | B | B | B |
--+---+---+---+---+---+---+--
|<--------- 页--------
2. 修改虚拟内存块末尾地址
~~~~~~~~~~~~~~~~~~~~~~~~~
#include <unistd.h>
int brk (
void* end_data_segment // 内存块末尾地址
);
成功返回0,失败返回-1。
内部维护一个指针,
指向当前堆内存最后一个字节的下一个位置。
brk 函数根据指针参数设置该指针的位置。
若发现页耗尽或空闲,则自动追加或取消页映射。
void* p=sbrk(0); brk(p+4);
^ |
| v
返回* * 设置
| |
v v
--+---+---+---+---+---+---+--
| B | B | B | B | B | B |
--+---+---+---+---+---+---+--
|<--------- 页--------
sbrk/brk 底层维护一个指针位置,
以页(4K)为单位分配和释放虚拟内存。
简便起见,可用sbrk 分配内存,用brk 释放内存。
范例:brk.c、malloc.c
3. 创建虚拟内存到物理内存或文件的映射
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~
#include <sys/mman.h>
void* mmap (
void* start, // 映射区内存起始地址,
// NULL 系统自动选定,成功返回之
size_t length, // 字节长度,自动按页(4K)对齐
int prot, // 映射权限
int flags, // 映射标志
int fd, // 文件描述符
off_t offset // 文件偏移量,自动按页(4K)对齐
);
成功返回映射区内存起始地址,失败返回MAP_FAILED(-1)。
prot 取值:
PROT_EXEC - 映射区域可执行。
PROT_READ - 映射区域可读取。
PROT_WRITE - 映射区域可写入。
PROT_NONE - 映射区域不可访问。
flags 取值:
MAP_FIXED - 若在start 上无法创建映射,
则失败(无此标志系统会自动调整)。
MAP_SHARED - 对映射区域的写入操作直接反映到文件中。
MAP_PRIVATE - 对映射区域的写入操作只反映到缓冲区中,
不会真正写入文件。
MAP_ANONYMOUS - 匿名映射,
将虚拟地址映射到物理内存而非文件,
忽略fd。
MAP_DENYWRITE - 拒绝其它对文件的写入操作。
MAP_LOCKED - 锁定映射区域,保证其不被置换。
4. 销毁虚拟内存到物理内存或文件的映射
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~
int munmap (
void* start, // 映射区内存起始地址
size_t length, // 字节长度,自动按页(4K)对齐
);
成功返回0,失败返回-1。
范例:mmap.c
mmap/munmap 底层不维护任何东西,只是返回一个首地址,
所分配内存位于堆中。
brk/sbrk 底层维护一个指针,记录所分配的内存结尾,
所分配内存位于堆中,底层调用mmap/munmap。
malloc 底层维护一个双向链表和必要的控制信息,
不可越界访问,所分配内存位于堆中,底层调用brk/sbrk。
每个进程都有4G 的虚拟内存空间,
虚拟内存地址只是一个数字,
并没有和实际的物理内存将关联。
所谓内存分配与释放,
其本质就是建立或取消虚拟内存和物理内存间的映射关系。
作业:实现一个基于顺序表的堆栈类模板,
其数据缓冲区内存可根据数据元素的多少自动增减,
但不得使用标准C 的内存分配与释放函数。
代码:stack.cpp
思考:该堆栈模板是否适用于类类型的数据元素。
====================
第三课文件系统(上)
====================
一、系统调用
------------
应用程序-----------+
| |
v |
各种库|
(C/C++标准库、Shell 命令和脚本、|
X11 图形程序及库) |
| |
v |
系统调用<----------+
(内核提供给外界访问的接口函数,
调用这些函数将使进程进入内核态)
|
v
内核
(驱动程序、系统功能程序)
1. Unix/Linux 大部分系统功能是通过系统调用实现的。
如:open/close。
2. Unix/Linux 的系统调用已被封装成C 函数的形式,
但它们并不是标准C 的一部分。
3. 标准库函数大部分时间运行在用户态,
但部分函数偶尔也会调用系统调用,进入内核态。
如:malloc/free。
4. 程序员自己编写的代码也可以调用系统调用,
与操作系统内核交互,进入内核态。
如:brk/sbrk/mmap/munmap。
5. 系统调用在内核中实现,其外部接口定义在C 库中。
该接口的实现借助软中断进入内核。
time 命令:测试运行时间
real : 总执行时间
user : 用户空间执行时间
sys : 内核空间执行时间
strace 命令:跟踪系统调用
二、文件系统
------------
图示:fs.bmp
三、一切皆文件
--------------
1. Linux 环境中的文件具有特别重要的意义,
因为它为操作系统服务和设备,
提供了一个简单而统一的接口。
在Linux 中,(几乎)一切皆文件。
2. 程序完全可以象访问普通磁盘文件一样,
访问串行口、网络、打印机或其它设备。
3. 大多数情况下只需要使用五个基本系统调用:
open/close/read/write/ioctl,
即可实现对各种设备的输入和输出。
4. Linux 中的任何对象都可以被视为某种特定类型的文件,
可以访问文件的方式访问之。
5. 广义的文件
1) 目录文件
# vim day01
图示:dir.bmp
2) 设备文件
A. 控制台:/dev/console
B. 声卡:/dev/audio
C. 标准输入输出:/dev/tty
D. 空设备:/dev/null
例如:
# cat /dev/tty
Hello, World !
Hello, World !
# echo Hello, World ! > /dev/tty
Hello, World !
# echo Hello, World ! > test.txt
# cat test.txt
Hello, World !
# cat /dev/null > test.txt
# cat test.txt
# find / -name perl 2> /dev/null
四、文件相关系统调用
--------------------
open - 打开/创建文件
creat - 创建空文件
close - 关闭文件
read - 读取文件
write - 写入文件
lseek - 设置读写位置
fcntl - 修改文件属性
unlink - 删除硬链接
rmdir - 删除空目录
remove - 删除硬链接(unlink)或空目录(rmdir)
注意:
1. 如果被unlink/remove 删除的是文件的最后一个硬链接,
并且没有进程正打开该文件,
那么该文件在磁盘上的存储区域将被立即标记为自由。
反之,如果有进程正打开该文件,
那么该文件在磁盘上的存储区域,
将在所有进程关闭该文件之后被标记为自由。
a -> +-----+
X b -> | ... |
X c -> +-----+
2. 如果被unlink/remove 删除的是一个软链接文件,
那么仅软链接文件本身被删除,其目标不受影响。
+-----+
a -> | ... |
+-----+
+-----+
X b -> | a |
+-----+
+-----+
X c -> | a |
+-----+
五、文件描述符
--------------
1. 非负的整数。
2. 表示一个打开的文件。
3. 由系统调用(open)返回,
被内核空间(后续系统调用)引用。
4. 内核缺省为每个进程打开三个文件描述符:
0 - 标准输入
1 - 标准输出
2 - 标准出错
在unistd.h 中被定义为如下三个宏:
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
范例:redir.c
# a.out 0<i.txt 1>o.txt 2>e.txt
5. 文件描述符的范围介于0 到OPEN_MAX 之间,
传统Unix 中OPEN_MAX 宏被定义为63,
现代Linux 使用更大的上限。
六、open/creat/close
--------------------
#include <fcntl.h>
int open (
const char* pathname, // 文件路径
int flags, // 状态标志
mode_t mode // 权限模式(仅创建文件有效)
); // 创建/读写文件时都可用此函数
int creat (
const char* pathname, // 文件路径
mode_t mode // 权限模式
); // 常用于创建文件
int open (
const char* pathname, // 文件路径
int flags // 状态标志
); // 常用于读写文件
成功返回文件描述符,失败返回-1。
flags 为以下值的位或:
O_RDONLY - 只读。\
|
O_WRONLY - 只写。> 只选一个
|
O_RDWR - 读写。/
O_APPEND - 追加。
O_CREAT - 创建。不存在即创建(已存在即直接打开,
并保留原内容,除非...),
有此位mode 参数才有效。
O_EXCL - 排斥。已存在即失败。\
> 只选一个,
O_TRUNC - 清空。已存在即清空/ 配合O_CREAT 使用
(有O_WRONLY/O_RDWR)。
O_NOCTTY - 若pathname 指向一个终端设备,
则该终端不会成为调用进程的控制终端。
O_NONBLOCK - 非阻塞。若pathname 指向FIFO/块/字符文件,
则该文件的打开及后续操作均为非阻塞模式。
O_SYNC - 写同步。write 等待数据和属性,
被物理地写入底层硬件后再返回。
O_DSYNC - 数据写同步。write 等待数据,
被物理地写入底层硬件后再返回。
O_RSYNC - 读同步。read 等待对所访问区域的所有写操作,
全部物理地写入底层硬件后,再读取并返回。
O_ASYNC - 异步读写。当文件描述符可读/写时,
向调用进程发送SIGIO 信号。
open/creat 所返回的一定是当前未被使用的,
最小文件描述符。
一个进程可以同时打开的文件描述符个数,
受limits.h 中定义的OPEN_MAX 宏的限制,
POSIX 要求不低于16,传统Unix 是63,现代Linux 是256。
#include <unistd.h>
int close (
int fd // 文件描述符
);
成功返回0,失败返回-1。
范例:open.c
操作系统可通过权限掩码(当前为0022),
屏蔽程序所创建文件的某些权限位。如:
0666 (rw-rw-rw-) & ~0022 = 0644 (rw-r--r--)
creat 函数是通过调用open 实现的。
int creat (const char* pathname, mode_t mode) {
return open (pathname,
O_WRONLY | O_CREAT | O_TRUNC, mode);
}
七、write
---------
#include <unistd.h>
ssize_t write (
int fd, // 文件描述符
const void* buf, // 缓冲区
size_t count // 期望写入的字节数
);
成功返回实际写入的字节数(0 表示未写入),失败返回-1。
size_t: unsigned int,无符号整数
ssize_t: int,有符号整数
范例:write.c
八、read
--------
#include <unistd.h>
ssize_t read (
int fd, // 文件描述符
void* buf, // 缓冲区
size_t count // 期望读取的字节数
);
成功返回实际读取的字节数(0 表示读到文件尾),
失败返回-1。
范例:read.c
二进制读写和文本读写。
范例:binary.c、text.c
练习:带覆盖检查的文件复制。
代码:copy.c
九、系统I/O 与标准I/O
--------------------
1. 当系统调用函数被执行时,需要切换用户态和内核态,
频繁调用会导致性能损失。
2. 标准库做了必要的优化,内部维护一个缓冲区,
只在满足特定条件时才将缓冲区与系统内核同步,
借此降低执行系统调用的频率,
减少进程在用户态和内核态之间来回切换的次数,
提高运行性能。
范例:sysio.c、stdio.c
# time ./sysio
real 0m17.442s
user 0m0.000s
sys 0m0.284s
# time ./stdio
real 0m0.056s
user 0m0.000s
sys 0m0.009s
十、lseek
---------
1. 每个打开的文件都有一个与其相关的“文件位置”。
2. 文件位置通常是一个非负整数,
用以度量从文件头开始计算的字节数。
3. 读写操作都从当前文件位置开始,
并根据所读写的字节数,增加文件位置。
4. 打开一个文件时,除非指定了O_APPEND,
否则文件位置一律被设为0。
5. lseek 函数仅将文件位置记录在内核中,
并不引发任何I/O 动作。
6. 在超越文件尾的文件位置写入数据,
将在文件中形成空洞。
7. 文件空洞不占用磁盘空间,但被算在文件大小内。
#include <sys/types.h>
#include <unistd.h>
off_t lseek (
int fd, // 文件描述符
off_t offset, // 偏移量
int whence // 起始位置
);
成功返回当前文件位置,失败返回-1。
whence 取值:
SEEK_SET - 从文件头
(文件的第一个字节)。
SEEK_CUR - 从当前位置
(上一次读写的最后一个字节的下一个位置)。
SEEK_END - 从文件尾
(文件的最后一个字节的下一个位置)。
范例:seek.c
思考:既然lseek 系统调用相当于标C 库函数fseek,
那么是否存在与标C 库函数ftell 相对应的系统调用?
不存在,
因为通过lseek(fd,0,SEEK_CUR)就可以获得当前文件位置。
思考:如何获取文件的大小?
通过lseek(fd,0,SEEK_END)可以获得文件的大小。
十一、打开文件的内核数据结构
--------------------------
通过ls -i 可查看文件的i 节点号。
i 节点记录了文件的属性和数据在磁盘上的存储位置。
目录也是文件,存放路径和i 节点号的映射表。
图示:open.bmp
范例:bad.c
十二、dup/dup2
--------------
#include <unistd.h>
int dup (int oldfd);
int dup2 (int oldfd, int newfd);
成功返回文件描述符oldfd 的副本,失败返回-1。
1. 复制一个已打开的文件描述符。
2. 返回的一定是当前未被使用的最小文件描述符。
3. dup2 可由第二个参数指定描述符的值。
若指定描述符已打开,则先关闭之。
4. 所返回的文件描述符副本,
与源文件描述符,对应同一个文件表。
图示:dup.bmp
范例:dup.c
注意区分通过dup 获得的文件描述符副本,
和两次open 同一个文件的区别:
dup 只复制文件描述符,不复制文件表。
fd1 \
> 文件表-> v 节点-> i 节点
fd2 /
open 创建新文件表,并为其分配新文件描述符。
fd1 -> 文件表1 \
> v 节点-> i 节点
fd2 -> 文件表2 /
图示:same.bmp
范例:same.c
作业:学生管理系统登录模块。
注册- 增加用户名和密码,
登录- 验证用户名和密码,
用户信息保存在文件中。
代码:mis.c
====================
第四课文件系统(下)
====================
一、sync/fsync/fdatasync
------------------------
1. 大多数磁盘I/O 都通过缓冲进行,
写入文件其实只是写入缓冲区,直到缓冲区满,
才将其排入写队列。
2. 延迟写降低了写操作的次数,提高了写操作的效率,
但可能导致磁盘文件与缓冲区数据不同步。
3. sync/fsync/fdatasync 用于强制磁盘文件与缓冲区同步。
4. sync 将所有被修改过的缓冲区排入写队列即返回,
不等待写磁盘操作完成。
5. fsync 只针对一个文件,且直到写磁盘操作完成才返回。
6. fdatasync 只同步文件数据,不同步文件属性。
#include <unistd.h>
void sync (void);
int fsync (
int fd
);
成功返回0,失败返回-1。
int fdatasync (
int fd
);
成功返回0,失败返回-1。
+-fwrite-> 标准库缓冲-fflush-+
sync
应用程序内存-+ +-> 内核缓冲
-fdatasync-> 磁盘(缓冲)
+------------write------------+ fsync
二、fcntl
---------
#include <fcntl.h>
int fcntl (
int fd, // 文件描述符
int cmd, // 操作指令
... // 可变参数,因操作指令而异
);
对fd 文件执行cmd 操作,某些操作需要提供参数。
1. 常用形式
~~~~~~~~~~~
#include <fcntl.h>
int fcntl (int fd, int cmd);
int fcntl (int fd, int cmd, long arg);
成功返回值因cmd 而异,失败返回-1。
cmd 取值:
F_DUPFD - 复制fd 为不小于arg 的文件描述符。
若arg 文件描述符已用,
该函数会选择比arg 大的最小未用值,
而非如dup2 函数那样关闭之。
范例:dup.c
F_GETFD - 获取文件描述符标志。
F_SETFD - 设置文件描述符标志。
目前仅定义了一个文件描述符标志位FD_CLOEXEC:
0 - 在通过execve()函数所创建的进程中,
该文件描述符依然保持打开。
1 - 在通过execve()函数所创建的进程中,
该文件描述符将被关闭。
F_GETFL - 获取文件状态标志。
不能获取O_CREAT/O_EXCL/O_TRUNC。
F_SETFL - 追加文件状态标志。
只能追加O_APPEND/O_NONBLOCK。
范例:flags.c
2. 文件锁
~~~~~~~~~
#include <fcntl.h>
int fcntl (int fd, int cmd, struct flock* lock);
其中:
struct flock {
short int l_type; // 锁的类型:
// F_RDLCK/F_WRLCK/F_UNLCK
// (读锁/写锁/解锁)
short int l_whence; // 偏移起点:
// SEEK_SET/SEEK_CUR/SEEK_END
// (文件头/当前位置/文件尾)
off_t l_start; // 锁区偏移,从l_whence 开始
off_t l_len; // 锁区长度,0 表示锁到文件尾
pid_t l_pid; // 加锁进程,-1 表示自动设置
};
cmd 取值:
F_GETLK - 测试lock 所表示的锁是否可加。
若可加则将lock.l_type 置为F_UNLCK,
否则通过lock 返回当前锁的信息。
F_SETLK - 设置锁定状态为lock.l_type,
成功返回0,失败返回-1。
若因其它进程持有锁而导致失败,
则errno 为EACCES 或EAGAIN。
F_SETLKW - 设置锁定状态为lock.l_type,
成功返回0,否则一直等待,
除非被信号打断返回-1。
1) 既可以锁定整个文件,也可以锁定特定区域。
2) 读锁(共享锁)、写锁(独占锁/排它锁)、解锁。
图示:rwlock.bmp、flock.bmp
3) 文件描述符被关闭(进程结束)时,自动解锁。
4) 劝谏锁(协议锁)、强制锁。
范例:lock1.c、lock2.c
5) 文件锁仅在不同进程间起作用。
6) 通过锁同步多个进程对同一个文件的读写访问。
范例:wlock.c、rlock.c
# wlock 达内科技| # wlock 有限公司
wlock.txt
<乱码>
# wlock 达内科技-l | # wlock 有限公司-l
wlock.txt
达内科技有限公司
-----------------------------------------
# wlock 达内科技有限公司| # rlock
<乱码>
# wlock 达内科技有限公司-l | # rlock -l
达内科技有限公司
三、stat/fstat/lstat
--------------------
获取文件属性。
#include <sys/stat.h>
int stat (
const char* path, // 文件路径
struct stat* buf // 文件属性
);
int fstat (
int fd, // 文件描述符
struct stat* buf // 文件属性
);
int lstat (
const char* path, // 文件路径
struct stat* buf // 文件属性
);
成功返回0,失败返回-1。
stat 函数跟踪软链接,lstat 函数不跟踪软链接。
struct stat {
dev_t st_dev; // 设备ID
ino_t st_ino; // i 节点号
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 属主ID
gid_t st_gid; // 属组ID
dev_t st_rdev; // 特殊设备ID
off_t st_size; // 总字节数
blksize_t st_blksize; // I/O 块字节数
blkcnt_t st_blocks; // 占用块(512 字节)数
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态改变时间
};
st_mode(0TTSUGO)为以下值的位或:
S_IFDIR - 目录\
S_IFREG - 普通文件|
S_IFLNK - 软链接|
S_IFBLK - 块设备> TT (S_IFMT)
S_IFCHR - 字符设备|
S_IFSOCK - Unix 域套接字|
S_IFIFO - 有名管道/
--------------------------------
S_ISUID - 设置用户ID \
S_ISGID - 设置组ID > S
S_ISVTX - 粘滞/
--------------------------------
S_IRUSR(S_IREAD) - 属主可读\
S_IWUSR(S_IWRITE) - 属主可写> U (S_IRWXU)
S_IXUSR(S_IEXEC) - 属主可执行/
--------------------------------
S_IRGRP - 属组可读\
S_IWGRP - 属组可写> G (S_IRWXG)
S_IXGRP - 属组可执行/
--------------------------------
S_IROTH - 其它可读\
S_IWOTH - 其它可写> O (S_IRWXO)
S_IXOTH - 其它可执行/
1. 有关S_ISUID/S_ISGID/S_ISVTX 的说明
1) 具有S_ISUID/S_ISGID 位的可执行文件,
其有效用户ID/有效组ID,
并不取自由其父进程(比如登录shell)所决定的,
实际用户ID/实际组ID,
而是取自该可执行文件的属主ID/属组ID。
如:/usr/bin/passwd
2) 具有S_ISUID 位的目录,
其中的文件或目录除root 外,
只有其属主可以删除。
3) 具有S_ISGID 位的目录,
在该目录下所创建的文件,继承该目录的属组ID,
而非其创建者进程的有效组ID。
4) 具有S_ISVTX 位的可执行文件,
在其首次执行并结束后,
其代码区将被连续地保存在磁盘交换区中,
而一般磁盘文件中的数据块是离散存放的。
因此,下次执行该程序可以获得较快的载入速度。
现代Unix 系统大都采用快速文件系统,
已不再需要这种技术。
5) 具有S_ISVTX 位的目录,
只有对该目录具有写权限的用户,
在满足下列条件之一的情况下,
才能删除或更名该目录下的文件或目录:
A. 拥有此文件;
B. 拥有此目录;
C. 是超级用户。
如:/tmp
任何用户都可在该目录下创建文件,
任何用户对该目录都享有读/写/执行权限,
但除root 以外的任何用户在目录下,
都只能删除或更名属于自己的文件。
2. 常用以下宏辅助分析st_mode
S_ISDIR() - 是否目录
S_ISREG() - 是否普通文件
S_ISLNK() - 是否软链接
S_ISBLK() - 是否块设备
S_ISCHR() - 是否字符设备
S_ISSOCK() - 是否Unix 域套接字
S_ISFIFO() - 是否有名管道
范例:stat.c
四、access
----------
#include <unistd.h>
int access (
const char* pathname, // 文件路径
int mode // 访问模式
);
1. 按实际用户ID 和实际组ID(而非有效用户ID 和有效组ID),
进行访问模式测试。
2. 成功返回0,失败返回-1。
3. mode 取R_OK/W_OK/X_OK 的位或,
测试调用进程对该文件,
是否可读/可写/可执行,
或者取F_OK,测试该文件是否存在。
范例:access.c
五、umask
---------
可以用umask 命令查看/修改当前shell 的文件权限屏蔽字:
# umask
0022
# umask 0033
# umask
0033
#include <sys/stat.h>
mode_t umask (
mode_t cmask // 屏蔽字
);
1. 为进程设置文件权限屏蔽字,并返回以前的值,
此函数永远成功。
2. cmask 由9 个权限宏位或组成(直接写八进制整数形式亦可,
如022 - 屏蔽属组和其它用户的写权限):
S_IRUSR(S_IREAD) - 属主可读
S_IWUSR(S_IWRITE) - 属主可写
S_IXUSR(S_IEXEC) - 属主可执行
------------------------------
S_IRGRP - 属组可读
S_IWGRP - 属组可写
S_IXGRP - 属组可执行
------------------------------
S_IROTH - 其它可读
S_IWOTH - 其它可写
S_IXOTH - 其它可执行
3. 设上屏蔽字以后,此进程所创建的文件,
都不会有屏蔽字所包含的权限。
范例:umask.c
六、chmod/fchmod
----------------
修改文件的权限。
#include <sys/stat.h>
int chmod (
const char* path, // 文件路径
mode_t mode // 文件权限
);
int fchmod (
int fd, // 文件路径
mode_t mode // 文件权限
);
成功返回0,失败返回-1。
mode 为以下值的位或(直接写八进制整数形式亦可,
如07654 - rwSr-sr-T):
S_ISUID - 设置用户ID
S_ISGID - 设置组ID
S_ISVTX - 粘滞
------------------------------
S_IRUSR(S_IREAD) - 属主可读
S_IWUSR(S_IWRITE) - 属主可写
S_IXUSR(S_IEXEC) - 属主可执行
------------------------------
S_IRGRP - 属组可读
S_IWGRP - 属组可写
S_IXGRP - 属组可执行
------------------------------
S_IROTH - 其它可读
S_IWOTH - 其它可写
S_IXOTH - 其它可执行
范例:chmod.c
七、chown/fchown/lchown
-----------------------
# chown <uid>:<gid> <file>
修改文件的属主和属组。
#include <unistd.h>
int chown (
const char* path, // 文件路径
uid_t owner, // 属主ID
gid_t group // 属组ID
);
int fchown (
int fildes, // 文件描述符
uid_t owner, // 属主ID
gid_t group // 属组ID
);
int lchown (
const char* path, // 文件路径(不跟踪软链接)
uid_t owner, // 属主ID
gid_t group // 属组ID
);
成功返回0,失败返回-1。
注意:
1. 属主和属组ID 取-1 表示不修改。
2. 超级用户进程可以修改文件的属主和属组,
普通进程必须拥有该文件才可以修改其属主和属组。
八、truncate/ftruncate
----------------------
修改文件的长度,截短丢弃,加长添零。
#include <unistd.h>
int truncate (
const char* path, // 文件路径
off_t length // 文件长度
);
int ftruncate (
int fd, // 文件描述符
off_t length // 文件长度
);
成功返回0,失败返回-1。
范例:trunc.c、mmap.c
注意:对于文件映射,
私有映射(MAP_PRIVATE)将数据写到缓冲区而非文件中,
只有自己可以访问。
而对于内存映射,
私有(MAP_PRIVATE)和公有(MAP_SHARED)没有区别,
都是仅自己可以访问。
九、link/unlink/remove/rename
-----------------------------
link: 创建文件的硬链接(目录条目)。
unlink: 删除文件的硬链接(目录条目)。
只有当文件的硬链接数降为0 时,文件才会真正被删除。
若该文件正在被某个进程打开,
其内容直到该文件被关闭才会被真正删除。
remove: 对文件同unlink,
对目录同rmdir (不能删非空目录)。
rename: 修改文件/目录名。
#include <unistd.h>
int link (
const char* path1, // 文件路径
const char* path2 // 链接路径
);
int unlink (
const char* path // 链接路径
);
#include <stdio.h>
int remove (
const char* pathname // 文件/目录路径
);
int rename (
const char* old, // 原路径名
const char* new // 新路径名
);
成功返回0,失败返回-1。
注意:硬链接只是一个文件名,即目录中的一个条目。
软链接则是一个独立的文件,
其内容是另一个文件的路径信息。
十、symlink/readlink
--------------------
symlink: 创建软链接。目标文件可以不存在,
也可以位于另一个文件系统中。
readlink: 获取软链接文件本身(而非其目标)的内容。
open 不能打开软链接文件本身。
#include <unistd.h>
int symlink (
const char* oldpath, // 文件路径(可以不存在)
const char* newpath // 链接路径
);
成功返回0,失败返回-1。
ssize_t readlink (
const char* restrict path, // 软链接文件路径
char* restrict buf, // 缓冲区
size_t bufsize // 缓冲区大小
);
成功返回实际拷入缓冲区buf 中软链接文件内容的字节数,
失败返回-1。
范例:slink.c
十一、mkdir/rmdir
-----------------
mkdir: 创建一个空目录。
rmdir: 删除一个空目录。
#include <sys/stat.h>
int mkdir (
const char* path, // 目录路径
mode_t mode // 访问权限,
// 目录的执行权限(x)表示可进入
);
#include <unistd.h>
int rmdir (
const char* path // 目录路径
);
成功返回0,失败返回-1。
十二、chdir/fchdir/getcwd
-------------------------
chdir/fchdir: 更改当前工作目录。
工作目录是进程的属性,只影响调用进程本身。
getcwd: 获取当前工作目录。
#include <unistd.h>
int chdir (
const char* path // 工作目录路径
);
int fchdir (
int fildes // 工作目录描述符(由open 函数返回)
);
成功返回0,失败返回-1。
char* getcwd (
char* buf, // 缓冲区
size_t size // 缓冲区大小
);
成功返回当前工作目录字符串指针,失败返回NULL。
范例:dir.c
十三、opendir/fdopendir/closedir/readdir/rewinddir/telldir/seekdir
------------------------------------------------------------------
opendir/fdopendir: 打开目录流。
closedir: 关闭目录流。
readdir: 读取目录流。
rewinddir: 复位目录流。
telldir: 获取目录流当前位置。
seekdir: 设置目录流当前位置。
#include <sys/types.h>
#include <dirent.h>
DIR* opendir (
const char* name // 目录路径
);
DIR* fdopendir (
int fd // 目录描述符(由open 函数返回)
);
成功返回目录流指针,失败返回NULL。
int closedir (
DIR* dirp // 目录流指针
);
成功返回0,失败返回-1。
struct dirent* readdir (
DIR* dirp // 目录流指针
);
成功返回下一个目录条目结构体的指针,
到达目录尾(不置errno)或失败(设置errno)返回NULL。
struct dirent {
ino_t d_ino; // i 节点号
off_t d_off; // 下一条目的偏移量
// 注意是磁盘偏移量
// 而非内存地址偏移
unsigned short d_reclen; // 记录长度
unsigned char d_type; // 文件类型
char d_name[256]; // 文件名
};
d_type 取值:
DT_DIR - 目录
DT_REG - 普通文件
DT_LNK - 软链接
DT_BLK - 块设备
DT_CHR - 字符设备
DT_SOCK - Unix 域套接字
DT_FIFO - 有名管道
DT_UNKNOWN - 未知
图示:de.bmp
范例:list.c
练习:打印给定路径下的目录树。
代码:tree.c
void rewinddir (
DIR* dirp // 目录流指针
);
long telldir (
DIR* dirp // 目录流指针
);
成功返回目录流的当前位置,失败返回-1。
void seekdir (
DIR* dirp, // 目录流指针
long offset // 位置偏移量
);
目录流:
+-----------------------+ +-----------------------+
| v |
v
+-------+---|---+-----+-------+ +-------+---|---+-----+-------+
+-------
| d_ino | d_off | ... | a.txt | ... | d_ino | d_off | ... | b.txt | ... |
d_ino
+-------+-------+-----+-------+ +-------+-------+-----+-------+
+-------
^ ^
| -- readdir() -> |
范例:seek.c
================
第五课进程管理
================
一、基本概念
------------
1. 进程与程序
~~~~~~~~~~~~~
1) 进程就是运行中的程序。一个运行着的程序,
可能有多个进程。进程在操作系统中执行特定的任务。
2) 程序是存储在磁盘上,
包含可执行机器指令和数据的静态实体。
进程或者任务是处于活动状态的计算机程序。
2. 进程的分类
~~~~~~~~~~~~~
1) 进程一般分为交互进程、批处理进程和守护进程三类。
2) 守护进程总是活跃的,一般是后台运行。
守护进程一般是由系统在开机时通过脚本自动激活启动,
或者由超级用户root 来启动。
3. 查看进程
~~~~~~~~~~~
1) 简单形式
# ps
以简略方式显示当前用户有控制终端的进程信息。
2) BSD 风格常用选项
# ps axu
a - 所有用户有控制终端的进程
x - 包括无控制终端的进程
u - 以详尽方式显示
w - 以更大列宽显示
3) SVR4 风格常用选项
# ps -efl
-e 或-A - 所有用户的进程
-a - 当前终端的进程
-u 用户名或用户ID - 特定用户的进程
-g 组名或组ID - 特定组的进程
-f - 按完整格式显示
-F - 按更完整格式显示
-l - 按长格式显示
4) 进程信息列表
USER/UID: 进程属主。
PID: 进程ID。
%CPU/C: CPU 使用率。
%MEM: 内存使用率。
VSZ: 占用虚拟内存大小(KB)。
RSS: 占用物理内存大小(KB)。
TTY: 终端次设备号,“?”表示无控制终端,如后台进程。
STAT/S: 进程状态。可取如下值:
O - 就绪。等待被调度。
R - 运行。Linux 下没有O 状态,就绪状态也用R 表示。
S - 可唤醒睡眠。系统中断,获得资源,收到信号,
都可被唤醒,转入运行状态。
D - 不可唤醒睡眠。只能被wake_up 系统调用唤醒。
T - 暂停。收到SIGSTOP 信号转入暂停状态,
收到SIGCONT 信号转入运行状态。
W - 等待内存分页(2.6 内核以后被废弃)。
X - 死亡。不可见。
Z - 僵尸。已停止运行,但其父进程尚未获取其状态。
< - 高优先级。
N - 低优先级。
L - 有被锁到内存中的分页。实时进程和定制IO。
s - 会话首进程。
l - 多线程化的进程。
+ - 在前台进程组中。
START/STIME: 进程开始时间。
TIME: 进程运行时间。
COMMAND/CMD: 进程指令。
F: 进程标志。可由下列值取和:
1 - 通过fork 产生但是没有exec。
4 - 拥有超级用户特权。
PPID: 父进程ID。
NI: 进程nice 值,-20 到19,可通过系统调用或命令修改。
PRI: 进程优先级。
静态优先级= 80 + nice,60 到99,值越小优先级越高。
内核在静态优先级的基础上,
根据进程的交互性计算得到实际(动态)优先级,
以体现对IO 消耗型进程的奖励,
和对处理器消耗型进程的惩罚。
ADDR: 内核进程的内存地址。普通进程显示“-”。
SZ: 占用虚拟内存页数。
WCHAN: 进程正在等待的内核函数或事件。
PSR: 进程被绑定到哪个处理器。
4. 父进程、子进程、孤儿进程和僵尸进程
-------------------------------------
内核进程(0)
init(1)
xinetd
in.telnetd <- 用户登录
login
bash
vi
1) 父进程启动子进程后,
子进程在操作系统的调度下与其父进程同时运行。
2) 子进程先于父进程结束,
子进程向父进程发送SIGCHLD(17)信号,
父进程回收子进程的相关资源。
3) 父进程先于子进程结束,子进程成为孤儿进程,
同时被init 进程收养,即成为init 进程的子进程。
4) 子进程先于父进程结束,
但父进程没有回收子进程的相关资源,
该子进程即成为僵尸进程。
5. 进程标识符(进程ID)
~~~~~~~~~~~~~~~~~~~~~
1) 每个进程都有一个以非负整数表示的唯一标识,
即进程ID/PID。
2) 进程ID 在任何时刻都是唯一的,但可以重用,
当一个进程退出时,其进程ID 就可以被其它进程使用。
3) 延迟重用。
a.out - 1000
a.out - 1010
a.out - 1020
...
范例:delay.c
二、getxxxid
------------
#include <unistd.h>
getpid - 获取进程ID
getppid - 获取父进程ID
getuid - 获取实际用户ID
geteuid - 获取有效用户ID
getgid - 获取实际组ID
getegid - 获取有效组ID
范例:id.c
假设a.out 文件的属主和属组都是root。以其它用户身份登录并执行
$ a.out
输出
进程ID:...
父进程ID:...
实际用户ID:1000 - 实际用户ID 取父进程(shell)的实际用户ID
有效用户ID:1000 - 有效用户ID 取实际用户ID
实际组ID:1000 - 实际组ID 取父进程(shell)的实际组ID
有效组ID:1000 - 有效组ID 取实际组ID
执行
# ls -l a.out
输出
-rwxr-xr-x. 1 root root ...
^ ^
为a.out 的文件权限添加设置用户ID 位和设置组ID 位
# chmod u+s a.out
# chmod g+s a.out
执行
# ls -l a.out
输出
-rwsr-sr-x. 1 root root ...
^ ^
以其它用户身份登录并执行
$ a.out
输出
进程ID:...
父进程ID:...
实际用户ID:1000 - 实际用户ID 取父进程(shell)的实际用户ID
有效用户ID:0 - 有效用户ID 取程序文件的属主ID
实际组ID:1000 - 实际组ID 取父进程(shell)的实际组ID
有效组ID:0 - 有效组ID 取程序文件的属组ID
进程的访问权限由其有效用户ID 和有效组ID 决定。
通过此方法可以使进程获得比登录用户更高的权限。
比如通过passwd 命令修改登录口令。
执行
ls -l /etc/passwd
输出
-rw-r--r--. 1 root root 1648 Nov 9 14:05 /etc/passwd
^
该文件中存放所有用户的口令信息,仅root 用户可写,
但事实上任何用户都可以修改自己的登录口令,
即任何用户都可以通过/usr/bin/passwd 程序写该文件。
执行
# ls -l /usr/bin/passwd
输出
-rwsr-xr-x. 1 root root 28816 Feb 8 2011 /usr/bin/passwd
^ ^
该程序具有设置用户ID 位,且其属主为root。
因此以任何用户登录系统,执行passwd 命令所启动的进程,
其有效用户ID 均为root,对/etc/passwd 文件有写权限。
三、fork
--------
#include <unistd.h>
pid_t fork (void);
1. 创建一个子进程,失败返回-1。
2. 调用一次,返回两次。
分别在父子进程中返回子进程的PID 和0。
利用返回值的不同,
可以分别为父子进程编写不同的处理分支。
范例:fork.c
3. 子进程是父进程的副本,
子进程获得父进程数据段和堆栈段(包括I/O 流缓冲区)的拷贝,
但子进程共享父进程的代码段。
范例:mem.c、os.c、is.c
4. 函数调用后父子进程各自继续运行,
其先后顺序不确定。
某些实现可以保证子进程先被调度。
5. 函数调用后,
父进程的文件描述符表(进程级)也会被复制到子进程中,
二者共享同一个文件表(内核级)。
图示:ftab.bmp
范例:ftab.c
6. 总进程数或实际用户ID 所拥有的进程数,
超过系统限制,该函数将失败。
7. 一个进程如果希望创建自己的副本并执行同一份代码,
或希望与另一个程序并发地运行,都可以使用该函数。
8. 孤儿进程与僵尸进程。
范例:orphan.c、zombie.c
注意:fork 之前的代码只有父进程执行,
fork 之后的代码父子进程都有机会执行,
受代码逻辑的控制而进入不同分支。
四、vfork
---------
#include <unistd.h>
pid_t vfork (void);
该函数的功能与fork 基本相同,二者的区别:
1. 调用vfork 创建子进程时并不复制父进程的地址空间,
子进程可以通过exec 函数族,
直接启动另一个进程替换自身,
进而提高进程创建的效率。
2. vfork 调用之后,子进程先被调度。
五、进程的正常退出
------------------
1. 从main 函数中return。
int main (...) {
...
return x;
}
等价于:
int main (...) {
...
exit (x);
}
2. 调用标准C 语言的exit 函数。
#include <stdlib.h>
void exit (int status);
1) 调用进程退出,
其父进程调用wait/waitpid 函数返回status 的低8 位。
2) 进程退出之前,
先调用所有事先通过atexit/on_exit 函数注册的函数,
冲刷并关闭所有仍处于打开状态的标准I/O 流,
删除所有通过tmpfile 函数创建的文件。
#include <stdlib.h>
int atexit (void (*function) (void));
function - 函数指针,
指向进程退出前需要被调用的函数。
该函数既没有返回值也没有参数。
成功返回0,失败返回非零。
int on_exit (void (*function) (int, void*), void* arg);
function - 函数指针,
指向进程退出前需要被调用的函数。
该函数没有返回值但有两个参数:
第一参数来自exit 函数的status 参数,
第二个参数来自on_exit 函数的arg 参数。
arg - 任意指针,
将作为第二个参数被传递给function 所指向的函数。
成功返回0,失败返回非零。
3) 用EXIT_SUCCESS/EXIT_FAILURE 常量宏
(可能是0/1)作参数,调用exit()函数表示成功/失败,
提高平台兼容性。
4) 该函数不会返回。
5) 该函数的实现调用了_exit/_Exit 函数。
3. 调用_exit/_Exit 函数。
#include <unistd.h>
void _exit (int status);
1) 调用进程退出,
其父进程调用wait/waitpid 函数返回status 的低8 位。
2) 进程退出之前,
先关闭所有仍处于打开状态的文件描述符,
将其所有子进程托付给init 进程(PID 为1 的进程)收养,
向父进程递送SIGCHILD 信号。
3) 该函数不会返回。
4) 该函数有一个完全等价的标准C 版本:
#include <stdlib.h>
void _Exit (int status);
4. 进程的最后一个线程执行了返回语句。
5. 进程的最后一个线程调用pthread_exit 函数。
图示:exit.bmp
范例:exit.c
六、进程的异常终止
------------------
1. 调用abort 函数,产生SIGABRT 信号。
2. 进程接收到某些信号。
3. 最后一个线程对“取消”请求做出响应。
七、wait/waitpid
----------------
等待子进程终止并获取其终止状态。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int* status);
pid_t waitpid (pid_t pid, int* status, int options);
成功返回终止子进程的PID,失败返回-1。
1. 当一个进程正常或异常终止时,
内核向其父进程发送SIGCHLD 信号。
父进程可以忽略该信号,
或者提供一个针对该信号的信号处理函数,默认为忽略。
2. 父进程调用wait 函数:
1) 若所有子进程都在运行,则阻塞。
2) 若有一个子进程已终止,
则返回该子进程的PID 和终止状态(通过status 参数)。
3) 若没有需要等待子进程,则返回失败,errno 为ECHILD。
3. 在任何一个子进程终止前,wait 函数只能阻塞调用进程,
而waitpid 函数可以有更多选择。
4. 如果有一个子进程在wait 函数被调用之前,
已经终止并处于僵尸状态,wait 函数会立即返回,
并取得该子进程的终止状态。
5. 子进程的终止状态通过输出参数status 返回给调用者,
若不关心终止状态,可将此参数置空。
6. 子进程的终止状态可借助
sys/wait.h 中定义的参数宏查看:
WIFEXITED(): 子进程是否正常终止,
是则通过WEXITSTATUS()宏,
获取子进程调用exit/_exit/_Exit 函数,
所传递参数的低8 位。
因此传给exit/_exit/_Exit 函数的参数最好不要超过255。
WIFSIGNALED(): 子进程是否异常终止,
是则通过WTERMSIG()宏获取终止子进程的信号。
WIFSTOPPED(): 子进程是否处于暂停,
是则通过WSTOPSIG()宏获取暂停子进程的信号。
WIFCONTINUED(): 子进程是否在暂停之后继续运行
范例:wait.c、loop.c
7. 如果同时存在多个子进程,又需要等待特定的子进程,
可使用waitpid 函数,其pid 参数:
-1 - 等待任一子进程,此时与wait 函数等价。
> 0 - 等待由该参数所标识的特定子进程。
0 - 等待其组ID 等于调用进程组ID 的任一子进程,
即等待与调用进程同进程组的任一子进程。
<-1 - 等待其组ID 等于该参数绝对值的任一子进程,
即等待隶属于特定进程组内的任一子进程。
范例:waitpid.c
8. waitpid 函数的options 参数可取0(忽略)或以下值的位或:
WNOHANG - 非阻塞模式,
若没有可用的子进程状态,则返回0。
WUNTRACED - 若支持作业控制,且子进程处于暂停态,
则返回其状态。
WCONTINUED - 若支持作业控制,且子进程暂停后继续,
则返回其状态。
范例:nohang.c
八、exec
--------
1. exec 函数会用新进程完全替代调用进程,
并开始从main 函数执行。
2. exec 函数并非创建子进程,新进程取调用进程的PID。
3. exec 函数所创建的新进程,
完全取代调用进程的代码段、数据段和堆栈段。
4. exec 函数若执行成功,则不会返回,否则返回-1。
5. exec 函数包括六种形式:
#include <unistd.h>
int execl (
const char* path,
const char* arg,
...
);
int execv (
const char* path,
char* const argv[]
);
int execle (
const char* path,
const char* arg,
...,
char* const envp[]
);
int execve (
const char* path,
char* const argv[],
char* const envp[]
);
int execlp (
const char* file,
const char* arg,
...
);
int execvp (
const char* file,
char* const argv[]
);
l: 新程序的命令参数以单独字符串指针的形式传入
(const char* arg, ...),参数表以空指针结束。
v: 新程序的命令参数以字符串指针数组的形式传入
(char* const argv[]),数组以空指针结束。
e: 新程序的环境变量以字符串指针数组的形式传入
(char* const envp[]),数组以空指针结束,
无e 则从调用进程的environ 变量中复制。
p: 若第一个参数中不包含“/”,则将其视为文件名,
根据PATH 环境变量搜索该文件。
图示:exec.bmp
范例:argenv.c、exec.c
九、system
----------
#include <stdlib.h>
int system (const char* command);
1. 标准C 函数。执行command,
成功返回command 对应进程的终止状态,失败返回-1。
2. 若command 取NULL,返回非零表示shell 可用,
返回0 表示shell 不可用。
3. 该函数的实现,
调用了fork、exec 和waitpid 等函数,
其返回值:
1) 如果调用fork 或waitpid 函数出错,则返回-1。
2) 如果调用exec 函数出错,则在子进程中执行exit(127)。
3) 如果都成功,则返回command 对应进程的终止状态
(由waitpid 的status 输出参数获得)。
4. 使用system 函数而不用fork+exec 的好处是,
system 函数针对各种错误和信号都做了必要的处理。
图示:system.bmp
范例:system.c、fexec.c
================
第六课信号处理
================
一、基本概念
------------
1. 中断
~~~~~~~
中止(注意不是终止)当前正在执行的程序,
转而执行其它任务。
硬件中断:来自硬件设备的中断。
软件中断:来自其它程序的中断。
2. 信号是一种软件中断
~~~~~~~~~~~~~~~~~~~~~
信号提供了一种以异步方式执行任务的机制。
3. 常见信号
~~~~~~~~~~~
SIGHUP(1):连接断开信号
如果终端接口检测一个连接断开,
则将此信号发送给与该终端相关的控制进程(会话首进程)。
默认动作:终止。
SIGINT(2):终端中断符信号
用户按中断键(Ctrl+C),产生此信号,
并送至前台进程组的所有进程。
默认动作:终止。
SIGQUIT(3):终端退出符信号
用户按退出键(Ctrl+\),产生此信号,
并送至前台进程组的所有进程。
默认动作:终止+core。
SIGILL(4):非法硬件指令信号
进程执行了一条非法硬件指令。
默认动作:终止+core。
SIGTRAP(5):硬件故障信号
指示一个实现定义的硬件故障。常用于调试。
默认动作:终止+core。
SIGABRT(6):异常终止信号
调用abort 函数,产生此信号。
默认动作:终止+core。
SIGBUS(7):总线错误信号
指示一个实现定义的硬件故障。常用于内存故障。
默认动作:终止+core。
SIGFPE(8):算术异常信号
表示一个算术运算异常,例如除以0、浮点溢出等。
默认动作:终止+core。
SIGKILL(9):终止信号
不能被捕获或忽略。常用于杀死进程。
默认动作:终止。
SIGUSR1(10):用户定义信号
用户定义信号,用于应用程序。
默认动作:终止。
SIGSEGV(11):段错误信号
试图访问未分配的内存,或向没有写权限的内存写入数据。
默认动作:终止+core。
SIGUSR2(12):用户定义信号
用户定义信号,用于应用程序。
默认动作:终止。
SIGPIPE(13):管道异常信号
写管道时读进程已终止,
或写SOCK_STREAM 类型套接字时连接已断开,均产生此信号。
默认动作:终止。
SIGALRM(14):闹钟信号
以alarm 函数设置的计时器到期,
或以setitimer 函数设置的间隔时间到期,均产生此信号。
默认动作:终止。
SIGTERM(15):终止信号
由kill 命令发送的系统默认终止信号。
默认动作:终止。
SIGSTKFLT(16):数协器栈故障信号
表示数学协处理器发生栈故障。
默认动作:终止。
SIGCHLD(17):子进程状态改变信号
在一个进程终止或停止时,将此信号发送给其父进程。
默认动作:忽略。
SIGCONT(18):使停止的进程继续
向处于停止状态的进程发送此信号,令其继续运行。
默认动作:继续/忽略。
SIGSTOP(19):停止信号
不能被捕获或忽略。停止一个进程。
默认动作:停止进程。
SIGTSTP(20):终端停止符信号。
用户按停止键(Ctrl+Z),产生此信号,
并送至前台进程组的所有进程。
默认动作:停止进程。
SIGTTIN(21):后台读控制终端信号
后台进程组中的进程试图读其控制终端,产生此信号。
默认动作:停止。
SIGTTOU(22):后台写控制终端信号
后台进程组中的进程试图写其控制终端,产生此信号。
默认动作:停止。
SIGURG(23):紧急情况信号
有紧急情况发生,或从网络上接收到带外数据,产生此信号。
默认动作:忽略。
SIGXCPU(24):超过CPU 限制信号
进程超过了其软CPU 时间限制,产生此信号。
默认动作:终止+core。
SIGXFSZ(25):超过文件长度限制信号
进程超过了其软文件长度限制,产生此信号。
默认动作:终止+core。
SIGVTALRM(26):虚拟闹钟信号
以setitimer 函数设置的虚拟间隔时间到期,产生此信号。
默认动作:终止。
SIGPROF(27):虚拟梗概闹钟信号
以setitimer 函数设置的虚拟梗概统计间隔时间到期,
产生此信号。
默认动作:终止。
SIGWINCH(28):终端窗口大小改变信号
以ioctl 函数更改窗口大小,产生此信号。
默认动作:忽略。
SIGIO(29):异步I/O 信号
指示一个异步I/O 事件。
默认动作:终止。
SIGPWR(30):电源失效信号
电源失效,产生此信号。
默认动作:终止。
SIGSYS(31):非法系统调用异常。
指示一个无效的系统调用。
默认动作:终止+core。
4. 不可靠信号(非实时信号)
~~~~~~~~~~~~~~~~~~~~~~~~~
1) 那些建立在早期机制上的信号被称为“不可靠信号”。
小于SIGRTMIN(34)的信号都是不可靠信号。
2) 不支持排队,可能会丢失。同一个信号产生多次,
进程可能只收到一次该信号。
3) 进程每次处理完这些信号后,
对相应信号的响应被自动恢复为默认动作,
除非显示地通过signal 函数重新设置一次信号处理程序。
5. 可靠信号(实时信号)
~~~~~~~~~~~~~~~~~~~~~
1) 位于[SIGRTMIN(34),
SIGRTMAX(64)]区间的信号都是可靠信号。
2) 支持排队,不会丢失。
3) 无论可靠信号还是不可靠信号,
都可以通过sigqueue/sigaction 函数发送/安装,
以获得比其早期版本kill/signal 函数更可靠的使用效果。
6. 信号的来源
~~~~~~~~~~~~~
1) 硬件异常:除0、无效内存访问等。
这些异常通常被硬件(驱动)检测到,并通知系统内核。
系统内核再向引发这些异常的进程递送相应的信号。
2) 软件异常:通过
kill/raise/alarm/setitimer/sigqueue
函数产生的信号。
7. 信号处理
~~~~~~~~~~~
1) 忽略。
2) 终止进程。
3) 终止进程同时产生core 文件。
4) 捕获并处理。当信号发生时,
内核会调用一个事先注册好的用户函数(信号处理函数)。
范例:loop.c
# a.out
按中断键(Ctrl+C),发送SIGINT(2)终端中断符信号。
# a.out
按退出键(Ctrl+\),发送SIGQUIT(3)终端退出符信号。
二、signal
----------
#include <signal.h>
typedef void (*sighandler_t) (int);
sighandler_t signal (int signum,
sighandler_t handler);
signum - 信号码,也可使用系统预定义的常量宏,
如SIGINT 等。
handler - 信号处理函数指针或以下常量:
SIG_IGN: 忽略该信号;
SIG_DFL: 默认处理。
成功返回原来的信号处理函数指针或SIG_IGN/SIG_DFL 常量,
失败返回SIG_ERR。
1. 在某些Unix 系统上,
通过signal 函数注册的信号处理函数只一次有效,
即内核每次调用信号处理函数前,
会将对该信号的处理自动恢复为默认方式。
为了获得持久有效的信号处理,
可以在信号处理函数中再次调用signal 函数,
重新注册一次。
例如:
void sigint (int signum) {
...
signal (SIGINT, sigint);
}
int main (void) {
...
signal (SIGINT, sigint);
...
}
2. SIGKILL/SIGSTOP 信号不能被忽略,也不能被捕获。
3. 普通用户只能给自己的进程发送信号,
root 用户可以给任何进程发送信号。
范例:signal.c
三、子进程的信号处理
--------------------
1. 子进程会继承父进程的信号处理方式,
直到子进程调用exec 函数。
范例:fork.c
2. 子进程调用exec 函数后,
exec 函数将被父进程设置为捕获的信号恢复至默认处理,
其余保持不变。
范例:exec.c
四、发送信号
------------
1. 键盘
~~~~~~~
Ctrl+C - SIGINT(2),终端中断
Ctrl+\ - SIGQUIT(3),终端退出
Ctrl+Z - SIGTSTP(20),终端暂停
2. 错误
~~~~~~~
除0 - SIGFPE(8),算术异常
非法内存访问- SIGSEGV(11),段错误
硬件故障- SIGBUS(7),总线错误
3. 命令
~~~~~~~
kill -信号进程号
4. 函数
~~~~~~~
1) kill
#include <signal.h>
int kill (pid_t pid, int sig);
成功返回0,失败返回-1。
pid > 0 - 向pid 进程发送sig 信号。
pid = 0 - 向同进程组的所有进程发送信号。
pid = -1 - 向所有进程发送信号,
前提是调用进程有向其发送信号的权限。
pid < -1 - 向绝对值等于pid 的进程组发信号。
0 信号为空信号。
若sig 取0,则kill 函数仍会执行错误检查,
但并不实际发送信号。这常被用来确定一个进程是否存在。
向一个不存在的进程发送信号,会返回-1,且errno 为ESRCH。
范例:kill.c
2) raise
#include <signal.h>
int raise (int sig);
向调用进程自身发送sig 信号。成功返回0,失败返回-1。
范例:raise.c
五、pause
---------
#include <unistd.h>
int pause (void);
1. 使调用进程进入睡眠状态,
直到有信号终止该进程或被捕获。
2. 只有调用了信号处理函数并从中返回以后,
该函数才会返回。
3. 该函数要么不返回(未捕获到信号),
要么返回-1(被信号中断),
errno 为EINTR。
4. 相当于没有时间限制的sleep 函数。
范例:pause.c
六、sleep
---------
#include <unistd.h>
unsigned int sleep (unsigned int seconds);
1. 使调用进程睡眠seconds 秒,
除非有信号终止该进程或被捕获。
2. 只有睡够seconds 秒,
或调用了信号处理函数并从中返回以后,
该函数才会返回。
3. 该函数要么返回0(睡够),
要么返回剩余秒数(被信号中断)。
4. 相当于有时间限制的pause 函数。
范例:sleep.c
#include <unistd.h>
int usleep (useconds_t usec);
使调用进程睡眠usec 微秒,
除非有信号终止该进程或被捕获。
成功返回0,失败返回-1。
七、alarm
---------
#include <unistd.h>
unsigned int alarm (unsigned int seconds);
1. 使内核在seconds 秒之后,
向调用进程发送SIGALRM(14)闹钟信号。
范例:clock.c
2. SIGALRM 信号的默认处理是终止进程。
3. 若之前已设过定时且尚未超时,
则调用该函数会重新设置定时,
并返回之前定时的剩余时间。
4. seconds 取0 表示取消之前设过且尚未超时的定时。
范例:alarm.c
八、信号集与信号阻塞(信号屏蔽)
------------------------------
1. 信号集
~~~~~~~~~
1) 多个信号的集合类型:
sigset_t,128 个二进制位,每个位代表一个信号。
2) 相关函数
#include <signal.h>
// 将信号集set 中的全部信号位置1
int sigfillset (sigset_t* set);
// 将信号集set 中的全部信号位清0
int sigemptyset (sigset_t* set);
// 将信号集set 中与signum 对应的位置1
int sigaddset (sigset_t* set, int signum);
// 将信号集set 中与signum 对应的位清0
int sigdelset (sigset_t* set, int signum);
成功返回0,失败返回-1。
// 判断信号集set 中与signum 对应的位是否为1
int sigismember (const sigset_t* set, int signum);
若信号集set 中与signum 对应的位为1,则返回1,否则返回0。
范例:sigset.c
2. 信号屏蔽
~~~~~~~~~~~
1) 当信号产生时,系统内核会在其所维护的进程表中,
为特定的进程设置一个与该信号相对应的标志位,
这个过程称为递送(delivery)。
2) 信号从产生到完成递送之间存在一定的时间间隔。
处于这段时间间隔中的信号状态,称为未决(pending)。
3) 每个进程都有一个信号掩码(signal mask)。
它实际上是一个信号集,
其中包括了所有需要被屏蔽的信号。
4) 可以通过sigprocmask 函数,
检测和修改调用进程的信号掩码。
也可以通过sigpending 函数,
获取调用进程当前处于未决状态的信号集。
5) 当进程执行诸如更新数据库等敏感任务时,
可能不希望被某些信号中断。
这时可以暂时屏蔽(注意不是忽略)这些信号,
使其滞留在未决状态。
待任务完成以后,再回过头来处理这些信号。
6) 在信号处理函数的执行过程中,
这个正在被处理的信号总是处于信号掩码中。
#include <signal.h>
int sigprocmask (int how, const sigset_t* set,
sigset_t* oldset);
成功返回0,失败返回-1。
how - 修改信号掩码的方式,可取以下值:
SIG_BLOCK: 新掩码是当前掩码和set 的并集
(将set 加入信号掩码);
SIG_UNBLOCK: 新掩码是当前掩码和set 补集的交集
(从信号掩码中删除set);
SIG_SETMASK: 新掩码即set(将信号掩码设为set)。
set - NULL 则忽略。
oldset - 备份以前的信号掩码,NULL 则不备份。
int sigpending (sigset_t* set);
set - 输出,调用进程当前处于未决状态的信号集。
成功返回0,失败返回-1。
注意:对于不可靠信号,
通过sigprocmask 函数设置信号掩码以后,
相同的被屏蔽信号只会屏蔽第一个,
并在恢复信号掩码后被递送,其余的则直接忽略掉。
而对于可靠信号,
则会在信号屏蔽时按其产生的先后顺序排队,
一旦恢复信号掩码,这些信号会依次被信号处理函数处理。
范例:sigmask.c
九、sigaction
-------------
#include <signal.h>
int sigaction (
int signum, // 信号码
const struct sigaction* act, // 信号处理方式
struct sigaction* oldact // 原信号处理方式
// (可为NULL)
);
struct sigaction {
void (*sa_handler) (int);
// 信号处理函数指针1
void (*sa_sigaction) (int, siginfo_t*, void*);
// 信号处理函数指针2
sigset_t sa_mask; // 信号掩码
int sa_flags; // 信号处理标志
void (*sa_restorer)(void);
// 保留,NULL
};
成功返回0,失败返回-1。
1. 缺省情况下,在信号处理函数的执行过程中,
会自动屏蔽这个正在被处理的信号,
而对于其它信号则不会屏蔽。
通过sigaction::sa_mask 成员可以人为指定,
在信号处理函数的执行过程中,
需要加入进程信号掩码中的信号,
并在信号处理函数执行完之后,
自动解除对这些信号的屏蔽。
2. sigaction::sa_flags 可为以下值的位或:
SA_ONESHOT/SA_RESETHAND - 执行完一次信号处理函数后,
即将对此信号的处理恢复为
默认方式(这也是老版本
signal 函数的缺省行为)。
SA_NODEFER/SA_NOMASK - 在信号处理函数的执行过程中,
不屏蔽这个正在被处理的信号。
SA_NOCLDSTOP - 若signum 参数取SIGCHLD,
则当子进程暂停时,
不通知父进程。
SA_RESTART - 系统调用一旦被signum 参数
所表示的信号中断,
会自行重启。
SA_SIGINFO - 使用信号处理函数指针2,
通过该函数的第二个参数,
提供更多信息。
typedef struct siginfo {
pid_t si_pid; // 发送信号的PID
sigval_t si_value; // 信号附加值
// (需要配合sigqueue 函数)
...
} siginfo_t;
typedef union sigval {
int sival_int;
void* sival_ptr;
} sigval_t;
范例:sigact.c
十、sigqueue
------------
#include <signal.h>
int sigqueue (pid_t pid, int sig,
const union sigval value);
向pid 进程发送sig 信号,附加value 值(整数或指针)。
成功返回0,失败返回-1。
范例:sigque.c
注意:sigqueue 函数对不可靠信号不做排队,会丢失信号。
十一、计时器
------------
1. 系统为每个进程维护三个计时器
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1) 真实计时器:
程序运行的实际时间。
2) 虚拟计时器:
程序运行在用户态所消耗的时间。
3) 实用计时器:
程序运行在用户态和内核态所消耗的时间之和。
实际时间(真实计时器) = 用户时间(虚拟计时器) + 内核时间+ 睡
眠时间
-------------------------------
(实用计时器)
2. 为进程设定计时器
~~~~~~~~~~~~~~~~~~~
1) 用指定的初始间隔和重复间隔为进程设定好计时器后,
该计时器就会定时地向进程发送时钟信号。
2) 三个计时器所发送的时钟信号分别为:
SIGALRM - 真实计时器
SIGVTALRM - 虚拟计时器
SIGPROF - 实用计时器
3) 获取/设置计时器
#include <sys/time.h>
int getitimer (int which,
struct itimerval* curr_value);
获取计时器设置。成功返回0,失败返回-1。
int setitimer (int which,
const struct itimerval* new_value,
struct itimerval* old_value);
设置计时器。成功返回0,失败返回-1。
which - 指定哪个计时器,取值:
ITIMER_REAL: 真实计时器;
ITIMER_VIRTUAL: 虚拟计时器;
ITIMER_PROF: 实用计时器。
curr_value - 当前设置。
new_value - 新的设置。
old_value - 旧的设置(可为NULL)。
struct itimerval {
struct timeval it_interval;
// 重复间隔(每两个时钟信号的时间间隔),
// 取0 将使计时器在发送第一个信号后停止
struct timeval it_value;
// 初始间隔(从调用setitimer 函数到第一次发送
// 时钟信号的时间间隔),取0 将立即停止计时器
};
struct timeval {
long tv_sec; // 秒数
long tv_usec; // 微秒数
};
范例:timer.c
================
第七课进程通信
================
一、基本概念
------------
1. 何为进程间通信
~~~~~~~~~~~~~~~~~
进程间通信(Interprocess Communication, IPC)是指两个,
或多个进程之间进行数据交换的过程。
2. 进程间通信分类
~~~~~~~~~~~~~~~~~
1) 简单进程间通信:命令行参数、环境变量、信号、文件。
2) 传统进程间通信:管道(fifo/pipe)。
3) XSI 进程间通信:共享内存、消息队列、信号量。
4) 网络进程间通信:套接字。
二、传统进程间通信――管道
--------------------------
1. 管道是Unix 系统最古老的进程间通信方式。
2. 历史上的管道通常是指半双工管道,
只允许数据单向流动。现代系统大都提供全双工管道,
数据可以沿着管道双向流动。
3. 有名管道(fifo):基于有名文件(管道文件)的管道通信。
1) 命令形式
# mkfifo fifo
# echo hello > fifo # cat fifo
2) 编程模型
------+----------+------------+----------+------
步骤| 进程A | 函数| 进程B | 步骤
------+----------+------------+----------+------
1 | 创建管道| mkfifo | ---- |
2 | 打开管道| open | 打开管道| 1
3 | 读写管道| read/write | 读写管道| 2
4 | 关闭管道| close | 关闭管道| 3
5 | 删除管道| unlink | ---- |
------+----------+------------+----------+------
范例:wfifo.c、rfifo.c
图示:fifo.bmp
4. 无名管道(pipe):适用于父子进程之间的通信。
#include <unistd.h>
int pipe (int pipefd[2]);
1) 成功返回0,失败返回-1。
2) 通过输出参数pipefd 返回两个文件描述符,
其中pipefd[0]用于读,pipefd[1]用于写。
3) 一般用法
A. 调用该函数在内核中创建管道文件,并通过其输出参数,
获得分别用于读和写的两个文件描述符;
B. 调用fork 函数,创建子进程;
C. 写数据的进程关闭读端(pipefd[0]),
读数据的进程关闭写端(pipefd[1]);
D. 传输数据;
E. 父子进程分别关闭自己的文件描述符。
图示:pipe.bmp
范例:pipe.c
三、XSI 进程间通信
-----------------
1. IPC 标识
~~~~~~~~~~
内核为每个进程间通信维护一个结构体形式的IPC 对象。
该对象可通过一个非负整数的IPC 标识来引用。
与文件描述符不同,IPC 标识在使用时会持续加1,
当达到最大值时,向0 回转。
2. IPC 键值
~~~~~~~~~~
IPC 标识是IPC 对象的内部名称。
若多个进程需要在同一个IPC 对象上会合,
则必须通过键值作为其外部名称来引用该对象。
1) 无论何时,只要创建IPC 对象,就必须指定一个键值。
2) 键值的数据类型在sys/types.h 头文件中被定义为key_t,
其原始类型就是长整型。
3. 客户机进程与服务器进程在IPC 对象上的三种会合方式
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~
1) 服务器进程以IPC_PRIVATE 为键值创建一个新的IPC 对象,
并将该IPC 对象的标识存放在某处(如文件中),
以方便客户机进程读取。
2) 在一个公共头文件中,
定义一个客户机进程和服务器进程都认可的键值,
服务器进程用此键值创建IPC 对象,
客户机进程用此键值获取该IPC 对象。
3) 客户机进程和服务器进程,
事先约定好一个路径名和一个项目ID(0-255),
二者通过ftok 函数,
将该路径名和项目ID 转换为一致的键值。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok (const char* pathname, int proj_id);
pathname - 一个真实存在的文件或目录的路径名。
proj_id - 项目ID,仅低8 位有效,其值域为[0,255]。
成功返回键值,失败返回-1。
注意:起作用的是pathname 参数所表示的路径,
而非pathname 字符串本身。
因此假设当前目录是/home/soft01/uc/day07,则
ftok (".", 100);
和
ftok ("/home/soft01/uc/day07", 100);
的返回值完全相同。
4. IPC 对象的创建
~~~~~~~~~~~~~~~~
1) 若以IPC_PRIVATE 为键值创建IPC 对象,
则永远创建成功。
2) 若所指定的键值在系统范围内未与任何IPC 对象相结合,
且创建标志包含IPC_CREAT 位,则创建成功。
3) 若所指定的键值在系统范围内已与某个IPC 对象相结合,
且创建标志包含IPC_CREAT 和IPC_EXCL 位,则创建失败。
5. IPC 对象的销毁/控制
~~~~~~~~~~~~~~~~~~~~~
IPC_STAT - 获取IPC 对象属性
IPC_SET - 设置IPC 对象属性
IPC_RMID - 删除IPC 对象
四、共享内存
------------
1. 基本特点
~~~~~~~~~~~
1) 两个或者更多进程,
共享同一块由系统内核负责维护的内存区域,
其地址空间通常被映射到堆和栈之间。
图示:shm.bmp
2) 无需复制信息,最快的一种IPC 机制。
3) 需要考虑同步访问的问题。
4) 内核为每个共享内存,
维护一个shmid_ds 结构体形式的共享内存对象。
2. 常用函数
~~~~~~~~~~~
#include <sys/shm.h>
1) 创建/获取共享内存
int shmget (key_t key, size_t size, int shmflg);
A. 该函数以key 参数为键值创建共享内存,
或获取已有的共享内存。
B. size 参数为共享内存的字节数,
建议取内存页字节数(4096)的整数倍。
若希望创建共享内存,则必需指定size 参数。
若只为获取已有的共享内存,则size 参数可取0。
C. shmflg 取值:
0 - 获取,不存在即失败。
IPC_CREAT - 创建,不存在即创建,
已存在即获取,除非...
IPC_EXCL - 排斥,已存在即失败。
D. 成功返回共享内存标识,失败返回-1。
2) 加载共享内存
void* shmat (int shmid, const void* shmaddr,
int shmflg);
A. 将shmid 参数所标识的共享内存,
映射到调用进程的地址空间。
B. 可通过shmaddr 参数人为指定映射地址,
也可将该参数置NULL,由系统自动选择。
C. shmflg 取值:
0 - 以读写方式使用共享内存。
SHM_RDONLY - 以只读方式使用共享内存。
SHM_RND - 只在shmaddr 参数非NULL 时起作用。
表示对该参数向下取内存页的整数倍,
作为映射地址。
D. 成功返回映射地址,失败返回-1。
E. 内核将该共享内存的加载计数加1。
3) 卸载共享内存
int shmdt (const void* shmaddr);
A. 从调用进程的地址空间中,
取消由shmaddr 参数所指向的,共享内存映射区域。
B. 成功返回0,失败返回-1。
C. 内核将该共享内存的加载计数减1。
4) 销毁/控制共享内存
int shmctl (int shmid, int cmd, struct shmid_ds* buf);
struct shmid_ds {
struct ipc_perm shm_perm; // 所有者及其权限
size_t shm_segsz; // 大小(以字节为单位)
time_t shm_atime; // 最后加载时间
time_t shm_dtime; // 最后卸载时间
time_t shm_ctime; // 最后改变时间
pid_t shm_cpid; // 创建进程PID
pid_t shm_lpid; // 最后加载/卸载进程PID
shmatt_t shm_nattch; // 当前加载计数
...
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主ID
gid_t gid; // 有效属组ID
uid_t cuid; // 有效创建者ID
gid_t cgid; // 有效创建组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
A. cmd 取值:
IPC_STAT - 获取共享内存的属性,通过buf 参数输出。
IPC_SET - 设置共享内存的属性,通过buf 参数输入,
仅以下三个属性可设置:
shmid_ds::shm_perm.uid
shmid_ds::shm_perm.gid
shmid_ds::shm_perm.mode
IPC_RMID - 标记删除共享内存。
并非真正删除共享内存,只是做一个删除标记,
禁止其被继续加载,但已有加载依然保留。
只有当该共享内存的加载计数为0 时,
才真正被删除。
B. 成功返回0,失败返回-1。
3. 编程模型
~~~~~~~~~~~
------+--------------+--------+--------------+------
步骤| 进程A | 函数| 进程B | 步骤
------+--------------+--------+--------------+------
1 | 创建共享内存| shmget | 获取共享内存| 1
2 | 加载共享内存| shmat | 加载共享内存| 2
3 | 使用共享内存| ... | 使用共享内存| 3
4 | 卸载共享内存| shmdt | 卸载共享内存| 4
5 | 销毁共享内存| shmctl | ---- |
------+--------------+--------+--------------+------
范例:wshm.c、rshm.c
五、消息队列
------------
1. 基本特点
~~~~~~~~~~~
1) 消息队列是一个由系统内核负责存储和管理,
并通过消息队列标识引用的数据链表。
2) 可以通过msgget 函数创建一个新的消息队列,
或获取一个已有的消息队列。
通过msgsnd 函数向消息队列的后端追加消息,
通过msgrcv 函数从消息队列的前端提取消息。
3) 消息队列中的每个消息单元除包含消息数据外,
还包含消息类型和数据长度。
4) 内核为每个消息队列,
维护一个msqid_ds 结构体形式的消息队列对象。
2. 常用函数
~~~~~~~~~~~
#include <sys/msg.h>
1) 创建/获取消息队列
int msgget (key_t key, int msgflg);
A. 该函数以key 参数为键值创建消息队列,
或获取已有的消息队列。
B. msgflg 取值:
0 - 获取,不存在即失败。
IPC_CREAT - 创建,不存在即创建,
已存在即获取,除非...
IPC_EXCL - 排斥,已存在即失败。
C. 成功返回消息队列标识,失败返回-1。
2) 向消息队列发送消息
int msgsnd (int msqid, const void* msgp,
size_t msgsz, int msgflg);
A. msgp 参数指向一个包含消息类型和消息数据的内存块。
该内存块的前4 个字节必须是一个大于0 的整数,
代表消息类型,其后紧跟消息数据。
消息数据的字节长度用msgsz 参数表示。
+---------------+-----------------+
msgp -> | 消息类型(>0) | 消息数据|
+---------------+-----------------+
|<----- 4 ----->|<---- msgsz ---->|
注意:msgsz 参数并不包含消息类型的字节数(4)。
B. 若内核中的消息队列缓冲区有足够的空闲空间,
则此函数会将消息拷入该缓冲区并立即返回0,
表示发送成功,否则此函数会阻塞,
直到内核中的消息队列缓冲区有足够的空闲空间为止
(比如有消息被接收)。
C. 若msgflg 参数包含IPC_NOWAIT 位,
则当内核中的消息队列缓冲区没有足够的空闲空间时,
此函数不会阻塞,而是返回-1,errno 为EAGAIN。
D. 成功返回0,失败返回-1。
3) 从消息队列接收消息
ssize_t msgrcv (int msqid, void* msgp,
size_t msgsz, long msgtyp, int msgflg);
A. msgp 参数指向一个包含消息类型(4 字节),
和消息数据的内存块,
msgsz 参数表示期望接收的字节数(不含消息类型的4 个字节)。
B. 若所接收到的消息数据字节数大于msgsz 参数,
即消息太长,且msgflg 参数包含MSG_NOERROR 位,
则该消息被截取msgsz 字节返回,剩余部分被丢弃。
C. 若msgflg 参数不包含MSG_NOERROR 位,消息又太长,
则不对该消息做任何处理,直接返回-1,errno 为E2BIG。
D. msgtyp 参数表示期望接收哪类消息:
=0 - 返回消息队列中的第一条消息。
>0 - 若msgflg 参数不包含MSG_EXCEPT 位,
则返回消息队列中第一个类型为msgtyp 的消息;
若msgflg 参数包含MSG_EXCEPT 位,
则返回消息队列中第一个类型不为msgtyp 的消息。
<0 - 返回消息队列中类型小于等于msgtyp 的绝对值的消息。
若有多个,则取类型最小者。
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 3 | 4 | 2 | 1 | 3 | 2 | 4 | 3 |
3 |
-> rear +- -+- -+- -+- -+- -+- -+- -+- -+- -+
front ->
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
^ ^ ^
^
4 3 2
1
msgrcv (..., ..., ..., 3, ...);
E. 若消息队列中有可接收消息,
则此函数会将该消息移出消息队列,
并立即返回所接收到的消息数据的字节数,
表示接收成功,否则此函数会阻塞,
直到消息队列中有可接收消息为止。
F. 若msgflg 参数包含IPC_NOWAIT 位,
则当消息队列中没有可接收消息时,此函数不会阻塞,
而是返回-1,errno 为ENOMSG。
G. 成功返回所接收到的消息数据的字节数,失败返回-1。
4) 销毁/控制消息队列
int msgctl (int msqid, int cmd, struct msqid_ds* buf);
struct msqid_ds {
struct ipc_perm msg_perm; // 权限信息
time_t msg_stime; // 最后发送时间
time_t msg_rtime; // 最后接收时间
time_t msg_ctime; // 最后改变时间
unsigned long __msg_cbytes; // 消息队列中的字节数
msgqnum_t msg_qnum; // 消息队列中的消息数
msglen_t msg_qbytes; // 消息队列能容纳的最大字
节数
pid_t msg_lspid; // 最后发送进程PID
pid_t msg_lrpid; // 最后接收进程PID
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主ID
gid_t gid; // 有效属组ID
uid_t cuid; // 有效创建者ID
gid_t cgid; // 有效创建组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
A. cmd 取值:
IPC_STAT - 获取消息队列的属性,通过buf 参数输出。
IPC_SET - 设置消息队列的属性,通过buf 参数输入,
仅以下四个属性可设置:
msqid_ds::msg_perm.uid
msqid_ds::msg_perm.gid
msqid_ds::msg_perm.mode
msqid_ds::msg_qbytes
IPC_RMID - 立即删除消息队列。
此时所有阻塞在对该消息队列的,
msgsnd 和msgrcv 函数调用,
都会立即返回失败,errno 为EIDRM。
B. 成功返回0,失败返回-1。
3. 编程模型
~~~~~~~~~~~
------+--------------+---------------+--------------+------
步骤| 进程A | 函数| 进程B | 步骤
------+--------------+---------------+--------------+------
1 | 创建消息队列| msgget | 获取消息队列| 1
2 | 发送接收消息| msgsnd/msgrcv | 发送接收消息| 2
3 | 销毁消息队列| msgctl | ---- |
------+--------------+---------------+--------------+------
范例:wmsq.c、rmsq.c
练习:基于消息队列的本地银行。
代码:bank/
六、信号量
----------
1. 基本特点
~~~~~~~~~~~
1) 计数器,用于限制多个进程对有限共享资源的访问。
2) 多个进程获取有限共享资源的操作模式
A. 测试控制该资源的信号量;
B. 若信号量大于0,则进程可以使用该资源,
为了表示此进程已获得该资源,需将信号量减1;
C. 若信号量等于0,则进程休眠等待该资源,
直到信号量大于0,进程被唤醒,执行步骤A;
D. 当某进程不再使用该资源时,信号量增1,
正在休眠等待该资源的其它进程将被唤醒。
2. 常用函数
~~~~~~~~~~~
#include <sys/sem.h>
1) 创建/获取信号量
int semget (key_t key, int nsems, int semflg);
A. 该函数以key 参数为键值创建一个信号量集合
(nsems 参数表示集合中的信号量数),
或获取已有的信号量集合(nsems 取0)。
B. semflg 取值:
0 - 获取,不存在即失败。
IPC_CREAT - 创建,不存在即创建,
已存在即获取,除非...
IPC_EXCL - 排斥,已存在即失败。
C. 成功返回信号量集合标识,失败返回-1。
2) 操作信号量
int semop (int semid, struct sembuf* sops,
unsigned nsops);
struct sembuf {
unsigned short sem_num; // 信号量下标
short sem_op; // 操作数
short sem_flg; // 操作标记
};
A. 该函数对semid 参数所标识的信号量集合中,
由sops 参数所指向的包含nsops 个元素的,
结构体数组中的每个元素,依次执行如下操作:
a) 若sem_op 大于0,
则将其加到第sem_num 个信号量的计数值上,
以表示对资源的释放;
b) 若sem_op 小于0,
则从第sem_num 个信号量的计数值中减去其绝对值,
以表示对资源的获取;
c) 若第sem_num 个信号量的计数值不够减(信号量不能为负),
则此函数会阻塞,直到该信号量够减为止,
以表示对资源的等待;
d) 若sem_flg 包含IPC_NOWAIT 位,
则当第sem_num 个信号量的计数值不够减时,
此函数不会阻塞,而是返回-1,errno 为EAGAIN,
以便在等待资源的同时还可做其它处理;
e) 若sem_op 等于0,
则直到第sem_num 个信号量的计数值为0 时才返回,
除非sem_flg 包含IPC_NOWAIT 位。
B. 成功返回0,失败返回-1。
3) 销毁/控制信号量
int semctl (int semid, int semnum, int cmd);
int semctl (int semid, int semnum, int cmd,
union semun arg);
union semun {
int val; // Value for SETVAL
struct semid_ds* buf; // Buffer for IPC_STAT, IPC_SET
unsigned short* array; // Array for GETALL, SETALL
struct seminfo* __buf; // Buffer for IPC_INFO
};
struct semid_ds {
struct ipc_perm sem_perm; // Ownership and permissions
time_t sem_otime; // Last semop time
time_t sem_ctime; // Last change time
unsigned short sem_nsems; // No. of semaphores in set
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主ID
gid_t gid; // 有效属组ID
uid_t cuid; // 有效创建者ID
gid_t cgid; // 有效创建组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
A. cmd 取值:
IPC_STAT - 获取信号量集合的属性,通过arg.buf 输出。
IPC_SET - 设置信号量集合的属性,通过arg.buf 输入,
仅以下四个属性可设置:
semid_ds::sem_perm.uid
semid_ds::sem_perm.gid
semid_ds::sem_perm.mode
IPC_RMID - 立即删除信号量集合。
此时所有阻塞在对该信号量集合的,
semop 函数调用,都会立即返回失败,
errno 为EIDRM。
GETALL - 获取信号量集合中每个信号量的计数值,
通过arg.array 输出。
SETALL - 设置信号量集合中每个信号量的计数值,
通过arg.array 输入。
GETVAL - 获取信号量集合中,
第semnum 个信号量的计数值,
通过返回值输出。
SETVAL - 设置信号量集合中,
第semnum 个信号量的计数值,
通过arg.val 输入。
注意:只有针对信号量集合中具体某个信号量的操作,
才会使用semnum 参数。针对整个信号量集合的操作,
会忽略semnum 参数。
B. 成功返回值因cmd 而异,失败返回-1。
3. 编程模型
~~~~~~~~~~~
------+------------+--------+------------+------
步骤| 进程A | 函数| 进程B | 步骤
------+------------+--------+------------+------
1 | 创建信号量| semget | 获取信号量| 1
2 | 初始信号量| semctl | ---- |
3 | 加减信号量| semop | 加减信号量| 2
4 | 销毁信号量| semctl | ---- |
------+------------+--------+------------+------
范例:csem.c、gsem.c
七、IPC 命令
-----------
1. 显示
~~~~~~~
ipcs -m - 显示共享内存(m: memory)
ipcs -q - 显示消息队列(q: queue)
ipcs -s - 显示信号量(s: semphore)
ipcs -a - 显示所有IPC 对象(a: all)
2. 删除
~~~~~~~
ipcrm -m ID - 删除共享内存
ipcrm -q ID - 删除消息队列
ipcrm -s ID - 删除信号量
================
第八课网络通信
================
一、基本概念
------------
1. ISO/OSI 七层网络协议模型
~~~~~~~~~~~~~~~~~~~~~~~~~~
+------------+--------------+ ---
| 应用层| Application | ^
+------------+--------------+ |
| 表示层| Presentation | 高层
+------------+--------------+ |
| 会话层| Session | v
+------------+--------------+ ---
| 传输层| Transport | ^
+------------+--------------+ |
| 网络层| Network | |
+------------+--------------+ 低层
| 数据链路层| Data Link | |
+------------+--------------+ |
| 物理层| Physical | v
+------------+--------------+ ---
2. TCP/IP 协议族
~~~~~~~~~~~~~~~
1) TCP (Transmission Control Protocol, 传输控制协议)
面向连接的服务。
2) UDP (User Datagram Protocol, 用户数据报文协议)
面向无连接的服务。
3) IP (Internet Protocol, 互联网协议)
信息传递机制。
图示:tcpip.bmp
3. TCP/IP 协议与ISO/OSI 模型的对比
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ISO/OSI TCP/IP
+------------+------------+
| 应用层| |
+------------+ |
| 表示层| 应用层| TELNET/FTP/HTTP
+------------+ |
| 会话层| |
+------------+------------+
| 传输层| 传输层| TCP/UDP
+------------+------------+
| 网络层| 互联网层| IP/路由
+------------+------------+
| 数据链路层| |
+------------+ 网络接口层| 驱动/设备
| 物理层| |
+------------+------------+
图示:osi.bmp
4. 消息流
~~~~~~~~~
ISO/OSI TCP/IP TCP/IP
ISO/OSI
+------------+------------+ V ^ +------------+------------+
| 应用层| | | | | | 应
用层|
+------------+ | | | | +------------+
| 表示层| 应用层| | | | 应用层| 表
示层|
+------------+ | | | | +------------+
| 会话层| | | | | | 会
话层|
+------------+------------+ | | +------------+------------+
| 传输层| 传输层| V ^ | 传输层|
传输层|
+------------+------------+ | | +------------+------------+
| 网络层| 互联网层| | | | 互联网层| 网
络层|
+------------+------------+ | | +------------+------------+
| 数据链路层| | | | | | 数据
链路层|
+------------+ 网络接口层| | | | 网络接口层+------------+
| 物理层| | + + | | 物
理层|
+------------+------------+ \____/ +------------+------------+
5. 消息包
~~~~~~~~~
+-----------------+
| TELNET/FTP/HTTP |
+-----------------+
| TCP/UDP |
+-----------------+
| IP |
+-----------------+
| ETHERNET |
+-----------------+
从上至下,消息包逐层递增,从下至上,消息包逐层递减。
6. IP 地址
~~~~~~~~~
1) IP 地址是Internet 中唯一的地址标识
A. 一个IP 地址占32 位,正在扩充至128 位。
B. 每个Internet 包必须带IP 地址。
2) 点分十进制表示法
0x01020304 -> 1.2.3.4,高数位在左,低数位在右。
3) IP 地址分级
A 级:0 + 7 位网络地址+ 24 位本地地址
B 级:10 + 14 位网络地址+ 16 位本地地址
C 级:110 + 21 位网络地址+ 8 位本地地址
D 级:1110 + 28 位多播(Muticast)地址
4) 子网掩码
IP 地址& 子网掩码= 网络地址
IP 地址: 192.168.182.48
子网掩码:255.255.255.0
网络地址:192.168.182
本地地址:48
二、套接字(Socket)
------------------
1. 接口
~~~~~~~
PuTTY -> telnet \
LeapFTP -> ftp -> socket -> TCP/UDP -> IP -> 网卡驱动->
网卡硬件
IE -> http / ^
|
应用程序----------------+
图示:bsd.bmp
2. 异构
~~~~~~~
Java @ UNIX -> socket <----> socket <- C/C++ @ Windows
3. 模式
~~~~~~~
1) 点对点(Peer-to-Peer, P2P):一对一的通信。
2) 客户机/服务器(Client/Server, C/S):一对多的通信。
4. 绑定
~~~~~~~
先要有一个套接字描述符,还要有物理通信载体,
然后将二者绑定在一起。
5. 函数
~~~~~~~
1) 创建套接字
#include <sys/socket.h>
int socket (int domain, int type, int protocol);
domain - 域/地址族,取值:
AF_UNIX/AF_LOCAL/AF_FILE: 本地通信(进程间通信);
AF_INET: 基于TCP/IPv4(32 位IP 地址)的网络通信;
AF_INET6: 基于TCP/IPv6(128 位IP 地址)的网络通信;
AF_PACKET: 基于底层包接口的网络通信。
type - 通信协议,取值:
SOCK_STREAM: 数据流协议,即TCP 协议;
SOCK_DGRAM: 数据报协议,即UDP 协议。
protocol - 特别通信协议,一般不用,置0 即可。
成功返回套接字描述符,失败返回-1。
套接字描述符类似于文件描述符,UNIX 把网络当文件看待,
发送数据即写文件,接收数据即读文件,一切皆文件。
2) 准备通信地址
A. 基本地址类型
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址值
};
B. 本地地址类型
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; // 地址族
char sun_path[]; // 套接字文件路径
};
C. 网络地址类型
#include <netinet/in.h>
struct sockaddr_in {
// 地址族
sa_family_t sin_family;
// 端口号
// unsigned short, 0-65535
// 逻辑上表示一个参与通信的进程
// 使用时需要转成网络字节序
// 0-1024 端口一般被系统占用
// 如:21-FTP、23-Telnet、80-WWW
in_port_t sin_port;
// IP 地址
struct in_addr sin_addr;
};
struct in_addr {
in_addr_t s_addr;
};
typedef uint32_t in_addr_t;
IP 地址用于定位主机,端口号用于定位主机上的进程。
3) 将套接字和通信地址绑定在一起
#include <sys/socket.h>
int bind (int sockfd, const struct sockaddr* addr,
socklen_t addrlen);
成功返回0,失败返回-1。
4) 建立连接
#include <sys/socket.h>
int connect (int sockfd, const struct sockaddr* addr,
socklen_t addrlen);
成功返回0,失败返回-1。
5) 用读写文件的方式通信:read/write
6) 关闭套接字:close
7) 字节序转换
#include <arpa/inet.h>
// 32 位无符号整数,主机字节序-> 网络字节序
uint32_t htonl (uint32_t hostlong);
// 16 位无符号整数,主机字节序-> 网络字节序
uint16_t htons (uint16_t hostshort);
// 32 位无符号整数,网络字节序-> 主机字节序
uint32_t ntohl (uint32_t netlong);
// 16 位无符号整数,网络字节序-> 主机字节序
uint16_t ntohs (uint16_t netshort);
主机字节序因处理器架构而异,有的采用小端字节序,
有的采用大端字节序。网络字节序则固定采用大端字节序。
8) IP 地址转换
#include <arpa/inet.h>
// 点分十进制字符串-> 网络字节序32 位无符号整数
in_addr_t inet_addr (const char* cp);
// 点分十进制字符串-> 网络字节序32 位无符号整数
int inet_aton (const char* cp, struct in_addr* inp);
// 网络字节序32 位无符号整数-> 点分十进制字符串
char* inet_ntoa (struct in_addr in);
6. 编程
~~~~~~~
1) 本地通信
服务器:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并绑定->
接收数据->关闭套接字
客户机:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并连接->
发送数据->关闭套接字
范例:locsvr.c、loccli.c
2) 网络通信
服务器:创建套接字(AF_INET)->准备地址(sockaddr_in)并绑定->接
收数据->关闭套接字
客户机:创建套接字(AF_INET)->准备地址(sockaddr_in)并连接->发
送数据->关闭套接字
范例:netsvr.c、netcli.c
三、基于TCP 协议的客户机/服务器模型
----------------------------------
1. 基本特征
~~~~~~~~~~~
1) 面向连接。
2) 可靠,保证数据的完整性和有序性。
ABCDEF
A -> -
B -> |
C -> +- 时间窗口
D -> |
E -> <- A OK -
F -> <- B OK
<- C OK
<- D OK
<- E OK
<- F OK
每个发送都有应答,若在时间窗口内没有收到A 的应答,
则从A 开始重发。
2. 编程模型
~~~~~~~~~~~
------+-------------------------+-------------------------+------
步骤| 服务器| 客户机|
步骤
------+------------+------------+------------+------------+------
1 | 创建套接字| socket | socket | 创建套接字|
1
2 | 准备地址| ... | ... | 准备地址| 2
3 | 绑定套接字| bind | | ---- |
4 | 监听套接字| listen | | ---- |
5 | 接受连接| accept | connect | 建立链接|
3
6 | 接收请求| recv | send | 发送请求|
4
7 | 发送响应| send | recv | 接收响应|
5
8 | 关闭套接字| close | close | 关闭套接字|
6
------+------------+------------+------------+------------+------
图示:handshake.bmp、tcpcs.bmp
3. 常用函数
~~~~~~~~~~~
#include <sys/socket.h>
int listen (int sockfd, int backlog);
将sockfd 参数所标识的套接字标记为被动模式,
使之可用于接受连接请求。
backlog 参数表示未决连接请求队列的最大长度,
即最多允许同时有多少个未决连接请求存在。
若服务器端的未决连接数已达此限,
则客户机端的connect()函数将返回-1,
且errno 为ECONNREFUSED。
成功返回0,失败返回-1。
图示:listen.bmp
int accept (int sockfd, struct sockaddr* addr,
socklen_t* addrlen);
从sockfd 参数所标识套接字的未决连接请求队列中,
提取第一个连接请求,同时创建一个新的套接字,
用于在该连接中通信,返回该套接字的描述符。
addr 和addrlen 参数用于输出连接请求发起者的地址信息。
成功返回通信套接字描述符,失败返回-1。
图示:accept.bmp
ssize_t recv (int sockfd, void* buf, size_t len,
int flags);
通过sockfd 参数所标识的套接字,
期望接收len 个字节到buf 所指向的缓冲区中。
成功返回实际接收到的字节数,失败返回-1。
ssize_t send (int sockfd, const void* buf,
size_t len, int flags);
通过sockfd 参数所标识的套接字,
从buf 所指向的缓冲区中发送len 个字节。
成功返回实际被发送的字节数,失败返回-1。
图示:concurrent.bmp
范例:tcpsvr.c、tcpcli.c
图示:inetd.bmp
四、基于UDP 协议的客户机/服务器模型
----------------------------------
1. 基本特征
~~~~~~~~~~~
1) 无连接。
2) 不可靠,不保证数据的完整性和有序性。
A
/ \
/ \
ABC ->+-(B)-+-> CA
\ /
\ /
C
效率高速度快。
2. 编程模型
~~~~~~~~~~~
------+-------------------------+-------------------------+------
步骤| 服务器| 客户机|
步骤
------+------------+------------+------------+------------+------
1 | 创建套接字| socket | socket | 创建套接字|
1
2 | 准备地址| ... | ... | 准备地址| 2
3 | 绑定套接字| bind | | ---- |
4 | 接收请求| recvfrom | sendto | 发送请求|
3
5 | 发送响应| sendto | recvfrom | 接收响应|
4
6 | 关闭套接字| close | close | 关闭套接字|
5
------+------------+------------+------------+------------+------
图示:udpcs.bmp
3. 常用函数
~~~~~~~~~~~
#include <sys/socket.h>
ssize_t recvfrom (int sockfd, void* buf, size_t len,
int flags, struct sockaddr* src_addr,
socklen_t* addrlen);
通过sockfd 参数所标识的套接字,
期望接收len 个字节到buf 所指向的缓冲区中。
若src_addr 和addrlen 参数不是空指针,
则通过这两个参数输出源地址结构及其长度。
注意在这种情况下,
addrlen 参数的目标应被初始化为,
src_addr 参数的目标数据结构的大小。
成功返回实际接收到的字节数,失败返回-1。
ssize_t sendto (int sockfd, const void* buf,
size_t len, int flags,
const struct sockaddr* dest_addr,
socklen_t addrlen);
通过sockfd 参数所标识的套接字,
从buf 所指向的缓冲区中发送len 个字节。
发送目的的地址结构及其长度,
通过dest_addr 和addrlen 参数输入。
成功返回实际被发送的字节数,失败返回-1。
范例:udpsvr.c、udpcli.c
图示:tcp_udp.bmp
127.0.0.1: 回绕地址,表示本机,不依赖网络。
练习:基于TCP 协议的网络银行。
代码:bank/
================
第九课线程管理
================
一、基本概念
------------
1. 线程就是程序的执行路线,即进程内部的控制序列,
或者说是进程的子任务。
2. 线程,轻量级,不拥有自己独立的内存资源,
共享进程的代码区、数据区、堆区(注意没有栈区)、
环境变量和命令行参数、文件描述符、信号处理函数、
当前目录、用户ID 和组ID 等资源。
3. 线程拥有自己独立的栈,因此也有自己独立的局部变量。
4. 一个进程可以同时拥有多个线程,
即同时被系统调度的多条执行路线,
但至少要有一个主线程。
二、基本特点
------------
1. 线程是进程的一个实体,
可作为系统独立调度和分派的基本单位。
2. 线程有不同的状态,系统提供了多种线程控制原语,
如创建线程、销毁线程等等。
3. 线程不拥有自己的资源,只拥有从属于进程的全部资源,
所有的资源分配都是面向进程的。
4. 一个进程中可以有多个线程并发地运行。
它们可以执行相同的代码,也可以执行不同的代码。
5. 同一个进程的多个线程都在同一个地址空间内活动,
因此相对于进程,线程的系统开销小,任务切换快。
6. 线程间的数据交换不需要依赖于类似IPC 的特殊通信机制,
简单而高效。
7. 每个线程拥有自己独立的线程ID、寄存器信息、函数栈、
错误码和信号掩码。
8. 线程之间存在优先级的差异。
三、POSIX 线程(pthread)
----------------------
1. 早期厂商各自提供私有的线程库版本,
接口和实现的差异非常大,不易于移植。
2. IEEE POSIX 1003.1c (1995)标准,
定义了统一的线程编程接口,
遵循该标准的线程实现被统称为POSIX 线程,即pthread。
3. pthread 包含一个头文件pthread.h,
和一个接口库libpthread.so。
#include <pthread.h>
...
gcc ... -lpthread
4. 功能
1) 线程管理:创建/销毁线程、分离/联合线程、
设置/查询线程属性。
2) 线程同步
A. 互斥量:创建/销毁互斥量、加锁/解锁互斥量、
设置/查询互斥量属性。
B. 条件变量:创建/销毁条件变量、等待/触发条件变量、
设置/查询条件变量属性。
四、线程函数
------------
1. 创建线程
~~~~~~~~~~~
int pthread_create (pthread_t* restrict thread,
const pthread_attr_t* restrict attr,
void* (*start_routine) (void*),
void* restrict arg);
thread - 线程ID,输出参数。
pthread_t 即unsigned long int。
attr - 线程属性,NULL 表示缺省属性。
pthread_attr_t 可能是整型也可能是结构,
因实现而异。
start_routine - 线程过程函数指针,
参数和返回值的类型都是void*。
启动线程本质上就是调用一个函数,
只不过是在一个独立的线程中调用的,
函数返回即线程结束。
arg - 传递给线程过程函数的参数。
线程过程函数的调用者是系统内核,
而非用户代码,
因此需要在创建线程时指定参数。
成功返回0,失败返回错误码。
注意:
1) restrict: C99 引入的编译优化指示符,
提高重复解引用同一个指针的效率。
2) 在pthread.h 头文件中声明的函数,
通常以直接返回错误码的方式表示失败,
而非以错误码设置errno 并返回-1。
3) main 函数即主线程,main 函数返回即主线程结束,
主线程结束即进程结束,
进程一但结束其所有的线程即结束。
4) 应设法保证在线程过程函数执行期间,
其参数所指向的目标持久有效。
创建线程。范例:create.c
线程并发。范例:concur.c
线程参数。范例:arg.c
2. 等待线程
~~~~~~~~~~~
int pthread_join (pthread_t thread, void** retval);
等待thread 参数所标识的线程结束,
成功返回0,失败返回错误码。
范例:ret.c
注意从线程过程函数中返回值的方法:
1) 线程过程函数将所需返回的内容放在一块内存中,
返回该内存的地址,保证这块内存在函数返回,
即线程结束,以后依然有效;
2) 若retval 参数非NULL,
则pthread_join 函数将线程过程函数所返回的指针,
拷贝到该参数所指向的内存中;
3) 若线程过程函数所返回的指针指向动态分配的内存,
则还需保证在用过该内存之后释放之。
3. 获取线程自身的ID
~~~~~~~~~~~~~~~~~~~
pthread_t pthread_self (void);
成功返回调用线程的ID,不会失败。
4. 比较两个线程的ID
~~~~~~~~~~~~~~~~~~~
int pthread_equal (pthread_t t1, pthread_t t2);
若参数t1 和t2 所标识的线程ID 相等,则返回非零,否则返回0。
某些实现的pthread_t 不是unsigned long int 类型,
可能是结构体类型,无法通过“==”判断其相等性。
范例:equal.c
5. 终止线程
~~~~~~~~~~~
1) 从线程过程函数中return。
2) 调用pthread_exit 函数。
void pthread_exit (void* retval);
retval - 和线程过程函数的返回值语义相同。
注意:在任何线程中调用exit 函数都将终止整个进程。
范例:exit.c
6. 线程执行轨迹
~~~~~~~~~~~~~~~
1) 同步方式(非分离状态):
创建线程之后调用pthread_join 函数等待其终止,
并释放线程资源。
2) 异步方式(分离状态):
无需创建者等待,线程终止后自行释放资源。
int pthread_detach (pthread_t thread);
使thread 参数所标识的线程进入分离(DETACHED)状态。
处于分离状态的线程终止后自动释放线程资源,
且不能被pthread_join 函数等待。
成功返回0,失败返回错误码。
范例:detach.c
7. 取消线程
~~~~~~~~~~~
1) 向指定线程发送取消请求
int pthread_cancel (pthread_t thread);
成功返回0,失败返回错误码。
注意:该函数只是向线程发出取消请求,
并不等待线程终止。
缺省情况下,线程在收到取消请求以后,并不会立即终止,
而是仍继续运行,直到其达到某个取消点。在取消点处,
线程检查其自身是否已被取消了,并做出相应动作。
当线程调用一些特定函数时,取消点会出现。
2) 设置调用线程的可取消状态
int pthread_setcancelstate (int state,
int* oldstate);
成功返回0,并通过oldstate 参数输出原可取消状态
(若非NULL),失败返回错误码。
state 取值:
PTHREAD_CANCEL_ENABLE - 接受取消请求(缺省)。
PTHREAD_CANCEL_DISABLE - 忽略取消请求。
3) 设置调用线程的可取消类型
int pthread_setcanceltype (int type, int* oldtype);
成功返回0,并通过oldtype 参数输出原可取消类型
(若非NULL),失败返回错误码。
type 取值:
PTHREAD_CANCEL_DEFERRED - 延迟取消(缺省)。
被取消线程在接收到取消请求之后并不立即响应,
而是一直等到执行了特定的函数(取消点)之后再响应该请求。
PTHREAD_CANCEL_ASYNCHRONOUS - 异步取消。
被取消线程可以在任意时间取消,
不是非得遇到取消点才能被取消。
但是操作系统并不能保证这一点。
范例:cancel.c
8. 线程属性
~~~~~~~~~~~
创建线程函数
int pthread_create (pthread_t* restrict thread,
const pthread_attr_t* restrict attr,
void* (*start_routine) (void*),
void* restrict arg);
的第二个参数即为线程属性,传空指针表示使用缺省属性。
typedef struct {
// 分离状态
//
// PTHREAD_CREATE_DETACHED
// - 分离线程。
//
// PTHREAD_CREATE_JOINABLE(缺省)
// - 可汇合线程。
//
int detachstate;
// 竞争范围
//
// PTHREAD_SCOPE_SYSTEM
// - 在系统范围内竞争资源。
//
// PTHREAD_SCOPE_PROCESS(Linux 不支持)
// - 在进程范围内竞争资源。
//
int scope;
// 继承特性
//
// PTHREAD_INHERIT_SCHED(缺省)
// - 调度属性自创建者线程继承。
//
// PTHREAD_EXPLICIT_SCHED
// - 调度属性由后面两个成员确定。
//
int inheritsched;
// 调度策略
//
// SCHED_FIFO
// - 先进先出策略。
//
// 没有时间片。
//
// 一个FIFO 线程会持续运行,
// 直到阻塞或有高优先级线程就绪。
//
// 当FIFO 线程阻塞时,系统将其移出就绪队列,
// 待其恢复时再加到同优先级就绪队列的末尾。
//
// 当FIFO 线程被高优先级线程抢占时,
// 它在就绪队列中的位置不变。
// 因此一旦高优先级线程终止或阻塞,
// 被抢占的FIFO 线程将会立即继续运行。
//
// SCHED_RR
// - 轮转策略。
//
// 给每个RR 线程分配一个时间片,
// 一但RR 线程的时间片耗尽,
// 系统即将移到就绪队列的末尾。
//
// SCHED_OTHER(缺省)
// - 普通策略。
//
// 静态优先级为0。任何就绪的FIFO 线程或RR 线程,
// 都会抢占此类线程。
//
int schedpolicy;
// 调度参数
//
// struct sched_param {
// int sched_priority; /* 静态优先级*/
// };
//
struct sched_param schedparam;
// 栈尾警戒区大小(字节)
//
// 缺省一页(4096 字节)。
//
size_t guardsize;
// 栈地址
//
void* stackaddr;
// 栈大小(字节)
//
size_t stacksize;
} pthread_attr_t;
不要手工读写该结构体,
而应调用pthread_attr_set/get 函数设置/获取具体属性项。
1) 设置线程属性
第一步,初始化线程属性结构体
int pthread_attr_init (pthread_attr_t* attr);
第二步,设置具体线程属性项
int pthread_attr_setdetachstate (
pthread_attr_t* attr,
int detachstate);
int pthread_attr_setscope (
pthread_attr_t* attr,
int scope);
int pthread_attr_setinheritsched (
pthread_attr_t* attr,
int inheritsched);
int pthread_attr_setschedpolicy (
pthread_attr_t* attr,
int policy);
int pthread_attr_setschedparam (
pthread_attr_t* attr,
const struct sched_param* param);
int pthread_attr_setguardsize (
pthread_attr_t* attr,
size_t guardsize);
int pthread_attr_setstackaddr (
pthread_attr_t* attr,
void* stackaddr);
int pthread_attr_setstacksize (
pthread_attr_t* attr,
size_t stacksize);
int pthread_attr_setstack (
pthread_attr_t* attr,
void* stackaddr, size_t stacksize);
第三步,以设置好的线程属性结构体为参数创建线程
int pthread_create (pthread_t* restrict thread,
const pthread_attr_t* testrict attr,
void* (*start_routine) (void*),
void* restrict arg);
第四步,销毁线程属性结构体
int pthread_attr_destroy (pthread_attr_t* attr);
2) 获取线程属性
第一步,获取线程属性结构体
int pthread_getattr_np (pthread_t thread,
pthread_attr_t* attr);
第二步,获取具体线程属性项
int pthread_attr_getdetachstate (
pthread_attr_t* attr,
int* detachstate);
int pthread_attr_getscope (
pthread_attr_t* attr,
int* scope);
int pthread_attr_getinheritsched (
pthread_attr_t* attr,
int* inheritsched);
int pthread_attr_getschedpolicy (
pthread_attr_t* attr,
int* policy);
int pthread_attr_getschedparam (
pthread_attr_t* attr,
struct sched_param* param);
int pthread_attr_getguardsize (
pthread_attr_t* attr,
size_t* guardsize);
int pthread_attr_getstackaddr (
pthread_attr_t* attr,
void** stackaddr);
int pthread_attr_getstacksize (
pthread_attr_t* attr,
size_t* stacksize);
int pthread_attr_getstack (
pthread_attr_t* attr,
void** stackaddr, size_t* stacksize);
以上所有函数成功返回0,失败返回错误码。
范例:attr.c
================
第十课线程同步
================
一、竞争与同步
--------------
当多个线程同时访问其所共享的进程资源时,
需要相互协调,以防止出现数据不一致、
不完整的问题。这就叫线程同步。
范例:vie.c
理想中的原子++:
-----------------+-----------------+------
线程1 | 线程2 | 内存
--------+--------+--------+--------+------
指令| 寄存器| 指令| 寄存器| g_cn
--------+--------+--------+--------+------
读内存| 0 | | | 0
算加法| 1 | | | 0
写内存| 1 | | | 1
| | 读内存| 1 | 1
| | 算加法| 2 | 1
| | 写内存| 2 | 2
--------+--------+--------+--------+------
现实中的非原子++:
-----------------+-----------------+------
线程1 | 线程2 | 内存
--------+--------+--------+--------+------
指令| 寄存器| 指令| 寄存器| g_cn
--------+--------+--------+--------+------
读内存| 0 | | | 0
| | 读内存| 0 | 0
算加法| 1 | | | 0
| | 算加法| 1 | 0
写内存| 1 | | | 1
| | 写内存| 1 | 1
--------+--------+--------+--------+------
二、互斥量
----------
int pthread_mutex_init (pthread_mutex_t* mutex,
const pthread_mutexattr_t* mutexattr);
亦可
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock (pthread_mutex_t* mutex);
int pthread_mutex_unlock (pthread_mutex_t* mutex);
int pthread_mutex_destroy (pthread_mutex_t* mutex);
1) 互斥量被初始化为非锁定状态;
2) 线程1 调用pthread_mutex_lock 函数,立即返回,
互斥量呈锁定状态;
3) 线程2 调用pthread_mutex_lock 函数,阻塞等待;
4) 线程1 调用pthread_mutex_unlock 函数,
互斥量呈非锁定状态;
5) 线程2 被唤醒,从pthread_mutex_lock 函数中返回,
互斥量呈锁定状态;
...
范例:mutex.c
三、信号量
----------
信号量是一个计数器,用于控制访问有限共享资源的线程数。
#include <semaphore.h>
// 创建信号量
int sem_init (sem_t* sem, int pshared,
unsigned int value);
sem - 信号量ID,输出。
pshared - 一般取0,表示调用进程的信号量。
非0 表示该信号量可以共享内存的方式,
为多个进程所共享(Linux 暂不支持)。
value - 信号量初值。
// 信号量减1,不够减即阻塞
int sem_wait (sem_t* sem);
// 信号量减1,不够减即返回-1,errno 为EAGAIN
int sem_trywait (sem_t* sem);
// 信号量减1,不够减即阻塞,
// 直到abs_timeout 超时返回-1,errno 为ETIMEDOUT
int sem_timedwait (sem_t* sem,
const struct timespec* abs_timeout);
struct timespec {
time_t tv_sec; // Seconds
long tv_nsec; // Nanoseconds [0 - 999999999]
};
// 信号量加1
int sem_post (sem_t* sem);
// 销毁信号量
int sem_destroy (sem_t* sem);
范例:sem.c
注意:
1) 信号量APIs 没有声明在pthread.h 中,
而是声明在semaphore.h 中,失败也不返回错误码,
而是返回-1,同时设置errno。
2) 互斥量任何时候都只允许一个线程访问共享资源,
而信号量则允许最多value 个线程同时访问共享资源,
当value 为1 时,与互斥量等价。
范例:pool.c
四、死锁问题
------------
线程1 线程2
| |
获取A 获取B
| |
获取B 获取A <- 死锁
\ /
释放B X 释放A
/ \
释放A 释放B
范例:dead.c
五、条件变量
------------
生产者消费者模型
生产者:产生数据的线程。
消费者:使用数据的线程。
通过缓冲区隔离生产者和消费者,与二者直连相比,
避免相互等待,提高运行效率。
生产快于消费,缓冲区满,撑死。
消费快于生产,缓冲区空,饿死。
条件变量可以让调用线程在满足特定条件的情况下暂停。
int pthread_cond_init (pthread_cond_t* cond,
const pthread_condattr_t* attr);
亦可
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 使调用线程睡入条件变量cond,同时释放互斥锁mutex
int pthread_cond_wait (pthread_cond_t* cond,
pthread_mutex_t* mutex);
int pthread_cond_timedwait (pthread_cond_t* cond,
pthread_mutex_t* mutex,
const struct timespec* abstime);
struct timespec {
time_t tv_sec; // Seconds
long tv_nsec; // Nanoseconds [0 - 999999999]
};
// 从条件变量cond 中唤出一个线程,
// 令其重新获得原先的互斥锁
int pthread_cond_signal (pthread_cond_t* cond);
注意:被唤出的线程此刻将从pthread_cond_wait 函数中返回,
但如果该线程无法获得原先的锁,则会继续阻塞在加锁上。
// 从条件变量cond 中唤出所有线程
int pthread_cond_broadcast (pthread_cond_t* cond);
int pthread_cond_destroy (pthread_cond_t* cond);
范例:cond.c
注意:当一个线程被从条件变量中唤出以后,
导致其睡入条件变量的条件可能还需要再判断一次,
因其随时有可能别其它线程修改。
范例:bc.c (if->while)
六、哲学家就餐问题
------------------
1965 年,著名计算机科学家艾兹格・迪科斯彻,提出并解决
了一个他称之为哲学家就餐的同步问题。从那时起,每个发
明同步原语的人,都希望通过解决哲学家就餐问题来展示其
同步原语的精妙之处。
这个问题可以简单地描述如下:
五个哲学家围坐在一张圆桌周围,每个哲学家面前都有一盘
面条。由于面条很滑,所以需要一双筷子才能夹住。相邻两
个盘子之间放有一根筷子。哲学家的生活中有两种交替活动
时段:即吃饭和思考。当一个哲学家觉得饿了时,他就试图
分两次去取其左边和右边的筷子,每次拿一根,不分次序。
如果成功地得到了两根筷子,就开始吃饭,吃完后放下筷子
继续思考。
图示:dining.png
关键问题是:能为每一个哲学家写一段描述其行为的程序,
且决不会死锁吗?
提示:
如果五位哲学家同时拿起左面的筷子,就没有人能够拿到他
们各自右面的筷子,于是发生了死锁。
如果每位哲学家在拿到左面的筷子后,发现其右面的筷子不
可用,那么就先放下左面的筷子,等待一段时间,再重复此
过程。可能在某一个瞬间,所有的哲学家都同时拿起左筷,
看到右筷不可用,又都放下左筷,等一会儿,又都同时拿起
左筷,如此重复下去。虽然程序在不停运行,但都无法取得
进展,于是发生了活锁。
思路:
解决问题的关键在于,必须保证任意一位哲学家只有在其左
右两个邻居都没有在进餐时,才允许其进入进餐状态。这样
做不仅不会发生死锁,而且对于任意数量的哲学家都能获得
最大限度的并行性。
范例:dining.c
本文出自 “日知其所无” 博客,谢绝转载!