程序由业务逻辑和系统访问两部分构成的。其中,业务逻辑是根据业务需求,按照设计好的逻辑规则,处理信息,与系统(平台)无关的;而系统访问则是利用操作系统所提供的各种功能,来辅助业务逻辑的实现,是跟系统相关的(平台相关性)。
使用标准库函数(如scanf/printf)实现的程序,可以做到源码级兼容,因为标准库函数的实现虽然基于系统函数,但是直接使用标准库函数时是不需要考虑系统函数的,也就是说标准库函数屏蔽了操作系统/平台之间的差异。与之相对,底层的系统调用函数只能做到接口级兼容。
既然标准库函数这么好,为什么还需要使用系统函数呢?——环境(裸板程序没有标准库)、性能、功能(某些功能标准库没有)
PS:有些内容的描述不准确
Ø 1961-1969年:史前时代
CTSS(Compatible Time-Sharing System,兼容分时系统),以MIT(麻省理工)为首的开发小组(包括AT&T的贝尔实验室),小而简单的实验室原型
分时系统:分时操作系统是指在一台主机上连接多个带有显示器和键盘的终端,同时允许多个用户通过主机的终端,以交互方式使用计算机,共享主机中的资源。分时操作系统是一个多用户交互式操作系统。分时操作系统,主要分为三类:单道分时操作系统,多道分时操作系统,具有前台和后台的分时操作系统。分时操作系统将CPU的时间划分成若干个片段,称为时间片。操作系统以时间片为单位,轮流为每个终端用户服务。
Multics(Multiplexd Information and Computing System,多路信息与计算系统),庞大而复杂,不堪重负,开发和维护变得非常困难,半成品
Unics(Uniplexed Information and Computing System,单路信息与计算系统),返璞归真,走上正道(剔除了Multics中不实用和太过复杂的功能)
Ø 1969-1971年:创世纪
Ken Thompson(肯.汤姆逊),Unix之父,B语言之父,内核用B语言和汇编语言进行开发,在PDP-7(被废弃的)机器上开发的,第一个Unix系统的核心和简单的应用程序。后来被移植到PDP-11平台上,功能更加完善。
目前仍然在世,在谷歌工作,开发了go语言
Ø 1971-1979年:出谷记
Dennis Ritchie(丹尼斯.里奇),C语言之父,用C语言重写了Unix系统内核,极大地提升了Unix系统的可读性、可维护性和可移植性——Unix V7,第一个真正意义上的Unix系统。
Ø 1980-1985年:第一次Unix战争
AT&T贝尔实验室:SVR4
和BSD(加州大学伯克利分校):BSD+TCP/IP
DQRPA,ARPANET(INTERNET),更加支持BSD,导致BSD开发出TCP/IP
IEEE,国际电气电子工程师协会,制定了POSIX标准,为Unix内核和外壳制定了一系列技术标准和规范,消除了系统版本之间的分歧,大一统的操作系统。—>战争结束
Ø 1988-1990年:第二次Unix战争
AT&T+Sun:
IBM+DEC+HP:
比尔.盖茨–>windows突然发布,战争结束
Ø 1992-至今:
1991年,Linus Torvalds创建了Linux系统的内核
1993年,Linux达到了产品级操作系统的水准
1993年,AT&T将Unix系统卖给Novell
1994年,Novell将Unix系统卖给X/Open组织
1995年,X/Open组织将Unix系统无偿捐赠给SCO
2000年,SCO把Unix系统卖给Celdear——Linux发行商
Linux就是现代版本的Unix
类Unix操作系统,开源免费。
虽然有不同的发行版本,但是它们都是用相同的内核
支持多种硬件平台:得益于它的免费和开源,手机、路由器、视频游戏控制器、个人PC、大型计算机等等
隶属于GNU功能,GNU = GNU Not Unix
受GPL许可证的限制:如果发布了一个可执行的二进制代码,就必须同时发布可读的源代码,并且在发布任何基于GPL许可证的软件时,不能添加限制性条款。—>后来又有了LGPL
早期版本:0.01、 0.02、 …, 1.00
旧计划:1.0.1——2.6.0(A.B.C)
A——主版本号:内核大幅更新
B——次版本号:内核重大修改,奇数表示测试版,偶数是稳定版
C——补丁序号:内核轻微修改
新计划:A.B.C-D.E(格式)
D——构建次数,反应极微小的更新
E——描述信息,rc/r(候选版本)、smp(支持对称多处理器核心)、EL(Rad Hat的企业版)、mm(试验新技术)、…
如何查看Linux系统版本号:cat /proc/version或者使用uname命令
遵循GNU/GPL许可证
开放性
多用户
多任务
设备无关性
丰富的网络功能
可靠的系统安全
良好的可移植性
ubuntu:大众化,简单易用
Linux Mint:新潮前卫,喜欢用一些新技术手段,可能不太稳定
Fedora:red hat的一个桌面版本
OpenSUSE:美观漂亮
Debian:自由开放
Slackware:简杰朴素、简陋
Red Hat:经典、稳定,企业应用,支持全面
CentOS:
Arch:
(1)支持多种硬件架构:x86_64、alpha、arm、mips、powerpc、SPARC、VAX……
(2)支持多种操作系统(可执行程序的组织形式):Linux、Unix、BSD、Android、MacOS、IOS、windows……
(3)支持多种编程语言:C、C++、Object-C、Java、Fortran、Pascal、Ada……
(4)查看GCC版本:gcc -v
源代码—>预编译(预处理)—>头文件和宏扩展—>编译—>汇编码—>汇编(.s)—>目标码(.o)—>链接—>可执行代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NM2G4Kq6-1578914622438)(./插图/day01/01_编译过程.png)]
nm命令:查看二进制文件符号表
vi hello.c:编辑源代码
gcc -E hello.c -o hello.i:预编译(编译预处理)
gcc -S hello.i -o hello.s:获得汇编代码
gcc -c hello.s -o hello.o:获得目标代码
gcc hello.o -o hello:获得可执行代码
./hello:运行可执行代码
参考代码:day01/code/01_编译过程
.h——C语言源代码头文件
.c——预处理之前的C语言代码文件
.s——汇编语言文件
以上文件都是可读的文本文件,以下文件是不可读的二进制文件(可以使用xxd/hexdump命令查看内容)
.o——目标文件
.a——静态库文件
.so——共享库文件(动态库文件)
.out——可执行文件,缺省的
gcc [选项][参数] 文件1 文件2 …
-o:指定输出文件
-E:预编译,缺省输出到屏幕,可以用-o指定输出文件
-S:编译,将高级语言文件编译成汇编语言文件
-c:汇编,将汇编语言文件汇编成机器语言文件
-Wall:产生全部警告
-std:指定编译器的版本
-Werror:将警告当做错误进行处理
-x:指定源代码语言
-g:产生调试信息,用于gdb调试
-O1/-O1/O2/O3:指定优化等级,O0不优化,缺省O1优化
(1)头文件里面写什么?
头文件卫士(#ifndef…#define…#endif):避免在编译阶段产生错误
其他头文件:
宏定义:
自定义类型:
类型别名:
外部变量的声明:
函数声明:
问:为什么不建议将函数定义写在头文件中?
答:一个头文件可能会被多个源文件包含,写在头文件里的函数定义也会因此被预处理器扩展到多个包含该头文件的源文件中,并在编译阶段被编译到多个不同的目标文件中,这将导致链接错误:multipe definition(多重定义)
(2)去哪里找头文件?
gcc -I <头文件附加搜索路径>
#include
先找-I指定的目录,如果没有找到,再找系统指定目录
#include “my.h”(双引号):
先找-I指定的目录,如果没有找到,再找当前目录,如果还没有找到,再找系统指定目录
头文件的系统目录:可以使用gcc <源文件> -v查看
usr/include:标准C库
/usr/local/include:第三方库
/usr/lib/gcc
/usr/lib/gcc/x86_64-linux-gnu/7/include:编译器库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-blmH7CHd-1578914622439)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day01/02_系统指定路径.png)]
代码:calc.h calc.c math.c
#include——将指定的文件内容插至此指令处
#define——定义宏
#undef——删除宏定义
#if——如果
#ifdef——如果宏已定义
#ifndef——如果宏未定义
#else——否则,与#if、#ifdef、#ifndef配合使用
#elif——否则如果,与#if、#ifdef、#ifndef配合使用
#endif——结束判定,与#if、#ifdef、#ifndef配合使用
#error——产生错误,结束预处理
#warning 字符串——产生警告,继续预处理
参考代码:error.c
#line 整数n——表示从下一行开始行号变更为第n行
参考代码:line.c
#pragma:设定编译器的状态或者指示编译器的操作
#pragma GCC dependency 被依赖文件:表示当前文件依赖于指定的文件名,如果指定的文件最后一次修改时间晚于当前文件,则产生警告信息
#pragma GCC poison 语法禁忌:一旦使用该标识符,则产生错误信息
#pragma pack(按几字节对齐,1/2/4/8):
#pragma pack()——按缺省字节数对齐
参考代码:dep.c pragma.c
无需自行定义,预处理器会根据事先设定好的规则将这些宏扩展成其对应的值,这些预定义宏不能被取消定义(#undef)或由编程人员重新定义
__BASE_FILE__:获取正在编译的文件名 %s
_FILE_ :获取当前宏所在的文件名 %s
_LINE_ :获取当前宏所在的行号 %d
_FUNCTION_ :获取当前宏所在的函数名 %s
__func___:同_FUNCTION
_DATE_ :处理日期%s
_TIME_ :处理时间%s
_INCLUDE_LEVEL_:包含层数,从0开始
__cplusplus: C++有定义,C无定义,可以检测环境是C编译器还是C++编译器
代码:print.h predef.h、predef.c
在进程上下文中保存的一些数据:键(功能,是什么)=值(具体内容)。
env(1):查看环境变量
grep(1):查找字符串
echo $环境变量名:打印该环境变量的值
C_INCLUDE_PATH:C头文件附加搜索路径,相当于-I选项
CPATH: 同C_INCLUDE_PATH
CPLUS__INCLUDE_PATH:C++头文件附加搜索路径,相当于-I选项
LIBRARY_PATH:链接库时查找的路径/链接器的默认搜索路径
LD_LIBRARY_PATH:加载库的路径/加载器的默认搜索路径
代码:calc.h calc.c math.c
Tips:包含自己的头文件的几种方式
(1) #include “头文件路径”:移植性差
(2) gcc -I指定头文件路径:推荐
(3) 通过C_INCLUDE_PATH或者CPATH指定头文件路径:易冲突
单一模型:将程序中的所有功能全部实现于一个单一的源文件内。缺点是编译时间长,不易于升级和维护,不易于协作开发
分离模型:将程序的不同功能模块划分到不同的源文件中。优点是缩短了编译时间,易于维护和升级,易于协作开发。不足之处在于,不同的源文件会生成很多的目标文件,使用是需要将每个目标文件依次链接,不方便,所以需要制作成库文件,方便使用和携带。
静态库的本质就是将多个目标文件打包成一个文件。
链接静态库就是将库中被调用的代码(函数**)复制到调用模块**中。
使用静态库的程序通常会占用较大的空间,库中代码一旦修改,所有使用该库的程序必须重新链接(缺点)。
使用静态库的程序运行时无需依赖静态库,执行效率高(优点)。
静态库的形式:lib库名.a
nm命令:查看二进制文件符号表
构建静态库:
构建静态库的过程就是将.c源文件编译生成.o目标文件,然后将.o目标文件打包生成.a库文件的过程,使用下面的命令制作静态库和使用静态库:
ar -r -o lib库名.a *.o
使用静态库:
参考代码:day02/static_lib/
补充:符号表含义
符号表是一种用于语言翻译器(例如编译器和解释器)中的数据结构。在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。对于符号表组织、构造和管理方法的好坏会直接影响编译系统的运行效率。
ldd命令:检查可执行文件依赖的动态库
动态库和静态库最大的不同就是,链接动态库并不需要将库中被调用的代码复制到调用模块中,被嵌入到调用模块中的代码仅仅是被调用代码在动态库中的相对地址。
如果动态库中的代码同时被多个进程所用,动态库的实例(在内存中)在整个内存中仅需一份,因此动态库也叫共享库/共享对象(shared object)。
使用动态库的模块是所占空间较小,即使修改了库中的代码,只要接口保持不变,无需重新链接。
使用动态库的代码,在运行时需要依赖库,因此执行的效率略低。
动态库的形式:lib库名.so
构建动态库:
gcc -c -fpic xxx.c -o xxx.o
#-fpic:生成与位置无关的代码,即在库内的函数调用也用相对地址表示
gcc -shared -o lib库名.so *.o #将生成的.o打包成动态库
使用动态库(和使用静态库一样)
运行时所调用的动态库必须位于LD_LIBRARY_PATH环境变量所表示的路径(加载器的搜索路径)。
参考代码:day02/dynamic_lib/
gcc缺省链接共享库,可通过-static选项强制链接静态库。
系统提供的针对动态库的动态加载函数
#include //头文件
Link with -ldl //编译选项
void *dlopen(const char *filename, int flags);
成功返回动态库的句柄,失败返回NULL。
filename-动态库的路径,若只给文件名,则根据LD_LIBRARY_PATH环境变量搜索动态库
**flags-**加载方式,可取以下值:
RTLD_LAZY-延迟加载(懒加载),使用动态库中的符号时才加载
RTLD_NOW-立即加载
该函数所返回的动态库句柄唯一的标识了系统内核所维护的动态库对象,将作为后续函数调用的参数
void *dlsym(void *handle, const char *symbol);
成功返回函数地址,失败返回NULL。
该函数所返回的函数指针是void*类型,使用时需要做强制类型为实际的函数指针类型才能调用。
int dlclose(void *handle);
关闭共享库,共享库的引用计数减一。如果引用计数为0,系统卸载共享库。成功返回0,失败返回非0
char *dlerror(void);
之前若有错误发生,则返回错误信息字符串,否则返回NULL
参考代码:load.c
#include
#include
int main(void)
{
//动态加载动态库libmath.so
void *handle =
dlopen("../dynamic_lib/libmath.so",RTLD_NOW);
if(!handle){
fprintf(stderr,"dlopen:%s\n",dlerror());
return -1;
}
//从动态库中获取add函数的入口地址
int (*add)(int, int) =
(int(*)(int, int))dlsym(handle,"add");
if(!add){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
//从动态库中获取sub函数的入口地址
int (*sub)(int, int) =
(int(*)(int,int))dlsym(handle,"sub");
if(!sub){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
//从动态库中获取show函数的入口地址
void (*show)(int,char,int, int) =
(void(*)(int,char,int,int))dlsym(handle,"show");
if(!show){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
int a=30, b=20;
show(a,'+',b,add(a,b));
show(a,'-',b,sub(a,b));
//卸载/关闭动态库
if(dlclose(handle)){
fprintf(stderr,"dlclose:%s\n",dlerror());
return -1;
}
return 0;
}
列出目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号
参考代码:nm.c
int a;//全局变量
static int b; //静态全局变量
void foo(void)
{
int c; //局部变量
static int d; //静态局部变量
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ifqH9rfZ-1578914622439)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day02/nm.png)]
删除目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号表和调试信息,即瘦身
查看可执行程序文件或动态库文件所依赖的动态库文件
返回整数的函数:通过返回合法值域以外的值表示错误
返回指针的函数:通过返回NULL指针表示错误
不需要通过返回值输出信息的函数:返回0表示成功,返回-1表示失败
#include
**char *strerror(int errnum);**根据错误号返回错误信息
**void perror(const char *s);**打印最近错误的错误信息
printf函数的**%m**标记会被替换为最近错误的错误信息
参考代码:./day02/error/errno.c
虽然所有的错误号都不是0,但是因为在函数执行成功的情况下,错误号全局变量errno不会被清0,因此不能用errno是否为0作为函数成功或失败的判断条件,是否出错还是应该根据函数的返回值来决定。
伪代码:
函数调用;
if(返回值表示函数调用失败){
根据errno判断发生了什么错误;
针对不同的错误提供不同的处理
};
每个进程都有一张独立的环境变量表,其中的每个条目都是一个形如“键=值”形式的环境变量。
环境变量是跟进程紧密相关的。
env命令用于显示bash的环境变量。
系统全局变量:erviron,指向了系统的环境向量表,使用时,需要自己在代码中做外部声明:*extern char *environ
所谓环境变量表就是一个以空指针NULL结束的字符指针数组,其中的每个元素都是一个字符指针,指向一个空字符结尾的字符串,该字符串就是形如“键=值”形式的环境变量。
#include
#include
void penv(char **env)
{
while(env && *env){
printf("%s",*env++);
}
}
int main(int argc, char *argv[], char *envp[])
{
extern char **environ;
penv(environ);
while(envp && *envp){
printf("%s",*envp++);
}
return 0;
}
(1)根据环境变量名获取该环境变量的值
char *getenv(const char *name)
成功返回变量名匹配的变量值,失败返回NULL。
(2)添加或修改环境变量
int putenv(char * string);
成功返回0,失败返回-1
(3)添加或修改环境变量
int setenv(const char *name, const char *value, int overwrite);
成功返回0,失败返回-1
(4)删除环境变量
int unsetenv(const char * name);
成功返回0,失败返回-1
(5)清空环境变量
int clearenv(void)
成功返回0,失败返回-1
参考代码:./day02/env/env.c
补充:
Linux环境变量的分类:
1.系统级环境变量:每一个登录到系统的用户都能够读取到系统级的环境变量,分为以下几类:
/etc/profile:在系统启动后第一个用户登录时运行,并从/etc/profile.d目录的配置文件中搜集shell的设置,使用该文件配置的环境变量将应用于登录到系统的每一个用户
/etc/bashrc(Ubuntu和Debian中是/etc/bash.bashrc):在 bash shell 打开时运行,修改该文件配置的环境变量将会影响所有用户使用的bash shell
/etc/environment:在系统启动时运行,用于配置与系统运行相关但与用户无关的环境变量,修改该文件配置的环境变量将影响全局
2.用户级环境变量:每一个登录到系统的用户只能够读取属于自己的用户级的环境变量
~/.profile:当用户登录时执行,每个用户都可以使用该文件来配置专属于自己使用的shell信息;
**/.bashrc**:当用户登录时以及每次打开新的shell时该文件都将被读取;注意:通常我们修改bashrc,有些linux的发行版本不一定有profile这个文件etc/profile等中设定的变量(全局)的可以作用于任何用户,而/.bashrc等中设定的变量(局部)只能继承/etc/profile中的变量, 他们是"父子"关系。
虚拟内存:地址空间,虚拟的存储区域,应用程序所访问的都是虚拟内存。
物理内存:存储空间,实际的存储区域,只有系统内核可以访问物理内存。物理内存包括半导体内存和换页文件两部分。当半导体内存不够用时,可以把一些长期闲置的代码和数据从半导体内存中缓存到换页文件中,这叫页面换出。一旦需要使用被换出的代码和数据,再把它们从换页文件恢复到半导体内存,这叫做页面换入。因此,系统中的虚拟内存比半导体内存大得多。
虚拟内存和物理内存之间存在对应关系,当应用程序访问虚拟内存时,系统内核会依据这种对应关系找到与之相应的物理内存。上述对应关系存储在内核中的内存映射表中。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-chkS95mb-1578914622439)(./插图/day02/内存映射关系.png)]
Unix/Linux操作系统采用虚拟内存管理技术,即每个进程都有各自互不干涉的4G线性虚拟地址空间(32位系统),用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间。
地址分类:物理地址、逻辑地址、线性地址。物理地址是内存单元的实际地址,用于芯片级内存单元寻址;逻辑地址是程序代码经过编译后出现在 汇编程序中地址,每个逻辑地址都由一个段和偏移量组成;线性地址其实就是虚拟地址,在32位CPU架构下,可以表示4GB的地址空间。
逻辑地址经段机制转化成线性地址,线性地址又经过页机制转化为物理地址。物理地址 = 段基址<<4 + 段内偏移(线性地址),但是在Linux系统中,令段的基地址为0,所以段内偏移量=线性地址,也就是说虚拟地址直接映射到了线性地址,Linux把段机制给绕过去了。
每个进程都拥有独立的4GB(32位系统)的虚拟内存(虚拟地址空间),分别被映射到不同的物理内存区域。
内存映射和换入换出都是以页为单位,1页=4096B(4K)。
4GB虚拟内存中高地址的1GB被映射到内核的代码和数据区,这1GB虚拟地址空间在各个进程间共享。用户的应用程序只能直接访问低地址的3GB虚拟内存,该区域称为用户空间,而高地址的1GB虚拟内存(虚拟地址空间)被称为内核空间。用户空间中的代码,只能直接访问用户空间的数据;如果要想访问内核中的代码和数据必须借助专门的系统调用完成。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aZt1FAlY-1578914622440)(./插图/day02/进程地址空间布局.png)]
用户空间的3GB虚拟内存可以进一步划分为如下区域:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-InmjUg9G-1578914622440)(./插图/day02/内存分布.png)]
参考代码:./day03/proc_maps/maps.c
size命令可以查看一个可执行程序的代码区、数据区和BSS区的大小。
cat /proc/
每个进程的用户空间都拥有独立的从虚拟内存到物理内存的映射,称之为进程间的内存壁垒(只能访问自己的用户空间,不能访问其他进程的用户空间)。
参考代码:./day03/proc_maps/vm.c
malloc、calloc、realloc、free都是调用的brk/sbrk两个函数,brk/sbrk两个函数调用mmap/munmap这两个函数,mmap/munmap这两个函数调用kmalloc/kree这两个函数
分配:映射+占有
映射:在地址空间(虚拟内存)和存储空间(物理内存)建立映射关系
占有:指定内存空间的归属性
释放:解除映射+放弃占有
放弃占有:解除对内存空间的归属约束
解除映射:消除地址空间(虚拟内存)和存储空间(物理内存)之间的映射关系
(1) 以增量方式分配或者释放虚拟内存
*void sbrk(intptr_t increment);
成功返回调用该函数之前的堆顶指针,失败返回-1
increment:可取以下值
>0—堆顶指针上移,增大堆空间,分配内存
<0—堆顶指针下移,缩小堆空间,释放内存
=0—不分配也不释放虚拟内存,仅仅返回当前堆顶指针
系统内核维护一个指针,指向堆内存的顶端,即有效堆内存中最后一个字节的下一个位置。sbrk函数根据增量参数increment调整该指针的位置,同时返回该指针原来的位置。期间若发生内存页耗尽或空闲,则自动追加或取消相应内存页的映射。
参考代码:./day03/sbrk/sbrk.c
(2) 以绝对地址的方式分配或者释放虚拟内存
*int brk(void end_data_segment);
成功返回0,失败返回-1
end_data_segment:可取以下值
>当前堆顶—分配虚拟内存
<当前堆顶—释放虚拟内存
=当前堆顶—空操作
系统内核维护一个指针,指向当前堆顶。brk函数会根据指针参数end_data_segment来设置堆顶的新位置。期间若发生内存页耗尽或空闲,则自动追加或取消相应内存页的映射。
参考代码:./day03/sbrk/brk.c
(3) 建立物理内存或文件到虚拟内存的映射
**void mmap(void start, size_t length, int prot, int flags, int fd, off_t offet);
成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(void *类型的-1)
start—映射区虚拟内存的起始地址,NULL表示由内核自动选择
length—映射区的字节数,自动按页(4096)取整
port—访问权限,可取以下值
PROT_READ-可读
PROT_WRITE-可写
PROT_EXEC-可执行
PROT_NONE-不可访问
**flags—**可以去以下值
MAP_ANONYMOUS—匿名映射,将虚拟内存映射到物理内存,函数的最后两个参数fd和offset被忽略
MAP_PRIVATE—私有映射,将虚拟内存映射到文件的内存缓冲区中,而非磁盘文件
MAP_SHARED—共享映射,将虚拟内存映射到磁盘文件中
MAP_DENYWRITE—拒绝写入映射,文件中被映射的区域不能存在其它写入操作
MAP_FIXED—固定映射,若在start上无法创建映射,则失败;如果没有此标志,系统自动调整
MAP_LOCKED—锁定映射,禁止被换出到换页文件
fd—文件描述符
offset—文件偏移量,自动按页对齐
(4) 解除物理内存或文件到的虚拟内存映射,可以一次性解除映射,也可以分次解除映射
int munmaps(void *start,size_t length );
成功返回0,失败返回-1
参考代码:mmap.c
PS:段错误的本质——访问了没有物理内存对应的虚拟地址或者该地址有物理内存对应但是没有访问权限
通常brk函数和sbrk函数配合使用:使用sbrk分配内存,使用brk释放内存
——摘自百度文库,仅作了解
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存):brk是数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
内存分配原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bAGovH4m-1578914622440)(./插图/day03/系统调用.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SQRYEvRK-1578914622440)(./插图/day03/操作系统层次.png)]
(1) Linux系统内核提供了一套用于实现各种系统功能的子程序,谓之系统调用。程序的编写者可以像调用普通C语言函数一样调用这些系统调用函数,以访问系统内核提供的各种服务。
(2) 系统调用函数在形式上与普通C语言函数并无差别,二者的不同之处在于,前者工作在内核态,而后者工作在用户态。
(3) 在intel的CPU上,运行代码分为4个安全级别:Ring0、Ring1、Ring2和Ring3,Linux系统只使用了Ring0和Ring3。用户代码工作在Ring3级,而内核代码工作在Ring0级。一般而言,用户代码无法访问Ring0级的资源,除非借助系统调用,使用户代码得以进入Ring0级,使用系统内核提供的功能。
(4) 在系统内核的内部维护了一张全局表sys_call_table,表中的每个条目记录着每个系统调用在内核代码中的实现的入口地址。
(5) 当用户代码调用了某个系统调用函数时,该函数会先将参数压入堆栈,将系统调用的标识存入eax寄存器,然后通过int 80H指令触发80h中断。
(6) 这时程序便从用户态(Ring3)进入内核态(Ring0).
(7) 工作在系统内核中的中断处理函数被调用,80h中断的处理函数名为system_call,该函数先从堆栈中取出参数,再从eax寄存器中取出系统调用标识,然后再从sys_call_table表中找到与该系统调用标识相对应的实现代码入口地址,携其参数去调用该实现,并将处理结果逐层返回到用户代码中。
(1) 机械硬盘的物理结构:驱动臂、盘片、主轴、磁头、控制器等。
(2) 磁表面存储器读写原理
硬盘盘片的表面都覆盖着薄薄的磁性涂层,涂层中含有无数微小的磁性颗粒,谓之磁畴。相邻的若干磁畴组成一个磁性存储元,以其剩磁的极性表示二进制数字0和1。为磁头写线圈施加脉冲电流,可把一位二进制数字转化为磁性存储元的剩磁极性。利用磁电变换,通过磁头的读线圈,可将磁性存储元的剩磁极性转换为相应的电信号,表示成二进制数。
(3) 磁道和扇区
磁盘旋转,磁头固定,每个磁头都会在盘片表面画出一个圆形轨迹。改变磁头的位置,可以形成若干大小不等的同心圆,这些同心圆就叫磁道(Track)。每张盘片的每个表面都有成千上万个磁道。一个磁道按照512Bytes为单位分成若干个区域,其中的每个区域就叫做一个扇区(Sector)。扇区是文件存储的基本单位。
(4) 柱面、柱面组、分区和磁盘驱动器
硬盘中不同盘片相同半径的磁道所组成的圆柱称为柱面(Cylinder)。整个硬盘的柱面数与每张盘片的磁道数是相等的。硬盘上的每个字节需要通过以下参数定位:磁头号用来确定哪个盘面,柱面号确定哪个磁道,扇区号确定哪个区域,偏移量确定扇区内的位置。磁头号、柱面号、扇区号和偏移量统称柱面I/O。若干个连续的柱面构成了一个柱面组,若干连续的柱面组构成了一个分区。每个分区都建有独立的文件系统,若干个分区组成磁盘驱动器。
磁盘驱动器:| 分区 | 分区 | 分区 |
分区 引导块 | 超级块 | 柱面组 | 柱面组 | 柱面组 | 柱面组 |
柱面组 引导块副本 | 柱面组信息 | i节点映射表 | 块位图 | i节点表 | 数据块集 |
i节点:| 文件元数据 | 数据块的索引表 |
根据目录文件中记录的i节点编号来检索i节点映射表,获得i节点下标,用该下标查i节点表,获得i节点。i节点中包含了数据块索引表,利用数据块索引表从数据块集中读取数据块,即获得文件数据。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ywN2FF8y-1578914622441)(./插图/day05/文件系统结构.png)]
直接块:存储文件的实际数据内容
间接块:存储下级文件数据的索引表[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ktjmrnG-1578914622441)(./插图/day05/直接块_间接块.png)]
普通文件(-):可执行程序、文本、图片、音频、视频、网页等
目录文件(d):该目录中每个硬链接名和i节点号的对应表
符号链接文件(l):存放一个目标文件的路径
管道文件§:有名管道,用于进程间通信
套接字文件(s):进程间通信
块设备文件(b):按块寻址,顺序或随机读写
字符设备文件©:按字节寻址,只能以字节为单位顺序读写
打开文件:在系统内核中建立一套数据结构,用于访问文件。
进程表项
文件描述符表
文件描述符标志 | 文件表项指针 | 0
文件描述符标志 | 文件表项指针 | 1
文件描述符标志 | 文件表项指针 | 2
…
这个结构中文件标志对应的数组的下标就是文件描述符。文件表项指针指向文件表项,V节点指针指向V节点
文件表项
文件状态标志
文件读写位置
V节点指针
…
V节点
i节点内容
…
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3k8qah4o-1578914622441)(./插图/day03/进程表项.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbkONcc0-1578914622441)(./插图/day03/文件的内核数据结构.png)]
关闭文件:释放打开文件过程中建立的数据结构
(1) 打开已有的文件或创建新文件
int open(const char *pathname, int flags, mode_t mode);
成功返回文件描述符,失败返回-1
pathname——文件路径
flags——状态标志,可取以下值:
O_RDONLY——只读
O_WRONLY——只写
O_RDWR——读写
O_APPEND——追加
O_CREAT——创建,不存在即创建,如果已存在立即打开,除非与以下两个标志之一合用,有此标志,mode参数才有效
O_EXCL——排它,已存在会失败,必须和O_CREAT合用
O_TRUNC——清空,已存在即清空,必须同时写权限,必须和O_CREAT合用
O_SYNC——写同步,在数据被写到磁盘之前写操作不会完成,读操作本来就是同步的,此标志对读操作没有意义
O_ASYNC——异步,在文件可读写时会产生一个SIGIO信号,在对该信号的处理过程中读写I/O就绪的文件,只能用于终端设备或者网络套接字,而不能用于磁盘文件
O_NONBLOCK——非阻塞,读操作不会因为无数据可读而阻塞,写操作也不会因为缓冲区满而阻塞,相反,会返回失败,并设置特定的errno
mode——权限模式,三位八进制数
所创建文件的实际权限除了跟mode参数有关,还受权限掩码umask的影响,实际权限 = mode & ~umask。比如,mode = 0666,umask = 0002,实际权限 = 0664.
创建新文件
*int creat(const char pathname, mode_t mode)
相当于把open的flags设为:O_WRONLY | O_CREAT | O_TRUNC
打开已有的文件
*int open(const char pathname, int flags)
关闭文件
**int close(int fd) **
成功返回0,失败返回-1. fd表示文件描述符。
参考代码:open.c redir.c
作为文件描述符表项在文件描述符表中的下标,合法的文件描述符一定是大于或等于0的整数。每次产生新的文件描述符表项。系统总是会从下标0开始在文件描述符表中寻找最小的未使用项。每关闭一个文件描述符,无论被其索引的文件表项和V节点是否被删除,与之对应的文件描述符表项一定会被标记为未使用,并在后续操作中为新的文件描述符所占用。系统内核缺省会为每个进程打开三个文件描述符:0、 1、 2,分别对应STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO这三个宏,标准错误和标准输出的区别是标准输出有缓冲,标准错误无缓冲。文件描述符是用户程序和系统内核关于文件的唯一联系方式。
标C | Uc | C++ | |
---|---|---|---|
标准输入 | stdin | 0/STDIN_FILENO | cin |
标准输出 | stdout | 1/STDOUT_FILENO | cout |
标准错误 | stderr | 2/STDERR_FILENO | cerr |
数据类型 | FILE * | int | iostream |
(1) 向指定的文件写入字节流
ssize_t write(int fd, const void *buf, size_t count)
成功返回实际写入的字节数,返回0表示未写入,失败返回-1
(2) 向指定的文件写入字节流
ssize_t read(int fd, void *buf, size_t count)
成功返回实际读取的字节数(0表示读到文件尾),失败返回-1
参考代码:write.c read.c
基于系统调用的文件读写本来就是面向二进制字节流的,因此对二进制读写而言,无需做任何额外的工作。如果要求文件中的内容必须是可阅读的,那么就必须通过格式化和文件解析处理二进制形式的数据和文本字符串之间的转换。
参考代码:binary.c text.c
每个打开的文件都有一个与其相关的文件读写位置保存在文件表项中,用以记录从文件头开始计算的字节偏移。文件读写位置通常是一个非负整数,用off_t类型表示,在32位系统上被定义为long int,而在64位系统上则被定义为long long int。打开一个文件时,除非指定了O_APPEND标志,否则文件读写位置一律被设为0,即文件首字节的位置。每一次读写操作,都从当前的文件读写位置开始,并根据所读写的字节数同步增加文件读写位置,为下一次读写做好准备。因为文件读写位置是保存在文件表项而不是V节点中,因此通过多次打开同一个文件得到多个文件描述符,各自会拥有各自的文件读写位置。
(1) 调整文件读写位置
off_t lseek(int fd, off_t offset, int whence)
成功返回调整后的文件读写位置,失败返回-1
fd——文件描述符
offset——文件读写位置,相对于whence参数的偏移量
whence——可取以下值
SEEK_SET——从文件头开始
SEEK_CUR——从当前位置开始
SEEK_END——从文件尾开始
lseek函数仅仅是修改文件表项中的文件读写位置,并不引发实际的I/O操作,速度很快。
lseek(fd,10,SEEK_SET);
lseek(fd,-10,SEEK_END);
lseek(fd,0,SEEK_CUR);//返回当前读写位置
lseek(fd,0,SEEK_END);//返回文件总字节数
lseek(fd,-10,SEEK_SET);//错误
lseek(fd,10,SEEK_END);//允许,但是会产生文件空洞,空洞部分补0
//文件空洞不占用磁盘空间,但被算在文件大小内
参考代码:seek.c
I/O速度比较:stdio.c sysio.c
标准和通过缓冲区优化,可以减少系统调用的次数,降低在用户态和内核态之间来回切换的频率,提高运行速度,缩短运行时间。
int dup(int oldfd)
成功返回目标文件描述符,失败返回-1
dup函数将oldfd对应的文件描述符表项复制到文件描述符表的第一个空闲项中,同时返回该表项所对应的文件描述符。
int dup2(int oldfd, int newfd)
成功返回目标文件描述符,失败返回-1
dup2函数在复制oldfd参数所标识的文件描述符表项时,会首先检查有newfd参数所标识的目标文件描述符表项是否空闲,若空闲则直接将前者复制给后者,否则会先将文件描述符newfd关闭,再进行复制。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zHZd9fpQ-1578914622442)(./插图/day04/open和dup区别.png)]
参考代码:dup.c same.c
int fcntl(int fd, int cmd, … );
(1) 复制文件描述符(表项)
int fcntl(int oldfd, F_DUPFD, int newfd )
成功返回目标文件描述符,失败返回-1
类似dup2函数,但略有不同。如果newfd处于打开状态,该函数并不会像dup2函数那样关闭它,而是另外寻找一个比它大的最小的空闲文件描述符作为复制目标。
参考代码:fcntl.c
(2) 获取/设置文件描述符标志
截至目前只有1个文件描述符标志位:FD_CLOEXEC。一个进程可以通过exec函数族启动另一个进程取代其自身。原进程中无FD_CLOEXEC标志位的文件描述符在新进程中依然保持打开状态,这也是文件描述符的默认标志;如果原进程中某个文件描述符带有此标志位,那么在新进程中该文件描述符会被关闭。
int fcntl(int fd, F_GETFD);——获取文件描述符标志
成功返回文件描述符标志,失败返回-1
int fcntl(int fd, F_SETFD, int flags);——设置文件描述符标志
成功返回0,失败返回-1
参考代码:fd.c
(3) 获取/追加文件状态标志
int fcntl(int fd, F_GETFL);——获取文件状态标志
成功返回文件状态标志,失败返回-1. 与文件创建有关的三个状态标志(O_CREAT、O_EXEC、O_TRUNC)无法被获取。
只读标志的值为0,不能用位与检测,其他标志可以用位与检测。
if((flags & O_ACCMODE) == O_RDONLY){
//只读文件
}
if(flags & O_WRONLY){
//只写文件
}
if(flags & O_RDWR){
//可读可写文件
}
int fcntl(int fd, F_SETFL, flags);——追加文件状态标志
成功返回0,失败返回-1
只有O_APPEND和O_NONBLOCK两个状态标志可被追加
参考代码:fl.c
文件的某个区域正在被访问|
读取 | 写入 | |
---|---|---|
无人访问 | OK | OK |
多人在读 | OK | NO |
一人在写 | NO | NO |
为了避免在读写同一文件的同一个区域时发生冲突,进程之间应遵循以下规则(读共享,写独占):
参考代码:write.c read.c
为了避免多个进程在读写同一个文件的同一区域时发生冲突,操作系统引入了文件锁机制,并把文件锁分为读锁和写锁。它们的区别在于:
锁模式:加锁—>读写—>解锁
读锁 | 写锁 | |
---|---|---|
无任何锁 | OK | OK |
多把读锁 | OK | NO |
一把写锁 | NO | NO |
int fcntl(int fd, F_SETLKW, struct flock *lock);——阻塞
int fcntl(int fd, F_SETLK, struct flock *lock);——非阻塞
struct flock结构体内容:
struct flock{
short int l_type; //锁类型,F_RDLCK(加读锁)/F_WRLCK(加写锁)/UNLCK(解锁——不会阻塞)
short int l_whence;//锁区偏移起点, SEEK_SET/SEEK_CUR/SEEK_END
off_t l_start;//锁区偏移
off_t l_len;//锁区长度(字节数),0表示到文件尾
pid_t l_pid;//加锁进程的PID,-1表示自动设置
};
//对相对于文件头10字节开始锁20字节长度加读锁,阻塞方式
struct flock lock;
lock.l_type = F_RDLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 10;
lock.l_len = 20;
lock.l_pid = -1;
fcntl(fd,F_SETLCKW,&lock);//停-等机制
//对相对于当前位置10字节开始到文件尾以非阻塞方式加写锁
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_CUR;
lock.l_start = 10;
lock.l_len = 0;
lock.l_pid = -1;
fcntl(fd,F_SETLCK,&lock);
//对整个文件解锁,解锁不会阻塞
struct flock lock;
lock.l_type = F_UNLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_pid = -1;
fcntl(fd,F_SETLCK,&lock);
参考代码:wlock.c rlcok.c
int fcntl(int fd, F_GETLK, struct flock *lock);——测试对文件的某个区域是否可以加某种锁;如果不能加锁,是什么原因导致加锁冲突
成功返回0,失败返回-1.
调用该函数时,lock参数表示欲加之锁的细节。成功返回时,通过lock参数输出欲加之锁是否可加以及存在冲突的锁信息。
参考代码:lock1.c lock2.c
V节点指针
i节点内容
锁表指针–>锁节点–>锁节点–>…
锁的类型
锁区偏移
锁区大小
加锁进程PID
每次对给定文件的特定区域加锁,都会通过fcntl函数向系统内核传递flock结构,该结构中包含了有关锁的一些细节,诸如锁的类型、锁区的起始位置和大小,甚至加锁进程的pid(填-1则由系统自动设置)。系统内核会收集所有进程对该文件所加的各种锁,并把这些flock结构中的信息以链表的形式组织成一张锁表,其起始地址保存在该文件的v节点中。任何一个进程通过fcntl函数对该文件加锁,系统内核都要遍历这张锁表,一旦发现有与欲加之锁构成冲突的锁,即阻塞或报错,否则,将欲加之锁插入到锁表;而解锁的过程实际上就是调整或者锁表中的相应节点。
文件锁属于劝谏锁,亦称协议锁。
i节点
文件元数据
数据块索引表
…………
文件元数据保存在下面的结构体中:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* file type and mode */
nlink_t st_nlink;/* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize;/* blocksize for filesystem I/O */
blkcnt_t st_blocks;/* number of 512B blocks allocated */
time_t st_atim; /* time of last access */
time_t st_mtim; /* time of last modification */
time_t st_ctim; /* time of last status change */
#define st_atime st_atim.tv_sec /*Backward ompatibility*/
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
文件类型和权限的数据类型mode_t其实就是一个整数,其中只有低16位有效。
B15~B12——文件类型,掩码:S_IFMT
1000——S_IFREG——普通文件(-)
0100——S_IFDIR——目录文件(d)
1100——S_IFSOCK——本地套接字文件(s)
0010——S_IFCHR——字符设备文件©
0110——S_IFBLK——块设备文件(b)
1010——S_IFLNK——符号链接文件(l)
0001——S_IFFIFO——有名管道文件§
B11~B9——设置用户ID位、设置组ID位和粘滞位
B8~B6——拥有者用户,分别对应读、写、可执行
B5~B3——拥有者组,分别对应读、写、可执行
B2~B0——其它用户,分别对应读、写、可执行
设置用户ID位——带有该位,用户无可执行权限,可执行权限位为S;有可执行权限,可执行权限位为s
~$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 59680 11月 27 06:27 /usr/bin/passwd
设置组ID位——带有该位,用户组无可执行权限,可执行权限位为S;用户组有可执行权限,可执行权限位为s
粘滞位——带有该位的目录文件,其他用户无可执行权限,可执行权限位为T;有可执行权限,可执行权限位为t
~$ ls -l / |grep tmp
drwxrwxrwt 11 root root 4096 12月 28 09:21 tmp
获取文件元数据
int stat(const char *pathname, struct stat *buf);
int fstat(int fd, struct stst*buf);
int lstat(char const *path, struct stat *buf);//不跟踪符号链接,if pathname is a symbolic link, then it returns information about the link itself, not the file that it refers to.
成功返回0,失败返回-1
参考代码:stat.c
int access(const char *pathname, int mode);
成功返回0,失败返回-1
pathname——文件路径
mode——访问权限,可取以下值:
R_OK——可读否
W_OK——可写否
X_OK——可执行否
F_OK——存在否
根据调用该函数的进程的实际用户ID和实际组ID,检测其是否可读、可写或可执行,也可以检测该文件是否存在。
参考代码:access.c
mode_t umask(mode_t cmask);
永远成功,返回原来的权限掩码
权限掩码是进程的属性之一,存储在系统内核中的进程表项中。umask函数所影响的仅仅是调用进程自己,对于其它进程包括其父进程都没有影响。
参考代码:umask.c
#include
int chmod(const char* pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
成功返回0,失败返回-1
调用进程的有效用户ID必须与文件拥有者用户ID匹配或者是root用户,才能修改该文件的权限,且受权限掩码的影响
参考代码:chmod.c
#include
int chown(const char* path, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char* path, uid_t owner, gid_t group);//不跟踪符号链接
成功返回0,失败返回-1
如果调用进程的有效用户ID为root用户,则它可以任意修改任何文件的拥有者和组;如果调用进程的有效用户ID为普通用户,则它只能把自己名下的文件的拥有者组改成自己隶属的其它组。
参考代码:chown.c
#include
int truncate(const char* path, off_t length);
int ftruncate(int fd, off_t length);
成功返回0,失败返回-1
由大变小:截掉文件尾的部分
由小变大:在文件尾之后增加0
参考代码:trunc.c
mmap映射磁盘文件
#include
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
参考代码:fmap1.c fmap2.c
硬链接就是文件路径,即由各级目录、分隔符(/)和文件名共同组成的字符串,与一个特定的i节点号所形成的对应关系。
ln <目标路径(已存在的路径)> <源路径(新建立的路径);——创建硬链接命令
根据一个已有的硬链接来创建一个新的硬链接
**int link(const char * oldpath, const char* newpath ); **
成功返回0,失败返回-1
删除硬链接
int unlink(const char pathname);*
从pathname所对应的目录文件中删除包含文件的条目,同时将其所对应的i节点中的硬链接数减一。若该硬链接数被减至0,则将该文件所占用的磁盘空间释放出来。
修改硬链接
int rename(const char* oldpath, const char* newpath)
成功返回0,失败返回-1
rename("./a.txt", "./b.txt");//改名
rename("a/1.txt", "b/1.txt");//移动
rename("a/1.txt", "b/2.txt");//移动+改名
另一个版本的unlink,还可以删除空目录
int remove(const char* pathname);
参考代码:link.c unlink.c remove.c rename.c
软链接文件的本质就是保存着另一个文件或者路径的文件
根据一个已有的硬链接创建一个符号链接
int symlink(const char* oldpath, const char* newpath);
成功返回0,失败返回-1
读取软链接文件本身的内容
ssize_t readlink(const char* path, char* buf, size_t size);
成功返回拷入buf的符号链接文件内容的字节数,失败返回-1
参考代码:slink.c
创建空目录
int mkdir(const char* pathname, mode_t mode);
成功返回0,失败返回-1
删除一个空目录
int rmdir(const char* pathname);
成功返回0,失败返回-1
remove = unlink + rmdir
参考代码:dir.c
获取当前工作目录
char* getcwd(char* buf, size_t size);
成功返回工作目录字符串指针,即buf,会自动追加结尾空字符;失败返回NULL
当前工作目录作为进程的属性之一,也是系统内核进程表项的一部分。
改变当前工作目录
int chdir(const char* path);
成功返回0,失败返回-1
参考代码:dir.c
打开目录
DIR* opendir(const char* name);
成功返回目录流指针,失败返回NULL
读取目录
struct dirent* readdir(DIR* dirp);
成功返回目录条目指针,读完(不设置errno)或者失败(设置errno)都会返回NULL
struct dirent 结构体内容如下:
struct dirent{
ino_t d_ino; //节点号
off_t d_off; //下一条位置(索引)
unsigned short d_reclen;//记录长度
unsigned char d_type; //文件类型
char d_name[];//文件名
……
};
关闭目录流
int closedir(DIR* dirp);
成功返回0,失败返回-1
程序:磁盘上的可执行文件
进程:内存中的可执行指令和数据,CPU执行指令和访问数据
补充:进程控制块
操作系统通过进程控制块(PCB——Process Control Block)来管理进程
进程控制块内容:
交互式进程:由shell启动,借助标准I/O与用户交互
批处理进程:在无需人工干预的条件下,自动运行一组批量任务
守护(精灵)进程:后台服务,多数时候处于待命状态,一旦由需要,可被激活完成特定的任务
ps——显示当前用户拥有控制终端的进程信息
ps auxw——BSD风格选项
a——所有用户
x——既包括有控制终端也包括无控制终端的进程
u——显示详细信息
w——更大列宽
ps -efFl——SVR4
e——所有用户的所有进程
f——完整格式
F——更完整格式
l——长格式
进程快照每列信息:
USER/UID——进程的实际用户ID
PID——进程标识
%CPU/C——CPU使用率
%MEM——内存使用率
VSZ——占用虚拟内存的大小(KB)
RSS——占用物理内存(不包括换页文件)的大小(KB)
TTY——终端次设备号,ttyn表示物理终端(硬件设备),pts/n表示虚拟终端(软件窗口),?表示无控制终端如后台进程
STAT/S——进程状态
START——进程的启动时间
TIME——进程的运行时间(占用CPU的时间)
COMMAND/CMD——进程的启动命令
F——进程标志,1代表通过fork产生的子进程,但是并没有exec创建新进程;4表示拥有超级用户(root)的特权
PPID——父进程的PID
NI——进程的nice值,-20~19,优先级的浮动量
PRI——进程的优先级=80+nice,60~99,值越小优先级越高。I/O消耗型进程,奖励,提高优先级,降低nice值;处理机消耗型进程,惩罚,降低优先级,提高nice值
ADDR——内核进程的内存地址,普通进程显示“-”
SZ——占用虚拟内存的页数
WCHAN——进程正在等待的内核函数或者事件
PSR——进程当前正在被哪个处理器执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MpMLiI4u-1578914622442)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day07/进程状态图.png)]
top命令:动态显示,实时刷新
pstree:显示进程树
父进程创建子进程,子进程继承父进程。一个父进程可以创建多个子进程,每个子进程有且仅有一个父进程。除非是根进程(PID = 0,调度器实例)没有父进程。
进程树:
调度进程,pid = 0
init,pid = 1
xinetd
in.telnetd <-------远程登录
login <----用户名和口令
bash <-等待用户输入shel命令
例如:ls—>显示目录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gyaaoZ8x-1578914622443)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day07/父子进程.png)]
创建进程的两种模式:
(1) 父进程在创建完子进程以后依然存在,甚至可以和子进程进程某种形式的交互,如传参、回收、通信等。
(2) 旧进程在创建完新进程以后被其取代,新进程沿用旧进程的PID,继续独立存在。
孤儿进程
父进程创建子进程以后,子进程在操作系统的调度下可以与其父进程同时运行。如果父进程先于子进程的终止而终止,子进程即成为孤儿进程,同时被init进程收养,即成为init进程的子进程,因此,init进程又被成为孤儿院进程。一个进程成为孤儿进程是正常的,系统中大多数的守护进程都是孤儿进程。
僵尸进程
如果子进程先于父进程的终止而终止,但父进程由于某种原因没有回收子进程的尸体(终止状态),子进程即成为僵尸进程。僵尸进程虽然不在活动,但其终止状态和PID会依然保留,也会占用系统资源,直到其被父进程或init进程回收为止。如果父进程直到其终止都没有回收其处于僵尸状态的子进程,init进程会立即回收这些僵尸进程。因此,一个进程不可能同时既是僵尸进程,又是孤儿进程。
系统内核会为每个进程维护一个进程表项,其中包括以下ID:
进程ID:系统为每个进程分配的唯一标识。内核在分配进程ID时会持续增加,直到无法再增加了,再从头寻找被释放的ID,即延迟重用
父进程ID:父进程的PID,在创建子进程的过程中被初始化到子进程的进程表项中
实际用户ID:启动该进程的用户ID
实际组ID:启动该进程的用户组ID
有效用户ID:通常情况下,取自进程的实际用户ID。如果该进程的可执行文件带有设置用户ID位,那么该进程的有效用户ID就取自其可执行文件的拥有者用户ID
有效组ID:通常情况下,取自进程的实际组ID,如果该进程的可执行文件带有设置组ID位,那么该进程的有效组ID就取自其可执行文件的拥有者组ID
一个进程的能力和权限,由其有效用户ID和有效组ID决定
(1) 获取各种ID
pid_t getpid(void);——返回调用进程的PID
pid_t getppid(void);——返回调用进程的ppid,即其父进程的PID
uid_t getuid(void);——返回调用进程的实际用户ID
uid_t getgid(void);——返回调用进程的实际用户组ID
uid_t geteuid(void);——返回调用进程的有效用户ID
uid_t getegid(void);——返回调用进程的有效用户组ID
参考代码:id.c
产生进程分之(fork):
pid_t fork(void);
成功分别在父子进程中返回子进程的pid和0,失败返回-1.
调用一次返回两次:在父进程中返回所创建子进程的pid,而在子进程中返回0.
函数的调用者往往可以根据该函数返回值的不同,分别为父子进程编写不同的处理分之。
pid_t pid = fork();
if(pid == -1){
perror("fork");
exit(EXIT_FAILURE);
}
if(pid == 0){
//子进程的处理分之;
exit(EXIT_SUCCESS);
}
//父进程的处理分之;
exit(EXIT_SUCCSS);
子进程是父进程的不完全副本,子进程的数据区、BSS区、堆栈区(I/O流缓冲区),甚至命令行参数和环境变量区,都从父进程拷贝,只有代码区与父进程共享。
fork函数成功返回后,父子进程就各自独立地运行,其被调度的先后顺序并不确定,某些实现可以保证子进程先被调度。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JYUA22nb-1578914622443)(./插图/day07/fork1.png)]
fork函数成功返回以后,系统内核为父进程维护的文件描述符表也被复制到子进程的进程表项中,文件表项并不复制。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9HOCQtvc-1578914622443)(./插图/day07/fork2.png)]
系统总线程数达到上限(cat /proc/sys/kernel/threads-max)或用户的总进程数(ulimit -u)达到上限,fork函数将返回失败。
一个进程如果希望创建自己的副本并执行同一份代码,或者是希望与另一个进程并发地运行,都可以使用fork函数。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h1uhAvbe-1578914622443)(./插图/day07/fork3.png)]
参考代码:fork.c mem.c os.c is.c ftab.c orphan.c zombie.c
pid_t vfork(void);
成功分别在父子进程中返回子进程的pid和0,失败返回-1.
vfork函数的功能基本相同,只有两点区别:
**(1) vfork函数创建的子进程不复制父进程的物理内存,也不拥有自己独立的内存映射,而是与父进程共享全部地址空间
(2) **vfork函数在创建子进程的同时会挂起父进程,直到子进程终止,或者通过exec函数创建新进程,再恢复父进程的运行
使用了**写时复制(拷贝)优化技术(copy-on-write)**的fork结合exec的使用,其性能并不弱于典型的vfork+exec的用法。
终止vfork函数创建的子进程,不要在main函数中使用return语句,也不要在任何函数中exit函数,而要调用**_exit**函数,以避免对父进程造成不利影响。
参考代码:vfork.c
(1)正常终止
进程一旦终止,被终止进程在用户空间所持有的资源都会被自动释放,如代码区、数据区、堆栈区等,但是在内核空间中与该进程相关的资源,如进程表项、文件描述符等未必会得到释放。
main函数的返回值和exit、_exit、_Exit函数的参数一样,构成了进程的退出码,可以被终止进程的父进程通过wait或waitpid函数获得,其中只有低8位可被获取。
注册退出处理函数(遗言函数):
int atexit(void(*function)(void));
int on_exit(void(*function)(int, void*), void* arg);
main函数的返回值和exit、_exit、_Exit函数的参数会传递给int,void* arg会传递给void*
宏:
EXIT_SUCCESS—— 0
EXIT_FAILURE—— -1
exit函数的执行过程:
_exit函数的执行过程:
_Exit和_exit函数的功能完全一致,唯一的区别是前者由标准库提供,声明于stdlib.h,而后者由系统调用提供,被声明于unistd.h
main函数中执行return语句,就相当于调用了exit函数
参考代码:exit.c
(2) 异常终止
ctrl+c—>SIGINT(2),终端终端符信号,进程收到该信号执行默认动作——(异常)终止。
ctrl+\–>SIGQUIT(3),终端退出符信号
SIGKILL(9)
SIGTERM(15)
SIGSEGV(11),内存段错误
SIGBUS(7),硬件错误
……
通过等待子进程结束实现某种进程间的同步;
获知子进程的退出码,根据子进不同的退出原因采取不同的对策;
避免过多的子进程僵尸拖垮系统。
#include
pid_t wait(int*status);
成功返回所回收子进程的pid,失败返回-1
父进程在创建若干子进程以后调用wait函数:
分析进程终止状态:
参考代码:wait1.c wait2.c
pid_t waipid(pid_t pid, int* status, int options);
成功返回所回收子进程的pid,失败返回-1
pid——进程标识,可取以下值:
< -1——等待并回收由**-pid**所标识的进程组中的任意子进程
-1——等待并回收任意的子进程,类似于wait函数
0——等待并回收与调用进程同组的任意子进程
> 0——等待并回收由pid所标识的特定的子进程
status——输出子进程的终止状态,不感兴趣可置NULL
options——选项,可取以下值:
0——阻塞模式,等不来就死等,类似于wait函数
WNOHANG——非阻塞模式,所等子进程仍在运行,则返回0
参考代码:waitpid1.c waitpid2.c
子进程:父子同在——并行(并发)
新进程:以新换旧——取代
为一个函数传递不定数量的字符串参数:
(1)void foo(const char * arg, …); //变长参数表
(2)void bar(const char *arg[ ]); //字符指针数组
exec函数族,包括6个函数,根据参数的形式和是否使用PATH环境变量进行区分。
#include
int execl(const char* path, const char* arg, …);
功能:用**arg, …**作为命令行参数,运行path所表示的可执行文件,创建新进程,并用新进程取代调用进程
返回值:成功不返回,失败返回-1
execl("/usr/bin/gcc","gcc","hello.c","-o", "hello",NULL);
// argv[0] argv[1] argv[2] argv[3]
int execlp(const char* file, const char* arg, …);
功能:通过file参数传入可执行文件的名字即可,无需带路径,该函数会遍历PATH环境变量中的所有路径,寻找可执行文件
execl("gcc","gcc","hello.c","-o", "hello",NULL);
// argv[0] argv[1] argv[2] argv[3]
int execle(const char* path, const char* arg, …, char* const envp[ ]);
int execv(const char* path, const char* const argv[ ];
char* const a[] = {"gcc","hello.c","-o", "hello",NULL};
// argv[0] argv[1] argv[2] argv[3]
execl("/usr/bin/gcc", a);
int execvp(const char* file, const char* const argv[ ]);
int execvp(const char* path, const char* const argv[ ], char* const envp[ ]);
规律:
参考代码:argenv.c exec.c
与fork或vfork函数不同,exec函数并不是创建调用进程的子进程,而是创建一个新的进程取代调用进程自身。新进程会用自己的全部地址空间,覆盖调用进程的地址空间,但是进程的PID保持不变。调用exec函数不仅改变了调用进程的地址空间和进程映像,调用进程的一些属性也会发生变化:
但是有些属性会被新进程继承下来,如PID、PPID、实际用户ID和实际组ID、优先级以及文件描述符(除非该文件描述符带有FD_CLOEXEC标志位)等。
vfork+exec模式:
调用exec函数固然可以创建出新的进程,但是新进程会取代原来的进程。如果既想创建新的进程,同时又希望原来的进程继续存在,则可以考虑使用vfork+exec模式,即在由vfork产生的子进程中调用exec函数,新进程取代了子进程,但父进程依然存在。
再次注意:vfork会挂起父进程,直到子进程结束或者子进程调用exec函数,父进程才恢复运行
参考代码:ve1.c ve2.c vfork.c
如果一个进程可以根据用户的输入创建不同的进程,并在所建进程结束以后继续重复这个过程,那么这个进程就是shell:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xLq0wTsr-1578914622444)(./插图/day08/shell.png)]
#include
int system(const char* command);
成功返回command命令行进程的终止状态,失败返回-1
如果调用vfork或waitpid函数出错,返回-1;如果调用exec函数出错,返回127;如果都成功,返回command进程的终止状态,由waitpid函数的status参数输出
如果command参数取NULL指针,该函数返回-1表示失败,返回其它非0值表示当前shell可用,返回0表示shell不可用
参考代码:sys.c
system=vfork+exec+waitpid
(1) 什么是信号?
信号是提供异步事件处理机制的软件中断。这些异步事件可能来自硬件设备,也可能来自系统内核,甚至可能来自用户程序。进程之间可以相互发送信号,这使得信号成为一种进程间通信(Inter-Process Communication, IPC)的基本手段。信号的异步特性不仅表现为它的产生是异步的,对它的处理同样也是异步的。程序的设计者不可能也不需要精确地预见什么时候触发什么信号,也同样无法预见该信号究竟在什么时候会被处理。一切尽在内核操控下异步地发生。
(2) 什么是信号处理?
每一个信号都有其生命周期:
产生——信号被生成,并被发送至系统内核
未决——信号被内核缓存,而后被递送至目标地址
递送——内核已将信号发送至目标进程
目标进程处理信号的方式:
(3) 信号的名称和编号
信号名称——字符串,形如SIGXXX的字符串或宏定义,提高可读性
信号编号——整数
通过kill -l命令查看当前系统所支持的全部信号名称和编号。
1~31, 31个不可靠信号,也叫非实时信号;34~64, 31个可靠信号,也叫实时信号:共62个信号,没有32\33信号。
SIGHUP(1),控制终端关闭,默认操作是终止进程
SIGINT(2),用户产生中断符(ctrl+c),默认操作终止进程
SIGQUIT(3),用户产生退出符(ctrl+\),默认操作终止+转储
SIGBUS(7),硬件或内存对其错误,默认操作是终止+转储
SIGKILL(9),不能被捕获和忽略,默认操作是终止
SIGSEGV(11),无效内存访问,默认操作是终止+转储
SIGPIPE(13),向读端已关闭的管道写入,默认操作是终止
SIGALARM(14),alarm函数设置的闹钟到期,默认操作是终止
SIGTERM(15),可被捕获和忽略,默认操作是终止
SIGCHLD(17),子进程终止,默认操作是忽略
SIGIO(29),异步I/O事件,默认操作是终止
参考代码:loop.c
#include
typedef void(*sighandler_t)(int);//函数指针,指向一个接收整形参数并且无返回值的信号处理函数
设置针对特定信号的处理方式,即捕获特定的信号:
sighandler_t signal(int signum, sighandler_t handler);
成功返回原信号处理方式,失败返回SIG_ERR(sighandler_t类型的-1)
signum——信号编号
handler——信号处理函数指针,也可以取以下值:
SIG_IGN——忽略信号
SIG_DFL——默认操作
//定义信号处理函数
void sigint(int signum){
SIGINT(2)信号的处理代码
}
//捕获SIGINT(2)信号
if(signal(SIGINT,sigint) == SIG_ERR){
perror("signal");
return -1;
}
…………………………
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1rDUWubW-1578914622444)(./插图/day08/信号处理.png)] SIGINT(2)
SIGINT(2)/PID/sigint->系统内核->调用目标进程中的sigint函数
当一个信号正在被处理的过程中,相同的信号再次产生,该信号会被阻塞,直到前一个信号处理完成,即从信号处理函数中返回,后一个被阻塞的信号才会被递送,进而再次执行信号处理函数。当一个不可靠信号正在被处理的过程中,多个相同的信号再次产生,只有第一个信号会被阻塞,其它信号直接被丢弃。如果是可靠信号,都会被阻塞,并按照产生的顺序依次被递送。
信号处理函数及被其调用的函数都有可能发生重入,由此可能引发不可预知的风险。
所有标准I/O函数都是不可重入函数,在信号处理过程中要慎用
参考代码:signal.c
补充:什么是可重入函数?
所谓可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。
一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括 static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括 static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。 编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。 说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。
strtok函数详解:
char *strtok(char *str, const char *delim);
(1) 当strtok()在参数s的字符串中发现参数delim中包含的分割字符时,则会将该字符改为**\0** 字符。在第一次调用时,strtok()必需给予参数s字符串,往后的调用则将参数s设置成NULL,每次调用成功则返回指向被分割出片段的指针。
(2) 返回值
从s开头开始的一个个被分割的串。当s中的字符查找到末尾时,返回NULL。如果查找不到delim中的字符时,返回当前strtok的字符串的指针。所有delim中包含的字符都会被滤掉,并将被滤掉的地方设为一处分割的节点。
(3) 需要注意的是,使用该函数进行字符串分割时,会破坏被分解字符串的完整,调用前和调用后的s已经不一样了。第一次分割之后,原字符串str是分割完成之后的第一个字符串,剩余的字符串存储在一个静态变量中,因此多线程同时访问该静态变量时,则会出现错误。
主控制流程----------------(中断)-------------->
信号处理函数 ------->
内核处理流程------------------------------------->
do_signal /system_call/
handle_signal/ sys_sigreturn
setup_frame /restore_sigcontext
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LHjlpflU-1578914622445)(./插图/day09/信号处理流程.png)]
信号的本质是一个中断的处理过程,而非多线程的并发过程。线程安全的函数未必是可重入函数。线程安全依靠锁机制,可重入依靠局部化。
在某些非Linux操作系统上,存在信号捕获的一次性问题:即使设置了对某个信号的捕获,只有设置后的第一个该信号被递送时,信号处理函数会被执行,以后再来相同的信号,均按默认方式处理。如果希望对信号的捕获具有持久性,可以在信号处理函数返回前,再次设置对该信号的捕获。
通过**SIGCHLD(17)**信号高效地回收子进程僵尸。
高效:及时性,适时性。
参考代码:sigchld.c
(1) fork和vfork函数创建的子进程会继承父进程的信号处理方式,直到子进程调用exec函数创建新进程替代自身为止。
(2) exec函数创建的新进程会将原进程中被设置为捕获的信号还原为默认处理,在原进程中被忽略的信号于新进程中继续被忽略。
参考代码:fork.c exec.c
(1) 通过键盘向当前拥有控制终端的前台进程发送信号
ctrl + c-------SIGINT(2),默认终止进程
ctrl + \-------SIGQUIT(3),默认终止进程且转储
ctrl + z-------SIGSTOP(20),默认停止(挂起)进程
(2) 来自硬件或者内核的错误和异常引发的信号
SIGILL(4)-----------进程试图执行非法指令
SIGBUS(7)----------硬件或者总线对齐错误
SIGFPE(8)----------浮点异常
SIGSEGV(11)-------无效内存访问
SIGPIPE(13)--------向无读端的管道写入
SIGSTKFLT(16)-----浮点数协处理器栈错误
SIGXFSZ(25)--------文件资源超限
SIGPWR(30)--------断电
SIGSYS(31)---------无效系统调用
(3) 通过kill命令发送信号
kill [-信号] PIDs,不谢信号编号,缺省发送SIGTERM(15)信号。
超级用户可以给任何进程发信号,普通用户只能给自己的进程发信号。
(4) 调用函数发送信号
向特性的进程或进程组发送信号
int kill(pid_t pid, int signum);
成功(至少发出去一个信号)返回0,失败返回-1
pid——进程(组)标识,可取以下值:
< -1——向-pid进程组中的所有进程发送信号
=-1——向系统中的所有进程发送信号
0——向调用进程同组的所有进程发送信号
>0——向进程标识为pid的特定进程发送信号
signum——信号编号。取0时用于检查pid进程是否存在,如果不存在,kill函数会返回-1,且置errno为ESRCH
参考代码:kill.c
向调用进程自己发送信号
int raise(int signum);
成功返回0,失败返回-1
参考代码:raise.c
通过raise或kill向调用进程发送信号,如果该信号被捕获,则要等到信号处理函数返回后,这两个函数才会返回。
暂停:不受时间限制的睡眠
int pause(void);
成功阻塞,失败返回-1
该函数会让调用进程进入无时限的睡眠状态,即不参与内核调度,直到有信号终止了调用进程或被捕获。如果有信号被调用进程捕获,当信号处理函数返回以后,pause函数才会返回,且返回值为-1,同时设置errno为EINTR,表示阻塞的系统调用被信号中断。pause函数要么不返回,要么返回-1,不会返回0.
参考代码:pause.c
受时间限制的睡眠
unsigned int sleep(unsigned int seconds);
功能:该函数使调用进程睡眠seconds秒,除非有信号终止了进程或被其捕获。如果有信号被调用进程捕获,在信号函数返回以后,sleep函数才会返回,且返回值为剩余秒数,否则该函数返回0,表示睡眠充足
返回0或剩余秒数
参考代码:sleep.c
int usleep(useconds_t usec);
以微秒为单位的睡眠
睡够了返回0,睡不够返回-1,同时设置errno为EINTR
Intel CPU:时间精度50~55毫秒
unsigned int alarm(unsigned int seconds);
返回0或者先前闹钟的剩余时间,单次的
alarm函数使系统内核在该函数被调用以后seconds秒的时候,向调用进程发送SIGALRM(14)信号。若在调用该函数前已设过闹钟但尚未到期,则该函数会重设闹钟,并返回先前所设闹钟的剩余秒数,否则返回0。若seconds参数取0,则取消之前设置过且未到期的闹钟。
参考代码:alarm.c
通过alarm函数所设置的定时只是一次性的,即在定时到期时,发送一次SIGALRM(14)信号,此后不会再发送该信号。如果希望获得周期性的定时效果,可以在SIGALRM(14)信号的处理函数中继续调用alarm函数,完成下一个定时的设置
参考代码:clock.c
信号集类型的定义:
#include
typedef __sigset_t sigset_t
#include
typedef struct{
unsigned long int __val[_SIGSET_NWORDS];//1024bits
} __sigset_t;
#define __SIGSET_NWORDS (1024 / (8*sizeof(unsigned long int)));
填满信号集,即将信号集的全部信号位置1
int sigfillset(sigset_t* sigset);
清空信号集,即将信号集的全部信号位置0
int sigemptyset(sigset_t* sigset);
加入信号,即将信号集中的特定信号位置1
int sigaddset(sigset_t* sigset, int signum);
删除信号,即将信号集中的特定信号位置0
int sigdelset(sigset_t* sigset, int signum);
以上函数成功返回0,失败返回-1
检查信号,即判断信号集中的特定位是否为1
int sigismember(sigset_t* sigset, int signum);
有返回1,无返回0,失败返回-1
参考代码:sigset.c
(1) 递送、未决和掩码
当信号产生时,系统内核会在目标进程的进程表项中,以信号位置1的方式存储该信号,这个过程就叫递送。信号从产生到完成递送之间存在一定的时间间隔,处于该间隔状态的信号就属于未决信号。每个进程都有一个信号掩码(集),它实际上是一个信号集,位于该信号集中的信号一旦产生,并不会递送给相应的进程,而是被阻塞于未决状态。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b6WbEehq-1578914622445)(./插图/day09/信号产生未决和递送.png)]
当进程正在执行类似更新数据库、设置硬件状态等敏感任务时,可能不希望被某些信号中断。这时可以通过信号掩码暂时屏蔽而非忽略这些信号,使其一旦产生即被阻塞于未决状态,待特定任务完成以后,再恢复对这些信号的处理。
(2)设置信号掩码
int sigprocmask(int how, const sigset_t* sigset, sigset_t* oldset);
how——信号掩码的修改方式,可取以下值:
SIG_BLOCK——将sigset中的信号加入当前掩码
SIG_UNBLOCK——从当前掩码中删除sigset中的信号
SIG_SETMASK——将sigset设置为当前掩码
sigset——信号集,取NULL则忽略此参数
oldset——原信号掩码,取NULL则忽略此参数
(3) 获取未决信号
int sigpending(sigset_t* sigset);
(4) 不可靠信号最多被信号掩码屏蔽一次,在屏蔽期间再有更多的相同信号一律被丢弃。可靠信号会全部被保留下来,且按照发送的顺序排成队列。
参考代码:sigmask.c
经典版本的信号处理与发送:signal、kill(raise)
现代版本的信号处理与发送:sigaction、sigqueue
int sigaction(int signum, const struct sigaction* sigact, struct sigaction* oldact);
当signum信号被递送时,按sigact结构所描述的行为响应之。若oldact参数非NULL,则通过该参数输出原来的相应行为
成功返回0,失败返回-1
struct sigaction结构内容:
struct sigaction{
void (*sa_handler)(int);//经典版本的信号处理函数指针
void (*sa_sigaction)(int,siginfo_t*,void*);//现代版本的信号处理函数指针
sigset_t sa_mask; //信号处理期间的附加掩码集
int sa_flags; //信号处理标志
void (*sa_restorer)(void); //预留项,目前置NULL即可
};
现代版本的信号处理函数:
void sigint(int signum, siginfo_t* si, void* reserved);
struct siginfo结构体内容(共18个字段):
typedef struct siginfo{
pid_t si_pid;//发送信号进程的PID
sigval_t si_value;//信号附加数据
…………;
}siginfo_t;
sigval_t的类型定义:
typedef union sigval{
int sival_int;//用整形作为信号附加数据
void* sival_ptr;//用任意类型的指针作为信号附加数据
}sigval_t;
(1) 增减信号掩码
缺省情况下,在信号处理函数的执行过程中,会自动屏蔽这个正在被处理的信号,而对其它信号则不会被屏蔽。通过sa_mask字段可以人为指定,在信号处理函数执行期间,除正在被处理的这个信号以外,还想屏蔽哪些信号,并在信号处理函数返回后,自动解除对它们的屏蔽。另一方面,还可以通过为sa_flags字段设置SA_NOMASK/SA_NODEFER标志位来告诉系统内核在信号处理函数执行期间,不要屏蔽这个正在被处理的信号。
(2) 选择信号处理函数的风格
如果sa_flags字段中没有SA_SIGINFO标志位,则sa_handler字段有效,即使用经典版本的信号处理函数。相反,如果sa_flags包含SA_SIGINFO标志位,则sa_sigaction字段有效,即使用现代版本的信号处理函数指针
(3) 一次性信号处理
如果sa_flags字段中包含SA_ONESHOT/SA_RESETHAND标志位,那么对所捕获的信号的处理就是一次性的,即在执行完一次信号处理函数后,就恢复为按默认方式处理
参考代码:sigact.c
(4) 系统调用中断重启
诸如pause/sleep/usleep等系统调用的阻塞过程会被信号打断,即在针对某个信号的处理函数返回后,这些系统调用也会从阻塞中返回。如果这类系统调用被所捕获的信号打断,即在针对该信号的处理函数返回后,能够自动重启阻塞过程而不要返回,可以为sa_flags字段添加SA_RESTART标志位
参考代码:restart.c
int sigqueue(pid_t pid, int signum, const union sigval value);
成功返回0,失败返回-1
参考代码:sigque.c
利用信号附加数据实现简单的进程间通信,参考代码:send.c recv.c
执行时间 = 用户时间 + 内核时间 + 睡眠时间
执行时间——直观感受/墙钟时间——真实计时器
用户时间——消耗在用户态的时间——虚拟计时器
内核时间——消耗在内核态的时间
用户时间+内核时间——实用计时器
睡眠时间——消耗在等待I/O、睡眠等不被调度的时间[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZyPmjkUO-1578914622445)(./插图/day10/计时器.png)]
利用真实计时器时,到期信号——SIGALRM(14)
利用虚拟计时器时,到期信号——SIGVTALRM(26)
利用实用计时器时,到期信号——SIGPROF(27)
基于每每种计时器的定时都有两个参数:
设置、开始、关闭定时器
int setitimer(int which, const struct itimerval* new_value, strct itimer* old_value);
成功返回0,失败返回-1
which——指定设置哪个定时器可取以下值:
ITIMER_REAL——基于真实计时器的定时
ITIMER_VIRTUAL——基于虚拟计时器的定时
ITIMER_PROF——基于实用计时器的定时
new_value——新设置值
old_value——输出原设置值,可置NULL
struct itimerval结构体内容:
struct itimerval{
struct timeval it_interval;//重复间隔,取0表示只发一个信号,没有周期性,不重复
struct timeval it_value;//初始间隔,取0表示停止定时器,不再发送信号
};
struct timeval结构体内容:
struct timeval{
long tv_sec; //秒
long tv_usec; //微秒
};
例如,5s以后开始发SIGALRM(14)信号,以后每隔3ms再发一次信号
struct itimerval it;
//设置初始间隔
it.it_value.tv_sec = 5;
it.it_value.tv_usec = 0;
//设置重复间隔
it.it_interval.tv_sec = 0;
it.it_interval.tv_usec = 3000;
//开启定时器
setitemer(ITIMER_REAL,&it,NULL);
………………;
//关闭定时器
it.it_value.tv_sec = 0;
setitemer(ITIMER_REAL,&it,NULL);
如果希望立即启动定时器,初始间隔至少是1微秒
参考代码:timer.c
获取定时器的设置
int getitimer(int which, struct itimerval* curr_value);
成功返回0,失败返回-1
which——指定设置哪个定时器可取以下值:
ITIMER_REAL——基于真实计时器的定时
ITIMER_VIRTUAL——基于虚拟计时器的定时
ITIMER_PROF——基于实用计时器的定时
curr_value——当前设置的值
Unix/Linux系统中的每个进程都拥有独立的4GB大小的虚拟内存空间。其中高地址的1GB被映射到相同的物理内存区域,用于保存内核代码和数据;低地址的3GB作为保存用户代码和数据的用户空间,被映射到彼此不同的物理内存。因此,同一虚拟内存地址,在不同的进程中,会被映射到不同的物理内存区域,在多个进程之间以交换虚拟内存地址的方式交换数据是不可能的。鉴于进程之间的天然的内存壁垒,为了能够在不同的进程之间高效地交换数据,需要有一种专门的机制,这就是所谓的进程间通信(Inter-Process Communication, IPC)。
(1) 命令行参数
进程1组织命令行参数—>execl—>进程2处理命令行参数
(2) 环境变量
进程1组织环境变量—>execl—>进程2处理环境变量
(3)wait/waitpid
进程1—>fork/vfork+exec—>进程2
wait/waitpid(…, &status); mian—>return /exit/_exit/_Exit
(4) 内存映射文件
进程1[虚拟内存]<----->文件区域<----->[虚拟内存]进程2
(5) 信号
进程1-----信号+附加数据----->进程2
(1) 有名管道(全双工)
进程1<----->管道文件----->进程2
管道文件有i节点,没有数据块,是内存文件
(2) 无名管道(半双工)
(1) 消息队列
进程1—消息3|消息2|消息1—进程2
(2) 共享内存(速度最快的一种进程间通信方式)
进程1[虚拟内存]<----->物理内存<----->[虚拟内存]进程2
(3) 信号量集
多个进程竞争有限的资源
进程1<----->本地套接字文件----->进程2
本地套接字文件:有i节点但没有数据块,是内存文件,类似与有名管道
以一种统一的编程模式和接口库,处理网络和本机通信
#include
int mkfifo(const char* pathname, mode_t mode);
成功返回0,失败返回-1
打开、关闭、读取和写入有名管道的方法与读写普通文件无异:open/close/read/write
参考代码:wfifo.c rfifo.c
有名管道逻辑模型[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KfQ7M5pi-1578914622445)(./插图/day10/有名管道逻辑模型.png)]
编程模型:
进程A | 进程B | |
---|---|---|
创建管道 | mkfifo | |
打开管道 | open | 打开管道 |
读写管道 | read/write | 读写管道 |
关闭管道 | close | 关闭管道 |
删除管道 | unlink |
#include
int pipe(int pipefd[2]);
成功返回0,失败返回-1
编程模型
(1) 父进程调用pipe函数在系统内核中创建无名管道对象,同时得到与该对象相关联的两个文件描述符,一个用于读取,另一个用于写入[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v268SELd-1578914622446)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day10/无名管道1.png)](2) 父进程调用fork函数,创建子进程,子进程会复制父进程的文件描述符表,因此子进程也同样拥有可用于读写管道对象的两个文件描述符[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EkYPBRMk-1578914622446)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day10/无名管道2.png)]
(3) 负责写数据的进程关闭管道的读端,即pipefd[0];而负责读数据的进程关闭管道的写端,即pipefd[1][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-61wqZL3G-1578914622446)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day10/无名管道3.png)](4) 父子进程通过各自持有的文件描述符分别向管道写入和读取数据,待完成通信后再关闭各自持有的文件描述符,内核中的无名管道对象即被释放[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JFMc8ZlP-1578914622446)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day10/无名管道4.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zs8cRHR6-1578914622447)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day10/无名管道5.png)]
参考代码:pipe.c
(1) 从写端已被关闭的管道中读取:只要管道缓冲区还有数据,依然可被正常读取,一直读到缓冲区空,这是read函数返回0(既不是返回-1也不是阻塞),如同读到文件尾。
(2) 向读端已被关闭的管道中写入:会直接触发SIGPIPE(13)信号,该信号的默认操作是终止执行写入动作的进程。但如果执行写入动作的进程已经事先将SIGPIPE(13)信号设置为忽略或者捕获,这时虽然进程不会因为写入无读端的管道而被终止,但是write函数会返回-1,并置errno为EPIPE
(3) 在**/usr/include/linux/limits.h头文件中定义的PIPE_BUF**宏(4096)表示管道写缓冲区的大小。如果写管道时发现缓冲区的空闲空间不足以容纳此次write调用所要写入的字节数,则write函数会阻塞,直到缓冲区中的空闲空间变得足够大为止。如果同时有多个进程向同一个管道写入数据,而每次调用write函数写入的字节数都不大于BUF_SIZE,则这些write操作不会互相穿插(原子化,atomic),反之,单次写入的字节数超过了BUF_SIZE,则它们的write操作可能会发生相互穿插。
读取一个缓冲区为空的管道,只要其写端没有被关闭,读操作就会被阻塞,除非该读文件描述符被设置为非阻塞(O_NONBLOCK),此时则会立即返回失败,并置errno为EAGAIN。
命令1 | 命令2 | 命令3…
输出 > 管道 > 输入
输出 > 管道 > 输入
…
“|”将前一个命令的输出作为后一个命令的输入
shell进程管道符号实现原理:
//shell进程:
int pipefd[2];
pipe(pipefd);//创建无名管道
vfork();//产生一个子进程
//子进程1:
close(pipefd[0]);//关闭无名管道的读端
dup2(pipefd[1],STDOUT_FILENO);//写端=标准输出
exec(A);//创建A进程,继承了原进程的文件描述符表。在A进程中,写端= //标注输出,printf/puts将数据写入无名管道的写端
vfork();//产生一个子进程2
//子进程2
close(pipefd[1]);//关闭无名管道的写端
dup2(pipefd[0],STDIN_FILENO);//读端=标准输入
exec(B);//创建B进程,继承了原进程的文件描述符表,在B进程中,读端= //标准输入,scanf/gets从无名管道的读端读取数据
//A和B就成为协作进程,A写入数据进管道,B从管道中读取A写入的数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UgzOh2Kd-1578914622447)(./插图/day11/管道符号.png)]
参考代码:output.c input.c shell.c
ps aux | grep bash | awk '{print $3}'
ps aux | grep bash | grep -v grep | awk ‘{print $4}’
(1) IPC对象的标识符(ID)和键(Key)
IPC对象在系统内核中的唯一名称用**键(Key,IPC对象的外部名)**表示。不同的进程可以通过键引用该IPC对象。一旦进程获得了该IPC对象,即通过其标识(ID)来称谓该对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ub485WkI-1578914622447)(./插图/day11/键.png)]
#include
key_t ftok(const char* pathname, int proj_id);
成功返回IPC对象的键,失败返回-1
相同项目使用相同的路径pathname和proj_id,保证key的一致性;不同项目使用不同的pathname或proj_id,避免key发生冲突
(2) IPC对象的编程接口
创建或获取IPC对象
int shmget(key_t key, size_t size, int shmflg);//共享内存
int msgget(key_t key, int msgflg);//消息队列
int semget(key_t, int nsems, int semflg);//信号量集
控制或销毁IPC对象
int shmctl(int shmid, int cmd, struct shmid_ds* buf);//共享内存
int msgctl(int msqid, int cmd, struct msqid_ds* buf);//消息队列
int semctl(int semid, int semnum, int cmd, union semun arg);//信号量集
cmd——控制命令,可取以下值:
IPC_STAT——获取IPC对象的属性
IPC_SET——设置IPC对象的属性
IPC_RMID——删除IPC对象
IPC对象的权限结构
struct ipc_perm{
key_t __key; //键
uid_t uid; //拥有者用户
gid_t gid; //拥有者组
uid_t cuid; //创建者用户
gid_t cgid; //创建者组
unsigned short mode; //权限
unsigned short __seq; //序号
};
其中,只有uid、gid和mode三个字段可以在创建完以后被修改
(3) 共享内存
共享内存:两个或者更多进程,共享一块由系统内核负责维护的物理内存,其地址空间通常位于被映射到每个进程虚拟内存堆和栈之间的不同区域。
共享内存属性结构:
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; /*当前加载计数*/
...
};
int shmget(key_t key, size_t size, int shmflg);
size——共享内存的字节数,按页向上取整。获取已有的共享内存对象时可以置0
shmflg——创建标志,可取以下值:
0——获取,不存在即失败
IPC_CREAT——创建兼获取,不存在即创建,已存在直接获取
IPC_EXCL——不存在即创建,已存在直接报错
加载共享内存到虚拟内存,建立虚拟内存和物理内存间的映射
void *shmat(int shmid, const void *shmaddr, int shmflg);
成功返回共享内存的起始地址,失败返回void*类型的-1
shmid——共享内存标识
shmaddr——共享内存起始地址,置NULL由系统内核选择
shmflg——加载标志,可取以下值:
0——可读可写
SHM_RDONLY——只读
SHM_RND——若shmaddr非空且不是页边界,将其自动向下圆整至页(4096)的整数倍
卸载共享内存
int shmdt(const void *shmaddr);
成功返回0,失败返回-1
shmat函数负责将给定共享内存映射到调用进程的虚拟内存空间,返回映射区域的起始地址,同时系统将内核中共享内存对象的加载计数(shm_nattch)。调用进程在获得shmat函数返回的共享内存起始地址以后,就可以像访问普通内存一样访问该共享内存中的数据。
shmdt函数负责从调用进程的虚拟内存中解除shmaddr所指向的映射区域到虚拟内存的映射,同时将系统内核中共享内存对象的加载计数(shm_nattch)减1。因此,加载计数为0的共享内存必定是没有任何进程使用的。
**shmctl(…, IPC_RMID, …)**调用可以用于销毁共享内存,但并非真的销毁,而只是做一个销毁标记,禁止任何进程对该共享内存形成新的加载,但已有的加载依然保留。只有当其使用者们纷纷卸载,直至其加载计数将为0时,共享内存才会真的被销毁。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qCVHTrdW-1578914622447)(./插图/day11/共享内存编程模型.png)]
参考代码:wshm.c rshm.c
通过共享内存实现进程间通信,可以直接访问由系统内核维护的公共内存区域,不需要额外构建用户缓冲区,也不需要在用户缓冲区和内核换成缓冲区之间来回复制数据,因此,共享内存是速度最快的进程间通信机制。但是共享内存因为缺乏必要的同步机制,往往需要借助其它进程间通信策略提供某种形式的停等机制。
(4) 消息队列
消息队列:由单个的类型各异的一系列消息结构组成的链表。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8EFN8z27-1578914622447)(./插图/day11/消息队列.png)]
消息:消息类型、数据长度、消息数据、消息指针
系统对于消息的限制:
最大可发送消息字节数:8192(8KB)
最大全队列消息字节数:16384(16KB)
最大全系统消息队列数:16
最大全系统消息总个数:262144
#include
int msgget(key_t key, int msgflg);
key——消息队列键值
msgflg——创建标志,可取以下值:
0——获取,不存在即失败
IPC_CREAT——创建兼获取,不存在即创建,已存在直接获取
IPC_EXCL——不存在即创建,已存在直接报错
发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
成功返回0,失败返回-1
msgid——消息队列标识
msgp——>|消息类型(长度:sizeof(long)) | 消息数据 |
msgsz——消息数据的长度
msgflg——标志位,可取以下值:
0——阻塞方式
IPC_NOWAIT——非阻塞方式
msgsnd函数的msgp参数所指向的内存包含4个字节大小的消息类型,其值必须大于0,但该函数的msgsz参数所表示的期望发送的字节数却不包含消息类型所占的4字节。
如果系统内核中的消息未达上限,则msgsnd函数会将欲发送消息加入消息队列并立即返回0。否则,该函数会阻塞,直到系统内核允许加入新消息为止(比如:有消息被接收而离开消息队列)。若msgflg参数中包含IPC_NOWAIT位,则msgsnd函数在系统内核中的消息已达上限的情况下不会阻塞,而是返回-1,并置errno为EAGAIN
接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
成功返回实际接收到的消息数据的长度,失败返回-1
msgid——消息队列标识
msgp——>|消息类型(长度:sizeof(long)) | 消息数据缓冲区 |
msgsz——缓冲区长度
msgtyp——消息类型,可取以下值
0——提取消息队列中的第一条消息而不论其类型
>0——msgflg不含MSG_EXCEPT位,提取第一条类型为msgtyp的消息
msgflg含MSG_EXCEPT位,提取第一条类型不为msgtyp的消息
<0——提取消息队列中类型小于或等于msgtyp绝对值的消息,类型越小的越先被提取
msgflg——标志位,可取以下值:
0——阻塞方式
IPC_NOWAIT——非阻塞方式
注意:msgrcv函数的msgp参数所指向的内存块中饱含4字节的消息类型,其值由该函数输出,但是该函数的msgsz参数所表示的期望接收字节数以及该函数所返回的实际接收字节数都不包含消息类型4个字节
若存在与msgtyp参数所匹配的消息,但是数据长度大于msgsz参数,且msgflg参数包含MSG_NOERROR位,则只截取该消息数据的前msgsz字节返回;但如果msgflg参数不包含MSG_NOERROR位,则不处理该消息,直接返回-1,并置errno为E2BIG
msgrcv函数根据msgtyp参数对消息队列中的消息有选择地接收,只有满足条件的消息才会被复制到应用程序缓冲区并从内核缓冲区删除。如果满足msgtyp条件的消息不只一条,则按照先进先出的规则提取。
若消息队列中有可接受消息,则msgrcv函数会将该消息移出消息队列,并立即返回所接收到的消息数据字节数,表示接收成功;否则此函数会阻塞,直到消息队列中有可接收消息为止。若msgflg参数包含IPC_NOWAIT位,则msgrcv函数在消息队列中没有可接收的消息的情况下不会阻塞,而是返回**-1**,并置errno为ENOMSG
参考代码:wmsg.c rmsg.c
(5) 信号量集
资源的需求多余资源本身,如何协调有限资源在多数需求者之间的分配,以使每个资源需求者都有相对均衡的几率获得其所要求的资源。
系统内核中为每一种资源维护一个资源计数器 ,其值为当前空闲资源的数量,每当一个进程试图获取该种资源时,会先尝试减少其计数器的值。如果计数器的值够减(计数器>=0),则说明空闲资源足够,该进程即获得资源;如果该计数器的值不够减,则说明空闲资源不够,该进程即进入等待模式,等候其它拥有该种资源的进程释放资源。任何一个拥有该种资源的进程,一旦决定释放该资源,都必须将其计数器的值予以增加,以表示空闲资源量的增加,为其它等候该资源的进程提供条件。
一个信号量对应一种类型资源,其值表示该种类型资源的空闲数量。用由多个信号量组成的信号量集表示多种类型的资源。
创建或获取信号量集
#include
int semget(key_t key, int nsems, int semflg);
成功返回信号量集标识符,失败返回-1
key——信号量集的键
nsems——信号量的个数,即资源的种类数
semflg——创建标志,可取以下值:
0——获取,不存在即失败
IPC_CREAT——创建,不存在即创建,已存在即获取
IPC_EXCL——排斥,已存在就失败
操作信号量集:减操作->拥有资源…释放资源->加操作
int semop(int semid, struct sembuf* sops, unsigned nops);
成功返回0,失败返回-1
struct sembuf结构体内容:
struct sembuf{
unsigned short sem_num; //信号量编号(集合索引)
short sem_op; //操作数(-获取/+释放)
short sem_flg;//操作标志(0-阻塞/IPC_NOWAIT-非阻塞)
};
sops指针指向一个struct sembuf类型的结构体数组,其中每个元素都是一个struct sembuf类型的结构体,该结构体包含三个字段,用于表示针对信号量集中的一个特定信号量的特定操作。
如果sem_op字段的值为负,则从semid信号量集第sem_num个信号量的值中减去|sem_op|,以表示对资源的获取;如果不够减(信号量的值不能负),则此函数会阻塞,直到够减为止,以表示对资源的等待,但如果sem_flg字段包含IPC_NOWAIT位,则即使不够减也不会阻塞,而是返回-1,并值errno为EAGAIN
销毁或控制信号量集
int semctl(int semid, int semnum, int cmd, …);
成功返回0或者其它与cmd有关的值,失败返回-1
参考代码:csem.c gsem.c
(6) IPC命令
查看IPC对象
ipcs -m,共享内存
ipcs -q,消息队列对象
ipcs -s,信号量集对象
ipcs - a,全部ipc对象
删除IPC对象
ipcrm -m <共享内存对象标识>
ipcrm -q <消息队列对象标识>
ipcrm -s <信号量集对象标识>
命令行参数和环境变量(给main函数传参):初始化设置
回收进程退出码(接收main函数的返回值或者exit函数的参数):获得终止信息
内存映射文件:通过内存的方式操作共享文件, 读写磁盘数据,速度慢但持久性好
信号:简单、异步,信息量有限,效率不高,可靠性不佳
有名管道或本地套接字:非近亲进程之间的中等规模数据通信
无名管道:近亲进程之间中等规模数据通信
共享内存:大数据量的快速数据通信,缺乏同步机制,需要依赖其它IPC机制实现同步
消息队列:天然的同步性,根据类型做细分,适用于中等规模数据通信
信号量集:多数进程竞争少数资源
(1) 什么是计算机网络?
计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过有形或无形的通信线路连接起来,在网络操作系统、网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。
(2) 什么是网络协议?
网络协议是一种特殊的软件,是计算机网络实现其功能的最基本的机制。网络协议的本质就是规则,即各种硬件和软件必须遵守的共同守则。网络协议本身并不是一套单独的软件,它融合于所有涉及网络通信的软件甚至硬件之中,因此可以说网络协议在网络应用中无处不在。
(3) 什么是协议栈?
为了减少网络设计的复杂性,绝大多数网络采用分层设计的方法。所谓分层设计,就是按照信息流动的过程将网络的整体功能分解为一个个的功能层,不同机器上的同等功能层之间采用相同的协议,同一机器上相邻的功能层之间通过接口进行信息传递。各层协议和接口统称为协议栈
ISO(国际标准化组织)/OSI(Open System Interconnection, 开放系统互联)网络协议模型:
应用层:业务逻辑
表示层:数据的表现形式
会话层:建立、管理和终止通信过程
传输层:源到目的地的点对点传输
网络层:路径选择、路由、寻址等与网络结构拓扑
数据链路层:物理寻址、数据通道、错误检测等通信路径
物理层:在数据和电平信号之间进行转换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uJdmHzKW-1578914622448)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day12/网络与网络协议/网络层次.png)]
(4) TCP/IP协议栈
传输层:TCP、UDP
网络层:IP、ICMP、IGMP
数据链路层:ARP、RARP
(5) 消息包和数据流
应用层:HTTP请求=用户数据包
传输层:TCP头+用户数据包=TCP包
网络层:IP头+TCP包=IP包
数据链路层:以太网头+IP包+以太网尾=以太网帧
物理层:以太网帧—>电平信号
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zztnt4XS-1578914622448)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day12/网络与网络协议/以太网数据帧的分用过程.png)]
发送数据流:数据自协议栈顶层向底层流动,逐层打包
接收数据流:数据自协议栈底层向顶层流动,逐层解析
(6) IP地址
IP地址(Internet Protocol Address,互联网协议地址)是一种地址格式,为互联网上的每个网络和主机分配一个逻辑地址,其目的是消除物理地址的差异性。
IP地址在计算机内部用一个网络字节序的32位(4字节)无符号整数表示。通常习惯将其表示为点分十进制整数字符串形式。
例如,点分十进制整数字符串:1.2.3.4
32位(4字节)无符号整数:0x01020304
内存布局:低地址-0x01|0x02|0x03|0x04-高地址
网络字节序就是大端字节序,高位在低地址,低位在高地址
一台计算机的IP地址包括网络地址+主机地址
A类地址:以0为首的8位网络地址 + 24位主机地址
B类地址:以10为首16位网络地址 + 16位主机地址
C类地址:以110为首24位网络地址 + 8位主机地址
D类地址:以1110为首的32位多播(组播)地址
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IyD1GwDY-1578914622448)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day12/网络与网络协议/五类互联网地址.png)]
例如:某台计算机的IP地址为192.168.182.48,其网络地址和主机地址分别如何?11000000 10101000 10110110 00110000,c类地址,网络地址192.168.182.0,主机地址48
主机IP地址 & 子网掩码 = 网络地址
主机IP地址 & (~子网掩码) = 主机地址
(1) 什么是套接字
套接字是一个由系统内核负责维护,可以通过文件描述符访问的对象,可用于同一台机器或不同机器中的进程之间实现通信。
套接字也可以被视为围绕表示网络的文件描述符的一套函数库,调用其中的函数就可以访问网络上的数据,实现不同主机之间的通信功能
进程表项
文件描述符表
0: 文件描述符标志| * -> 标准输入文件表项–>键盘
1: 文件描述符标志| * -> 标准输出文件表项–>显示器
2: 文件描述符标志| * -> 标准错误文件表项–>显示器
3: 文件描述符标志| * -> 套接字对象–>网卡
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1Weo7fF-1578914622448)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day12/文件描述符和套接字描述符对比.png)]
(2) 绑定和连接
套接字就是系统内核内存中的一块数据——逻辑对象
借助包含了IP地址和端口号等参数的网络设备——物理对象
把这个逻辑对象和物理对象建立联系的过程就是绑定(bind)
IP地址区分互联网上的不同机器,端口号区分同一机器上的不同进程(应用)
通过IP地址(网络地址+主机地址)和端口号就可以唯一定位互联网上的一个通信应用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2xtmubnW-1578914622449)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day12/IP地址和端口号作用.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eefmdsxy-1578914622449)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day12/绑定.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yXMMdBLK-1578914622449)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day12/连接.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZNFHdSb4-1578914622449)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day12/绑定和连接.png)]
(3) 常用函数
创建套接字
int socket(int domain, int type, int protocol);
成功返回套接字描述符,失败返回-1
domain——通信域,即协议族,可取以下值:
PF_LOCAL/PF_UNIX——本地通信,即进程间通信
PF_INET——互联网通信,ipv4
PF_INET6——互联网通信,ipv6
PF_PACKET——底层包通信(嗅探器)
type——套接字类型,可取以下值:
SOCK_STREAM——流式套接字,用于TCP协议
SOCK_DGRAM——数据报式套接字,使用UDP协议
SOCK_RAW——原始套接字,使用自定义协议
protocol——特殊协议
对于流式套接字和数据报式套接字,取0
套接字描述符和文件描述符在逻辑层面是一致的,所有关于文件描述符的规则对于套接字描述符也同样成立,同样也通过close函数关闭套接字释放内核中的有关资源。
基本地址结构
struct sockaddr{
sa_family_t sa_samily;//地址族
char sa_sata[14];//地址值
};
基本地址结构仅用于函数传参时做强制类型转换
本地地址结构:
#include
struct sockaddr_un{
sa_family_t sun_family;//地址族(AF_LOCAL/AF_UNIX)
char sun_path[];//套接字文件路径
};
网络地址结构:
#include
struct sockaddr_in{
sa_family_t sin_family;//地址族(AF_INET)
in_port_t sin_port;//端口号(网络字节序)
struct in_addr sin_addr;//IP地址
};
………………
struct in_addr{
in_addr_t s_addr;//网络字节序的32位无符号整数形式的IP地址
};
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
发送时:主机字节序—>网络字节序(大端字节序)
接收时:网络字节序(大端字节序)—>主机字节序
将套接字对象和自己的地址结构绑定在一起
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
成功返回0,失败返回-1
将套接字对象所代表的物理对象和对方的的地址结构连接在一起
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
成功返回0,失败返回-1
通过套接字描述符接收和发送数据的过程就完全与通过文件描述符读取和写入数据的过程完全一样。
接收数据
*ssize_t read(int fd, void buf, size_t count);
发送数据
*ssize_t write(int fd, const void buf, size_t count);
字节序转换函数
通过网络传输多字节整数,需要在发送前转换为网络字节序,在接收后转换为主机字节序。
#include
uint32_t htonl(unit32_t hostlong;)
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
说明:
IP地址转换
(网络字节序32位无符号)整数<—>(点分十进制)字符串
#include
in_addr_t inet_addr(const char* cp); //串->数(网络字节序)
int inet_aton(const char* cp, struct in_addr* inp);//串–>数,成功返回0,失败返回-1
char* inet_ntoa(struct in_addr in);//数->串,成功返回字符串指针,失败返回NULL
补充:
int inet_pton(int af, const char *src, void *dst);//convert IPv4 and IPv6 addresses from text to binary
form
基于本地套接字的进程间通信模型:
服务器 | 客户机 |
---|---|
创建套接字(socket) | 创建套接字(socket) |
准备地址结构(sockaddr_un) | 准备服务器地址结构(sockaddr_un) |
绑定地址(bind) | 建立连接(connect) |
接收请求(read) | 发送请求(write) |
业务处理(…) | 等待处理(…) |
发送响应(write) | 接收响应(read) |
关闭套接字(close) | 关闭套接字(close) |
参考代码:locsvr.c loccli.c
服务器:提供业务服务的计算机程序
客户端:请求业务服务的计算机程序
基于网络套接字的进程间通信模型:
服务器 | 客户机 |
---|---|
创建套接字(socket) | 创建套接字(socket) |
准备地址结构(sockaddr_in) | 准备服务器地址结构(sockaddr_in) |
绑定地址(bind) | 建立连接(connect) |
接收请求(read) | 发送请求(write) |
业务处理(…) | 等待处理(…) |
发送响应(write) | 接收响应(read) |
关闭套接字(close) | 关闭套接字(close) |
参考代码:netsvr.c netcli.c
(1) TCP协议的基本特征:
(2) TCP连接的生命周期
(3) 常用函数
在指定套接字上启动对连接请求的侦听,即将该套接字置为被动侦听模式,因为套接字都缺省为主动模式。
int listen(int sockfd, int backlog);
成功返回0,失败返回-1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0BIEDV70-1578914622451)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day13/启动侦听.png)]
在指定的侦听套接字上等待并接受连接请求
int accept(int sockfd, struct sockaddr* addr, size_t* addrlen);
成功返回连接套接字描述符用于后续的通信,失败返回-1。
该函数由TCP服务器调用,返回排在已决队列连接队列首部的连接套接字对象的描述符,若已决队列为空,该函数会阻塞
非并发的TCP服务器模型:迭代服务器是指对于客户端的请求和连接,服务器逐个进行处理,处理完一个连接后再处理下一个连接,属于串行处理方式,结构比较简单,同一时刻只能处理一个客户端的请求
并发的TCP服务器模型:并发服务器可以同时处理多个客户机请求
接收数据
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
返回值、sockfd、buf、len等参数的含义同read函数
flags——接收标志,取0等价于read函数,还可以取以下值:
MSG_DONTWAIT——非阻塞接收。对于阻塞模式,当接收缓冲区为空时,该函数会阻塞,直到接收缓冲区不空为止。如果使用此标志位,当接收缓冲区为空时,该函数会返回-1,并置errno为EAGAIN或者EWOULDBLOCK
MSG_OOB——接收带外数据
MSG_PEEK——瞄一眼数据,只将接收缓冲区中的数据复制到buf缓冲区中,但并不将其从接收缓冲区中删除
MSG_WAITALL——接收到所有期望接收的数据才返回,如果接收缓冲区中的数据不到len字节,该函数会阻塞,直到可接收到len字节为止
发送数据
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
flags——发送标志,取0时等价于write函数,还可以取以下值:
MSG_DONTWAIT——非阻塞发送。对于阻塞模式,当发送缓冲区的空余空间不足以容纳期望的字节数时,该函数会阻塞,直到发送缓冲区的空余空间足以容纳期望发送的字节数为止。如果使用了此标志位,则能发送多少字节就发送多少字节,不会阻塞,甚至可能返回0表示发送缓冲区满,无法发送。
MSG_OOB——发送带外数据
MSG_DONTROUT——不查路由表,直接在本地网中寻找目的主机
参考代码:tcpsvr.c tcpcli.c
(1) UDP协议的基本特征
通过在可靠性方面的部分牺牲,来换区高速度[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Uqieyqh-1578914622452)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day13/UDP首部.png)]
(2) 常用函数
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
返回值和前4各参数同send函数
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr,socklen_t* addrlen);
返回值和前4各参数同recv函数
参考代码:udpsvr.c udpcli.c
针对UDP套接字的connect函数,并不像TCP套接字connect一样通过三次握手过程建立所谓的虚电路连接,而仅仅是将传递给该函数的对方地址结构缓存在套接字对象中。此后通过该套接字发送数据时,可以不适用sendto函数,而直接调用send函数,有关接受放的地址信息从套接字对象的地址缓存中提取即可。
参考代码:concli.c
字符串形式的域名—>DNS服务器—>整数形式的IP地址—>套接字编程
根据主机域名获取信息
#include
struct hostent* gethostbyname(const char* name);
成功返回主机信息条目指针,失败返回NULL
struct hostent结构体内容:
struct hostent{
char* h_name; //主机官方名
char** h_aliases; //别名表
int h_addrtype; //AF_INET,地址类型
int h_length; //地址字节数
char** h_addr_list;//地址表,指向struct in_addr地址结构
}
参考代码:dns.c
HTTP(Hyper Text Transfotm Protocol),超文本传输协议,属于应用层协议
www服务器通过http协议提供页面服务
HTTP请求包头
方法 | 资源路径(URI ,统一资源定位符) | 通信协议和版本 |
---|---|---|
GET | /user/project/main.html | HTTP/1.1 |
键 | 值 | 每行结束 |
---|---|---|
Host | www.tmooc.cn | \r\n |
Accept | text/html | \r\n |
Connection | Keep-Alive/Close | \r\n |
User-Agent | Mozalla/5.0 | \r\n |
Referer | www.tmooc.cn | \r\n |
…… | …… | 最后一行:\r\n\r\n |
参考代码:http.c
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NGOWEg2c-1578914622452)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day14/线程.png)]
线程就是进程的执行过程,即进程内存的控制序列,或者说是进程中的一个任务。
一个进程可以同时拥有多个线程,即同时被系统调度的多个执行路径,但至少要有一个主线程——main函数及被其调用的其它函数。
一个进程的所有线程共享进程的代码区、数据区、BSS区、堆区、环境变量和命令行参数区、文件描述符表、信号处理函数、当前工作目录、用户和组的各种ID等。但是,栈区不是共享的,一个进程的每个线程都拥有自己独立的栈区
进程是资源分配的基本单位,线程是执行/调度的基本单位
线程调度:
进程:内存壁垒,通信
线程:内存共享,同步
IEEE POSIX 1003.1c(1995颁布),定义了统一的线程编程接口,遵循该标准的线程实现被统称为POSIX线程。
头文件:#include
编译和链接时,Compile and link with -pthread.
线程过程函数:在一个线程中被内核调用的函数,对该函数的调用过程就是线程的执行过程,从该函数中返回意味着该线程的结束。因此,main函数其实一个进程的主线程的线程过程函数。所有自创建的线程都必须有一个线程过程函数(由程序员定义内核调用): void* 线程过程函数(void* 线程参数指针){线程执行过程}
int pthread_create(pthread_t* tid, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
成功返回0,失败返回错误码
pthread_create----->创建一个新线程----->调用线程过程函数(start_routine)并传入线程参数指针(arg)
被创建的子线程和创建该子线程的父线程是并行的关系,其调度顺序无法预知,因此当pthread_create函数返回时,子线程执行的位置无从确定,其线程过程函数可能尚未被调用,也可能正在执行,甚至可能已经返回。传递给线程的参数对象,一定要在线程过程函数不再使用它的情况下才能被释放。
参考代码:create.c
主线程和通过pthread_create函数创建的多个子进程,在时间上“同时”运行,如果不去附加任何同步条件,则它们每一个执行步骤的先后顺序无法预知,这种叫做自由并发
参考代码:concur.c
为了让贤臣过程函数的实现更加灵活,可以通过线程参数来传递特定的信息,帮助线程过程函数执行不同的任务。
参考代码:arg.c
int pthread_join(pthread_t tid, void** retval);
成功返回0,失败返回错误码
当调用pthread_join函数时,以下几种情况:
pthread_join函数的作用:等待子线程终止,清理线程的资源,获得线程过程函数的返回值
参考代码:join.c
在有些时候,作为子线程的创建者,父线程并不关心子线程何时终止,同时父线程也不需要获得子线程的返回值。在这种情况下,就可以将子线程设置为分离线程,这样的线程一旦终止,他们的资源会被系统自动回收,而无需在其父线程中调用pthread_join函数
int pthread_detach(pthread_t tid);
成功返回0,失败返回错误码
参考代码:detach.c
//使用线程属性创建分离线程
pthread_attr_t attr;
pthread_attr_init(&attr);//用缺省值来初始化线程属性
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//设置成分离属性
pthread_create(&tid,&attr,thread_proc,NULL);
pthread_attr_destroy(&attr);
pthread_create函数通过tid参数输出线程ID,pthread_self()函数可以返回线程ID
if(tid1 == tid2) //兼容性不好
比较线程tid是否相等
int pthread_equal(pthread_t tid1, pthread_t tid1);
两个TID相等返回非0,否则返回0
if(pthread_equal(tid1, tid2)){…}//兼容性好
补充
pthread_self()返回的tid是POSIX线程库维护的主线程虚拟的tid,真正的由内核维护的真实的tid可以通过下面的函数获得:
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include
#include
long syscall(SYS_gettid);
成功返回调用线程的tid,失败返回-1
通过pthread_self函数返回的线程ID和pthread_create函数输出的线程ID一样,都是由POSIX库内部维护的虚拟(伪)线程ID,可用于其它需要提供线程ID的PTHREAD函数。系统内核维护的真实线程ID可通过**syscall(SYS_gettid)**获得。
在Linux系统中,一个进程的PID实际上就是它的主线程的TID
参考代码:equal.c
(1) 从线程过程函数中返回,执行该线程过程函数的线程即终止。其返回值可通过pthread_join函数的第二个参数输出给调用函数。
(2) 在线程过程函数及其被其调用的任何函数中都可以调用pthread_exit函数终止当前线程
void pthread_exit(void* retval);
该函数的参数retval就相当于线程过程函数的返回值,同样可以被pthread_join函数的第二个参数输出给调用者
参考代码:exit.c
注意:在子线程中调用pthread_exit函数,只会终止调用线程自己,对其它兄弟线程和主线程没有影响。但是如果在主线程中调用pthread_exit函数,被终止的将是整个进程及其所包含的全部线程
int pthread_cancel(pthread_t tid);
成功返回0,失败返回错误码
该函数只是向特定线程发出取消请求,并不等待其终止运行。缺省情况下,线程在收到取消请求以后,并不会立即终止,而是仍继续运行,直到达到某个取消点。在取消点出,线程会检查其自身是否已被取消,若是则立即终止。取消点通常出现在特定的系统调用中。
设置调用调用线程的取消状态为接收或者忽略取消请求
int pthread_setcancelstate(int state, int* oldstate);
设置调用调用线程的取消类型为延迟或者立即取消
int pthread_setcanceltype(int type, int* oldtype);
参考代码:cancel.c
(1) 线程属性结构pthread_attr_t
在Linux上线程属性pthread_attr_t是一个结构体,包括:
int detachstate; // 分离状态
int scope; // 竞争范围
int inheritsched; // 继承调度
int schedpolicy; // 调度策略
struct sched_param schedparam; // 调度参数
struct sched_param {
int sched_priority; // 静态优先级,
};
该静态优先级仅对实施调度SCHED_FIFO/SCHED_RR有意义。此值越大优先级越高。可以通过**sched_get_priority_max()和sched_get_priority_min()**函数,获得当前系统所支持的最大和最小静态优先级。
size_t guardsize; // 栈尾警戒区字节数,缺省4096
size_t stacksize; // 栈区字节数
void* stackaddr; // 栈区起始地址
(2) 常用函数
初始化线程属性结构,分配内部资源,设为缺省值
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 scopy);
…
销毁线程结构的内部动态资源
int pthread_attr_destroy(pthread_attr_t* attr);
获取特定线程的属性结构
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);
…
参考代码:attr.c
实际的g++执行过程(非原子化的):把内存中的值(g)读入CPU寄存器(eax),把寄存器(eax)中的值加1,把寄存器(eax)中的值存入内存(g)
当两个或者两个以上的线程同时以非原子化的方式访问同一个对象时,如果不能相互协调配合,极有可能导致对象的最终状态不稳定(不一致或者不完整),这就是所谓的并发冲突。解决冲突的基本原则就是敏感操作的原子化,即保证一个线程完成这组敏感操作以后,再允许另一个线程执行类似的操作,位于与共享资源有关的执行代码,在任何时候都只允许一个线程执行。
现象参见代码:vie.c
静态初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化:
pthread_mutex_t mutex; //一般在全局域定义
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
成功返回0,失败返回错误码。
动态销毁互斥锁:
int pthread_mutex_destroy(pthread_mutex_t* mutex);
锁定互斥锁:
int pthread_mutex_lock(pthread_mutex_t* mutex);
成功返回0,失败返回错误码。
任何时候只会有一个线程对特定的互斥锁加锁成功,其它试图对其加锁的线程会在此函数的阻塞中等待,直到该互斥锁的持有者将其解锁。
解锁互斥锁:
int pthread_mutex_unlock(pthread_mutex_t* mutex);
成功返回0,失败返回错误码。
对特定互斥锁加锁成功的线程通过此函数将其解锁,那些阻塞于对该互斥锁对象试图加锁的线程中的一个会被唤醒,得到该互斥锁,并从pthread_mutex_lock函数中返回。
编程模型:
pthread_mutex_init(...); //一般在全局域定义,因为每个线程都要使用该互斥所
...
void* thread_proc(void* arg) {
...
pthread_mutex_lock(...); //加锁 \
xxx |
xxx //执行操作,访问共享对象 > 锁区
xxx |
pthread_mutex_unlock(...);//解锁 /
...
}
...
pthread_mutex_destroy(...);
参考代码:mutex.c mutex2.c
互斥锁在保证数据一致性和防止并发冲突的同时,也牺牲了多线程应用的并发性,因此在设计上,应尽量少地使用互斥锁,或者说只有在需要需要被互斥锁保护的操作过程中,使用它;而对于保护需求不明显的场合尽量不用。
功能和用法与XSI/IPC对象中的信号量集非常类似,但是XSI/IPC对象中的信号量集只能用于进程,而这里的信号量既能用于进程,也可以用于线程。
(1) 创建信号量
#include
int sem_init(sem_t* sem, int pshared, unsigned int value);
成功返回0,失败返回-1,同时置errno。
sem——信号量
pshared——0表示用于线程,非0表示用于进程。
当pshared信号量取0,信号量仅用于一个进程中的多个线程,其本质就是一个普通的全局变量。但如果该参数的值为非0,则信号量可用于不同的进程,其存储位置在可为这些进程所访问的共享内存中。
value ——信号量初值,即初始空闲资源数
(2) 销毁信号量
int sem_destroy(sem_t* sem);
成功返回0,失败返回-1,同时置errno。
(3) 等待信号量
int sem_wait(sem_t* sem);
int sem_trywait(sem_t* sem);
int sem_timedwait(sem_t* sem, const struct timespec abs_timeout);*
成功返回0,失败返回-1,同时置errno。
struct timespec结构体内容:
struct timespec {
time_t tv_sec; // 秒
long tv_nsec; // 纳秒
};
超时时间的计算起点:UTC19700101T000000
调用以上函数时,若信号量计数器的值大于等于1,则将其减1并立即返回0,表示获得资源;如果该信号量的当前值等于0,这就意味着已经没有空闲资源可供分配,调用以上函数的进程或线程,会有以下三种情况::
(4) 释放信号量
int sem_post(sem_t* sem);
成功返回0,失败返回-1,同时置errno。
调用sem_post函数将直接导致sem信号量计数器的值加1。那些此刻正阻塞于针对该信号量的sem_wait函数调用的线程或进程中的一个将被唤醒,并sem信号量计数器的值减1并从sem_wait函数中返回。
(5) 获取信号量的当前值
int sem_getvalue(sem_t* sem, int* sval);
成功返回0,失败返回-1,同时置errno
代码:sem.c
现象参见代码:dl.c
死锁的四个必要条件:
死锁问题的解决方案:
//线程1
if(依赖的条件不满足)
睡眠于条件变量;
后续操作;
...;
//线程2
...;
创造出后续操作锁依赖的条件;
唤醒在条件变量中睡眠的线程;
......;
条件变量可以使一个线程在对某个条件的等待中发生阻塞,直到其它线程满足其所等待的条件后再被唤醒。
静态初始化条件变量
pthread_cond_t g_cond =PTHREAD_COND_INITIALIZER;
动态初始化条件变量
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* attr);
成功返回0,失败返回-1。
销毁动态初始化的条件变量
int pthread_cond_destroy(pthread_cond_t* cond);
成功返回0,失败返回-1。
等候条件变量,即在天剑变量中睡眠
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* abs_timeout);
成功返回0,失败返回-1。
当调用线程在cond条件变量睡眠期间,mutex互斥锁会被解锁,直到该线程从条件变量中苏醒,即从该函数中返回再重新拥有该互斥锁。
以上函数会令调用线程进入阻塞状态,直到条件变量cond收到信号为止,阻塞期间互斥锁mutex被解锁。有时需要将条件变量与互斥锁配合使用,以防止多个线程同时进入条件等候队列时发生竞争。线程在调用pthread_cond_wait函数前必须通过pthread_mutex_lock函数锁定mutex互斥锁。在调用线程进入条件变量等候队列之前,mutex互斥锁一直处于锁定状态,直到调用线程进入条件等候队列才被解锁。当调用线程即将从pthread_cond_wait函数返回时,mutex互斥锁会被重新锁定,回到调用该函数之前的状态。
向指定的条件变量发送信号
int pthread_cond_signal(pthread_cond_t cond);* //唤醒在条件变量cond中睡眠的第一个线程(如果),即条件等待队列的队首线程,待其重新获得互斥锁以后,从pthread_cond_wait函数中返回。
具体唤醒哪个线程呢?:假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。
int pthread_cond_broadcast(pthread_cond_t cond);* //唤醒在条件变量cond中睡眠的所有线程,只有重新获得锁的线程会从pthread_cond_wait函数中返回
成功返回0,失败返回-1。
通过pthread_cond_signal函数向条件变量cond发送信号,该条件变量的条件等候队列中的一个线程将离开等候队列,并重新锁定mutex互斥锁后,从pthread_cond_wait函数中返回。
通过pthread_cond_broadcast函数向条件变量cond发送信号,该条件变量的条件等候队列中的所有线程将同时离开等候队列,但其中只有一个线程在重新锁定mutex互斥锁后,从pthread_cond_wait函数中返回,其余线程会继续为等待mutex互斥锁而阻塞。
生产者-消费者模型:
硬件/网络—数据—>生产者线程-----数据----->消费者线程:生产者直接把数据交给消费者——刚性耦合
硬件/网络—数据—>生产者线程–数据–>缓冲区–数据–>消费者线程:经过缓冲区给消费者——柔性耦合
理想缓冲区:永远不满也不空
实际缓冲区:满不能放入(撑死),空不能提取(饿死)
//生产者线程
if(缓冲区满)
睡入非满条件变量,释放缓冲区锁;
被唤醒,离开条件等待队列,重新获得缓冲区锁;
生产--->非空;
唤醒在非空条件变量中睡眠的消费者线程;
//消费者线程
if(缓冲区空)
睡入非空条件变量,释放缓冲区锁;
被唤醒,离开条件等待队列,重新获得缓冲区锁;
消费--->非满;
唤醒在非满条件变量中睡眠的生产者线程;
参考代码:cond.c
哲学家就餐问题,参见代码:day15/dinning.c