基础知识
不会的命令可以查看 鸟哥私房菜这本书 或者自带的帮助文档 man 命令
ifconfig : 查看 IP 地址,MAC地址
可以用 ip 代替,ifconfig已经过时了
增加用户 sudo adduser client
密码:0000
pwd
重命名文件
mv既可用于改名,也可用于移动
mv mytest newtest (如果第二个参数是文件,那么就是改名,如果是目录就是移动至该目录。)
kill pid :杀死某个进程
killall 服务名:杀死这个服务对应的所有进程
创建文件:touch dir
创建目录:mkdir mkd
使用 ssh 命令时,IP不要加后面的 /24 , 192.168.92.9
man man 1 2 3 5 是主要章节
echo $PATH 从变量中取值
sudo poweroff
sudo reboot
- 命令解析器:
shell ----- unix操作系统
bash ----- Linux操作系统
本质:根据命令的名字,调用对应的可执行程序 - Linux快捷键
A. 命令和路径补充:
B. 主键盘快捷键:
a. 历史命令切换:
历史命令:history
向上遍历:Ctrl + P
向下遍历:Ctrl + N
b. 光标移动:
向左:Ctrl + B
向右:Ctrl + F
移动到头部:Ctrl + A
移动到尾部:Ctrl + E
c. 删除字符:
删除光标后边字符:Ctrl + D
删除光标前边字符:Ctrl + S/Backspace
删除光标前的所有内容:Ctrl + U
清屏:Ctrl + L
C. linux 变量
a. 特殊变量【$?】
1、用来获取上一个命令的退出状态
2、获取函数的返回值
对于shell函数来说,return关键字用来表示函数的退出状态,而不是函数的返回值,shell与其他语言不同,他没有处理返回值的关键字
D. history 查看历史命令
history > lalala.txt
这样就已经复制history的内容到lalala.txt这个文本文件了
3. Linux系统目录结构:
A. 根目录 /:/下挂载下面所有目录
B. /bin:存放经常使用的命令
C. /boot:这里存放的是启动Linux是使用的可惜文件,包括一些链接文件以及镜像文件。
D. /dev:device缩写,存放外部设备,在linux在访问设备的方式和访问文件的方式相同。
E. /etc:存放所有系统管理所需要的配置文件和子目录。
F. /home:各个普通用户的主目录。
G. /lib:存放着系统最基本的动态连接共享库,类似于Windows中的DLL文件,几乎所有文件都要运用到共享库。
H. /lost +found:一般情况是空的,当系统非法关机后,存放一些文件。
I. /media:linux系统会自动识别一些设备,例如U盘,光驱等,当识别后,linux会将这些识别后的设备挂载在该文件下。
J. /mnt:系统提供该目录是为了让用户临时挂载别的文件系统,可将光驱挂载在/mnt/上,然后进入该文件就可以看见光驱中的内容。
K. /opt:主机额外安装软件所摆放的目录。
L. /proc:虚拟目录,系统内存的映射,我们可以直接访问这个目录来获取系统信息,,这个目录的内容不在硬盘上而在内存里,我们可以直接修改里面的某些文件,比如通过下面的命令来屏蔽主机的ping命令,是别人无法ping你的机器。
M. /root:超级用户主目录。
N. /sbin:超级用户使用的
系统管理程序。
O. /selinux:redhat/centos特有,类似于防火墙。
P. /srv:存放一些服务启动之后需要提供的数据。
Q. /sys:2.6内核系统文件。
R. /tmp:临时文件。
S. /usr:非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似于Windows下的program files。
T. /usr/bin:系统用户使用的应用程序。
U. /usr/sbin:超级用户使用的比较高级的管理程序和系统守护程序。
V. /usr/src:内核源代码的放置目录.
W. /var:这个目录中存放着不断扩充的东西,我们习惯将那些经常被修改的目录存放在这个目录下,包括各种日志文件。
4. 用户目录:
A. 绝对路径:从根目录开始写 /home/itcast/aa
B. 相对路径:相对于当前的工作目录而言
a. . 当前目录
b. … 当前的上一级目录
c. - 在临近的两个目录直接切换 cd -
C. [czy@localhost ~]$
a. czy:当前登录的用户
b. localhost:主机名
c. ~:家目录,可代替czy@localhost,宿主目录
d. KaTeX parse error: Expected 'EOF', got '#' at position 17: …当前目录为普通用户 e. #̲:超级用户root,可用sud…之间
C. 创建目录:
a. 创建一个目录:mkdir 【目录名】
b. 创建目录和子目录:mkdir dir/dir1/dir2 -p(三层及以上要加-p)
D. 删除目录:
a. 删除空目录:rmdir dir
b. 删除非空目录:rm -r aa(直接删除,不会进入回收站)
c. 删除时询问是否删除:rm -ri bb
E. 创建文件:
touch dir (创建文件或者当文件已存在时修改文件创建时间)
F. 删除所有东西:rm
G. 复制:
cp mytest newtest
cp mydir newdir -r
H. 查看文件的具体信息:
a. 将文件内容展示在屏幕上:cat mytest(不能完全显示内容过长的文件)
b. 如果文件过长:more mytest(回车一行行显示,空格一页一页显示,但是只能往后看)
c. 如果想往前翻页:less mytest(ctrl+B向前翻页,ctrl+F向后翻页)
d. 用vi写文件后,可以vi查看文件
e. head默认显示前十行内容,head -5默认显示前五行
f. tail默认显示后十行内容,同理tail -5
I. 文件改名:mv mytest newtest
mv既可用于改名,也可用于移动,如果第二个参数是文件,那么就是改名,如果是目录就是移动至该目录
J. 软链接:创建快捷方式
ln -s 【带绝对路径的文件 】 【软链接名字】
K. 硬链接:相当于超链接,不额外占用磁盘资源,映射到inode结点。用ln创建。
6. 文件和目录属性:
A. wc:获取文本文件信息(行数,字数,字节数,文件名字),wc【文件名】
参数: -c:只显示字节数
-l:只显示行数
-w:只显示字数
B. od:查看二进制文件(可执行文件):-t 指定数据的显示格式
参数: c:ASCⅡ字符
d:有符号十进制数
f:浮点型
o:八进制数
u:无符号十进制数
x:十六进制数
C. du:查看当前目录大小,du -h
D. df:查看磁盘使用情况,df -h
E. which命令:命令解析器,查找命令在哪个目录下(只可查外部命令),例如which ls,即可知道ls这个命令在哪个目录下
7. 文件权限、用户、用户组:
A. 查看当前登录用户:
w[查看当前系统信息和用户登录信息]
who[查看当前正在登录的用户]
第一行,系统当前运行的基本信息
时间 | 距离上次重新启动的时间 | 上次启动时间 | 当前在线用户数 | 系统负载程度,接近0表示负载越低
显示用户的详尽信息
TTY 用户登录的终端代号 pts表示远程访问
LOGIN用户登录时间
IDLE用户登录后闲置时间
#JCPU用户执行程序耗费CPU的时间
WHAT 表示用户正在执行的程序
PCPU则表示执行
#WHAT字段内的程序所耗费的时间。
B. 修改文件权限:
a. 文字设定法:chmod [who] [+|-|=] [mode]
1) chmod:change mode
2) who:文件所有者(u)、文件所属组(g)、
其他人(o)、所有人(a,如果什么都不写,默认为a)
3) +:增加权限、-:减少权限、=:覆盖原来权限
4) mode:读(r)、写(w)、执行(x)
b. 数字设定法:chmod 777 【文件名】
1) -:没有权限
2) r:读权限(4)
3) w:写权限(2)
4) x:执行权限(1)
5) 7 —rwx— 文件所有者
6) 6 —rw— 文件所属组
7) 5 —rx— 其他人
8) 减权限:chmod -001 【文件名】
9) 覆盖权限:chmod 765 【文件名】
C. 改变文件或目录的所有者或所属者:
Linux中,文件可以属于某一所有者并属于某一所属组,但所有者可以不在该所属组中。
用chown 【所有者】【文件】,文件所有者改变,但所属组未改变。
chown 【所有者:所属组】【文件】则可以改变所有者和所属组。
D. 改变文件或目录所属的组:chgrp 【所属组】【文件】则可以改变所属组。
8. 查找和检索:
A. 按文件属性查找:
a. 文件名:find + 查找的目录 + -name +“文件的名字”,若不知道全面,可用通配符:*代表多个字
符、?代表1个字符。
b. 文件大小:find + 查找的目录 + -size + 文件的大小(大于10k则为+10k,小于10k则为-10k)
c. 文件类型:find + 查找的目录 + -type + d(目录)/f(普通文件)/b(块设备)/c(字符设备)/s
(套接字)/p(管道)/l(链接符号)
B. 按文件内容查找:grep -r “查找内容”+查找路径,-r的目的就是递归查找,与上面同理。
C. 查找命令行所对应的可执行文件
相关博客链接
a. find 在指定目录下查找命令行名的文件
b. whereis 查找指定命令的所有 二进制文件(-b)、man说明文件(-m)、源代码文件(-s)
c. which 只在 PATH 变量的指定路径下查找
d. type 判断命令行是 bash 自带的,还是外部二进制文件提供,并输出路径
9. 软件的安装和卸载:
A. 在线安装:(CentOs下为yum下载rpm包,Ubuntu下为apt-get下载deb包,Ubuntu下的aptitude相比于apt-get处理依赖最佳)
a. 安装:sudo yum -y install xxx(在线下载安装)
b. 卸载:sudo yum -y remove xxx
c. 更新:sudo yum -y update (升级所有包同时也升级软件和系统内核。更新软件列表。);sudo yum -y upgrade(只升级所有包,不升级软件和系统内核。)
d. 清空:sudo yum -y clean(情况安装包缓存,实际清理的是:/var/cache/apt/archives目录下的.deb文件)
B. deb包/rpm包:
a. 安装:sudo dpkg -I 【deb包】/sudo rpm -ivh 【rpm包】
b. 卸载:sudo dpkg -r 【卸载包】/sudo rpm -e gcc
C. 源码安装:
1) 解压缩源代码包
2) 进入到安装目录:cd dir
3) 检测文件是否缺失,创建Makefile,检测编译环境:./configure
4) 编译源代码,生成库和可执行程序make
5) 把库和可执行程序,安装到系统目录下:sudo make install
6) 删除和卸载软件:sudo make distclean
7) 上述安装步骤并不绝对,应该查看附带的README文件,会有详细说明。
10. U盘的挂载和卸载:
A. 挂载(mount命令):
a. 系统默认挂载目录:/media
b. 手动挂载目录:/mnt(sudo mount 【设备名】【/mnt】),可用sudo fdisk -l查看设备信息
B. 卸载(umount命令):sudo umount 【卸载路径】(注意:你不能在要卸载的目录中)
11. 查看日志
Linux日志文件说明
/var/log/message 系统启动后的信息和错误日志,是Red Hat Linux中最常用的日志之一
/var/log/secure 与安全相关的日志信息
/var/log/maillog 与邮件相关的日志信息
/var/log/cron 与定时任务相关的日志信息
/var/log/spooler 与UUCP和news设备相关的日志信息
/var/log/boot.log 守护进程启动和停止相关的日志消息
/var/log/wtmp 该日志文件永久记录每个用户登录、注销及系统的启动、停机的事件
Linux查看日志的命令有多种: tail、cat、tac、head、echo、grep等
12. git 的使用
CH2
-
压缩包管理:
a. 初级版:
i. gzip — .gz格式的压缩包,不会打包压缩也不会保留原文件。gzip 【文件名】
ii. bzip2 — .bz2格式的压缩包,不能压缩目录,只能压缩文件,
可以保留原文件(bzip2 -k 【文件名】即可保留)。
b. 进阶版:
i. tar:-------不使用z/j参数,该命令这只能对文件或目录打包
1) 参数:
a) c – 创建
b) x – 释放(与c互斥,不能一起用)
c) v – 显示提示信息 – 压缩解压缩时可以省略
d) f – 指定压缩文件名字(压缩解压缩时都要使用)
e) z – 使用gzip方式压缩文件 – .gz
f) j – 使用bzip2方式压缩文件 – .bz2
2) 压缩:
a) tar zcvf 【生成的压缩包的名字(xxx.tar.gz)】【要压缩的文件或目录】
b) tar jcvf 【生成的压缩包的名字(xxx.tar.bz2)】【要压缩的文件或目录】
3) 解压缩:
a) tar zxvf 【压缩包的名字】(解压到当前目录)/
tar zxvf 【压缩包的名字】-C 【路径】(压缩到指定路径)
b) tar jxvf 【压缩包的名字】(解压到当前目录)/
tar jxvf 【压缩包的名字】-C 【路径】(压缩到指定路径)
ii. rar:-------必须手动安装该软件
1) 参数:
a) 压缩:a
b) 解压缩:x
2) 压缩:rar a 【生成的压缩文件的名字(无需指定后缀,会自动生成)】【要压缩的文件或目录】
3) 解压缩:rar x 【压缩文件名】/rar x 【压缩文件名】【指定路径】
iii. zip:
1) 参数:压缩目录需要加递归参数 -r/解压缩要加-d
2) 压缩:zip 【生成的压缩包名字】【压缩到的文件或目录】
3) 解压缩:unzip 【压缩包的名字】/unzip 【压缩包的名字】 -d 【解压缩到指定路径】
c. 总结:
i. 相同之处:
1) tar/rar/zip 参数 生成的压缩文件的名字 压缩的文件或目录 ------压缩时的语法
2) tar/rar/unzip 参数 压缩包的名字/tar/rar/unzip 参数 压缩包的名字 参数(rar没有该参数) 解压缩目录 ------解压缩语法 -
进程管理:
a. 查看当前在线用户的情况:who
b. 查看整个系统内部运行的进程状况:ps
i. 参数:一般使用aux,三个可以一起连用
1) a(查看所有用户信息)
2) u(用户的信息)可与a连用:ps au
3) TTY(终端),TTY1-6为文字终端,TTY7为图形界面终端,
可通过Ctrl+Alt+F1-7切换,:0代表TTY7
4) x(查看没有终端的应用程序)ps aux,终端的作用是与用户进行交互。
c. 管道:如果想对文件进行过滤就需要管道(-p)
i. 什么是管道:
ii. 指令:ps aux | grep xxx(其实就是查找进程里的xxx进程,ps aux是查看所有进程信息,
grep是查找这些进程中有没有xxx进程)
d. 终止进程:
i. 查看信号编号:PID为启动的进程ID,可通过PID找到对应的程序。
ii. 杀死进程:
kill -l(查看64个信号,每个信号干的事不一样,第九个SIGKILL是杀死信号),
kill -9 【进程PID】
e. 查看当前进程的环境变量:
env(所有环境变量设置)/env |grep PATH(该操作为将环境变量信息中的PATH单独过滤出来)
i. Linux下的环境变量的格式:key - value
key(图中PATH)=value(value值可以有多个,每个value值之间用:分开)
f. 任务管理器:top命令,只能看,无法操作,Ctrl+C关闭。 -
网络管理:
a. 获取网络接口配置信息:ifconfig命令/Windows下为ipconfig
i. eth0:当前电脑网卡,如果还有网卡则为eth1以此类推 ii. 硬件地址(MAC地址)
iii. inet(IP地址)
iv. IO:回环地址
b. 测试与目标主机是否联通:
i. ping 【IP】-c 4(后面参数-c 4意思为回馈四条信息即可,也可以不加参数)
ii. ping 【域名】(例如ping www.baidu.com)
c. 查看服务器域名对应的IP地址:nslookup 【域名】
d. 访问一个 url
i. curl会显示出源码
curl http://www.baidu.com/index.html
ii. wget 这个会将访问的首页下载到本地
wget http://www.baidu.com
iii. elinks - lynx-like替代角色模式WWW的浏览器
elinks --dump http://www.baidu.com
iv. lynx(这个以前在群里面见有人讨论过,但是没有尝试过,想用的话还需要下载软件)
lynx http://www.baidu.com -
用户管理:
a. 创建用户:
i. sudo adduser 【用户名(不能包含大写字母)】(是一个脚本)
ii. sudo useradd -s 【/bin/bash】 -g 【xxx】-d 【/home/xxx】 -m 【xxx】
1) -s指定新用户登录时shell类型
2) -g指定所属组,该组必须已经存在
3) -d用户家目录
4) -m用户家目录不存在时自动创建该目录
5) 如果组不存在,则sudo groupadd 【组名】
6) 退出:exit
b. 设置用户组:sudo groupadd 【组名】
c. 删除用户:
i. sudo deluser 【用户名】
ii. sudo userdel -r 【用户名】(-r的作用是把用户的主目录一起删除)
d. 切换用户:su 【用户】
e. 设置密码:
i. 修改密码:sudo passwd 【用户】
ii. 修改当前用户密码:passwd(直接passwd)
iii. 修改root密码:sudo passwd root -
ftp服务器搭建 – vsftpd软件。作用:文件的上传和下载
a. 服务器端:
i. 修改配置文件:(配置哪些用户可以登录,登陆后权限是只允许下载还是都可以之类的)
1) 进入/etc目录下
2) ls -l vsftpd.conf(修改此配置文件)
3) 用文本编辑器Vi或者gedit修改配置
4) anonymous_enable=YES(匿名用户使用权限)
5) local_enable=YES(本地用户使用权限)
6) write_enable=YES(实名登录用户拥有写权限(上传数据))
7) local——vmask=022(设置本地掩码为022)
8) anon_upload_enable=YES(匿名用户可以向服务器上传数据)
9) anon_mkdir_write_enable=YES(匿名用户可以在FTP服务器上创建目录)
ii. 重启服务:sudo service vsftpd restart(修改后不能立马生效需要重启服务,!!!但是此为通用命令,centos7已经不再使用)
iii. centos7下重启服务:systemctl restart vsftpd.service(重启)/systemctl status vsftped.service(查看状态)
以此,在Centos7 以后,凡是要service … 动词的命令,如果执行不了,可以尝试下systemctl 动词进一步说明可参考<https://blog.csdn.net/weixin_34280237/article/details/89617656>
b. 客户端:
i. 实名用户登录:
1) ftp+IP(server)
2) 输入用户名(server)
3) 输入密码
4) 上传put 【文件名】(必须是登录时所在目录里)
5) 下载get 【文件名】
6) 只允许上传、下载文件,如果想操作文件,只能打包tar/rar/zip
ii. 匿名用户登录:
1) ftp+serverIP
2) 用户名:anonymous
3) 密码:无(直接回车)
4) 不允许匿名用户在任意目录直接切换,只能在一个指定的目录范围内工作,需要在ftp服务器上创建一个匿名用户的目录----匿名用户的根目录
5) 步骤:指定匿名用户根目录:
a) 自己指定:mkdir 【目录名】
b) 默认目录:/srv/ftp/
c) anon_root=【指定目录路径】---->匿名用户根目录(在vi vsftpd.conf中随便找一行添加这一段话设置匿名用户根目录)
6) 创建目录:供匿名用户使用
a) mkdir anonDir
b) 修改目录所有者:sudo chown ftp anonDir/修改目录权限:chmod 777 anonDir(二选一)
7) 修改配置文件:sudo gedit(vi)/dev/vsftpd.conf
8) 重启服务器
iii. lftp客户端访问ftp服务器:
1) lftp是ftp的一个客户端工具,可以上传和下载目录。
2) 软件安装:sudo apt-get install lftp
3) 登录服务器:
a) 匿名:
i) lftp【服务器IP】(回车)
ii) login
b) 实名:
i) lftp【username@IP】(回车)
ii) 输入服务器密码
4) 操作:
a) put上传文件
b) mput上传多个文件
c) get下载文件
d) mget下载多个文件
e) mirror下载整个目录及其子目录
f) mirror -R上传整个目录及其子目录 -
nfs服务器搭建
-
nfs服务器搭建:(相当于Windows下的共享文件夹)net file system ---->网络文件系统,它允许网络中的计算机之间通过TCP/IP网络共享资源。 a. 服务器端:
i. 安装:sudo apt-get install nfs-kernel-server
ii. 创建共享目录:mkdir 【目录名】(找个合适的地方创建)
iii. 修改配置文件:
1) sudo vi /etc/exports2) 把第11行删掉重新写,【共享目录路径】*(rw,sync)(路径后加个*,代表IP网段,后面这句括号要加,rw代表读写权限,ro代表只读,sync代表实时更新数据到磁盘上) iv. 重启服务:sudo service nfs-kernel-server restart/systemctl restart service nfs-kernel-server
b. 客户端:
i. 挂载服务器共享目录:sudo mount 【serverIP】:【shareIP】【挂载路径(一般为/mnt)】 -
ssh服务器
-
ssh服务器:远程操作服务器
a. 服务器端:
i. 安装ssh:
1) sudo apt-get install openssh-server
2) 查看SSH是否安装:sudo aptitude show openssh-server
b. 客户端:
i. 远程登录:ssh 【登录名@IP】(确认连接的时候一定要写yes/no)
ii. 退出登录:logout -
scp命令:
a. scp==super copy
b. 使用该命令的前提条件:目录主机已经成功安装openssh-server
c. 使用格式:
i. scp -r 【目标用户名@目标主机IP地址】:【/目标文件的绝对路径】【/保存到本机的绝对(相对)路径】(后续输入yes,不能输入y)
ii. 拷贝目录需要加参数-r -
其他命令:
a. 翻页:
i. Shift+PageUp -->上翻页
ii. Shift+PageDown -->下翻页
b. 清屏:clear/Ctrl + l
c. 创建终端:Ctrl + Alt + T(Ubuntu)/Ctrl + Shift + T 添加新标签页
d. 看手册:man man ---->共九个章节(1可执行程序或shell命令,2系统调用(内核提供的函数),3库函数(程序库中的函数),5文件格式和规范(如/etc/passwd))
e. 设置查看别名:
i. 查看:alias
ii. 设置:alias pag=‘ps aux |grep’(需要长久有效需要设置配置文件:.bashrc)
f. echo命令:在显示器上显示数据
i. 普通数据:echo 字符串
ii. 显示环境变量:echo $PATH
iii. 显示上一次程序退出值:echo ? ( ?( ?(:取值,?:最近一次程序退出时的返回值)
g. 关机:poweroff
h. 重启:reboot
i. shutdown:
CH3 -
vim
a. 命令模式下的操作:打开文件(vi 【文件名】)之后,默认进入命令模式。
i. 光标的移动:vim中不能使用鼠标,如果想要移动光标,需要使用HJKL四个按键,
H(前)、L(后)、J(下)、K(上)、
0(行首)、 ( 行 尾 ) 、 g g ( 文 本 首 ) 、 s h i f t + G ( 文 本 尾 ) , N + G ( 跳 到 N 行 ) i i . 删 除 ( v i 中 删 除 的 本 质 是 剪 切 ) 操 作 : x ( 删 除 光 标 上 的 字 符 ( 也 就 是 光 标 后 的 ) ) 、 s h i f t + x ( X 删 除 光 标 前 的 字 符 ) 、 d w 删 除 整 个 单 词 ( 光 标 后 面 的 字 符 一 直 到 空 格 ) , 如 果 要 删 整 个 单 词 , 要 把 光 标 放 在 单 词 首 字 母 d 0 ( 从 光 标 往 前 删 到 行 首 ) 、 d (行尾)、 gg(文本首)、shift+G(文本尾), N+G(跳到N行) ii. 删除(vi中删除的本质是剪切)操作: x(删除光标上的字符(也就是光标后的))、 shift+x(X删除光标前的字符)、 dw 删除整个单词(光标后面的字符一直到空格),如果要删整个单词,要把光标放在单词首字母 d0(从光标往前删到行首)、 d (行尾)、gg(文本首)、shift+G(文本尾),N+G(跳到N行)ii.删除(vi中删除的本质是剪切)操作:x(删除光标上的字符(也就是光标后的))、shift+x(X删除光标前的字符)、dw删除整个单词(光标后面的字符一直到空格),如果要删整个单词,要把光标放在单词首字母d0(从光标往前删到行首)、d(删除光标以后的部分)、
D(光标后边所有部分)、
dd(删除整行)、
4dd(删除从光标开始数四行)
iii. 撤销操作:
u(撤销)
Ctrl+R(反撤销)
iv. 复制操作:
p(粘贴,粘贴至光标所在行的下一行)
shift+p(粘贴至光标所在行)、
yy(复制光标所在行)、
Nyy(复制N行)
v. 可视模式:
v(切换为可视模式,然后用hjkl控制光标移动选择所需内容进行复制(y)或者删除(d))
vi. 查找操作:
1) /【要查找的单词】:在左下角输入【/+查找内容】(输入n可在查找高亮目标进行切换)(从光标位置向下查找,到尾部后,返回到头部)
2) ?【要查找的单词】:也是查找(在光标位置向上查找,到头部后,返回尾部)
3) #:将光标放在要查找的单词上,按#键即可查找。
vii. r:替换当前字符(光标所指),只能替换单个字符
viii. 查看man文档,光标移动到要查的命令(单词),输入shift+k即可跳到man文档,3+shift+k(跳到man文档第三章)
ix. 缩减:向右>>,向左<<
b. 文本(编辑)模式下的操作:需要输入一些命令,切换到编辑模式。(命令(aios)———>编辑)
i. a:插入字符,输入到光标后面(A:输入字符到行尾)
ii. i:插入字符到光标前面(I:输入到当前行行首)
iii. o:在光标所在行下面一行创建新的一行,输入文本内容(O:光标上面一行创建新的行)
iv. s:删除光标后边的一个字符并开始在光标处输入(S:删除光标所在一整行并在该行输入)
c. 末行模式下的操作:在末行模式下可以输入一些命令。
(编辑模式不能直接切换为末行模式!!!)(:!【命令】即可操作相关命令,如:(:!pwd))
i. 光标跳转:直接输入N,就能跳到N行
ii. 替换:
1) 先将光标移动到要替换的单词那一行,
2) 输入:切换至末行模式,输入s/【要替换的内容】/【替换成的内容】
3) 若要替换当前行所有的这个内容,需输入s/【要替换的内容】/【替换成的内容】/g
4) %s/【要替换的内容】/【替换成的内容】:替换文本中每一行第一个该内容
5) s/【要替换的内容】/【替换成的内容】/g:替换文本中所有该内容
6) 27,30s/【要替换的内容】/【替换成的内容】/g:替换27到30行
iii. 保存:w
iv. 退出:q(退出)、wq(保存并退出,输入x相当于wq)、q!(退出不保存)或者命令模式下按ZZ(两个大写)
v. 显示行号:set nu
vi. 粘贴保留原格式:set paste
d. 分屏操作:
i. 水平分屏:末行模式下输入sp,Ctrl+w+w可以上下切换屏幕操作,wqall全部退出,不加all就是操作一个
ii. 垂直分屏:末行模式下输入vsp,Ctrl+w+w可以在两个屏幕之间切换,vsp 【当前目录下文件名字】即可用一个分屏操作另一个文件。
e. vim打造IDE:
i. 系统级配置文件目录:/etc/vim/vimrc
ii. 用户级配置文件目录:~/.vim/vimrc
iii. 修改配置文件:vimrc或者.vimrc
2. gcc编译器:
a. gcc工作流程:
b. gcc hello.c -o myapp(默认执行中间的步骤,直接由C文件生成exe文件)
c. gcc的一些参数(esc)使用:
i. -E:预处理
ii. -S:C转汇编(例如gcc -S hello.i -o hello.s,其他同理)
iii. -c:汇编转二进制(.o文件)
iv. -o:生成文件,有了这个参数可以修改最后生成的可执行文件的名称
v. -I:gcc hello.c -I 【头文件路径】-o app
(要么头文件与C文件在同一目录下,要么指定头文件目录)
vi. -D:gcc hello.c -I 【头文件路径】-o app -D 【宏】
(编译时定义宏)
vii. -O:优化(0、1、3三个等级优化,可以优化冗余代码,例如-O0没有优化、-O1缺省值、-O3优化级别最高)
viii. -Wall:输出警告信息
ix. -g:添加调试信息(文件会变大)(gdb调试的时候必须加此参数)
3. 静态库的制作:
a. 命名规则:lib + 库的名字 + .a(所有静态库都是.a结尾),例如libmytest.a(库的名字就是mytest)
b. 制作步骤:
i. 生成对应的.o文件(xxx.c---->xxx.o,只需要-c即可转换):gcc -c *.c
ii. 将生成的.o文件打包(打包工具ar)ar rcs 【静态库的名字】【生成的所有的.o文件】
iii. -L(指定库的目录),
-l(指定库的名字mytest,掐头去尾)
iv. nm 【libmytest.a】查看库的内容
两种使用示例:
gcc main.c -Iinclude -L lib -l MyCalc -o myapp
gcc main. c lib/libMyCalc.a -o sum -Iinclude
lib/libMyCalc.a :lib 库中的 libMyCalc.a 库
-Iinclude :包含的头文件
-L lib :这个是大写 L,指定静态库的目录
-l MyCalc :这个是小写 l,静态库目录中的某一个库目录,
这里需要对库的名字掐头(lib)去尾(.a)
c. 发布和使用静态库(两部分文件):
i. 发布静态库,是一堆二进制文件
ii. 头文件,
d. 优缺点:
i. 优点:
1) 发布程序的时候不需要提供对应的库
2) 加载库的速度快
ii. 缺点:
1) 库被打包到应用程序中导致库的体积很大
2) 库发生了改变,需要重新编译
4. 共享库(也就是动态库)的制作
静态库:与位置有关,存放在内存四区的代码段,是一个绝对地址,每次运行时,.o 文件都会存在固定的位置。
动态库:与位置无关,存放在栈区和堆区之间的 共享库区,是一个相对路径,.o文件不会直接打包到程序中,而是当程序运行起来时才会动态加载,每次存放的位置不同,通过动态库变化的相对路径结合代码段中的第几行来寻找正确的代码。
a. 命名规则:lib+名字+.so
b. 制作步骤:
i. 生成与位置无关的代码(生成与位置无关的.o(静态库是与位置有关的.o)):
gcc -fPIC -c *.c
ii. 将.o打包成共享库(动态库)(这里不是使用ar打包):
gcc -shared -o 【生成的共享库名字 libmytest.os】 *.o -Iinclude
c. 发布和使用共享库:
i. 发布:将 libmytest.so 和 include 发布给用户即可。
ii. 使用:
1) 第一种:gcc main.c libmytest.os -o app -Iinclude
2) 第二种:gcc main.c -Iinclude -L【库的路径 ./lib】 -lmytest -o app
d. 解决程序执行时动态库无法被加载的问题:
i. ldd myapp(第二种方式执行时如果报错未找到动态库则执行ldd命令,查看myapp所有使用的库信息,动态库和执行程序之间有个动态链接器需要配置)
-
粗暴设置:将库复制粘贴到lib目录即可使用动态链接器(PATH环境变量),
sudo cp lib/libmytest.so /lib即可运行(!!!不要这样做,因为怕扰乱系统文件)
2) 临时设置:可以将动态库路径指定到环境变量:LD_LIBRARY_PATH
(临时的,关掉就没有了,开发过程中测试场景)
a) echo L D L I B R A R Y P A T H ( 发 现 没 有 值 ) b ) 赋 值 : LD_LIBRARY_PATH(发现没有值) b) 赋值: LDLIBRARYPATH(发现没有值)b)赋值:LD_LIBRARY_PATH=【动态库路径】
c) export $LD_LIBRARY_PATH(将设置的值导入到系统环境变量中)
3) 永久设置(不推荐):
a) 在家目录下,ls -a找到.bashrc(配置文件,重启终端生效)
b) vi .bashrc
c) G将光标移动到最后一行,将export LD_LIBRARY_PATH=【绝对路径】写在在最后一行即可永久操作动态库。 -
第四种设置方法 (重要的):
a) 需要找到动态链接器的配置文件————ls -l ld.so.conf(/etc目录下)
b) 动态库的路径写到配置文件中————sudo vi ld.so.conf
c) o添加一行(将动态库绝对路径写上即可),wq
d) 更新————sudo ldconfig -v(-v信息输出,tar zxvf中有)
e) ldd发现链接好了e. 优缺点:
i. 优点:
1) 执行程序体积小
2) 动态库更新不需要重新编译(前提是接口不变,内容可以变)
ii. 缺点:
1) 需要把动态库提供给用户
2) 没有被打包到应用程序中,相对静态库较慢
CH4
- gdb调试
- gdb调试:
首先要生成可执行文件,一定要加 -g,作用是输出调试信息
gcc *.c -o app -g
a. 启动gdb:
gdb 【应用程序名字】
这样就进入了 gdb,等待用户的输入,
b. 查看程序源代码:
1、默认打开包含main的文件:
l
2、查看除main之外其他文件:
l【文件名】:20,打开其他代码文件的第20行代码
3、查看某个文件中的某个函数:
l【文件名】:【函数名】,再按l继续查看下面的内容,之后再按下enter默认与 l功能一样
c. 设置断点:
break 7(在第七行打断点)break也可简写为b
i. 设置当前文件断点:break、b
ii. 设置指定文件断点:b 【文件名】:行号/【函数名】
iii. 设置条件断点:b 15 if i15 (如果i15则在第15行打断点停下来)
iv. 删除断点:
1) info break(第一步:查看断点信息,找到要删除的断点编号)
2) d 【编号】 (第二步:删除某个编号的断点)
d. 查看设置的断点:i b (info break)查看断点对应的编号信息
e. 开始执行gdb调试:
i. 执行一步操作:start (只执行一步)
ii. 继续执行:
n(next 单步执行、跳过函数体)
c(continue 继续执行,知道停在断点位置)
s(single 单步、可进入函数体内部)
查看函数体中源代码:l ,此时中文会变成乱码
p 【变量】 查看对应变量的值
ptype 【变量】 查看某个变量类型
iii. 执行多步,最直接停 在断点处:run或者 r
f. 单步调试
i. 进入函数体内部:s
ii. 从函数体内部跳出:finish
iii. 不进入函数体内部:n
iv. 退出当前循环:u(跳出单次循环)
g. 查看变量的值:p 【要查看的值】
h. 查看变量的类型:ptype 【要查看的变量】
i. 设置变量的值:set var i=10
j. 设置追踪变量,通过用于循环语句中的变量:display 【要追踪的变量】
k. 取消追踪变量:
i. info dispaly(第一步要先查看设置的追踪变量的编号)
ii. undisplay 【追踪变量的编号】 (第二步取消追踪遍历值)
l. 退出gdb调试:quit - makefile的编写
是项目源代码管理工具,用于管理源代码(文件太多时gcc会非常长,这时要用到makefile)
a. makefile的命名:Makefile或makefile
b. makefile的规则:
i. makefile第一种方法:规则中的三要素:目标、依赖、命令
/第一行/目标:依赖条件app:main.c add.c sub.c mul.c
(这些 .c 文件要在同一级目录,否则要指定目录)
/第二行/(tab缩减)命令 gcc main.c add.c sub.c mul.c -o app
ii. makefile第二种方法:
第一种方法修改后,修改任一.c文件都需要将整个重新编译,很麻烦。
所以引申出第二种方法,即全部生成.o文件(先编译),
如图,第一行确定总目标是由.o文件生成app,接下来几行是由.c文件生成.o文件(即编译),如果某个.c文件有做修改,那么只需再次编译对应.c为.o文件。(可以理解为第一行子目标是为了生成对应的依赖,第一条中的命令最后被执行。)
c. makefile工作原理
d. makefile第三种方法(取变量中的值用$(变量))
原先的程序修改简化:
第6行: %.o:%.c
e. makefile中的变量:
i. 普通变量:
1) 变量取值:foo=$(obj)
2) makefile中自己维护的变量:
(大写的都是makefile库中自己维护的变量,部分有默认值,用户都可以自己设定默认值)
a) CC : 默认值为cc
b) CPPFLAGS 预编译工程中的参数选项,可以自己设定默认值
c) 还有如下变量:
i.CPPFLAGS 预处理器需要的选项,如-I
ii.CFLAGS 编译的时候使用的参数,如-Wall -g -c
iii.LDFLAGS 链接库使用的选项,如-L -l
d) 用户可以自己设定默认值CC=gcc
ii. 自动变量:
1) 变量:
a) $<:规则中的第一个依赖
b) $@:规则中的目标
c)
:
规
则
中
的
所
有
依
赖
d
)
都
必
须
在
规
则
中
的
命
令
使
用
2
)
横
式
规
则
:
a
)
在
规
则
的
目
标
定
义
中
使
用
b
)
在
规
则
的
依
赖
条
件
中
使
用
f
.
m
a
k
e
f
i
l
e
中
函
数
的
使
用
i
.
s
r
c
=
^:规则中的所有依赖 d) 都必须在规则中的命令使用 2) 横式规则: a) 在规则的目标定义中使用% b) 在规则的依赖条件中使用% f. makefile中函数的使用 i. src=
:规则中的所有依赖d)都必须在规则中的命令使用2)横式规则:a)在规则的目标定义中使用b)在规则的依赖条件中使用f.makefile中函数的使用i.src=(wildcard 【指定目录】.c) — — — — 函数wildcard:在当前目录(./.c)下查找所有.c文件
ii. obj=$(patsubst【指定目录】%.c,【指定目录】%.o, $(src)) — — — — 匹配替换,将src找到的.c文件替换为.o文件
iii. 然后make clean,则只执行make clean命令
.PHONY:clean // 伪目标,写了这个就不会提示 “这个最新的”
clean:
rm $(obj) $(target) -f // -f 强制执行
make hello,不执行其他,只执行hello,makefile中输入如下:
hello:
echo “hello,makefile”
退出makefile,并在unbuntu下输入make hello,则只执行makefile文件中的hello命令。
- 系统IO函数C库
a. 文件描述符:
b. 虚拟地址空间:程序运行时相当于一个进程,会分配一个虚拟地址空间,32位的电脑是2^32(即4G)空间
cpu为什么要使用虚拟空间地址与物理空间地址映射?解决了什么样的问题?
- 方便编译器和操作系统安排程序的地址分布。
程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。 - 方便进程之间隔离
不同进程使用虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程使用的物理内存。 - 方便OS使用你那可怜的内存
程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓存区。当物理内存的供应量变小时,
内存管理器会将物理内存页(通常为4KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
c. 库函数与系统函数的关系:
CH5
- Linux文件操作相关函数
a. stat函数:
stat【文件名】
i. (获取文件属性,从inote上获取)
ii. 返回值0、-1
iii. Inode号:通过Inote号查找磁盘文件,文件本身就是一个硬链接,该文件没有其他链接。
iv. man 2 stat(stat不打开文件,直接告知路径。fstat打开文件)
1) 头文件:<sys/types.h>、<sys/stat.h>、<unistd.h>
-
int stat(const char *pathname,struct stat *buf)
const char *pathname(传入路径)
struct stat *buf(结构体指针,定义了一块缓存区,此处为传出数据,将数据写入缓存区)
v. struct stat *buf (文件属性)st_mode:
-
该变量占2byte共16位,整型
2) 掩码的使用st_mode &(与运算)掩码(8进制)(获得全部权限需要&掩码,如果是单一权限则直接用前面的即可)
3) 其他人权限(0-2bit)(掩码:S_IRWXO 00007过滤st_mode中除其他人权限以外的信息)
a) S_IROTH 00004
b) S_IWOTH 00002
c) S_IXOTH 00001
4) 所属组权限(3-5bit)(掩码:S_IRWXU 00070过滤st_mode中除所属组权限以外的信息)
a) S_IRGRP 00040
b) S_IWGRP 00020
c) S_IXGRP 00010
5) 文件所有者权限(6-8bit)(掩码:S_IRWXU 00700过滤st_mode中除所有者权限以外的信息)
a) S_IRUSR 00400
b) S_IWUSR 00200
c) S_IXUSR 00100
6) 特殊权限位(9-11bit)(很少用)
a) S_ISUID 0004000 设置用户ID
b) S_ISGID 0002000 设置组ID
c) S_ISVTX 0001000 黏着位
7) 文件类型(12-15bit)(掩码:S_IFMT 0170000过滤st_mode中除文件类型以外的信息)
与掩码做与运算过滤掉除文件类型以外的信息得到文件类型的八进制码与下面七个文件类型比对即可找出是哪个文件类型
a) S_IFSOCK 0140000 套接字
b) S_IFLINK 0120000 符号链接(软链接)
c) S_IFREG 0100000普通文件
d) S_IFBLK 0060000 块设备
e) S_IFDIR 0040000目录
f) S_IFCHR 0020000字符设备
g) S_IFIFO 0010000 管道
b. lstat函数:lstate和state区别
i. state函数:穿透(追踪)函数 ——软连接,即根据软连接追踪到所需执行的文件查看大小
ii. lstate函数:不穿透(不追踪)函数 ——直接读软连接文件的大小
iii. 区别就是操作软连接时是否追踪,
c. access函数:
i. 作用:测试指定文件是否拥有某种权限
ii. 原型:int access(const char *pathname,int mode);
1) 参数:
a) pathname -->文件名
b) mode -->权限类别
i) R_OK 是否有读权限
ii) W_OK 是否有写权限
iii) X_OK 是否有执行权限
iv) F_OK 测试一个文件是否存在
2) 返回值:
a) 0 -->所有欲查核的权限都通过了检查
b) -1 -->有权限被禁止
d. chmod函数:
i. 作用:改变文件的权限
ii. 原型:int chmod(const char *filename,int pmode)
1) 参数:
a) filenam -->文件名
b) pmode -->权限(必须是一个8进制数)(可用strtol函数传入一个数转换为八进制数)
iii. 返回值: -
0:改变成功
2) -1:失败
e. chown函数:
i. 作用:chown改变文件所有者,fchown文件操作,lchown不穿透
ii. 参数:(在/etc/passwd下找uid和pid)
1) path文件路径 2) owner文件所有者uid
3) group文件所属组gid
f. truncate函数:
lseek也能实现文件拓展,将指针偏移量偏移n来实现,但是要额外执行一步写操作,truncate则不需要
i. 作用:将参数path指定的文件大小改为参数length指定的大小。
如果原来的文件大小比参数length的大,则超过的部分会被删去
ii. int truncate(const char *path,off_t length);
1) path文件路径
2) length指定的文件长度 :如果文件本身长为100,length指定为20,
那么后面80长度的字符被截断,只保留前面20个字符。如果指定为300,则文件被扩展,
后面扩展的内容以空洞(@符号)的形式显示
g. 链接函数:
i. link函数:
1) 作用:创建一个硬链接
2) 原型:int link(const char oldpath,const char newpath);
ii. symlink函数:作用:创建一个软链接(符号链接)
iii. readlink函数:作用:读软连接对应的文件名,不是读内容。
readlink(const charpath,charbuf,size_t bufsize)只能读软链接,
buf 读出来的是软链接所指文件的绝对路径。
iv. unlink函数:int unlink(const char *pathname) -
作用:删除一个文件的目录项并减少它的链接数,若成功返回0,失败返回-1,
错误信息存与errno。
2) 如果想要通过调用这个函数来成功删除文件,
你必须拥有这个文件的所属目录的写(w)和执行(x)权限
3) 使用:
a) 如果是符号链接,删除符号链接
b) 如果是硬链接,硬链接数减1,当减为0时,释放数据块和inode
c) !!!如果文件硬链接为0,但有进程已打开该文件,并持有文件描述符,
则等进程关闭该文件时,kernel才真正去删除该文件。
i) 利用该特性创建临时文件,先open或creat创建一个文件,马上unlink此文件
h. rename函数:
i. 作用:文件重命名
ii. 头文件:stdio.h
iii. 函数原型:int rename(const char *oldpath,const char *newpath)
oldpath旧名字,newpath新名字
i. 目录操作函数:
i. chdir函数:(与getcwd结合一起理解)
1) 修改当前进程的路径
2) 原型:int chdir(const char path);
ii. getcwd函数:
1) 获取当前进程工作目录
2) 原型:char getcwd(char *buf,size_t size);
iii. mkdir函数:
1) 作用:创建目录
2) !!!注意!!!:创建的目录需要有执行权限,否则无法进入目录
3) 原型:int mkdir(const char *pathname,mode_t mode);
iv. rmdir函数:
1) 作用:删除一个空目录
2) 函数原型:int rmdir(const char pathname);
v. opendir函数
1) 作用:打开一个目录
2) 原型:DIRopendir(const *name);
3) 返回值:
a) DIR结构指针,该结构是一个内部结构,保存所打开的目录信息,
作用类似于FILE结构(openfile -->read—>write—>close)
b) 函数出错返回NULL
vi. readdir函数!!!(指定一个while循环,遍历目录中的文件,将文件信息通过返回值反馈直至反馈
NULL值即遍历完成,当在遍历过程中又遇到目录,则进入目录继续遍历,对树状结构遍历最好
的方法即递归)
1) 作用:读目录
2) 函数原型:struct dirent *readdir(DIR *dirp);
3) 返回值(返回一条记录项)
//ino 此目录进入点的inode
//off 目录文件开头至此目录进入点的位移,偏移量
//d_reclen d_name(文件名)的长度,不包含NULL字符
//d_type d_name所指文件类型
//d_name[NAME_MAX+1] 文件名
4) d_type:
a) DT_BLK 块设备
b) DT_CHR 字符设备
c) DT_DIR 目录
d) DT_LNK 软链接
e) DT_FIFO 管道
f) DT_REG 普通文件
g) DT_SOCK 套接字
h) DT_UNKOWN 未知
5) -D_BSD_SOURCE 编译时添加宏定义
v.closedir:关闭目录
j. dup、,dup2函数
i. 作用:复制现有的文件描述符(重定向文件描述符)
ii. int dup(int oldfd); 返回值是文件描述符中没有被占用的最小的文件描述符。
由前面的文件描述符的知识可知,0、1、2是已经被系统占用(常开)的文件描述符,那么文件会
从第三个文件描述符开始使用,例如该文件使用的是第三个文件描述符,那么使用了dup函数后,
将第四个文件描述符(未占用且最小)复制给他)
iii. int dup2(int oldfd,int newfd);将old复制给new,如果new是一个被打开的文件描述符,在拷贝之前先
关掉new,如果old和new是同一个文件描述符,则不会关闭而是返回同一个文件描述符old。
iv. dup示例:
v. dup2示例:
k. fcntl函数:
i. 作用:改变已经打开的文件的属性,例如打开文件的时候(文件为只读),修改文件的时候,如果要
添加追加O_APPEND,可使用fcntl在文件打开时修改。
ii. 原型:
1) int fcntl(int fd,int cmd);
2) int fcntl(int fd,int cmd,long arg);!!!
3) int fcntl(int fd,int cmd,struct flock *lock);
iii. 功能:
1) 复制一个现有的描述符 – cmd F_DUPFD
2) 获得/设置文件描述符标记 – cmd F_GETFD/F_SETFD
3) 获得/设置文件标志标记(打开文件时追加)!!!
a) cmd F_GETFL
i) 只读打开 O_RDONLY
ii) 只写打开 O_WRONLY
iii) 读写打开 O_RDWR
iv) 执行打开 O_RXEC
v) 搜索打开目录 O_SEARCH
vi) 追加写 O_APPEND
vii) 非阻塞模式 O_NONBLOCK
b) cmd F_SETFL 可更改的几个标识
i) O_APPEND
ii) O_NONBLOCK
4) 获得/设置异步I/O所有权 --cmd F_GETOWN/F_SETOWN
5) 获取/设置记录锁 – cmd
a) F_GETLK
b) F_SETLK
c) F_SETLKW
iv. 示例
系统编程-01
- 进程相关概念
1.1 程序和进程
程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁…)
进程,是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行。(程序运行起来,产生一个进程)
程序→剧本(纸)进程→戏(舞台、演员、灯光、道具…)
同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)
如:同时开两个终端。各自都有一个bash但彼此ID不同。
并发
并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但,任一个时刻点上仍只有一个进程在运行。
例如,当下,我们使用计算机时可以边听音乐边聊天边上网。若笼统的将他们均看做一个进程的话,为什么可以同时运行呢,因为并发。
分时复用cpu
单道程序设计
所有进程一个一个排对执行。若A阻塞,B只能等待,即使CPU处于空闲状态。而在人机交互时阻塞的出现时必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。
多道程序设计
在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。
时钟中断即为多道程序设计模型的理论基础。并发时,任意进程在执行期间都不希望放弃cpu。因此系统需要一种强制让进程让出cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。操作系统中的中断处理函数,来负责调度程序执行。
在多道程序设计模型中,多个进程轮流使用CPU (分时复用CPU资源)。而当下常见CPU为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。
1s = 1000ms, 1ms = 1000us, 1us = 1000ns1000000000
实质上,并发是宏观并行,微观串行!-----推动了计算机蓬勃发展,将人类引入了多媒体时代。
CPU和MMU
CPU 中央处理器
ALU只会加法和左移运算
中央处理器(CPU)
MMU 内存管理单元,只是CPU中一个重要部件,可以划分为与ALU等同等位置上
作用:
虚拟内存与物理内存的映射,在从内存取址读数据以及往内存写数据时,需要搭配MMU才能真正的完成读写
设置修改对应内存访问级别,用户空间是3级,内核空间访问级别是0级,虽然unix一共有4级,但是linux只用0和3
内存管理
段页式内存管理:
将程序的逻辑地址空间划分为固定大小的页(page),物理内存与虚拟内存的页的大小是一样的,分页仅仅是由于系统管理的需要。通常大小为4Kb,MMU内存管理单元就可以将虚拟内存地址转换为物理地址
段是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了更好地满足用户的需要
一条指令或一个操作数可能会跨越两个页的分界处,而不会跨越两个段的分界处。
页式和段式管理区别页式和段式系统有许多相似之处。
比如,两者都采用离散分配方式,且都通过地址映射机构来实现地址变换。但概念上两者也有很多区别,主要表现在:
1)、需求:是信息的物理单位,分页是为了实现离散分配方式,以减少内存的碎片,提高内存的利用率。或者说,分页仅仅是由于系统管理的需要,而不是用户的需要。段是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了更好地满足用户的需要。
一条指令或一个操作数可能会跨越两个页的分界处,而不会跨越两个段的分界处。
2)、大小:页大小固定且由系统决定,把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的。段的长度不固定,且决定于用户所编写的程序,通常由编译系统在对源程序进行编译时根据信息的性质来划分。
3)、逻辑地址表示:页式系统地址空间是一维的,即单一的线性地址空间,程序员只需利用一个标识符,即可表示一个地址。分段的作业地址空间是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。
4)、比页大,因而段表比页表短,可以缩短查找时间,提高访问速度。
内存管理单元MMU
进程控制块PCB
我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
已知结构体名字,怎么查找头文件?
find:find不能,因为find是查找文件名字,且路径名需要再 find后跟着
grep:按照字符串查,用 -r [grep -r “task_struct {” /usr/src/]
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h文件中可以查看struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:
- 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
- 进程的状态,有就绪、运行、挂起、停止等状态。
- 进程切换时需要保存和恢复的一些CPU寄存器。
- 描述虚拟地址空间的信息。
- 描述控制终端的信息。
- 当前工作目录(Current Working Directory)。
- umask掩码。
- 文件描述符表,包含很多指向file结构体的指针。
- 和信号相关的信息。
- 用户id和组id。
- 会话(Session)和进程组。
- 进程可以使用的资源上限(Resource Limit)。
进程状态
进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。
R:TASK_RUNNING,可执行(就绪)状态。运行(正在运行或在运行队列中等待)
S:TASK_INTERRUPTIBLE,可中断的睡眠状态。中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号)
D:TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。 不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生)
T:TASK_STOPPED or TASK_TRACED,暂停状态或跟踪状态。
Z:TASK_DEAD – EXIT_ZOMBIE,退出状态,进程成为僵尸进程。
X:TASK_DEAD – EXIT_DEAD,退出状态,终止状态,进程即将被销毁。
1.2 环境变量:
环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:
①字符串(本质) ②有统一的格式:名=值[:值] ③值用来描述进程环境信息。
存储形式:与命令行参数类似。char *[]数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。
使用形式:与命令行参数类似。
加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。
引入环境变量表:须声明环境变量。extern char ** environ;
练习:打印当前进程的所有环境变量。【environ.c】
常见环境变量
按照惯例,环境变量字符串都是name=value这样的形式,大多数name由大写字母加下划线组成,一般把name的部分叫做环境变量,value的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下:
PATH
可执行文件的搜索路径。ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH环境变量的值里面包含了ls命令所在的目录/bin,却不包含a.out所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开。在Shell中用echo命令可以查看这个环境变量的值:
$ echo $PATH
SHELL
当前Shell,它的值通常是/bin/bash。
TERM
当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。
LANG
语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
HOME
当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
getenv函数
获取环境变量值
char *getenv(const char *name); 成功:返回环境变量的值;失败:NULL (name不存在)
练习:编程实现getenv函数。【getenv.c】
setenv函数
设置环境变量的值
int setenv(const char *name, const char *value, int overwrite);成功:0;失败:-1
参数overwrite取值:1:覆盖原环境变量
0:不覆盖。(该参数常用于设置新环境变量,如:ABC = haha-day-night)
unsetenv函数
删除环境变量name的定义
int unsetenv(const char *name); 成功:0;失败:-1
注意事项:name不存在仍返回0(成功),当name命名为"ABC="时则会出错。
1.3 进程控制
fork函数
创建一个子进程。
pid_t fork(void);失败返回-1;成功返回:①父进程返回子进程的ID(非负)②子进程返回 0
pid_t类型表示进程ID,但为了表示-1,它是有符号整型。(0不是有效进程ID,init最小,为1)
注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需【各自】返回一个。
循环创建n个子进程
一次fork函数调用可以创建一个子进程。那么创建N个子进程应该怎样实现呢?
简单想,for(i = 0; i < n; i++) { fork() } 即可。但这样创建的是N个子进程吗?
循环创建N个子进程
从上图我们可以很清晰的看到,当n为3时候,循环创建了(2^n)-1个子进程,而不是N的子进程。需要在循环的过程,保证子进程不再执行fork ,因此当(fork() == 0)时,子进程应该立即break;才正确。
练习:通过命令行参数指定创建进程的个数,每个进程休眠1S打印自己是第几个被创建的进程。如:第1个子进程休眠0秒打印:“我是第1个子进程”;第2个进程休眠1秒打印:“我是第2个子进程”;第3个进程休眠2秒打印:“我是第3个子进程”。【fork1.c】
通过该练习掌握框架:循环创建n个子进程,使用循环因子i对创建的子进程加以区分。
getpid函数
获取当前进程ID
pid_t getpid(void);
getppid函数
获取当前进程的父进程ID
pid_t getppid(void);
区分一个函数是“系统函数”还是“库函数”依据:
②是否访问内核数据结构
②是否访问外部硬件资源二者有任一→系统函数;二者均无→库函数
getuid函数
获取当前进程实际用户ID
uid_t getuid(void);
获取当前进程有效用户ID
uid_t geteuid(void);
getgid函数
获取当前进程使用用户组ID
gid_t getgid(void);
获取当前进程有效用户组ID
gid_t getegid(void);
进程共享
父子进程之间在fork后。有哪些相同,那些相异之处呢?
刚fork之后:
父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
父子不同处: 1.进程ID2.fork返回值3.父进程ID4.进程运行时间5.闹钟(定时器)6.未决信号集
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
练习:编写程序测试,父子进程是否共享全局变。【fork_shared.c】
重点注意!躲避父子进程共享全局变量的知识误区!
【重点】:父子进程共享:1. 文件描述符(打开文件的结构体)2. mmap建立的映射区(进程间通信详解)
特别的,fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
gdb调试
使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。
set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程。
set follow-fork-mode parent 设置跟踪父进程。
注意,一定要在fork函数调用之前设置才有效。【follow_fork.c】
1.4 exec函数族
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程(就是main函数开始的部分)开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text(代码区)、.data(数据区)替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
其实有六种以exec开头的函数,统称exec函数:
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg, …, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
l 表示 list,
p 表示 path,
e 表示环境变量
v 参数列表 argv
execlp函数
加载一个进程,借助PATH环境变量
int execlp(const char *file, const char *arg, …);成功:无返回;失败:-1
参数1:要加载的程序的名字。
该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
参数2:argv[0]
参数3:argv[1]
参数4:argv[2]
…
该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
execl函数
加载一个进程,通过路径+程序名来加载。
int execl(const char *path, const char *arg, …);成功:无返回;失败:-1
对比execlp,如加载"ls"命令带有-l,-F参数
execlp(“ls”, “ls”, “-l”, “-F”, NULL);使用程序名在PATH中搜索。
execl("/bin/ls", “ls”, “-l”, “-F”, NULL);使用参数1给出的绝对路径搜索。
execvp函数
加载一个进程,使用自定义环境变量env
int execvp(const char *file, const char *argv[]);
变参形式:①… ② argv[](main函数也是变参函数,形式上等同于 int main(int argc, char *argv0, …))
变参终止条件:① NULL结尾②固参指定
execvp与execlp参数形式不同,原理一致。
练习:将当前系统中的进程信息,打印到文件中。【exec_ps.c】
exec函数族一般规律
exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。
l (list)命令行参数列表
p (path)搜素file时使用path变量
v (vector)使用命令行参数数组
e (environment)使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量
事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
exec函数族
1.5 回收子进程
孤儿进程
孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
【orphan.c】
僵尸进程
僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
【zoom .c】
特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。思考!用什么办法可清除掉僵尸进程呢?
wait函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
①阻塞等待子进程退出
②回收子进程残留资源
③获取子进程结束状态(退出原因)。
pid_t wait(int *status); 成功:清理掉的子进程ID;失败:-1 (没有子进程)
当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
1.WIFEXITED(status) 为非0→进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏→获取进程退出状态 (exit的参数)
2. WIFSIGNALED(status) 为非0 →进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏→取得使进程终止的那个信号的编号。
*3. WIFSTOPPED(status) 为非0 →进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏→取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真→进程暂停后已经继续运行
【wait1.c、wait2.c】
waitpid函数
作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options);成功:返回清理掉的子进程ID;失败:-1(无子进程)
特殊参数和返回情况:
参数pid:
0 回收指定ID的子进程
-1 回收任意子进程(相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
< -1 回收指定进程组内的任意子进程
返回0:参3为WNOHANG,且子进程正在运行。
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。【waitpid.c】
作业:父进程fork 3 个子进程,三个子进程一个调用ps命令,一个调用自定义程序1(正常),一个调用自定义程序2(会出段错误)。父进程使用waitpid对其子进程进行回收。
-
IPC方法
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:
①管道 (使用最简单)
pipe(fd[2]) fd[0] – read; fd[1] — write
fork()
管道一般读写行为
读: 数据 字节数
无数据
写端全部关闭 read – 0 (读到结尾)
仍有写端打开 阻塞等待
写: 读端全闭关 程序异常终止(SIGPIPE)
读端仍有打开
管道写满: 阻塞等待
未满: 返回实际写入的字节数.
fifo: (有名管道) 应用于非血缘关系进程间(不能反复读取)
用于非血缘关系进程间通信
命令: mkfifo
②信号 (开销最小)
③共享映射区 (无血缘关系)
mmap
④本地套接字 (最稳定)
2.1 管道
管道的概念:
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质: -
其本质是一个伪文件(实为内核缓冲区)
-
由两个文件描述符引用,一个表示读端,一个表示写端。
-
规定数据从管道的写端流入管道,从读端流出。
管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性:
①数据自己读不能自己写。
②数据一旦被读走,便不在管道中存在,不可反复读取。
③由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
④只能在有公共祖先的进程间使用管道。
常见的通信方式有,单工通信、半双工通信、全双工通信。
文件类型共有7种,前三种是占用内存的,后四种不占用内存,可称之为伪文件
- 普通文件
d 目录
l 符号链接,也就是软连接
伪文件
s 套接字
b 块设备
c 字符设备
p 管道
pipe函数
创建管道
int pipe(int pipefd[2]);
成功:0;失败:-1,设置errno
函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。
管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:
- 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
- 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
- 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
练习:父子进程使用管道通信,父写入字符串,子进程读出并,打印到屏幕。【pipe.c】
思考:为甚么,程序中没有使用sleep函数,但依然能保证子进程运行时一定会读到数据呢?
管道的读写行为
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
- 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
- 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
- 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。
- 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
总结:
①读管道:
- 管道中有数据,read返回实际读到的字节数。
- 管道中无数据:
(1) 管道写端被全部关闭,read返回0 (好像读到文件结尾)
(2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
②写管道: - 管道读端全部被关闭,进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
- 管道读端没有全部关闭:
(1) 管道已满,write阻塞。
(2) 管道未满,write将数据写入,并返回实际写入的字节数。
练习1:使用管道实现父子进程间通信,完成:ls | wc –l。假定父进程实现ls,子进程实现wc。
ls命令正常会将结果集写出到stdout,但现在会写入管道的写端;wc –l 正常应该从stdin读取数据,但此时会从管道的读端读。【pipe1.c】
程序执行,发现程序执行结束,shell还在阻塞等待用户输入。这是因为,shell → fork → ./pipe1,程序pipe1的子进程将stdin重定向给管道,父进程执行的ls会将结果集通过管道写给子进程。若父进程在子进程打印wc的结果到屏幕之前被shell调用wait回收,shell就会先输出$提示符。
练习2:使用管道实现兄弟进程间通信。兄:ls 弟: wc -l 父:等待回收子进程。
要求,使用“循环创建N个子进程”模型创建兄弟进程,使用循环因子i标示。注意管道读写行为。【pipe2.c】
测试:是否允许,一个pipe有一个写端,多个读端呢?是否允许有一个读端多个写端呢?
【pipe3.c】
课后作业: 统计当前系统中进程ID大于10000的进程个数。
管道缓冲区大小
可以使用ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:
pipe size(512 bytes, -p) 8
也可以使用fpathconf函数,借助参数选项来查看。使用该宏应引入头文件<unistd.h>
long fpathconf(int fd, int name);成功:返回管道的大小失败:-1,设置errno
管道的优劣
优点:简单,相比信号,套接字实现进程间通信,简单很多。
缺点:1. 只能单向通信,双向通信需建立两个管道。
2. 只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决。
2.2 FIFO
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。
FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。
创建方式:
- 命令:mkfifo 管道名
- 库函数:int mkfifo(const char *pathname, mode_t mode);成功:0;失败:-1
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。
【fifo_w.c/fifo_r.c】
2.3 共享存储映射
文件进程间通信
使用文件也可以完成IPC,理论依据是,fork后,父子进程共享文件描述符。也就共享打开的文件。
在讲共享内存之前,可以借助 open,read,write来实现对文件共享,完成进程间的通信,有了共享内存,就可以将需要共享的信息存放在磁盘上,就可以实现借助指针来访问磁盘文件,就可以除了用 read,write之外,还可以用指针进行访问
练习:编程测试,父子进程共享打开的文件。借助文件进行进程间通信。
【fork_shared_fd.c】
思考,无血缘关系的进程可以打开同一个文件进行通信吗?为什么?
答:父子进程之间是可以的。可以借助
存储映射I/O
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
mmap函数
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
返回:成功:返回创建的映射区首地址;失败:MAP_FAILED宏
参数:
addr: 建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL
length: 欲创建映射区的大小
prot:映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags:标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
MAP_SHARED:会将映射区所做的操作反映到物理设备(磁盘)上。
MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
fd:用来建立映射区的文件描述符
offset:映射文件的偏移(4k的整数倍)
munmap函数
同malloc函数申请内存空间类似的,mmap建立的映射区在使用结束后也应调用类似free的函数来释放。
int munmap(void *addr, size_t length);成功:0;失败:-1
借鉴malloc和free函数原型,尝试装自定义函数smalloc,sfree来完成映射区的建立和释放。思考函数接口该如何设计?
【smalloc.c】
mmap注意事项
【mmap.c】
思考:
- 可以open的时候O_CREAT一个新文件来创建映射区吗?
可以 - 如果open时O_RDONLY, mmap时PROT参数指定PROT_READ | PROT_WRITE会怎样?
权限不足
映射区的权限应 <=文件打开的权限
创建映射区的过程中,隐含着一次对映射文件的读操作 - 文件描述符先关闭,对mmap映射有没有影响?
没有
文件描述符是用来操作文件的句柄,映射区的释放与文件关闭无关
映射区一旦创建成功,(创建的过程是需要文件描述符的)文件描述符就没有意义了,剩下的工作是通过指针进行操作的 - 如果文件偏移量为1000会怎样?
不行
必须是4K的整数倍,4K是一页的大小,映射区是内核中的MMU帮助来映射的,MMU映射的单位就是4K。 - 对mem越界操作会怎样?(其实就是 mem++)
不可以 - 如果mem++,munmap可否成功?
不能。 - mmap什么情况下会调用失败?
- 如果不检测mmap的返回值,会怎样?
会出错的
总结:使用mmap时务必注意以下事项:
-
创建映射区的过程中,隐含着一次对映射文件的读操作。
-
当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
3.映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。
4.特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!!mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
5. munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++或者 – 操作,但是可以通过临时变量操作。
6.如果文件偏移量必须为4K的整数倍
7.mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
mmap父子进程通信
父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
MAP_PRIVATE: (私有映射)父子进程各自独占映射区;
MAP_SHARED:(共享映射)父子进程共享映射区;
练习:父进程创建映射区,然后fork子进程,子进程修改映射区内容,而后,父进程读取映射区内容,查验是否共享。【fork_mmap.c】
结论:父子进程共享:1. 打开的文件2. mmap建立的映射区(但必须要使用MAP_SHARED)
匿名映射
通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。可以直接使用匿名映射来代替。其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定。
使用MAP_ANONYMOUS (或MAP_ANON),如:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
“4"随意举例,该位置表大小,可依实际需要填写。
【fork_map_anon_linux.c】
需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
① fd = open(”/dev/zero", O_RDWR);
② p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
【fork_map_anon.c】
mmap无血缘关系进程间通信
实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。
【mmp_w.c/mmp_r.c】
3. 信号
3.1 概念
信号在我们的生活中随处可见,如:古代战争中摔杯为号;现代战争中的信号弹;体育比赛中使用的信号枪…他们都有共性:1. 简单 2. 不能携带大量信息 3. 满足某个特设条件才发送。
信号是信息的载体,Linux/UNIX 环境下,古老、经典的通信方式,现下依然是主要的通信手段。
Unix早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley 和 AT&T都对信号模型做了更改,增加了可靠信号机制。但彼此不兼容。POSIX.1对可靠信号例程进行了标准化。
信号的机制
A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
与信号相关的事件和状态
产生信号:
- 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
- 系统调用产生,如:kill、raise、abort
- 软件条件产生,如:定时器alarm
- 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生,如:kill命令
递达:递送并且到达进程。
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
信号的处理方式: - 执行默认动作
- 忽略(丢弃)
- 捕捉(调用户处理函数)
Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
未决信号集: - 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
- 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
信号的编号
可以使用kill –l命令查看当前系统可使用的信号有哪些。
- SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
- SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
- SIGSEGV12) SIGUSR213) SIGPIPE14) SIGALRM15) SIGTERM
- SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
- SIGTTIN22) SIGTTOU23) SIGURG24) SIGXCPU25) SIGXFSZ
- SIGVTALRM27) SIGPROF28) SIGWINCH29) SIGIO30) SIGPWR
- SIGSYS34) SIGRTMIN35) SIGRTMIN+136) SIGRTMIN+237) SIGRTMIN+3
- SIGRTMIN+439) SIGRTMIN+540) SIGRTMIN+641) SIGRTMIN+742) SIGRTMIN+8
- SIGRTMIN+944) SIGRTMIN+1045) SIGRTMIN+1146) SIGRTMIN+1247) SIGRTMIN+13
- SIGRTMIN+1449) SIGRTMIN+1550) SIGRTMAX-1451) SIGRTMAX-1352) SIGRTMAX-12
- SIGRTMAX-1154) SIGRTMAX-1055) SIGRTMAX-956) SIGRTMAX-857) SIGRTMAX-7
- SIGRTMAX-659) SIGRTMAX-560) SIGRTMAX-461) SIGRTMAX-362) SIGRTMAX-2
- SIGRTMAX-164) SIGRTMAX
不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前32个名字各不相同。
信号4要素
与变量三要素类似的,每个信号也有其必备4要素,分别是:
- 编号 2. 名称 3. 事件 4. 默认处理动作
可通过man 7 signal查看帮助文档获取。也可查看/usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h
SignalValueActionComment
────────────────────────────────────────────
SIGHUP1TermHangup detected on controlling terminal or death of controlling process
SIGINT2TermInterrupt from keyboard
SIGQUIT3CoreQuit from keyboard
SIGILL4CoreIllegal Instruction
SIGFPE8CoreFloating point exception
SIGKILL9TermKill signal
SIGSEGV11CoreInvalid memory reference
SIGPIPE13TermBroken pipe: write to pipe with no readers
SIGALRM14TermTimer signal from alarm(2)
SIGTERM15TermTermination signal
SIGUSR130,10,16TermUser-defined signal 1
SIGUSR231,12,17TermUser-defined signal 2
SIGCHLD20,17,18Ign Child stopped or terminated
SIGCONT19,18,25ContContinue if stopped
SIGSTOP17,19,23StopStop process
SIGTSTP18,20,24StopStop typed at terminal
SIGTTIN21,21,26StopTerminal input for background process
SIGTTOU22,22,27StopTerminal output for background process
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
在标准信号中,有一些信号是有三个“Value”,第一个值通常对alpha和sparc架构(CPU架构)有效,中间值针对x86、arm和其他架构,最后一个应用于mips架构。一个‘-’表示在对应架构上尚未定义该信号。
不同的操作系统定义了不同的系统信号。因此有些信号出现在Unix系统内,也出现在Linux中,而有的信号出现在FreeBSD或Mac OS中却没有出现在Linux下。这里我们只研究Linux系统中的信号。
默认动作:
Term:终止进程
Ign:忽略信号 (默认即时对该种信号忽略操作)
Core:终止进程,生成Core文件。(core文件用来查验进程死亡原因,用于gdb调试)
Stop:停止(暂停)进程
Cont:继续运行进程
注意从man 7 signal帮助文档中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
这里特别强调了9) SIGKILL 和19) SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
另外需清楚,只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!
Linux常规信号一览表
- SIGHUP: 当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
- SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动
作为终止进程。 - SIGQUIT:当用户按下<ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信
号。默认动作为终止进程。 - SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
- SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程并产生core文件。
- SIGABRT: 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
- SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
- SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
- SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
- SIGUSE1:用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
- SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
- SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
- SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
- SIGALRM: 定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程。
- SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
- SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
- SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
- SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
- SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
- SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
- SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
- SIGTTOU: 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
- SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
- SIGXCPU:进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程。默认动作为终止进程。
- SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
- SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
- SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
- SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
- SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
- SIGPWR:关机。默认动作为终止进程。
- SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
- SIGRTMIN ~(64) SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。
3.2 信号的产生
终端按键产生信号
Ctrl + c → 2) SIGINT(终止/中断) “INT” ----Interrupt
Ctrl + z → 20) SIGTSTP(暂停/停止) “T” ----Terminal 终端。
Ctrl + \ → 3) SIGQUIT(退出)
硬件异常产生信号
除0操作 → 8) SIGFPE (浮点数例外) “F” -----float 浮点数。
非法访问内存 → 11) SIGSEGV (段错误)
总线错误 → 7) SIGBUS
kill函数/命令产生信号
kill命令产生信号:kill -SIGKILL pid (编号为9)
kill函数:给指定进程发送指定信号(不一定杀死)
int kill(pid_t pid, int sig);成功:0;失败:-1 (ID非法,信号非法,普通用户杀init进程等权级问题),设置errno
sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
pid > 0:发送信号给指定的进程。
pid = 0:发送信号给与调用kill函数进程属于同一进程组的所有进程。
pid < 0:取|pid|发给对应进程组。
pid = -1:发送给进程有权限发送的系统中所有进程。
进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。
权限保护:super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。 kill -9 (root用户的pid)是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。普通用户基本规则是:发送者实际或有效用户ID == 接收者实际或有效用户ID
练习:循环创建5个子进程,任一子进程用kill函数终止其父进程。【kill.c】
raise和abort函数
raise 函数:给当前进程发送指定信号(自己给自己发)raise(signo) == kill(getpid(), signo);
int raise(int sig); 成功:0,失败非0值
abort 函数:给自己发送异常终止信号 6) SIGABRT 信号,终止并产生core文件
void abort(void); 该函数无返回
软件条件产生信号
alarm函数
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一个定时器。
unsigned int alarm(unsigned int seconds); 返回0或剩余的秒数,无失败。
常用:取消定时器alarm(0),返回旧闹钟余下秒数。
例:alarm(5) → 3sec → alarm(4) → 5sec → alarm(5) → alarm(0)
定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都计时。
练习:编写程序,测试你使用的计算机1秒钟能数多少个数。【alarm .c】
使用time命令查看程序执行的时间。程序运行的瓶颈在于IO,优化程序,首选优化IO。
实际执行时间 = 系统时间 + 用户时间 + 等待时间
setitimer函数
设置定时器(闹钟)。可代替alarm函数。精度微秒us,可以实现周期定时。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);成功:0;失败:-1,设置errno
参数:which:指定定时方式
①自然定时:ITIMER_REAL → 14)SIGLARM计算自然时间
②虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM只计算进程占用cpu的时间
③运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF计算占用cpu及执行系统调用的时间
练习: 使用setitimer函数实现alarm函数,重复计算机1秒数数程序。【setitimer.c】
拓展练习,结合man page编写程序,测试it_interval、it_value这两个参数的作用。【setitimer1.c】
提示:it_interval:用来设定两次定时任务之间间隔的时间。
it_value:定时的时长
两个参数都设置为0,即清0操作。
3.3 信号集操作函数
内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。已达到屏蔽指定信号的目的。
信号集设定
sigset_tset;// typedef unsigned long sigset_t;
int sigemptyset(sigset_t *set);将某个信号集清0成功:0;失败:-1
int sigfillset(sigset_t *set);将某个信号集置1成功:0;失败:-1
int sigaddset(sigset_t *set, int signum);将某个信号加入信号集成功:0;失败:-1
int sigdelset(sigset_t *set, int signum);将某个信号清出信号集成功:0;失败:-1
int sigismember(const sigset_t *set, int signum);判断某个信号是否在信号集中返回值:在集合:1;不在:0;出错:-1
sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
对比认知select 函数。
sigprocmask函数
用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB中)
严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);成功:0;失败:-1,设置errno
参数:
set:传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。
oldset:传出参数,保存旧的信号屏蔽集。
how参数取值:假设当前的信号屏蔽字为mask
1.SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
2.SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
3.SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于 mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
sigpending函数
读取当前进程的未决信号集
int sigpending(sigset_t *set);set传出参数。返回值:成功:0;失败:-1,设置errno
练习:编写程序。把所有常规信号的未决状态打印至屏幕。【sigpending.c】
3.4 信号捕捉
signal函数
注册一个信号捕捉函数:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
void (*signal(int signum, void (*sighandler_t)(int))) (int);
能看出这个函数代表什么意思吗?注意多在复杂结构中使用typedef。
sigaction函数
修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);成功:0;失败:-1,设置errno
参数:
act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。【signal.c】
struct sigaction结构体
struct sigaction {
void(*sa_handler)(int);
void(*sa_sigaction)(int, siginfo_t *, void *);
sigset_tsa_mask;
intsa_flags;
void(*sa_restorer)(void);
};
sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
重点掌握:
① sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略或 SIG_DFL表执行默认动作
② sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
③ sa_flags:通常设置为0,表使用默认属性。
信号捕捉特性
1.进程正常运行时,默认PCB中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。
2.XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。
3.阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)
练习1:为某个信号设置捕捉函数【sigaction1.c】练习2:验证在信号处理函数执行期间,该信号多次递送,那么只在处理函数之行结束后,处理一次。【sigaction2.c】练习3:验证sa_mask在捕捉函数执行期间的屏蔽作用。【sigaction3.c】
内核实现信号捕捉过程:
3.5 竞态条件(时序竞态):
pause函数
调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu) 直到有信号递达将其唤醒。
int pause(void);返回值:-1 并设置errno为EINTR
返回值:
①如果信号的默认处理动作是终止进程,则进程终止,pause函数么有机会返回。
②如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
③如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause返回-1】
errno设置为EINTR,表示“被信号中断”。想想我们还有哪个函数只有出错返回值。
④ pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。
练习:使用pause和alarm来实现sleep函数。【mysleep.c】
注意,unslept = alarm(0)的用法。
例如:睡觉,alarm(10)闹铃。
正常: 10后闹铃将我唤醒,这时额外设置alarm(0)取消闹铃,不会出错。
异常: 5分钟,被其他事物吵醒,alarm(0)取消闹铃防止打扰。
时序竞态
前导例
设想如下场景:
欲睡觉,定闹钟10分钟,希望10分钟后闹铃将自己唤醒。
正常:定时,睡觉,10分钟后被闹钟唤醒。
异常:闹钟定好后,被唤走,外出劳动,20分钟后劳动结束。回来继续睡觉计划,但劳动期间闹钟已经响过,不会再将我唤醒。
时序问题分析
回顾,借助pause和alarm实现的mysleep函数。设想如下时序:
- 注册SIGALRM信号处理函数(sigaction…)
- 调用alarm(1) 函数设定闹钟1秒。
- 函数调用刚结束,开始倒计时1秒。当前进程失去cpu,内核调度优先级高的进程(有多个)取代当前进程。当前进程无法获得cpu,进入就绪态等待cpu。
- 1秒后,闹钟超时,内核向当前进程发送SIGALRM信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)
- 优先级高的进程执行完,当前进程获得cpu资源,内核调度回当前进程执行。SIGALRM信号递达,信号设置捕捉,执行处理函数sig_alarm。
- 信号处理函数执行结束,返回当前进程主控流程,pause()被调用挂起等待。(欲等待alarm函数发送的SIGALRM信号将自己唤醒)
- SIGALRM信号已经处理完毕,pause不会等到。
解决时序问题
可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这个两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”。sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause。
int sigsuspend(const sigset_t *mask);挂起等待信号。
sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。
可将某个信号(如SIGALRM)从临时信号屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend函数返回后仍然屏蔽该信号。
改进版mysleep【sigsuspend.c】
总结
竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。
不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。
这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。
全局变量异步I/O
分析如下父子进程交替数数程序。当捕捉函数里面的sleep取消,程序即会出现问题。请分析原因。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int n = 0, flag = 0;
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(int num)
{
printf(“I am child%d\t%d\n”, getpid(), n);
n += 2;
flag = 1;
sleep(1);
}
void do_sig_parent(int num)
{
printf(“I am parent %d\t%d\n”, getpid(), n);
n += 2;
flag = 1;
sleep(1);
}
int main(void)
{
pid_t pid;
struct sigaction act;
if ((pid = fork()) < 0)
sys_err(“fork”);
else if (pid > 0) {
n = 1;
sleep(1);
act.sa_handler = do_sig_parent;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR2, &act, NULL);//注册自己的信号捕捉函数父使用SIGUSR2信号
do_sig_parent(0);
while (1) {
/* wait for signal */;
if (flag == 1) {//父进程数数完成
kill(pid, SIGUSR1);
flag = 0;//标志已经给子进程发送完信号
}
}
} else if (pid == 0) {
n = 2;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, NULL);
while (1) {
/* waiting for a signal */;
if (flag == 1) {
kill(getppid(), SIGUSR2);
flag = 0;
}
}
}
return 0;
}【sync_process.c】
示例中,通过flag变量标记程序实行进度。flag置1表示数数完成。flag置0表示给对方发送信号完成。
问题出现的位置,在父子进程kill函数之后需要紧接着调用 flag,将其置0,标记信号已经发送。但,在这期间很有可能被kernel调度,失去执行权利,而对方获取了执行时间,通过发送信号回调捕捉函数,从而修改了全局的flag。
如何解决该问题呢?可以使用后续课程讲到的“锁”机制。当操作全局变量的时候,通过加锁、解锁来解决该问题。
现阶段,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步IO可能造成的问题。
可/不可重入函数
一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入”。根据函数实现的方法可分为“可重入函数”和“不可重入函数”两种。看如下时序。
显然,insert函数是不可重入函数,重入调用,会导致意外结果呈现。究其原因,是该函数内部实现使用了全局变量。
注意事项
1.定义可重入函数,函数内不能含有全局变量及static变量,不能使用malloc、free
2.信号捕捉函数应设计为可重入函数
3.信号处理程序可以调用的可重入函数可参阅man 7 signal
4.没有包含在上述列表中的函数大多是不可重入的,其原因为:
a)使用静态数据结构
b)调用了malloc或free
c)是标准I/O函数
3.6 SIGCHLD信号
SIGCHLD的产生条件
子进程终止时
子进程接收到SIGSTOP信号停止时
子进程处在停止态,接受到SIGCONT后唤醒时
借助SIGCHLD信号回收子进程
子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(int signo)
{
int status;pid_t pid;
while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
if (WIFEXITED(status))
printf(“child %d exit %d\n”, pid, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf(“child %d cancel signal %d\n”, pid, WTERMSIG(status));
}
}
int main(void)
{
pid_t pid;int i;
for (i = 0; i < 10; i++) {
if ((pid = fork()) == 0)
break;
else if (pid < 0)
sys_err(“fork”);
}
if (pid == 0) {
int n = 1;
while (n–) {
printf(“child ID %d\n”, getpid());
sleep(1);
}
return i+1;
} else if (pid > 0) {
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
while (1) {
printf(“Parent ID %d\n”, getpid());
sleep(1);
}
}
return 0;
}
分析该例子。结合 17)SIGCHLD 信号默认动作,掌握父使用捕捉函数回收子进程的方式。【sigchild.c】
如果每创建一个子进程后不使用sleep可以吗?可不可以将程序中,捕捉函数内部的while替换为if?为什么?
if ((pid = waitpid(0, &status, WNOHANG)) > 0) { … }
思考:信号不支持排队,当正在执行SIGCHLD捕捉函数时,再过来一个或多个SIGCHLD信号怎么办?
子进程结束status处理方式
pid_t waitpid(pid_t pid, int *status, int options)
options
WNOHANG
没有子进程结束,立即返回
WUNTRACED
如果子进程由于被停止产生的SIGCHLD,waitpid则立即返回
WCONTINUED
如果子进程由于被SIGCONT唤醒而产生的SIGCHLD,waitpid则立即返回
获取status
WIFEXITED(status)
子进程正常exit终止,返回真
WEXITSTATUS(status)返回子进程正常退出值
WIFSIGNALED(status)
子进程被信号终止,返回真
WTERMSIG(status)返回终止子进程的信号值
WIFSTOPPED(status)
子进程被停止,返回真
WSTOPSIG(status)返回停止子进程的信号值
WIFCONTINUED(status)
SIGCHLD信号注意问题
1.子进程继承了父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集spending。
2.注意注册信号捕捉函数的位置。
3.应该在fork之前,阻塞SIGCHLD信号。注册完捕捉函数后解除阻塞。
3.7 信号传参
发送信号传参
sigqueue函数对应kill函数,但可在向指定进程发送信号的同时携带参数
int sigqueue(pid_t pid, int sig, const union sigval value);成功:0;失败:-1,设置errno
union sigval {
intsival_int;
void *sival_ptr;
};
向指定进程发送指定信号的同时,携带数据。但,如传地址,需注意,不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一进程没有实际意义。
捕捉函数传参
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void(*sa_handler)(int);
void(*sa_sigaction)(int, siginfo_t *, void *);
sigset_tsa_mask;
intsa_flags;
void(*sa_restorer)(void);
};
当注册信号捕捉函数,希望获取更多信号相关信息,不应使用sa_handler而应该使用sa_sigaction。但此时的sa_flags必须指定为SA_SIGINFO。siginfo_t是一个成员十分丰富的结构体类型,可以携带各种与信号相关的数据。
3.8 中断系统调用
系统调用可分为两类:慢速系统调用和其他系统调用。
1.慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如,read、write、pause、wait…
2.其他系统调用:getpid、getppid、fork…
结合pause,回顾慢速系统调用:
慢速系统调用被中断的相关行为,实际上就是pause的行为:如,read
①想中断pause,信号不能被屏蔽。
②信号的处理方式必须是捕捉 (默认、忽略都不可以)
③中断后返回-1,设置errno为EINTR(表“被信号中断”)
可修改sa_flags参数来设置被信号中断后系统调用是否重启。SA_INTERRURT不重启。 SA_RESTART重启。
扩展了解:
sa_flags还有很多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号。
- 终端:
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),进程中,控制终端是保存在PCB中的信息,而fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。信号中还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表示SIGINT,Ctrl-\表示SIGQUIT。
Alt + Ctrl + F1、F2、F3、F4、F5、F6 字符终端
pts (pseudo terminal slave) 指伪终端。
Alt + F7 图形终端
SSH、Telnet… 网络终端
终端的启动流程:
文件与I/O中讲过,每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。
简单来说,一个Linux系统启动,大致经历如下的步骤:
init --> fork --> exec --> getty --> 用户输入帐号 --> login --> 输入密码 --> exec --> bash
硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程像一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理,比如在键盘上按下Ctrl-z,对应的字符并不会被用户程序的read读到,而是被线路规程截获,解释成SIGTSTP信号发给前台进程,通常会使该进程停止。线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。
终端设备模块
line disciline: 线路规程,用来过滤键盘输入的内容。
ttyname函数
由文件描述符查出对应的文件名
char *ttyname(int fd); 成功:终端名;失败:NULL,设置errno
下面我们借助ttyname函数,通过实验看一下各种不同的终端所对应的设备文件名。
#include <unistd.h>
#include <stdio.h>
int main(void)
{
printf(“fd 0: %s\n”, ttyname(0)); // ttyname 是终端设备
printf(“fd 1: %s\n”, ttyname(1));
printf(“fd 2: %s\n”, ttyname(2));
return 0;
}【ttyname.c】
网络终端:
虚拟终端或串口终端的数目是有限的,虚拟终端(字符控制终端)一般就是/dev/tty1∼/dev/tty6六个,串口终端的数目也不超过串口的数目。然而网络终端或图形终端窗口的数目却是不受限制的,这是通过伪终端(Pseudo TTY)实现的。一套伪终端由一个主设备(PTY Master)和一个从设备(PTY Slave)组成。主设备在概念上相当于键盘和显示器,只不过它不是真正的硬件而是一个内核模块,操作它的也不是用户而是另外一个进程。从设备和上面介绍的/dev/tty1这样的终端设备模块类似,只不过它的底层驱动程序不是访问硬件而是访问主设备。网络终端或图形终端窗口的Shell进程以及它启动的其它进程都会认为自己的控制终端是伪终端从设备,例如/dev/pts/0、/dev/pts/1等。下面以telnet为例说明网络登录和使用伪终端的过程。
网络终端
TCP/IP协议栈:在数据包上添加报头。
如果telnet客户端和服务器之间的网络延迟较大,我们会观察到按下一个键之后要过几秒钟才能回显到屏幕上。这说明我们每按一个键telnet客户端都会立刻把该字符发送给服务器,然后这个字符经过伪终端主设备和从设备之后被Shell进程读取,同时回显到伪终端从设备,回显的字符再经过伪终端主设备、telnetd服务器和网络发回给telnet客户端,显示给用户看。也许你会觉得吃惊,但真的是这样:每按一个键都要在网络上走个来回!
4.1 进程组
概念和特性
进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。
当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID其进程ID
可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死。 【kill_multprocess.c】
ps -ajx 可以查看进程组ID
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
一个进程可以为自己或子进程设置进程组ID
进程组操作函数
getpgrp函数
获取当前进程的进程组ID
pid_t getpgrp(void); 总是返回调用者的进程组ID
getpgid函数
获取指定进程的进程组ID
pid_t getpgid(pid_t pid);成功:0;失败:-1,设置errno
如果pid = 0,那么该函数作用和getpgrp一样。
练习:查看进程对应的进程组ID【getpgid.c】
setpgid函数
改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。
int setpgid(pid_t pid, pid_t pgid); 成功:0;失败:-1,设置errno
将参1对应的进程,加入参2对应的进程组中。
注意:
- 如改变子进程为新的组,应fork后,exec前。
- 权级问题。非root进程只能改变自己创建的子进程,或有权限操作的进程
练习:修改子进程的进程组ID【setpgid.c】
4.3 会话
创建会话
创建一个会话需要注意以下6点注意事项:
1.调用进程不能是进程组组长,该进程变成新会话首进程(session header)
2.该进程成为一个新进程组的组长进程。
3.需有root权限(ubuntu不需要) -
新会话丢弃原有的控制终端,该会话没有控制终端,只在当前操作系统的后台执行,没有前台
5.该调用进程是组长进程,则出错返回
6.建立新会话时,先调用fork, 父进程终止,子进程调用setsid
getsid函数
获取进程所属的会话ID
pid_t getsid(pid_t pid); 成功:返回调用进程的会话ID;失败:-1,设置errno
pid为0表示察看当前进程session ID
ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
setsid函数
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。
pid_t setsid(void); 成功:返回调用进程的会话ID;失败:-1,设置errno
调用了setsid函数的进程,既是新的会长,也是新的组长。
练习:fork一个子进程,并使其创建一个新会话。查看进程组ID、会话ID前后变化【session.c】
4.4 守护进程
Daemon(精灵)进程,是Linux中的后台服务(没有前台)进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等。
创建守护进程,最关键的一步是调用setsid函数创建一个新的Session会话,并成为Session Leader。
创建守护进程模型
-
创建子进程,父进程退出
所有工作在子进程中进行形式上脱离了控制终端
-
在子进程中创建新会话 setsid()函数 使子进程完全独立出来,脱离控制
-
改变当前目录为根目录 chdir()函数 防止占用可卸载的文件系统 也可以换成其它路径
-
重设文件权限掩码 umask()函数 防止继承的文件创建屏蔽字拒绝某些权限 增加守护进程灵活性
-
关闭文件描述符 继承的打开文件不会用到,浪费系统资源,无法卸载
-
开始执行守护进程核心工作
-
守护进程退出处理程序模型 【mydaemond.c】
基本概念:
4.5 Q&A
tty(终端设备的统称):
tty一词源于Teletypes,或者teletypewriters,原来指的是电传打字机,是通过串行线用打印机键盘通过阅读和发送信息的东西,后来这东西被键盘与显示器取代,所以现在叫终端比较合适。
终端是一种字符型设备,它有多种类型,通常使用tty来简称各种类型的终端设备。
pty(虚拟终端):
但是如果我们远程telnet到主机或使用xterm时不也需要一个终端交互么?是的,这就是虚拟终端pty(pseudo-tty)
pts/ptmx(pts/ptmx结合使用,进而实现pty):
pts(pseudo-terminal slave)是pty的实现方法,与ptmx(pseudo-terminal master)配合使用实现pty。
Linux终端:
在Linux系统的设备特殊文件目录/dev/下,终端特殊设备文件一般有以下几种:
1、串行端口终端(/dev/ttySn)
串 行端口终端(Serial Port Terminal)是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。有段时间这些串行端口设备通常被称为终端设备,因为 那时它的最大用途就是用来连接终端。这些串行端口所对应的设备名称是/dev/tts/0(或/dev/ttyS0), /dev/tts/1(或/dev/ttyS1)等,设备号分别是(4,0), (4,1)等,分别对应于DOS系统下的COM1、COM2等。若要向一个端口发送数据,可以在命令行上把标准输出重定向到这些特殊文件名上即可。例如, 在命令行提示符下键入:echo test > /dev/ttyS1会把单词”test”发送到连接在ttyS1(COM2)端口的设备上。可接串口来实验。
2、伪终端(/dev/pty/)
伪终端(Pseudo Terminal)是成对的逻辑终端设备(即master和slave设备, 对master的操作会反映到slave上)。
例 如/dev/ptyp3和/dev/ttyp3(或者在设备文件系统中分别是/dev/pty/m3和 /dev/pty/s3)。它们与实际物理设备并不直接相关。如果一个程序把ptyp3(master设备)看作是一个串行端口设备,则它对该端口的读/ 写操作会反映在该逻辑终端设备对应的另一个ttyp3(slave设备)上面。而ttyp3则是另一个程序用于读写操作的逻辑设备。
这 样,两个程序就可以通过这种逻辑设备进行互相交流,而其中一个使用ttyp3的程序则认为自己正在与一个串行端口进行通信。这很象是逻辑设备对之间的管道 操作。对于ttyp3(s3),任何设计成使用一个串行端口设备的程序都可以使用该逻辑设备。但对于使用ptyp3的程序,则需要专门设计来使用 ptyp3(m3)逻辑设备。
例如,如果某人在网上使用telnet程序连接到你的计算机上,则telnet程序就可能会开始连接到设备 ptyp2(m2)上(一个伪终端端口上)。此时一个getty程序就应该运行在对应的ttyp2(s2)端口上。当telnet从远端获取了一个字符 时,该字符就会通过m2、s2传递给 getty程序,而getty程序就会通过s2、m2和telnet程序往网络上返回”login:”字符串信息。这样,登录程序与telnet程序就通 过“伪终端”进行通信。通过使用适当的软件,就可以把两个甚至多个伪终端设备连接到同一个物理串行端口上。
在使用设备文件系统 (device filesystem)之前,为了得到大量的伪终端设备特殊文件,使用了比较复杂的文件名命名方式。因为只存在16个ttyp(ttyp0—ttypf) 的设备文件,为了得到更多的逻辑设备对,就使用了象q、r、s等字符来代替p。例如,ttys8和ptys8就是一个伪终端设备对。不过这种命名方式目前 仍然在RedHat等Linux系统中使用着。
但Linux系统上的Unix98并不使用上述方法,而使用了”pty master”方式,例如/dev/ptm3。它的对应端则会被自动地创建成/dev/pts/3。这样就可以在需要时提供一个pty伪终端。目录 /dev/pts是一个类型为devpts的文件系统,并且可以在被加载文件系统列表中看到。虽然“文件”/dev/pts/3看上去是设备文件系统中的 一项,但其实它完全是一种不同的文件系统。
即: TELNET —> TTYP3(S3: slave) —> PTYP3(M3: master) —> GETTY
实验:
1、在X下打开一个或N个终端窗口
2、#ls /dev/pt*
3、关闭这个X下的终端窗口,再次运行;比较两次输出信息就明白了。
在RHEL4环境下: 输出为/dev/ptmx /dev/pts/1存在一(master)对多(slave)的情况
3、控制终端(/dev/tty)
如 果当前进程有控制终端(Controlling Terminal)的话,那么/dev/tty就是当前进程的控制终端的设备特殊文件。可以使用命令”ps –ax”来查看进程与哪个控制终端相连。对于你登录的shell,/dev/tty就是你使用的终端,设备号是(5,0)。使用命令”tty”可以查看它 具体对应哪个实际终端设备。/dev/tty有些类似于到实际所使用终端设备的一个联接。
4、控制台终端(/dev/ttyn, /dev/console)
在Linux 系统中,计算机显示器通常被称为控制台终端 (Console)。它仿真了类型为Linux的一种终端(TERM=Linux),并且有一些设备特殊文件与之相关联:tty0、tty1、tty2 等。当你在控制台上登录时,使用的是tty1。使用Alt+[F1—F6]组合键时,我们就可以切换到tty2、tty3等上面去。tty1–tty6等 称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名,系统所产生的信息会发送到该终端上。因此不管当前正在使用哪个虚拟终端,系统信息都会发送到 控制台终端上。你可以登录到不同的虚拟终端上去,因而可以让系统同时有几个不同的会话期存在。只有系统或超级用户root可以向 /dev/tty0进行写操作 即下例:
1、# tty(查看当前TTY)
/dev/tty1
2、#echo “test tty0” > /dev/tty0
test tty0
5 虚拟终端(/dev/pts/n)
在Xwindows模式下的伪终端.
6 其它类型
Linux系统中还针对很多不同的字符设备存在有很多其它种类的终端设备特殊文件。例如针对ISDN设备的/dev/ttyIn终端设备等。这里不再赘述。
FAQ: 终端和控制台
RROM:[url]http://blog.footoo.org/?p=73[/url]
Posted on Tuesday, November 28th, 2006 by CLIFF
吴晋 (cliffwoo@gmail.com)
FoOTOo OpenSource Lab
由于在很多朋友对终端的概念一直不是很清楚,因此写了这个FAQ,希望能够帮助大家理解这些概念。不妥之处,还请大家来信指出。
Q:/dev/console 是什么?
A:/dev/console即控制台,是与操作系统交互的设备,系统将一些信息直接输出到控制台上。目前只有在单用户模式下,才允许用户登录控制台。
Q:/dev/tty是什么?
A:tty设备包括虚拟控制台,串口以及伪终端设备。
/dev/tty代表当前tty设备,在当前的终端中输入 echo “hello” > /dev/tty ,都会直接显示在当前的终端中。
Q:/dev/ttyS*是什么?
A:/dev/ttyS*是串行终端设备。
Q:/dev/pty*是什么?
A:/dev/pty*即伪终端,所谓伪终端是逻辑上的终端设备,多用于模拟终端程序。例如,我们在X Window下打开的终端,以及我们在Windows使用telnet 或ssh等方式登录Linux主机,此时均在使用pty设备(准确的说应该pty从设备)。
Q:/dev/tty0与/dev/tty1 …/dev/tty63是什么?它们之间有什么区别?
A:/dev/tty0代表当前虚拟控制台,而/dev/tty1等代表第一个虚拟控制台,例如当使用ALT+F2进行切换时,系统的虚拟控制台为/dev/tty2 ,当前的控制台则指向/dev/tty2
Q:如何确定当前所在的终端(或控制台)?
A:使用tty命令可以确定当前的终端或者控制台。
Q:/dev/console是到/dev/tty0的符号链接吗?
A: 目前的大多数文本中都称/dev/console是到/dev/tty0的链接(包括《Linux内核源代码情景分析》),但是这样说是不确切的。根据内 核文档,在2.1.71之前,/dev/console根据不同系统的设定可以链接到/dev/tty0或者其他tty*上,在2.1.71版本之后则完 全由内核控制。目前,只有在单用户模式下可以登录/dev/console(可以在单用户模式下输入tty命令进行确认)。
Q:/dev/tty0与/dev/fb*有什么区别?
A: 在Framebuffer设备没有启用的系统中,可以使用/dev/tty0访问显卡。
Q:关于终端和控制台的区别可以参考哪些文本
A: 可以参考内核文档中的 Documents/devices.txt 中关于”TERMINAL DEVICES” 的章节。另外,《Linux内核源代码情景分析》的8.7节 以及《Operating Systems : Design and Implementation》中的3.9节(第3版中为3.8节)都对终端设备的概念和历史做了很好的介绍。另外在《Modern Operating system》中也有对终端设备的介绍,由于与《Operating Systems : Design and Implementation》的作者相同,所以文本内容也大致相同。需要注意的一点是《Operating Systems : Design and Implementation》中将终端设备分为3类,而《Modern Operating system》将终端硬件设备分为2类,差别在于前者将 X Terminal作为一个类别。
资料出处:http://blog.chinaunix.net/uid-25256412-id-91256.html
-
线程
5.1 线程概念
什么是线程
LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下)
进程:独立地址空间,拥有PCB
线程:也有PCB,但没有独立的地址空间(共享)
区别:在于是否共享地址空间。 独居(进程);合租(线程)。Linux下: 线程:最小的执行单位,CPU划分时间片是以线程单位的
进程:最小分配资源单位,可看成是只有一个线程的进程。在分配资源时,是以进程为单位的
Linux内核线程实现原理
类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。 -
轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
-
从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
-
进程可以蜕变成线程
-
线程可看做寄存器和栈的集合
-
在linux下,线程最是小的执行单位;进程是最小的分配资源单位
察看LWP号:ps –lf pid 查看指定线程的lwp号。
三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元
参考:《Linux内核源代码情景分析》 ----毛德操
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。
如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
线程共享资源
1.文件描述符表
2.每种信号的处理方式
3.当前工作目录
4.用户ID和组ID
5.内存地址空间 (.text/.data/.bss/heap/共享库)
线程非共享资源
1.线程id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.errno变量
5.信号屏蔽字
6.调度优先级
线程优、缺点
优点:1. 提高程序并发性2. 开销小3. 数据通信、共享数据方便
缺点:1. 库函数,不稳定2. 调试、编写困难、gdb不支持3. 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
5.2 线程控制原语
pthread_self函数
获取线程ID。其作用对应进程中 getpid() 函数。
pthread_t pthread_self(void);
返回值:成功:0; 失败:无!
线程ID:pthread_t类型,
本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现
线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)
注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。
pthread_create函数
创建一个新线程。其作用,对应进程中fork() 函数。
int pthread_create ( pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void ),
void arg);
返回值:成功:0;失败:错误号-----Linux环境下,所有线程特点,失败均直接返回错误号。
参数:
pthread_t:当前Linux中可理解为:typedefunsigned long intpthread_t;
参数1:传出参数,保存系统为我们分配好的线程ID
参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
参数3:函数指针,返回值为void,函数参数为 void,指向线程主函数(线程体),该函数运行结束,则线程结束。
参数4:线程主函数执行期间所使用的参数。
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态,稍后详细介绍pthread_join。
pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid(2)可以获得当前进程的id,是一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self(3)可以获得当前线程的id。
attr参数表示线程属性,本节不深入讨论线程属性,所有代码例子都传NULL给attr参数,表示线程属性取缺省值,感兴趣的读者可以参考APUE。
【练习】:创建一个新线程,打印线程ID。注意:链接线程库 -lpthread 【pthrd_crt.c】
由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印。如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下一节我们会看到更好的办法。
【练习】:循环创建多个线程,每个线程打印自己是第几个被创建的线程。(类似于进程循环创建子进程)【more_pthrd.c】
拓展思考:将pthread_create函数参4修改为(void )&i, 将线程主函数内改为 i=((int *)arg) 是否可以?
线程与共享
线程间共享全局变量!
【牢记】:线程默认共享数据段、代码段等地址空间,常用的是全局变量。
而进程不共享全局变量,只能借助mmap。
【练习】:设计程序,验证线程之间共享全局数据。【glb_var_pthrd.c】
pthread_exit函数
将单个线程退出
void pthread_exit(void *retval); 参数:retval表示线程退出状态,通常传NULL
思考:使用exit将指定线程退出,可以吗? 【pthrd_exit.c】
结论:线程中,禁止使用exit函数,会导致进程内所有线程全部退出。
在不添加sleep控制输出顺序的情况下。pthread_create在循环中,几乎瞬间创建5个线程,但只有第1个线程有机会输出(或者第2个也有,也可能没有,取决于内核调度)如果第3个线程执行了exit,将整个进程退出了,所以全部线程退出了。
所以,多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit导致进程退出,其他线程未工作结束,主控线程退出时不能return或exit。
另注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
【练习】:编写多线程程序,总结exit、return、pthread_exit各自退出效果。
return:返回到调用者那里去。
pthread_exit():将调用该函数的线程
exit: 将进程退出。
pthread_join函数
阻塞等待线程退出,获取线程退出状态 ,回收就是回收PCB系统资源
其作用,对应进程中 waitpid() 函数。
int pthread_join(pthread_t thread, void **retval); 成功:0;失败:错误号
参数:thread:线程ID (【注意】:不是指针);retval:存储线程结束状态。
对比记忆:
进程中:main返回值、exit参数–>int;等待子进程结束 wait 函数参数–>int *
线程中:线程主函数返回值、pthread_exit–>void *;等待线程结束 pthread_join 函数参数–>void **
【练习】:参数 retval 非空用法。 【pthrd_exit_join.c】
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
1.如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
2.如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
3.如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
4.如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
【练习】:使用pthread_join函数将循环创建的多个子线程回收。【pthrd_loop_join.c】
pthread_detach函数
实现线程分离
int pthread_detach(pthread_t thread); 成功:0;失败:错误号
线程分离状态:指定该状态,线程主动与主控线程断开关系。
线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。
进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。
也可使用 pthread_create函数参2(线程属性)来设置线程分离。
【练习】:使用pthread_detach函数实现线程分离【pthrd_detach.c】
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
pthread_cancel函数
杀死(取消)线程
其作用,对应进程中 kill() 函数。
int pthread_cancel(pthread_t thread); 成功:0;失败:错误号
【注意】:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。
杀死线程也不是立刻就能完成,必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。
可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调用pthreestcancel函数自行设置一个取消点。
被取消的线程, 退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是-1。可在头文件 pthread.h中找到它的定义:#define PTHREAD_CANCELED ((void *) -1)。因此当我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值为-1。
【练习】:终止线程的三种方法。注意“取消点”的概念。 【pthrd_endof3.c】
终止线程方式
总结:终止某个线程而不终止整个进程,有三种方法:
1.从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit。
2.一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
3.线程可以调用pthread_exit终止自己。
pthread_equal函数
比较两个线程ID是否相等。
int pthread_equal(pthread_t t1, pthread_t t2);
有可能Linux在未来线程ID pthread_t 类型被修改为结构体实现。
控制原语对比
进程 线程
fork pthread_create()
exit() pthread_exit(void )
wait pthread_join(, void*)
kill pthread_cancel()
getpid pthread_self
pthread_detach() 线程分离,好处是线程结束时无需调用pthread_join() 就可以自动清理PCB
pthread_self create
exit
join
对比记忆
fork pthead_create
exit(10) pthead_exit(void *);
wait(int *) pthread_join(, void **) 阻塞 分离 22 ; cancel -1
kill() pthread_cancel(); 取消点(检查点):系统调用
man 7 pthreads
pthread_testcancel(); 系统调用
getpid() pthread_self();
pthread_detach(); 分离。 --自动清理pcb。
5.3 线程属性
本节作为指引性介绍,linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
} pthread_attr_t;
可以用 ulimit -a 查看空间
主要结构体成员:
- 线程分离状态
- 线程栈大小(默认平均分配) // 进程的栈空间默认 8192字节,就是8KB,那么线程是均分这个栈空间
- 线程栈警戒缓冲区大小(位于栈末尾)
参 APUE.12.3 线程属性
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。
线程属性主要包括如下属性:
作用域(scope)
栈尺寸(stack size)
栈地址(stack address)
优先级(priority)
分离的状态(detached state)
调度策略和参数(scheduling policy and parameters)。
默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
线程属性初始化
注意:应先初始化线程属性,再pthread_create创建线程
初始化线程属性
int pthread_attr_init (pthread_attr_t *attr); 成功:0;失败:错误号
销毁线程属性所占用的资源
int pthread_attr_destroy (pthread_attr_t *attr); 成功:0;失败:错误号
线程的分离状态
线程的分离状态决定一个线程以什么样的方式来终止自己。
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
线程分离状态的函数:
设置线程属性,分离or非分离
int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate);
获取程属性,分离or非分离
int pthread_attr_getdetachstate (pthread_attr_t *attr, int *detachstate);
参数:
attr:已初始化的线程属性
detachstate:PTHREAD_CREATE_DETACHED(分离线程)
PTHREAD _CREATE_JOINABLE(非分离线程)
这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
线程的栈地址
POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性。也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE来进行检测。
当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack和pthread_attr_getstack两个函数分别设置和获取线程的栈地址。
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); 成功:0;失败:错误号
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize); 成功:0;失败:错误号
参数:attr:指向一个线程属性的指针
stackaddr:返回获取的栈地址
stacksize:返回获取的栈大小
线程的栈大小
当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
函数pthread_attr_getstacksize和 pthread_attr_setstacksize提供设置。
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); 成功:0;失败:错误号
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize); 成功:0;失败:错误号
参数:attr:指向一个线程属性的指针
stacksize:返回线程的堆栈大小
线程属性控制示例
#include <pthread.h>
#define SIZE 0x100000
void *th_fun(void *arg)
{
while (1)
sleep(1);
}
int main(void)
{
pthread_t tid;
int err, detachstate, i = 1;
pthread_attr_t attr;
size_t stacksize;
void *stackaddr;
pthread_attr_init(&attr);
pthread_attr_getstack(&attr, &stackaddr, &stacksize);
pthread_attr_getdetachstate(&attr, &detachstate);
if (detachstate == PTHREAD_CREATE_DETACHED)
printf(“thread detached\n”);
else if (detachstate == PTHREAD_CREATE_JOINABLE)
printf(“thread join\n”);
else
printf(“thread unknown\n”);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
while (1) {
stackaddr = malloc(SIZE);
if (stackaddr == NULL) {
perror(“malloc”);
exit(1);
}
stacksize = SIZE;
pthread_attr_setstack(&attr, stackaddr, stacksize);
err = pthread_create(&tid, &attr, th_fun, NULL);
if (err != 0) {
printf("%s\n", strerror(err));
exit(1);
}
printf("%d\n", i++);
}
pthread_attr_destroy(&attr);
return 0;
}【pthrd_attr_change.c】
5.4 NPTL
1.察看当前pthread库版本getconf GNU_LIBPTHREAD_VERSION
2.NPTL实现机制(POSIX),Native POSIX Thread Library
3.使用线程库时gcc指定–lpthread
5.5 线程使用注意事项
1.主线程退出其他线程不退出,主线程应调用pthread_exit
2.避免僵尸线程
pthread_join
pthread_detach
pthread_create指定分离属性
被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
3.malloc和mmap申请的内存可以被其他线程释放
4.应避免在多线程模型中调用fork除非,马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
5. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
6. 线程同步
6.1 同步概念
所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两个设备之间规定一个共同的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致;文件同步,是指让两个或多个文件夹里的文件保持一致。等等
而编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。
线程同步
同步即协同步调,按预定的先后次序运行。
线程同步,指一个线程发出某一功能(发出功能,即调用一个函数)调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
举例1:银行存款 5000。柜台,折:取3000;提款机,卡:取 3000。剩余:2000
举例2:内存中100字节,线程T1欲填入全1,线程T2欲填入全0。但如果T1执行了50个字节失去cpu,T2执行,会将T1写过的内容覆盖。当T1再次获得cpu继续从失去cpu的位置向后写入1,当执行结束,内存中的100字节,既不是全1,也不是全0。
产生的现象叫做“与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。
“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。
因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。
数据混乱原因:
- 资源共享(独享资源则不会)
- 调度随机(意味着数据访问会出现竞争)
- 线程间缺乏必要的同步机制。
以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。
所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。
6.2 互斥量mutex
Linux中提供一把互斥锁mutex(也称之为互斥量)。
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
资源还是共享的,线程间也还是竞争的,
但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。
但,应注意:同一时刻,只能有一个线程持有该锁。
当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。
所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。
因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。
主要应用函数:
pthread_mutex_init 函数
pthread_mutex_destroy 函数
pthread_mutex_lock 函数
pthread_mutex_trylock 函数
pthread_mutex_unlock 函数
以上5个函数的返回值都是:成功返回0,失败返回错误号。
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。
pthread_mutex_t mutex; 变量mutex只有两种取值1、0。
pthread_mutex_init函数
初始化一个互斥锁(互斥量) —> 初值可看作1
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参1:传出参数,调用时应传 &mutex
restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改
参2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。参APUE.12.4同步属性
1.静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。e.g. pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
2.动态初始化:局部变量应采用动态初始化。e.g. pthread_mutex_init(&mutex, NULL)
pthread_mutex_destroy函数
销毁一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_lock函数
加锁。可理解为将mutex–(或-1)
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_unlock函数
解锁。可理解为将mutex ++(或+1)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_trylock函数
尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
加锁与解锁
lock与unlock:
lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
例如:T1 T2 T3 T4 使用一把mutex锁。T1加锁成功,其他线程均阻塞,直至T1解锁。T1解锁后,T2 T3 T4均被唤醒,并自动再次尝试加锁。
可假想mutex锁 init成功初值为1。lock 功能是将mutex–。unlock将mutex++
lock与trylock:
lock加锁失败会阻塞,等待锁释放。
trylock加锁失败直接返回错误号(如:EBUSY),不阻塞。
加锁步骤测试:
看如下程序:该程序是非常典型的,由于共享、竞争而没有加任何同步机制,导致产生于时间有关的错误,造成数据混乱:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *tfn(void *arg)
{
srand(time(NULL));
while (1) {
printf("hello ");
sleep(rand() % 3);/模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误/
printf(“world\n”);
sleep(rand() % 3);
}
return NULL;
}
int main(void)
{
pthread_t tid;
srand(time(NULL));
pthread_create(&tid, NULL, tfn, NULL);
while (1) {
printf("HELLO ");
sleep(rand() % 3);
printf(“WORLD\n”);
sleep(rand() % 3);
}
pthread_join(tid, NULL);
return 0;
}【mutex.c】
【练习】:修改该程序,使用mutex互斥锁进行同步。
1.定义全局互斥量,初始化init(&m, NULL)互斥量,添加对应的destry
2.两个线程while中,两次printf前后,分别加lock和unlock
3.将unlock挪至第二个sleep后,发现交替现象很难出现。
线程在操作完共享资源后本应该立即解锁,但修改后,线程抱着锁睡眠。睡醒解锁后又立即加锁,这两个库函数本身不会阻塞。
所以在这两行代码之间失去cpu的概率很小。因此,另外一个线程很难得到加锁的机会。
4.main 中加flag = 5 将flg在while中–这时,主线程输出5次后试图销毁锁,但子线程未将锁释放,无法完成。
5.main 中加pthread_cancel()将子线程取消。【pthrd_mutex.c】
结论:
在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。
6.3 死锁
- 线程试图对同一个互斥量A加锁两次。
- 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁
【作业】:编写程序,实现上述两种死锁现象。
6.4 读写锁
与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。
读写锁状态:
一把读写锁具备三种状态:
- 读模式下加锁状态 (读锁)
- 写模式下加锁状态 (写锁)
- 不加锁状态
读写锁特性:
1.读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
2.读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。 -
读写锁是“读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
读写锁非常适合于对数据结构读的次数远大于写的情况。
主要应用函数:
pthread_rwlock_init函数
pthread_rwlock_destroy函数
pthread_rwlock_rdlock函数
pthread_rwlock_wrlock函数
pthread_rwlock_tryrdlock函数
pthread_rwlock_trywrlock函数
pthread_rwlock_unlock函数
以上7 个函数的返回值都是:成功返回0,失败直接返回错误号。
pthread_rwlock_t类型用于定义一个读写锁变量。
pthread_rwlock_t rwlock;
pthread_rwlock_init函数
初始化一把读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
参2:attr表读写锁属性,通常使用默认属性,传NULL即可。
pthread_rwlock_destroy函数
销毁一把读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
pthread_rwlock_rdlock函数
以读方式请求读写锁。(常简称为:请求读锁)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock函数
以写方式请求读写锁。(常简称为:请求写锁)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_unlock函数
解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_rwlock_tryrdlock函数
非阻塞以读方式请求读写锁(非阻塞请求读锁)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_trywrlock函数
非阻塞以写方式请求读写锁(非阻塞请求写锁)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
读写锁示例
看如下示例,同时有多个线程对同一全局数据读、写操作。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int counter;
pthread_rwlock_t rwlock;
/* 3个线程不定时写同一全局资源,5个线程不定时读同一全局资源 */
void *th_write(void *arg)
{
int t, i = (int)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock);
t = counter;
usleep(1000);
printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(10000);
}
return NULL;
}
void *th_read(void *arg)
{
int i = (int)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock);
printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}
int main(void)
{
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock, NULL);
for (i = 0; i < 3; i++)
pthread_create(&tid[i], NULL, th_write, (void *)i);
for (i = 0; i < 5; i++)
pthread_create(&tid[i+3], NULL, th_read, (void *)i);
for (i = 0; i < 8; i++)
pthread_join(tid[i], NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}【rwlock.c】
6.5 条件变量:
条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。
主要应用函数:
pthread_cond_init函数
pthread_cond_destroy函数
pthread_cond_wait函数
pthread_cond_timedwait函数
pthread_cond_signal函数
pthread_cond_broadcast函数
以上6 个函数的返回值都是:成功返回0,失败直接返回错误号。
pthread_cond_t类型用于定义条件变量
pthread_cond_t cond;
pthread_cond_init函数
初始化一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参2:attr表条件变量属性,通常为默认值,传NULL即可
也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_destroy函数
销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_wait函数
阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
函数作用:
1.阻塞等待条件变量cond(参1)满足
2.释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
1.2.两步为一个原子操作。
3.当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
pthread_cond_timedwait函数
限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t restrict cond, pthread_mutex_t restrict mutex, const struct timespec restrict abstime);
参3:参看man sem_timedwait函数,查看struct timespec结构体。
struct timespec {
time_t tv_sec;/ seconds / 秒
longtv_nsec;/ nanosecondes/ 纳秒
}
形参abstime:绝对时间。
如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 1970年1月1日00:00:01秒(早已经过去)
正确用法:
time_t cur = time(NULL); 获取当前时间。
struct timespec t;定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
pthread_cond_timedwait (&cond, &mutex, &t); 传参参APUE.11.6线程同步条件变量小节
在讲解setitimer函数时我们还提到另外一种时间类型:
struct timeval {
time_ttv_sec;/ seconds / 秒
suseconds_t tv_usec; / microseconds */ 微秒
};
pthread_cond_signal函数
唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast函数
唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
生产者消费者条件变量模型
线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。
看如下示例,使用条件变量模拟生产者、消费者问题:
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
struct msg {
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for (;😉 {
pthread_mutex_lock(&lock);
while (head == NULL) {//头指针为空,说明没有节点可以为if吗
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = mp->next;//模拟消费掉一个产品
pthread_mutex_unlock(&lock);
printf("-Consume —%d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
struct msg *mp;
while (1) {
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;//模拟生产一个产品
printf("-Produce —%d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);//将等待在该条件变量上的一个线程唤醒
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}【conditionVar_product_consumer.c】
条件变量的优点:
相较于mutex而言,条件变量可以减少竞争。
如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
6.6 信号量
进化版的互斥锁(1 --> N)
信号量与信号没有任何关系嗷~~~
由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
主要应用函数:
sem_init函数
sem_destroy函数
sem_wait函数
sem_trywait函数
sem_timedwait函数
sem_post函数
以上6 个函数的返回值都是:成功返回0,失败返回-1,同时设置errno。(注意,它们没有pthread前缀)
sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。
sem_t sem; 规定信号量sem不能 < 0。头文件<semaphore.h>
信号量基本操作:
sem_wait:1. 信号量大于0,则信号量–(类比pthread_mutex_lock)
|2. 信号量等于0,造成线程阻塞
对应
|
sem_post:将信号量++,同时唤醒阻塞在信号量上的线程(类比pthread_mutex_unlock)
但,由于sem_t的实现对用户隐藏,所以所谓的++、–操作只能通过函数来实现,而不能直接++、–符号。
信号量的初值,决定了占用信号量的线程的个数。
sem_init函数
初始化一个信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参1:sem信号量
参2:pshared取0用于线程间;取非0(一般为1)用于进程间
参3:value指定信号量初值
sem_destroy函数
销毁一个信号量
int sem_destroy(sem_t *sem);
sem_wait函数 ----相当于lock
给信号量加锁 –
int sem_wait(sem_t *sem);
sem_post函数 ----相当于unlock
给信号量解锁 ++
int sem_post(sem_t *sem);
sem_trywait函数
尝试对信号量加锁 --(与sem_wait的区别类比lock和trylock)
int sem_trywait(sem_t *sem);
sem_timedwait函数
限时尝试对信号量加锁 –
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
参2:abs_timeout采用的是绝对时间。
定时1秒:
time_t cur = time(NULL); 获取当前时间。
struct timespec t;定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
t.tv_nsec = t.tv_sec +100;
sem_timedwait(&sem, &t); 传参
生产者消费者信号量模型
【练习】:使用信号量完成线程间同步,模拟生产者,消费者问题。【sem_product_consumer.c】
分析:
规定:如果□中有数据,生产者不能生产,只能阻塞。
如果□中没有数据,消费者不能消费,只能等待数据。
定义两个信号量:S满 = 0, S空 = 1 (S满代表满格的信号量,S空表示空格的信号量,程序起始,格子一定为空)
所以有:T生产者主函数 {T消费者主函数 {
sem_wait(S空);sem_wait(S满);
生产…消费…
sem_post(S满);sem_post(S空);
}}
假设:线程到达的顺序是:T生、T生、T消。
那么:T生1 到达,将S空-1,生产,将S满+1
T生2 到达,S空已经为0,阻塞
T消到达,将S满-1,消费,将S空+1
三个线程到达的顺序是:T生1、T生2、T消。而执行的顺序是T生1、T消、T生2
这里,S空表示空格子的总数,代表可占用信号量的线程总数–>1。其实这样的话,信号量就等同于互斥锁。
但,如果S空=2、3、4……就不一样了,该信号量同时可以由多个线程占用,不再是互斥的形式。因此我们说信号量是互斥锁的加强版。
【推演练习】:理解上述模型,推演,如果是两个消费者,一个生产者,是怎么样的情况。
【作业】:结合生产者消费者信号量模型,揣摩sem_timedwait函数作用。编程实现,一个线程读用户输入,另一个线程打印“hello world”。如果用户无输入,则每隔5秒向屏幕打印一个“hello world”;如果用户有输入,立刻打印“hello world”到屏幕。
6.7 进程间同步
互斥量mutex
进程间也可以使用互斥锁,来达到同步的目的。但应在pthread_mutex_init初始化之前,修改其属性为进程间共享。mutex的属性修改函数主要有以下几个。
主要应用函数:
pthread_mutexattr_t mattr 类型:用于定义mutex锁的【属性】
pthread_mutexattr_init函数:初始化一个mutex属性对象
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
pthread_mutexattr_destroy函数:销毁mutex属性对象 (而非销毁锁)
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
pthread_mutexattr_setpshared函数:修改mutex属性。
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
参2:pshared取值:
线程锁:PTHREAD_PROCESS_PRIVATE (mutex的默认属性即为线程锁,进程间私有)
进程锁:PTHREAD_PROCESS_SHARED(进程间共享,即表示线程间私有)
进程间mutex示例
进程间使用mutex来实现同步:
#include <fcntl.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/wait.h>
struct mt {
int num;
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
};
int main(void)
{
int fd, i;
struct mt *mm;
pid_t pid;
fd = open(“mt_test”, O_CREAT | O_RDWR, 0777);
ftruncate(fd, sizeof(*mm));
mm = mmap(NULL, sizeof(*mm), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
unlink(“mt_test”);
//mm = mmap(NULL, sizeof(*mm), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
memset(mm, 0, sizeof(*mm));
pthread_mutexattr_init(&mm->mutexattr);//初始化mutex属性对象
pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED);//修改属性为进程间共享
pthread_mutex_init(&mm->mutex, &mm->mutexattr);//初始化一把mutex琐
pid = fork();
if (pid == 0) {
for (i = 0; i < 10; i++) {
pthread_mutex_lock(&mm->mutex);
(mm->num)++;
printf("-child----num++%d\n", mm->num);
pthread_mutex_unlock(&mm->mutex);
sleep(1);
}
} else if (pid > 0) {
for ( i = 0; i < 10; i++) {
sleep(1);
pthread_mutex_lock(&mm->mutex);
mm->num += 2;
printf("-parent—num+=2%d\n", mm->num);
pthread_mutex_unlock(&mm->mutex);
}
wait(NULL);
}
pthread_mutexattr_destroy(&mm->mutexattr);//销毁mutex属性对象
pthread_mutex_destroy(&mm->mutex); //销毁mutex
munmap(mm,sizeof(*mm));//释放映射区
return 0;
}【process_mutex.c】
文件锁
借助 fcntl函数来实现锁机制。
操作文件的进程没有获得锁时,可以打开,但无法执行read、write操作。
fcntl函数: 获取、设置文件访问控制属性。
int fcntl(int fd, int cmd, … /* arg */ );
参2:
F_SETLK (struct flock *) 设置文件锁(trylock)
F_SETLKW (struct flock *) 设置文件锁(lock)W --> wait
F_GETLK (struct flock *) 获取文件锁
参3:
struct flock {
…
short l_type;锁的类型:F_RDLCK 、F_WRLCK 、F_UNLCK
short l_whence; 偏移位置:SEEK_SET、SEEK_CUR、SEEK_END
off_t l_start;起始偏移:1000
off_t l_len;长度:0表示整个文件加锁
pid_t l_pid;持有该锁的进程ID:(F_GETLK only)
…
};
用二进制位来表示上述的一些属性,就称之为位图。
进程间文件锁示例
多个进程对加锁文件进行访问:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
void sys_err(char *str)
{
perror(str); exit(1);
}
int main(int argc, char *argv[])
{
int fd;
struct flock f_lock;
if (argc < 2) {
printf("./a.out filename\n"); exit(1);
}
if ((fd = open(argv[1], O_RDWR)) < 0)
sys_err(“open”);
//f_lock.l_type = F_WRLCK;/选用写琐/
f_lock.l_type = F_RDLCK;/选用读琐/
f_lock.l_whence = SEEK_SET;
f_lock.l_start = 0;
f_lock.l_len = 0;/* 0表示整个文件加锁 */
fcntl(fd, F_SETLKW, &f_lock);
printf(“get flock\n”);
sleep(10);
f_lock.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &f_lock);
printf(“un flock\n”);
close(fd); return 0;
}【file_lock.c】
依然遵循“读共享、写独占”特性。但!如若进程不加锁直接操作文件,依然可访问成功,但数据势必会出现混乱。
【思考】:多线程中,可以使用文件锁吗?
多线程间共享文件描述符,而给文件加锁,是通过修改文件描述符所指向的文件结构体中的成员变量来实现的。因此,多线程中无法使用文件锁。线程间可以使用读写锁,
6.8 哲学家用餐模型分析
多线程版:
选用互斥锁mutex,如创建5个, pthread_mutex_t m[5];
模型抽象:
5个哲学家 --> 5个线程;
5支筷子 --> 5把互斥锁 int left(左手), right(右手)
面 --> 表示共享资源
5个哲学家使用相同的逻辑,
可通用一个线程主函数,void *tfn(void *arg),使用参数来表示线程编号:int i = (int)arg;
哲学家线程根据编号知道自己是第几个哲学家,而后选定锁,锁住,吃饭。否则哲学家thinking。
A B C D E 5支筷子,在逻辑上形成环:
0 1 2 3 4 分别对应5个哲学家:
所以有:
if(i == 4)
left = i, right = 0;
else
left = i, right = i+1;
振荡:如果每个人都攥着自己左手的锁,尝试去拿右手锁,拿不到则将锁释放。过会儿五个人又同时再攥着左手锁尝试拿右手锁,依然拿不到。如此往复形成另外一种极端死锁的现象——振荡。
避免振荡现象:只需5个人中,任意一个人,拿锁的方向与其他人相逆即可
(如:E,原来:左:4,右:0现在:左:0,右:4)。
所以以上if else语句应改为:
if(i == 4)
left = 0, right = i;
else
left = i, right = i+1;
而后,首先应让哲学家尝试加左手锁:
while {
pthread_mutex_lock(&m[left]); 如果加锁成功,函数返回再加右手锁,
如果失败,应立即释放左手锁,等待。
如果,左右手都加锁成功 --> 吃 --> 吃完 --> 释放锁(应先释放右手、再释放左手,是加锁顺序的逆序)
}
主线程(main)中,初始化5把锁,销毁5把锁,创建5个线程(并将i传递给线程主函数),回收5个线程。
避免死锁的方法:
- 当得不到所有所需资源时,放弃已经获得的资源,等待。
- 保证资源的获取顺序,要求每个线程获取资源的顺序一致。
如:A获取顺序1、2、3;B顺序应也是1、2、3。若B为3、2、1则易出现死锁现象。
多进程版
相较于多线程需注意问题:
需注意如何共享信号量 (注意:坚决不能使用全局变量 sem_t s[5])
实现:
main函数中:
循环 sem_init(&s[i], 0, 1); 将信号量初值设为1,信号量变为互斥锁。
循环 sem_destroy(&s[i]);
循环创建 5 个子进程。 if(i < 5) 中完成子进程的代码逻辑。
循环回收 5 个子进程。
子进程中:
if(i == 4)
left = 0, right == 4;
else
left = i, right = i+1;
while (1) {
使用 sem_wait(&s[left]) 锁左手,尝试锁右手,若成功 --> 吃;若不成功 --> 将左手锁释放。
吃完后,先释放右手锁,再释放左手锁。
}
【重点注意】:
直接将sem_t s[5]放在全局位置,试图用于子进程间共享是错误的!应将其定义放置与mmap共享映射区中。main中:
sem_t *s = mmap(NULL, sizeof(sem_t) * 5, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
使用方式:将s当成数组首地址看待,与使用数组s[5]没有差异。