课程链接:https://www.bilibili.com/video/BV1KE411q7ee
001-Linux命令基础习惯
终端: 一系列输入输出设备的统称;
$ echo $SHELL #查看当前正在使用的命令解析器
$ cat /etc/shells #查看支持的所有shell
$ ls -l file #查看file的详细信息
Ctrl+a
-回到命令首;
Ctrl+e
-回到命令尾;
Ctrl+u
-删除所有内容;
002-类Unix系统目录
- bin-存放可执行文件;
- boot-存放OS启动例程;
- dev-存放设备文件(Linux系统中所见皆文件);
- etc-存放用户信息, 如密码等;
- home-用户的家目录;
- lib-库目录
- media和mnt-挂载磁盘的设备文件;
- opt和proc-进程相关;
- usr-UnixSoftwareResource;
- root-管理员宿主目录(家目录);
sudo su
-切换到管理员
003-目录和文件操作1
cd -
-在两个目录之间来回切换;
ls -a
-显示隐藏文件, 每个目录下面都有一个.和…的隐藏文件;
$ ls -l dir #查看dir目录下文件的详细信息
$ ls -ld dir #查看dir目录本身的详细信息
$ ls -R #递龟进入子目录
Linux文件类型:
- 普通-
- 目录d
- 字符设备c
- 块设备b
- 软连接l
- 管道p
- 套接字s
- Unknown
which date
-查看可执行文件的路径;
rmdir
-删除空目录;
cp -a/-r srcdir dstdir
-拷贝目录;
004-目录和文件操作2
more
-分屏显示文件内容, 空格翻页;
less
同理;
head -n file
-查看file的前n行;
tail -n file
-查看file的后n行;
sudo apt-get install tree
-安装命令;
005-软连接和硬连接
ln -s hello.c hello.c.s
-创建软连接;
软连接中存的就是文件的路径, 路径有几个字符就占几个字节, 所以建议用绝对路径创建软连接;
另外注意文件的权限, 软连接的权限代表其本身的权限, 与指向的目的文件无关;
ln hello.c hello.c.h
-创建硬链接;
创建硬链接会增加硬链接计数;
这些硬链接只想同一个文件, 修改一个其余的会同步变化;
查看文件状态:
所有的硬链接有相同的Inode(文件统一id);
删除只是把硬链接计数-1;
006-创建修改用户和用户组
chmod:
文字设定法:
- u-所有者;
- g-同组用户;
- o-其他用户;
- a-上面所有;
chmod o+w file
-给其他用户写权限;
chmod a+x file
-给所有用户执行权限;
数字设定法:
rwx分别对应421
chmod 471 file
r–rwx–x;
sudo adduser tom
-添加用户;
chown tom file
-改变文件的所有者;
su tom
- 切换用户;
sudo addgroup g77
-添加一个新组;
sudo chgrp g77 file
-修改所属组;
sudo chown tom:tom file
-同时修改用户和用户组;sudo deluser tom
-删除用户;
sudo delgroup g77
-删除用户组;
007-find命令1
目录紧跟在find之后
find ./ -type 'l'
找当前目录下的软连接, 子目录会递龟进入;
find ./ -name '*.jpg'
-找当前目录下的jpg文件, 子目录会递龟进入;
find ./ -maxdepth 3 -name '*.jpg'
-指定目录层级深度为3层;
find ./ -size +20M -size -50M
-指定大小范围;
ls -h
-以人类可读的方式显示结果;
man手册中反斜杠**/**可以用于查找关键字;
关于size的单位(find默认用b):
按时间查找:
- -atime(access访问时间)
- -ctime(change更改时间)
- -mtime(modify改动时间)
find ./ -ctime 3
查找三天内被改动的文件;
008-午后复习
None
009-find命令2
find /usr/ -name "\*temp\*" -exec ls -l {} \;
大括号表示前面命令返回的结果集, 对其指定**-exec**后面的命令, ;是转义后的;
find ~/ -type f -ok rm -r {} \;
exec的缓冲版, 操作前会询问, 保证安全性;
010-grep和xargs
grep:按文件内容搜索"return"关键字:
grep -r "return" ./ -n
ps:监控后台进程的工作情况;
ps aux
加个管道过滤内容
ps aux | grep "kernel"
(搜索本身会占一个进程)
如果将管道的手法用在find上(用xargs):
find /usr/ -maxdepth 3 -type -f | xargs ls -l
-exec
与xargs
的区别:前者会将结果不论多少一股脑的交给-exec, 而xargs会做分片处理(效率更高);
创建名字中有空格的文件:
$ touch abc\ def
$ touch "abc def"
由于xargs会将文件名中的空格误认为是分隔符, 解决方式: 控制分隔符:
find /usr/ -maxdepth 3 -type f -print0 | xargs -0 ls -l
011-xargs和awk说明
awk
是按行拆分;
sed
是按列拆分;
通常用于shell的正则表达式中;
012-软件包的安装
更新源服务器列表:sudo vim /etc/apt/sources.list
更新源:sudo apt-get update
安装:sudo apt-get install package
卸载:sudo apt-get remove package
deb包安装(Ubuntu属于debain系列)
源码安装:
013-压缩命令gzip和bzip2
压缩文件:
tar -zcvf test.tar.gz 039_serverMultiProcess.c hello.c repository
系统中真正进行压缩的是gzip
, 解压缩是gunzip
, 但是gzip
只能一对一压缩;
tar
命令实际上是用于打包的, 参数z
就是用gzip
进行压缩, c
是create
的意思, v
表示可见, f
表示生成文件;
file
是文件照妖镜, 看文件属性;
bzip2
和gzip
类似, 都是单个文件所用, 如果使用bzip2
进行压缩:
tar -jcvf test2.tar.gz 039_serverMutiProcess.c hello.c repository
014-rar压缩和zip压缩
rar压缩:
rar a -r rartest.rar hello.c hello.cpp
rar解压缩:
rar x rartest.rar
sudo aptitude show tree
可以查看安装软件的详细信息;
zip压缩:
zip -r ziptest.zip hello.c hello.cpp
zip解压缩:
unzip ziptest.zip
015-其他命令
vi是Visual Interface的简称;
who
whoami
cat &
让cat去后台运行;
则用jobs
可以将其拿出来查看(查看OS后面用户的作业);
fg
和bg
前后台切换;
env
查看环境变量;
env | grep SHELL
top
是调出任务管理器;
sudo passwd daniel
改密码;
ifconfig
查看网卡信息;
man手册:
clear
=Ctrl+l
, 操, 才知道;
alias
起别名:
daniel@ubuntu:~/sys$ alias
alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"'
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l='ls -CF'
alias la='ls -A'
alias ll='ls -alF'
alias ls='ls --color=auto'
umask
指定用户创建文件时的掩码, 其中的mode和chmod命令中的格式一样;
OS不认为你新touch出来的文件具有执行能力, 所以他会将umask
的执行权限给去掉:
例如touch一个新文件的权限为rw-r–r--, 对应的数字表示法为644, 则对应的umask应为133, 但实际上时022, 默认把执行权限去掉了;
再例如你设置了一个umask为511, 则对应的文件权限为266, -w-rw-rw-, 本身没有执行权限, 操作系统认为合法, 不会改动你的设置;
但是你设置umask为522, 对应的文件权限为255, 对应的文件权限为-w-r-xr-x, 但是执行权限会被抹掉, 所以最终的权限只能得到 -w-r–r--;
reboot
重启;
free -m
查看空闲内存;
016-总结
None(第一天结束)
017-复习
终端中Ctrl+h
是BackSpace;
一个目录所占的磁盘大小为4K;
018-vim的三种工作模式
命令模式下ZZ也可以保存退出哦;
019-vim基本操作-跳转和删除字符
命令模式下:
I->光标到行首, 插入;
A->光标到行尾, 插入;
S->直接干掉整行, 切换到文本模式书写;
末行模式下直接输入行号就可以跳转到指定行;
命令模式下的%可以跳转到匹配的括号;
D->从当前一直删到本行结尾;
d0->删到行首;
X->删除光标前一个字符;
020-vim基本操作-删除
r->不改变工作模式的替换单个字符;
021-vim基本操作-复制粘贴
P->向光标所在行的上一行粘贴;
vim没有删除, 全是剪切操作;
022-vim基本操作-查找和替换
找设想内容: 命令模式下输入斜杠/, 然后输入查找的内容, 进行查找;
回车后按n找到下一个;
找看到的内容: 在一定范围内检索单词: 在单词名字上星号(向后)*, 或者井号(向前)#;
替换: 在本行的末行模式下:
:s /printf/println
通篇替换: 只会替换每一行的首个:
:%s /printf/println
如果想要把每行后面的也替换, 加参数g;
局部替换:
:30,37 s /int/unsigned int/g
023-vim基本操作-其他
Ctrl+r
反撤销;
:sp
水平分屏;
[d
查看宏定义;
! gcc hello.c -o hello
在文件中执行命令;
024-vim配置思路
vim配置文件路径:
~/.vimrc
025-gcc编译4步骤
优化编译速度主要集中则编译阶段;
026-gcc编译常用参数
指定头文件位置-I:gcc hello.c -I ./headers -o hello
只做预处理, 编译, 汇编, 不做连接, 拿到二进制文件-c;
编译时添加调试信息, 支持gdb调试-g:gcc hello.c -I ./headers -o hello2 -g
显示所有警告信息-Wall;
向程序中动态注册一个宏-D:gcc hello.c -o hello -D HELLO
#ifdef HELLO
#define HI 20
#endif
#include "hello.h"
int main(int argc,char* argv[]){
printf("----------------\n");
printf("%d\n",HI);
return 0;
}
这种宏定义常可以做开关使用, 用于输出调试信息;
027-午后复习
None
028-静态库和动态库理论对比
静态库会大量占用存储空间:
如果使用动态库情况则大不相同:
动态库不需要编译入程序, 运行时动态加载, 导致速度慢了一些;
二者的适合场景:
- 静态库: 对空间要求较低, 对时间要求较高;
- 动态库: 对时间要求较低, 对空间要求较高;
029-静态库制作
ar rcs libMyLib.a add.o sub.o div1.o
先用gcc的-c参数将源文件编译成二进制文件, 再用ar命令封装静态库;
如果直接编译, collect2是链接器, 报错了
说明链接阶段出错;
将库直接加入编译的源文件中就可以了:gcc test.c libMyMath.a -o test1
030-静态库使用及头文件对应
隐式声明: 编译过程中没有遇到函数定义和函数声明, 编译器会帮你做隐式声明;
但是这种隐式声明只能对于返回值为int型的;
解决方法: 添加头文件;
/*添加头文件守卫,防止头文件重复包含,一旦头文件被展开过一次,_MYMATH_H_就被定义过了,后面就不会再展开*/
#ifndef _MYMATH_H_
#define _MYMATH_H_
int add(int,int);
int sub(int,int);
int div1(int,int);
#endif
然后将源文件和库联编即可, 注意源文件在前;
工程化一点的话, 考虑目录的组织结构:
gcc test.c ./lib/libMyMath.a -o test -I ./inc
031-动态库制作-生成与位置无关代码
将源文件.c编译为目标文件.o, 生成与位置无关的代码, 借助参数-fPIC;
编译生成hello.o的时候, 各个函数的地址还是相对于main的地址, 链接阶段填入main的地址;
由于动态库的函数在库里, 不能像程序内部的函数一样直接填入main的地址, 动态函数在a.out中没有位置, 依赖于@plt, 进行延迟绑定;
查看二进制文件的反汇编代码:objdump -dS test
输出重定向:objdump -dS test > test.s
032-动态库制作演示
- 将.c文件生成.o文件(生成与位置无关的代码-fPIC):
gcc -c add.c -o add.o -fPIC
- 使用gcc -shared制作动态库:
gcc -shared add.o sub.o div1.o -o libMyMath.so
- 编译可执行程序时, 指定所使用的动态库, -l 指定库名, -L 指定库路径:
gcc test.c -o test -l MyMath -L ./lib
- 运行可执行程序
编译过了, 执行出错: 找不到文件;
033- 动态库加载错误原因及解决方式
上面的错误原因:
- 链接器:工作于链接阶段, 工作时需要指定-l和-L参数, 上面已经指定了;
- 动态链接器:工作于程序运行阶段, 工作时需要提供动态库所在目录;
上面两者没有任何关系
动态链接器要根据环境变量寻找动态库:LD_LIBRARY_PATH
export LD_LIBRARY_PATH=./lib
指定后就可以执行了(但是上面指定的只是临时的, 环境变量是进程的概念)
要想永久指定, 需要更改配置文件, 加入环境变量, 重启终端使之生效:
034- 动态库加载错误原因及解决方式2
像标准C库这种本身就在系统的环境变量里, 所以他能找到;
滥竽充数法:将库文件放到系统根目录下的lib里就可以了;
ldd test
可以查看程序运行所需要的动态库
最后一种方法:修改配置文件法;
sudo vim /etc/ld.so.conf
写入动态库绝对路径, 保存;
sudo ldconfig -v
, 使配置文件生效;
035-扩展讲解-数据段合并
为了节省内存, 将只读的.rodata和只读的.text段合并到一页内存;
同样的也将.bss和.data合并到一页内存;
036-总结
None
037-复习
动态库和静态库共存时, 编译器优先使用动态库;
038-gdb调试基础指令
gdb a.out
:开始调试
list 1
:从第一行开始显示源码, 后面再展开用l;
break 52
:在第52行设置断点;
run
:开始执行, 到断点暂停;
next
:下一个, 转到下一条语句或函数;
step
:单步, 进入函数, 单步执行, 注意系统函数只能用n, 不要用s进入;
print i
:打印变量i的值;
continue
:继续执行断点后续指令;
quit
:退出gdb调试工具;
039-gdb调试其他指令
用gdb调试段错误: 直接run, 程序停止的位置就是出段错误的位置;
start
:单步执行;
finish
:结束当前函数调用, 返回调用点;
set args aa bb cc
:给函数添加参数, 或者run aa bb cc
;
info b
:查看断点信息;
b 20 if i=5
:设置条件断点;
ptype arr
:查看变量类型;
栈帧:随着函数调用而在stack上开辟的一块内存空间, 用于存放函数调用时产生的局部变量和临时值;
backtrace
:简称bt查看函数调用的栈帧和层级关系;
frame 1
:切换函数栈帧;
display j
:一直显示j变量;
undisplay num
:取消监视;
delete
:删除断点;
040-gdb常见错误说明
file a.out
:不退出gdb打开另一个文件进行调试;
041-makefile基础规则
makefile的名字只能是makefile或Makefile;
用途:
-
1个规则:
目标:依赖条件
命令(前面是一个Tab缩进);
-
2个函数:
-
3个自动变量:
若想生成目标, 检查规则中的依赖条件是否存在, 如果不存在, 则寻找是否有规则用来生成该依赖文件;
检查规则中的目标是否需要被更新, 必须先检查他的所有依赖, 依赖中有任何一个被更新, 则目标必须被更新;
- 分析各个目标和依赖之间的关系;
- 根据依赖关系自底向上执行命令;
- 根据修改时间比目标新旧与否确定更新;
- 如果目标不依赖任何条件, 则执行对应命令, 以示更新;
/*一个最简单的makefile*/
hello:hello.c
gcc hello.c -o hello
考虑中间步骤:
hello:hello.o
gcc hello.o -o hello
hello.o:hello.c
gcc -c hello.c -o hello.o
042-makefile的1个规则
多文件联编:
hello:hello.c
gcc hello.c add.c sub.c div1.c -o hello
考虑到多文件编译的时间成本, 应该先将各个模块编译成.o目标文件, 由目标文件链接成可执行文件;
这样, 只有改动了的模块会被再次编译, 其他的保持不变;
hello:hello.o add.o sub.o div1.o
gcc hello.o add.o sub.o div1.o -o hello
hello.o:hello.c
gcc -c hello.c -o hello.o
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
div1.o:div1.c
gcc -c div1.c -o div1.o
当依赖条件的时间比目标的时间还晚, 说明目标该更新了;
依赖条件如果不存在, 找寻新的规则去产生依赖;
make只会认为第一行是自己的最终目标, 如果最终目标没有写在第一行, 通过ALL来指定;
ALL:hello
hello.o:hello.c
gcc -c hello.c -o hello.o
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
div1.o:div1.c
gcc -c div1.c -o div1.o
hello:hello.o add.o sub.o div1.o
gcc hello.o add.o sub.o div1.o -o hello
043-午后回顾
None
044-makefile的2个函数和clean
2个函数:
src=$(wildcard ./*.c)
:匹配当前目录下的所有.c源文件, 赋值给变量src(与shell类似, 变量只有字符串类型);
obj=$(patsubst %.c,%.o,$(src))
:将参数3中包含参数1的部分替换为参数2;
src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))
ALL:hello
hello:$(obj)
gcc $(obj) -o hello
hello.o:hello.c
gcc -c hello.c -o hello.o
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
div1.o:div1.c
gcc -c div1.c -o div1.o
clean:
-rm -rf $(obj) hello
执行make clean
时必须加上-n参数检查, 否则一迷糊把源码删掉就烷基八氮了;
clean相当于一个没有依赖条件的规则;
rm前面的横杠表示出错(文件不存在)仍然执行;
045-makefile的3个自动变量和模式规则
三个自动变量:
-
$@
:在规则的命令中, 表示规则中的目标; -
$^
:在规则的命令中, 表示所有依赖条件; -
$<
:在规则的命令中, 表示第一个依赖条件;
src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))
ALL:hello
hello:$(obj)
gcc $^ -o $@ #目标依赖于所有依赖条件
hello.o:hello.c
gcc -c $< -o $@ #目标依赖于第一个(唯一一个)依赖条件
add.o:add.c
gcc -c $< -o $@ #目标依赖于第一个(唯一一个)依赖条件
sub.o:sub.c
gcc -c $< -o $@ #目标依赖于第一个(唯一一个)依赖条件
div1.o:div1.c
gcc -c $< -o $@ #目标依赖于第一个(唯一一个)依赖条件
clean:
-rm -rf $(obj) hello
模式规则:
鉴于上面的都是某个.o文件依赖于某个.c文件的形式, 可以将其总结为一个模式规则:
%.o:%.c
gcc -c $< -o $@
关于$<:如果将该变量应用在模式规则中, 它可将依赖条件列表中的依赖项依次取出, 套用模式规则;
src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))
ALL:hello
hello:$(obj)
gcc $^ -o $@
%.o:%.c
gcc -c $< -o $@
clean:
-rm -rf $(obj) hello
加入了模式规则后, 当再加入新的模块, 比如mul模块, 不需要改动makefile就可以实现自动编译链接, 非常的方便;
扩展:
(1)静态模式规则(制定了模式规则给谁用):
$(obj)%.o:%.c gcc -c $< -o $@
(2)加入伪目标(为了防止目录下的与clean和ALL的同名文件的干扰):
.PHONY:clean ALL
(3)加入常用参数(-Wall, -I, -l, -L, -g), 形成了最终版本:
src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))
myArgs=-Wall -g
ALL:hello
hello:$(obj)
gcc $^ -o $@ $(myArgs)
%.o:%.c
gcc -c $< -o $@ $(myArgs)
clean:
-rm -rf $(obj) hello
.PHONY:clean ALL
046-习题和作业
考虑工程目录结构:
makefile文件:
src=$(wildcard ./src/*.c)
obj=$(patsubst ./src/%.c,./obj/%.o,$(src)) #注意百分号的匹配和锁定作用
myArgs=-Wall -g
inc_path=./inc #头文件所在目录
ALL:hello
hello:$(obj)
gcc $^ -o $@ $(myArgs)
$(obj):./obj/%.o:./src/%.c #目标和依赖都需要改变
gcc -c $< -o $@ $(myArgs) -I $(inc_path)
.PHONY: ALL clean
clean:
-rm -rf ./obj/*.o hello
当你的文件名不叫makefile:
make -f m6make -f m6 clean
047-系统编程阶段说在前面的话
系统调用: 内核提供的函数: 由操作系统实现并提供给外部应用程序的编程接口, 是应用程序同操作系统之间交互数据的桥梁;
为了保证系统的安全性, manPage中的系统调用都是对系统调用的一次浅封装, 比如open对应的是sys_open…;
逼逼叨: 不要一味地去追求底层原理, 底层永远无穷无尽, 适度的抽象有助于对整体的把握, 这也正是任何东西工程化的关键不是吗?
048-open函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode); //mode_t是一个8进制整型,指定文件权限,只有当参2指定了CREAT才有用
参数:
- O_RDONLY
- O_WRONLY
- O_RDWR
- O_APPEND
- O_CREAT
- O_EXCL
- O_TRUNC
- O_NONBLOCK
成功返回文件描述符, 失败返回-1并设置errno;
int main(int argc,char* argv[]){
int fd1=0;
int fd2=0;
fd1=open("./dirt.txt",O_RDONLY|O_CREAT|O_TRUNC,0644);
/*打开的文件不存在*/
fd2=open("./dirt2.txt",O_RDONLY);
printf("fd1=%d\n",fd1);
printf("fd2=%d,errno=%d:%s\n",fd2,errno,strerror(errno));
close(fd1);
close(fd2);
return 0;
}
创建文件权限时, 指定文件访问权限, 权限同时受umask影响:文件权限=mode&(~umask)
049-总结
None(第3天结束)
050-复习
None
051-makefile作业
将当前目录下的所有C程序编译成可执行文件:
src=$(wildcard ./*.c)
target=$(patsubst %.c,%,$(src))
ALL:$(target)
myArgs=-Wall -g
%:%.c
gcc $< -o $@ $(myArgs)
clean:
-rm -rf $(target)
.PHONY:ALL clean
052-read和write实现cp
read:从文件中读数据到缓冲区
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t count);
参3是缓冲区的大小;
成功返回实际读到的字节数, 返回0时意味着读到了文件末尾, 失败返回-1并设置errno;
wirte:从缓冲区中读数据到文件
#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t count);
参3是数据的大小(字节数);
成功返回实际写入的字节数, 失败返回-1, 并设置errno;
int main(int argc,char* argv[]){
char buf[1024];
int n=0;
int fd1=open(argv[1],O_RDONLY);
if(fd1==-1){
perror("open argv1 error");
exit(1);
}
int fd2=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd2==-1){
perror("open argv2 error");
exit(1);
}
while((n=read(fd1,buf,sizeof(buf)))!=0){
if(n<0){
perror("open argv2 error");
break;
}
write(fd2,buf,n);
}
close(fd1);
close(fd2);
return 0;
}
053-系统调用和库函数比较-预读入和缓输出
strace-跟踪一个程序执行时所需要的系统调用;
如果规定逐字节的进行拷贝, 用库函数会比用系统调用快很多, 因为有预读入和缓输出机制:
OS绝不会让你逐字节的向Disk上写数据, 实际上它维护了一个系统级缓冲, 只有当从用户空间过来的数据在该缓冲上写满时, 他才会一次性将数据冲刷到Disk上;
当使用系统调用的方法时, 要不断的在用户空间和内核空间进行来回切换, 这会消耗大量时间;
而使用fputc
(库函数)时, 他在设计之初自己在用户空间维护了一个缓冲, 这样在用户空间把自己的缓冲写满, 再一次性写入内核缓冲(写入了内核缓冲就认为写到了磁盘上了), 可见这样大大减少了在用户空间和内核空间来回切换的次数;
read
和write
函数常被称为UnbufferedIO
, 指无用户级缓冲区, 但不保证不使用内核缓冲区;
综上所述-少造*;
054-文件描述符
PCB
中有一根指针, 指向了该进程的文件描述符表, 每个表项都是一个键值对, 其中的value是指向文件结构体的指针, 其中的索引是fd, OS暴露给用户的唯一操作文件的依据;
新打开的文件描述符一定是所有文件描述符表中可用的, 最小的那个文件描述符;
文件描述符最大1023, 说明一个进程最多能打开1024个文件;
文件结构体:
055-阻塞和非阻塞
一个自己的echo程序:
int main(int argc,char* argv[]){
int n=0;
char buf[10];
n=read(STDIN_FILENO,buf,sizeof(buf));
if(n==-1){
perror("read error");
exit(1);
}
write(STDOUT_FILENO,buf,n);
return 0;
}
当不敲入换行符时,read
会一直阻塞等待用户输入;
阻塞是设备文件, 网络文件的属性;
当然也可以设置以非阻塞方式从tty中读数据:
int main(int argc,char* argv[]){
int fd=0;
char buf[10];
int n=0;
/*以非阻塞方式打开终端文件*/
fd=open("/dev/tty",O_RDONLY|O_NONBLOCK);
if(fd<0){
perror("open /dev/tty error");
exit(1);
}
tryagain:
n=read(fd,buf,sizeof(buf));
/*当read的返回值小于0*/
if(n<0){
/*errno不是EWOULDBLOCK,说明出现了其他问题*/
if(errno!=EWOULDBLOCK){
perror("read /dev/tty error");
exit(1);
}else{
/*errno是EWOULDBLOCK,说明读到为空,则打印提示信息,并再次尝试*/
write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
sleep(2);
goto tryagain;
}
}
/*当read的返回值大于0,说明读到了数据,写到标准输出上*/
write(STDOUT_FILENO,buf,n);
close(fd);
return 0;
}
当read
函数返回-1, 并且errno=EAGAIN
或EWOULDBLOCK
, 说明不是read
失败, 而是read
在以非阻塞方式读一个设备文件或网络文件, 而文件中无数据;
阻塞方式存在的问题也正是后来网络IO中select
, poll
和epoll
函数存在的原因;
056-fcntl改文件属性
fcntl
函数:改变一个已经打开文件的访问控制属性
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
用fcntl
改写上面的程序, 不用重新打开文件:
int main(int argc,char* argv[]){
int fd=0;
char buf[10];
int n=0;
int ret=0;
int flags=0;
/*获取原来的flags*/
flags=fcntl(STDIN_FILENO,F_GETFL);
if(flags==-1){
perror("fcntl error");
exit(1);
}
/*位或上新的属性*/
flags|=O_NONBLOCK;
/*将新的flags设置回去*/
ret=fcntl(STDIN_FILENO,F_SETFL,flags);
if(ret==-1){
perror("fcntl error");
exit(1);
}
/*与上面的相同*/
tryagain:
n=read(STDIN_FILENO,buf,sizeof(buf));
if(n<0){
if(errno!=EWOULDBLOCK){
perror("read /dev/tty error");
exit(1);
}else{
write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
sleep(2);
goto tryagain;
}
}
write(STDOUT_FILENO,buf,n);
close(fd);
return 0;
}
文件的flags
是一个位图, 每一位代表不同属性的真假值;
057-午后回顾
None
058-lseek函数
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
举个栗子:
int main(int argc,char* argv[]){
int fd=0;
int n=0;
char msg[]="It's a lseek test\n";
char c;
fd=open("./lseek.txt",O_CREAT|O_RDWR,0644);
if(fd==-1){
perror("open error");
exit(1);
}
write(fd,msg,strlen(msg));
/*如果这里不进行lseek,由于读写共用同一个偏移位置,下面的读会从文件末尾开始读,读不到任何数据*/
lseek(fd,0,SEEK_SET);
while((n=read(fd,&c,1))){
if(n==-1){
perror("read error");
exit(1);
}
write(STDOUT_FILENO,&c,n);
}
close(fd);
return 0;
}
用lseek
获取文件大小:
int main(int argc,char* argv[]){
int fd=open(argv[1],O_RDWR);
if(fd==-1){
perror("open error");
exit(1);
}
/*从0开始向后偏移到结尾,返回值表示偏移量,即为文件大小*/
int size=lseek(fd,0,SEEK_END);
printf("The file's size:%d\n",size);
close(fd);
return 0;
}
使用lseek
拓展文件大小: 要想使文件大小真正拓展, 必须引起IO操作;
int main(int argc,char* argv[]){
int fd=open(argv[1],O_RDWR);
if(fd==-1){
perror("open error");
exit(1);
}
/*从文件的结束位置开始,向后偏移110*/
int size=lseek(fd,110,SEEK_END);
printf("The file's size:%d\n",size);
/*然后写入一个空字符*/
write(fd,"\0",1);
close(fd);
return 0;
}
被填入的是文件空洞:
以HEX查看文件:od -tcx filename
;
也可以使用truncate
拓展文件大小:
int ret=truncate("dict.cp",250);
059-传入传出参数
传入参数:
- 指针作为函数参数;
- 同时有
const
关键字修饰; - 指针指向有效区域, 在函数内部做读操作;
传出参数:
- 指针作为函数参数;
- 在函数调用前, 指针指向的空间可以无意义, 但必须有效;
- 在函数内部做写操作;
- 函数调用结束后充当函数返回值;
传入传出参数:
- 指针作为函数参数;
- 在函数调用前, 指针指向的空间有实际意义;
- 在函数内部, 先做读操作, 再做写操作;
- 函数调用结束后, 充当函数返回值;
060-目录项和inode
增加文件的硬链接只是增加dentry
, 指向相同的inode
;
同样, 删除硬链接也只是删除dentry
, 要注意删除文件并不会让数据在磁盘消失, 只是OS丢失了inode
, 磁盘只能覆盖, 不能擦除;
061-stat函数
stat
函数作用:获取文件属性(从inode
中获取);
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char* pathname, struct stat* statbuf);
/*结构体信息*/
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; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
/* Since Linux 2.6, the kernel supports nanosecond precision for the following timestamp fields.For the details before Linux 2.6, see NOTES. */
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
参数:
- path:文件路径;
- buf(传出参数)存放文件属性;
返回值: 成功返回0, 失败返回-1并设置errno;
利用stat
获取文件大小:
int main(int argc,char* argv[]){
struct stat sbuf;
int ret=0;
ret=stat(argv[1],&sbuf);
if(ret==-1){
perror("stat error");
exit(1);
}
printf("file size:%ld\n",sbuf.st_size);
return 0;
}
062-lstat函数和stat
使用宏函数获取文件属性:
int main(int argc,char* argv[]){
struct stat sbuf;
int ret=0;
ret=stat(argv[1],&sbuf);
if(ret==-1){
perror("stat error");
exit(1);
}
/*宏函数一般返回布尔值*/
if(S_ISREG(sbuf.st_mode))
printf("It's a regular\n");
else if(S_ISDIR(sbuf.st_mode))
printf("It's a dir\n");
else if(S_ISFIFO(sbuf.st_mode))
printf("It's a pipe\n");
else if(S_ISLNK(sbuf.st_mode))
printf("It's a symbol");
/*and so on...*/
return 0;
}
ln -s makefile makefile.soft
:创建软连接;
mkfifo f1
:创建管道文件;
stat
穿透: 当用stat获取软连接的文件属性时, 会穿透符号连接直接返回软连接指向的本尊的文件属性;
解决方法: 换lstat
函数;
vim,cat命令也有穿透作用;
S_IFMT
是一个文件类型掩码(文件类型那四位全1), st_mode
与它位与后就可以提取出文件类型(后面的权限位被归零);
063-传出参数重充当返回值
064-link和unlink的隐式回收
关于特殊权限位:
link
函数:可以为已经存在的文件创建目录项(硬链接);
ln makefile makefile.hard
:为makefile创建硬连接;
int link(const char *oldpath, const char *newpath);
使用link
和unlink
函数实现mv
命令:
int main(int argc,char* argv[]){
int ret=0;
ret=link(argv[1],argv[2]);
if(ret==-1){
perror("link error");
exit(1);
}
ret=unlink(argv[1]);
if(ret==-1){
perror("unlink error");
exit(1);
}
return 0;
}
Linux下的文件删除机制: 不断的将文件的st_nlink-1
, 直到减到0为止. 无目录项对应的文件, 会被操作系统择机释放;
因此我们删除文件, 从某种意义上来说只是让文件具备了被删除的条件;
unlink
函数的特征:清楚文件时, 如果文件的硬连接计数减到了0, 没有dentry
与之对应, 但该文件仍不会马上被释放掉. 要等到所有打开该文件的进程关闭该文件, 系统才会择机将文件释放;
一个demo:
int main(int argc,char* argv[]){
int fd=0;
int ret=0;
char* p="test of unlink\n";
char* p2="after write something\n";
fd=open("temp.txt",O_RDWR|O_TRUNC|O_CREAT,0644);
if(fd<0)
perr_exit("open file error");
ret=write(fd,p,strlen(p));
if(ret==-1)
perr_exit("write error");
printf("hello,I'm printf\n");
ret=write(fd,p2,strlen(p2));
if(ret==-1)
perr_exit("write error");
printf("Entry key to continue\n");
/*程序在此阻塞等待用户输入*/
getchar();
close(fd);
/*删除该文件*/
ret=unlink("temp.txt");
if(ret==-1)
perr_exit("unlink error");
return 0;
}
但是如果在unlink
之前诱发段错误, 程序崩溃, temp.txt
就会存活下来. 所以将unlink
这一步放到打开文件之后紧接着就unlink掉;
虽然文件被unlink
掉了, 用户用cat查看不到磁盘上的对应文件, 但是write
函数拿到fd写文件是向内核的buffer中写, 仍可正常写入;
隐式回收:
当进程运行结束时, 所有该进程打开的文件会被关闭, 申请的内存空间会被释放, 系统的这一特性称为隐式回收系统资源;
065-文件目录rwx权限差异
readlink m1.soft
:查看软连接的内容;
Linux下所见皆文件, 如果用vim打开一个目录:
066-目录操作函数
文件名不能超过255个字符, 因为dirent
中的d_name
长度为256, 再算上\0, 有255个字符可用;
#include <dirent.h>
DIR* opendir(const char* name); /*返回的是一个目录结构体指针*/
int closedir(DIR* dirp);
struct dirent* readdir(DIR* dirp);
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};
用目录操作函数实现ls的功能:
int main(int argc,char* argv[]){
DIR* dp;
struct dirent* sdp;
/*根据输入的内容打开一个目录文件,拿到一个类似文件描述符的东西dp*/
dp=opendir(argv[1]);
if(dp==NULL)
perr_exit("opendir error");
/*循环从dirent流中读取数据*/
while((sdp=readdir(dp))!=NULL){
/*跳过当前目录和上一级目录*/
if(!strcmp(sdp->d_name,"."))
continue;
if(!strcmp(sdp->d_name,".."))
continue;
/*打印文件名*/
printf("%s\n",sdp->d_name);
}
printf("\n");
/*关闭文件*/
closedir(dp);
return 0;
}
067-总结
None
068-复习
Linux下文件存储原理:
069-递归遍历目录思路分析
1.判断命令行参数, 获取用户要查询的目录名-argv[1]
;
注意如果argc==1
, 说明要查询的是当前目录./;
2.判断用户指定的是否是目录: stat S_ISDIR()
->封装函数isFile();
3.读目录:
opendir(dir);
while(readdir()){
普通文件:直接打印;
目录文件:拼接目录访问绝对路径:sprintf(path,"%s%s",dir,d_name);
递归调用自己:opendir(path), readdir, closedir;
}
closedir();
070-递归遍历目录代码预览
/*参2是回调函数名*/
void fetchdir(const char* dir,void(*fcn)(char*)){
char name[PATH_LEN];
struct dirent* sdp;
DIR* dp;
/*打开目录失败*/
if((dp=opendir(dir))==NULL){
fprintf(stderr,"fetchdir:can't open %s\n",dir);
return;
}
/*循环读取内容*/
while((sdp=readdir(dp))!=NULL){
/*遇到当前目录和上一级目录,跳过,否则会陷入死循环*/
if((strcmp(sdp->d_name,".")==0)||(strcmp(sdp->d_name,"..")==0))
continue;
/*路径名是否越界*/
if(strlen(dir)+strlen(sdp->d_name)+2>sizeof(name)){
fprintf(stderr,"fetchdir:name %s %s is too long\n",dir,sdp->d_name);
}else{
/*拼接为一个路径,传给isFile函数*/
sprintf(name,"%s/%s",dir,sdp->d_name);
(*fcn)(name);
}
}
closedir(dp);
}
void isFile(char* name){
struct stat sbuf;
/*获取文件属性失败*/
if(stat(name,&sbuf)==-1){
fprintf(stderr,"isFile:can't access %s\n",name);
exit(1);
}
/*这是一个目录文件:调用函数fetchdir*/
if((sbuf.st_mode&S_IFMT)==S_IFDIR){
fetchdir(name,isFile);
}
/*不是目录文件:是一个普通文件,打印文件信息*/
printf("%ld\t\t%s\n",sbuf.st_size,name);
}
int main(int argc,char* argv[]){
/*不指定命令行参数*/
if(argc==1)
isFile(".");
else{
while(--argc>0)
isFile(*++argv);
}
return 0;
}
071-递归遍历目录实现
None
072-递归遍历目录总结
None(把上面的代码真正掌握);
073-dup和dup2
duplicate:复制, 副本;
cat makefile > m1
:将cat的结果重定向到m1(此时m1与makefile内容相同);
cat makefile >> m1
:将cat的结果重定向并追加到m1后面(此时m1是双份的makefile);
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
The dup() system call creates a copy of the file descriptor oldfd, using the lowest-numbered unused file descriptor for the new descriptor.
传入已有的文件描述符, 返回一个新的文件描述符;
int main(int argc,char* argv[]){
/*open或创建一个文件,拿到文件描述符fd1*/
int fd1=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd1==-1)
perr_exit("open error");
/*fd2作为fd1的副本,拿着fd2也可以向被open的文件写入*/
int fd2=dup(fd1);
if(fd2==-1)
perr_exit("dup error");
printf("fd1=%d fd2=%d\n",fd1,fd2);
/*向fd2(fd1)中写入一句话*/
write(fd2,"fuckyou\n",8);
return 0;
}
dup
的返回值fd2相当于fd1的副本, 拿着它也可以操作fd1;
(这里视频好像讲错了, 凭什么瞧不起dup)
int main(int argc,char* argv[]){
int fd1=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0644);
int fd2=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
/*dup2后fd2也指向了fd1的文件*/
int fdret=dup2(fd1,fd2);
printf("fdret=%d\n",fdret);
int ret=write(fd2,"fuckyou\n",8);
printf("ret=%d\n",ret);
/*现在标准输出也指向了fd1*/
dup2(fd1,STDOUT_FILENO);
printf("--------fuckyou--------\n");
return 0;
}
总之, dup2
是后面的指向前面的;
074-fcntl实现dup描述符
int main(int argc,char* argv[]){
int fd1=open(argv[1],O_RDWR|O_CREAT,0644);
printf("fd1=%d\n",fd1);
int newfd1=fcntl(fd1,F_DUPFD,0);
printf("newfd1=%d\n",newfd1);
int newfd2=fcntl(fd1,F_DUPFD,8);
printf("newfd2=%d\n",newfd2);
int ret=write(newfd2,"fuckyou\n",8);
printf("ret=%d\n",ret);
return 0;
}
参3传0, 则从0开始向下寻找可用的文件描述符返回给newfd1
;
参3传8, 则从8开始向下寻找可用的文件描述符返回给newfd2
;
075-复习
dup2
的newfd比dup
的灵活一些: 他能打破可用最小的文件描述符限制;
076-进程和程序以及CPU相关
程序: 死的, 只占用磁盘空间(剧本);
进程: 活得, 运行起来的程序, 占用内存和CPU等系统资源(演出);
关于并发:
关于CPU和MMU相关:
MMU(内存管理单元), 位于CPU内部;
077-虚拟内存和物理内存映射关系
对于一个32位的机器来说, 每个进程都能看到4GB的虚拟地址空间, 且他们的3G~4G的位置都是kernel(每个进程都有kernel区);
从虚拟内存到物理内存的映射由MMU完成, 不同进程的用户空间被映射到物理内存的不同位置, 而不同进程的kernel空间被映射到物理内存的相同位置, 对于物理内存来用户空间和内核空间有不同的特权级, 从用户空间到内核空间的转换实质上是特权级的切换;
078-PCB进程控制块
每个进程在内核中都有一个PCB来维护进程相关信息, Linux内核的进程控制块是task_struct
类型的结构体;
主要内容:
着重掌握的:
- 进程id;
- 文件描述符表;
- 信号相关的信息资源;
- 进程状态:初始态, 就绪态, 运行态, 挂起态, 终止态;
- 进程工作目录位置;
- 用户id和组id;
079-环境变量
- PAHT:存放可执行程序的目录位置;
- SHELL:当前使用的命令解析器;
- TERM:查看终端类型;
- LANG:语言和编码;
- env:查看所有环境变量;
080-fork函数原理
fork
函数原型:
pid_t fork(); /*函数原型相当简单:空参,返回一个整数pid*/
“On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created,and errno is set appropriately.”
成功fork后, 在子进程中返回0, 在父进程中返回子进程的pid;
失败返回-1并设置errno;
081-fork创建子进程
int main(int argc,char* argv[]){
printf("---before fork 1---\n");
printf("---before fork 2---\n");
printf("---before fork 3---\n");
printf("---before fork 4---\n");
pid_t pid=0;
pid=fork();
if(pid==-1)
perr_exit("---fork error---\n");
else if(pid==0)
printf("---I'm child,my pid is %d,my parent's pid is %d---\n",getpid(),getppid());
else if(pid>0)
printf("---I'm parent,my pid is %d,my child's pid is %d---\n---My parent's pid is %d---\n",getpid(),pid,getppid());
printf("---end of file---\n\n");
return 0;
}
执行结果:
082-getpid和getppid
父进程的父进程是bash:
思考如何循环创建n个子进程(我给你八分钟!飞哥震怒):
083-循环创建多个子进程
int main(int argc,char* argv[]){
int i=0;
/*如果是子进程则跳出循环*/
for(i=0;i<5;++i){
if(fork()==0)
break;
}
/*子进程从i==5跳出for循环*/
if(i==5){
sleep(5);
printf("I'm parent\n");
}
else{
sleep(i);
printf("I'm %dth child\n",i+1);
}
sleep(1);
return 0;
}
printf
前不加sleep的执行效果:
乱序输出反映了了操作系统对进程调度的无序性;
加上了sleep
后就能控制输出顺序;
084-父子进程共享哪些内容
刚fork之后:
父子的相同之处: 全局变量, .data段, .text段, 栈, 堆, 环境变量, 用户ID, 宿主目录, 进程工作目录, 信号处理方式…;
父子进程的不同之处: 进程ID, fork返回值, 父进程ID, 进程运行时间, 闹钟(定时器), 未决信号集;
但是子进程并不是把父进程0~3G地址空间完全cpoy一份, 然后映射到物理内存;
父子进程间遵循读时共享, 写时复制的原则, 这样设计, 无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销;
int var=100;
int main(int argc,char* argv[]){
pid_t pid=fork();
if(pid==-1)
perr_exit("fork error");
/*读时共享,写时复制*/
if(pid==0){
var=288;
printf("I'm child,my pid=%d,my parent's pid=%d\n",getpid(),getppid());
printf("My var=%d\n\n",var);
}else if(pid>0){
var=200;
printf("I'm parent,my pid=%d,my parent's pid=%d\n",getpid(),getppid());
printf("My var=%d\n\n",var);
}
return 0;
}
输出结果:
躲避父子进程共享全局变量的误区(线程之间是共享全局变量的);
重点要掌握的共享的内容: 文件描述符, mmap建立的映射区;
085-父子进程共享
086-总结
None;
087-复习
多道程序设计模型: 宏观并行, 围观串行;
088-父子进程gdb调试
使用gdb调试的时候, gdb只能跟踪一个进程, 可以在fork函数调用之前通过指令设置gdb跟踪父进程还是子进程:
set follow-fork-mode child
set follow-fork-mode parent
089-exec函数族原理
fork
创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支), 子进程往往要执行一种exec
函数以执行另一个程序;
当进程调用一种exec
函数族时, 该进程的用户空间代码和数据完全被新程序替换, 从新程序的启动例程开始执行;
调用exec
函数并不会创建新的进程, 所以调用exec
前后该进程的id并未改变;
将当前进程的.text和.data替换为所加载程序的.text和.data, 然后进程从新的.text的第一条指令开始执行, 但进程id不变, 换核不换壳;
exec
函数不会返回任何值给任何人;
090-execlp和execl函数
execlp
中的p表示环境变量, 所以该函数通常用来调用系统程序;
int execlp(const char* file, const char* arg, ... /* (char *) NULL */);
注意结尾加上NULL
指定变参结束,printf
函数也是变参, 结尾也要加上NULL
作为哨兵;
int main(int argc,char* argv[]){
pid_t pid=fork();
if(pid==-1)
perr_exit("fork error");
if(pid==0){
/*好家伙,参数从argv[0]开始算*/
execlp("ls","ls","-l","-R","-h",NULL);
/*正常情况下是不会执行到这里的,只有当出错时才会返回到这里执行*/
perror("execlp error");
exit(1);
}else if(pid>0){
printf("I'm parent:%d\n",getpid());
sleep(1);
}
return 0;
}
先fork
, 再exec
, 这就是bash的大概原理;
如果要执行自己的可执行文件:
execl("./test","./test",NULL);
091-exec函数族特性
将ps aux
的输出打印到文件当中:
int main(int argc,char* argv[]){
int ret=0;
int fd1=0;
/*打开或创建一个文件*/
fd1=open("ps.log",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd1==-1)
perr_exit("open error");
/*将STDOUT_FILENO指向fd1*/
ret=dup2(fd1,STDOUT_FILENO);
if(ret==-1)
perr_exit("dup2 error");
/*执行命令*/
execlp("ps","ps","aux",NULL);
perror("execlp error");
exit(1);
return 0;
}
execvp
(v是vector的意思):
int execvp(const char* file, char* const argv[]);
就是将execlp
中的参数组织成字符串传入(或许你也可以传入从main函数中传来的参数)
/*execlp("ls","ls","-l","-R","-h",NULL)的等效形式*/
char* argv[]={"ls","-l","-R","-h",NULL};
execvp("ls",argv);
exec
函数族的一般规律:调用成功立即执行新的程序, 不会返回, 只有调用失败才会返回-1;
092-孤儿进程和僵尸进程
孤儿进程: 父进程先于子进程结束, 子进程的父进程变为init进程, init进程又称为进程孤儿院, 专门收养孤儿进程(为了回收);
僵尸进程: 进程终止, 父进程尚未回收子进程残留在内核的资源(PCB), 变为僵尸(defunct)进程(每一个进程都会经历僵尸态);
ps ajx
:查看进程ID和父进程ID;
kill -9 pid
:杀死进程, 但是杀不死僵尸进程, 杀僵尸只能杀死他父亲;
093-wait回收子进程
父进程调用wait函数可以回收子进程终止信息, 该函数有三个功能:
- 阻塞等待子进程退出;
- 回收子进程残留资源;
- 获取子进程结束状态(退出原因);
pid_t wait(int* wstatus);
成功返回清理掉的子进程ID, 失败返回-1;
094-获取子进程退出值和异常终止信号
通过调用宏函数获取子进程退出状态:
int main(int argc,char* argv[]){
pid_t pid,wpid;
int status=0;
pid=fork();
if(pid==-1)
perr_exit("fork error");
if(pid==0){
printf("I'm child:%d,my parent is %d,I'm going to sleep 10s\n",getpid(),getppid());
sleep(10);
printf("I'm child,I'm going to die\n");
return 73;
}else if(pid>0){
//wpid=wait(NULL); //不关心子进程退出原因
wpid=wait(&status);
if(wpid==-1)
perr_exit("wait error");
/*如果子进程正常终止,则可获取它的退出值*/
if(WIFEXITED(status))
printf("My child exited with:%d\n",WEXITSTATUS(status));
/*如果子进程被信号终止,可获取结束它的信号*/
else if(WIFSIGNALED(status))
printf("My child killed by:%d\n",WTERMSIG(status));
/*提示回收完成*/
printf("I'm parent,wait %d finish\n",wpid);
}
return 0;
}
子进程被信号杀死:
子进程正常终止:
各种信号的宏值:
程序所有异常终止的原因都是因为信号;
095-waitpid回收子进程
waitpid
可以指定某一个子进程回收;
一次wait
或waitpid
函数调用, 只能回收一个子进程, 如果你循环创建了多个子进程, 那么就碰到哪个算哪个;
pid_t waitpid(pid_t pid, int* wstatus, int options);
参1传要回收的pid, 传-1表示回收任意子进程, 传0表示回收同一组的所有子进程;
参2传进程结束状态, 如果不关心直接传NULL
(传出参数);
参3传回收方式:WNOHANG
(非阻塞);
一个有bug的版本:
int main(int argc,char* argv[]){
int i=0;
int wpid=0;
int pid=0;
for(i=0;i<5;++i){
if(fork()==0){
if(i==2)
pid=getpid();
break;
}
}
if(i==5){
//wpid=waitpid(-1,NULL,WNOHANG); //以非阻塞的方式回收任意子进程
sleep(5);
wpid=waitpid(pid,NULL,WNOHANG);
if(wpid==-1)
perr_exit("waitpid error");
printf("I'm parent,wait a child finish:%d\n",wpid);
}
else{
sleep(i);
printf("I'm %dth child,my pid=%d\n",i+1,getpid());
}
sleep(1);
return 0;
}
bug的原因:在fork()==0
时是在子进程的执行逻辑中保存了pid, 但是子进程执行结束后直接返回, 用户空间的地址空间被回收, 当然也就没有了pid这个变量, 所以后面父进程waitpid
时拿到的pid一直是0;
096-中午回顾
None;
097-错误解析
waitpid
的参1传0表示回收同组的所有子进程, 一般以父进程的id号为组号, 可通过系统调用将子进程分离出去;
waitpid
的参1传进程组号取反, 表示回收指定进程组的任意子进程;
bug改掉后:
int main(int argc,char* argv[]){
int i=0;
int wpid=0;
int pid=0;
int temppid=0;
for(i=0;i<5;++i){
pid=fork();
if(pid==0)
break;
/*在父进程中,如果i==2,将fork的返回值存入temppid*/
if(i==2)
temppid=pid;
}
if(i==5){
//wpid=waitpid(-1,NULL,WNOHANG); //以非阻塞的方式回收任意子进程
sleep(5);
wpid=waitpid(temppid,NULL,WNOHANG);
if(wpid==-1)
perr_exit("waitpid error");
printf("I'm parent,wait a child finish:%d\n",wpid);
}
else{
sleep(i);
printf("I'm %dth child,my pid=%d\n",i+1,getpid());
}
sleep(1);
return 0;
}
098-waitpid回收多个子进程
回收多个: 用while
循环;
int main(int argc,char* argv[]){
int i=0;
int wpid=0;
int pid=0;
for(i=0;i<5;++i){
pid=fork();
if(pid==0)
break;
}
if(i==5){
/*以非阻塞忙轮询的方式回收子进程*/
while((wpid=waitpid(-1,NULL,WNOHANG))!=-1){
if(wpid>0)
printf("wait chile:%d\n",wpid);
else if(wpid==0)
sleep(1);
}
}else{
sleep(i);
printf("I'm %dth child,my pid=%d\n",i+1,getpid());
}
sleep(1);
return 0;
}
099-wait和waitpid总结
waitpid(-1,&status,0)==wait(&status);
100-进程间通信常见方式
常见的进程间通信方式:
- 管道(使用最简单);
- 信号(开销最小);
- 共享映射区(可以用于无血缘关系的进程之间);
- 本地套接字(最稳定);
101-管道的特性
管道是一种最基本的IPC机制, 作用于有血缘关系的进程之间, 完成数据传递. 调用pipe
系统函数即可创建一个管道, 有如下特质:
- 其本质是一个伪文件(实为内核缓冲区);
- 有两个文件描述符引用, 一个表示读端, 一个表示写端;
- 规定数据从管道的写端流入管道, 从读端流出, 只能单向流动;
管道的原理: 管道实际为内核使用环形队列机制, 借助内核缓冲区(4k)实现;
管道的局限性:
- 数据不能进程自己写, 自己读;
- 管道中的数据不可反复读取, 一旦读走, 管道中不再存在;
- 采用半双工通信方式, 数据只能在单方向上流动;
- 只能在有公共祖先的进程之间使用管道;
102-管道的基本用法
pipe函数: 创建并打开管道
int pipe(int pipefd[2]);
pipefd[0]
-读端;
pipefd[1]
-写端;
成功返回0, 失败返回-1并设置errno
;
刚fork
完成时:
则父进程关闭读端, 子进程关闭写端, 数据就能在pipe
中单向流动, 父子进程能够完成通信;
int main(int argc,char* argv[]){
int ret=0;
int pipefd[2];
pid_t pid=0;
char* str="fuckyou\n";
char buf[1024];
/*创建管道,文件描述符保存在数组里*/
ret=pipe(pipefd);
if(ret==-1)
perr_exit("pipe error");
pid=fork();
if(pid>0){
close(pipefd[0]); //父进程关闭读端
write(pipefd[1],str,sizeof(str)); //向管道中写入数据
close(pipefd[1]); //父进程关闭写端
}else if(pid==0){
close(pipefd[1]); //子进程关闭写端
ret=read(pipefd[0],buf,sizeof(buf)); //从管道中读取数据
write(STDOUT_FILENO,buf,ret); //写到标准输出打印
close(pipefd[0]);
}
return 0;
}
103-管道读写行为
读管道:
1.管道中有数据, read
返回实际读到的字节数;
2.管道中无数据:
管道写端被全部关闭, read
返回0;
写端没有被全部关闭,read
阻塞等待(不久的将来可能会有数据抵达, 此时会让出CPU);
写管道:
1.管道读端全部被关闭, 进程异常终止(也可以捕捉SIGPIPE
信号, 使进程不终止);
2.管道读端没有全部关闭:
管道已满, write
阻塞;
管道未满, write
将数据写入, 并返回实际读到的字节数;
104-父子进程通信练习分析
ls | wc -l
:统计行数;
需要使用的函数:
- exec();
- dup2();
- pipe();
105-总结
106-复习
wait/waitpid
只能回收子进程, 爷孙的也不行;
107-父子进程lswc-l
int main(int argc,char* argv[]){
int fd[2];
pid_t pid;
int ret=0;
ret=pipe(fd);
if(ret==-1)
perr_exit("pipe error");
pid=fork();
if(pid==-1)
perr_exit("fork error");
if(pid>0){
/*父进程先读管道,如果子进程还没起来,他就会阻塞,这样子进程就会先于父进程结束*/
close(fd[1]);
dup2(fd[0],STDIN_FILENO);
execlp("wc","wc","-l",NULL);
perr_exit("execlp wc error");
}else if(pid==0){
/*子进程写管道*/
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);
execlp("ls","ls",NULL);
perr_exit("execlp ls error");
}
return 0;
}
108-兄弟进程间通信
上面的内容如果用兄弟进程间通信来做:
int main(int argc,char* argv[]){
int fd[2];
pid_t pid;
int ret=0;
int i=0;
/*创建管道*/
ret=pipe(fd);
if(ret==-1)
perr_exit("pipe error");
/*循环创建2个子进程*/
for(i=0;i<2;++i){
pid=fork();
if(pid==-1)
perr_exit("fork error");
if(pid==0)
break;
}
/*父进程关闭管道读写两端*/
if(i==2){
close(fd[0]);
close(fd[1]);
wait(NULL);
wait(NULL);
}else if(i==0){
/*兄进程关闭读端,将STDOUT指向fd[1]*/
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);
execlp("ls","ls",NULL);
perr_exit("execlp ls error");
}else if(i==1){
/*弟进程关闭写端,将STDIN指向fd[0]*/
close(fd[1]);
dup2(fd[0],STDIN_FILENO);
execlp("wc","wc","-l",NULL);
perr_exit("execlp wc error");
}
return 0;
}
注意创建完进程后, 父进程要将管道的读写两端全部关闭;
109-多个读写端操作管道和管道缓冲区大小
管道可以一个读端, 多个写端, 但是不建议这样做;
默认管道的大小是4k;
110-命名管道FIFO的创建和原理
匿名管道pipe
的优缺点:
为区分pipe
, 将FIFO
称为命名管道;
FIFO
可以用于不相关的进程间交换数据;
FIFO
是Linux基础文件类型中的一种, 但是FIFO文件在磁盘上没有数据块, 仅仅用来标识内核中的一条通道, 各进程可以打开这个文件进行read/write, 实际上是在读写内核通道, 这样就实现了进程间通信;
创建方式:
int mkfifo(const char* pathname,mode_t mode);
成功返回0, 失败返回-1并设置errno
;
111-FIFO实现非血缘关系进程间通信
用FIFO
进行通信几乎只有文件读写操作, 比较简单;
写进程
int main(int argc,char* argv[]){
int fd=0;
int i=0;
char buf[4096];
/*靠已经创建好的FIFO,如果命令行参数没给指定,报错*/
if(argc<2){
printf("Enter like this:./a.out fifoname\n");
return -1;
}
/*以只写方式打开FIFO文件,拿到fd*/
fd=open(argv[1],O_WRONLY);
if(fd==-1)
perr_exit("open error");
while(1){
/*将数据写到buf中*/
sprintf(buf,"fuckyou:%d\n",i++);
write(fd,buf,strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
读进程:
int main(int argc,char* argv[]){
int fd=0;
int i=0;
int len=0;
char buf[4096];
/*同样要依靠已经创建好的FIFO,从命令行参数中指定*/
if(argc<2){
printf("Enter like this:./a.out fifoname\n");
return -1;
}
/*以只读方式打开FIFO文件,拿到fd*/
fd=open(argv[1],O_RDONLY);
if(fd==-1)
perr_exit("open error");
/*从fd中读取数据,并写到标准输出上*/
while(1){
len=read(fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,len);
sleep(1);
}
close(fd);
return 0;
}
112-文件用于进程间通信
读普通文件不会造成read阻塞, 如果子进程睡1秒再写, 父进程由于刚开始读不到数据read
直接返回0;
没有血缘关系的进程也可以用文件进行进程间通信;
113-MMAP函数原型
存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射, 于是当从缓冲区中取数据, 就相当于读文件中的相应字节;
于此类似, 将数据存入缓冲区, 则相应的字节就自动写入文件, 这样就可以在不使用read和write
函数的情况下, 使用指针完成I/O操作;
使用这种方法, 首先应通知内核, 将一个文件映射到存储区域中, 这个映射工作可以通过mmap函数来实现;
void* mmap(void* addr, size_t length, int prot, int flags,int fd, off_t offset);
参数:
- addr:指定映射区的首地址, 通常传NULL, 表示让系统自动分配;
- length:共享内存映射区大小(<=文件的实际大小);
- prot:共享内存映射区的读写属性, PROT_READ, PROT_WRITE及PROT_READ|PROT_WRITE;
- flags:标注共享内存的共享属性, MAP_SHARED或MAP_PRIVATE(shared内存的变化会反映到文件上, private不会反映到文件上);
- fd:用于创建共享内存映射区的那个文件描述符;
- offset:偏移位置, 需是4k的整数倍. 默认0, 表示映射文件全部;
返回值:
- 成功返回映射区首地址;
- 失败返回
MAP_FAILED((void*)-1)
, 设置errno
;
114-复习
None;
115-MMAP建立映射区
int munmap(void* addr, size_t length);
int main(int argc,char* argv[]){
char* p=NULL;
int fd=0;
int len=0;
int ret=0;
fd=open("./testmap",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd==-1)
perr_exit("open error");
/*
lseek(fd,10,SEEK_END);
write(fd,"\0",1);
*/
ret=ftruncate(fd,20);
if(ret==-1)
perr_exit("ftruncate error");
len=lseek(fd,0,SEEK_END);
p=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(p==MAP_FAILED)
perr_exit("mmap error");
/*内存的写操作,会被映射到文件的写操作*/
strcpy(p,"fuckyou\n");
/*内存的读操作,会被映射到文件的读操作*/
printf("----%s\n",p);
/*与malloc一样,申请的内存要还回去*/
ret=munmap(p,len);
if(ret==-1)
perr_exit("munmap error");
return 0;
}
od -tcx filename
:以16进制查看文件;
116-MMAP使用注意事项1
1.可以, 但是要拓展文件大小, 否则会出现总线错误. 当然, 如果破罐子破摔, mmap时指定size=0, mmap会报错;
2.mmap会报错: 无效参数(注意ftruncte()函数需要写权限, 否则无法拓展文件大小). 如果都用只读权限, 不会出错. 要创建映射区, 文件必须有读权限;
3.没有影响, 建立完映射区后fd就能关闭;
4.mmap报错: 无效参数, 偏移量必须是4k的整数倍(因为MMU映射的最小单位就是4k);
5.小范围的越界问题不大, 但是最好不要这么做(操纵不安全的内存, 操作系统不给你保障);
6.不能成功. 与malloc一样, 释放的内存的指针必须是申请得来的初始的指针, 如果要改变指针的值, 拷贝一份用;
7.除了第一个参数, 后面的参数都可能导致失败;
8.会死的很难看;
117-MMAP使用注意事项2
总结使用注意事项:
所以MMAP的保险调用方式:
fd=open("filename",O_RDWR);
mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAX_SHARED,fd,0);
118-MMAP总结
119-父子进程间MMAP通信
必须指定内存映射区为shared
属性, 如果指定了private
属性, 内核只会给子进程mmap的拷贝, 不会给他真正的mmap;
int main(int argc,char* argv[]){
int* p;
pid_t pid;
int fd=0;
int ret=0;
/*先打开(创建)一个文件*/
fd=open("temp",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd==-1)
perr_exit("open error");
ftruncate(fd,4);
/*创建映射区*/
p=(int*)mmap(NULL,4,PROT_WRITE|PROT_READ,MAP_SHARED,fd,0);
if(p==MAP_FAILED)
perr_exit("mmap error");
/*创建完就可以关闭文件了*/
close(fd);
/*创建子进程*/
pid=fork();
if(pid==-1)
perr_exit("fork error");
if(pid==0){
/*子进程写一个int*/
*p=2000;
var=1000;
printf("child:*p=%d,var=%d\n",*p,var);
}else if(pid>0){
/*父进程读出这个int*/
sleep(1);
printf("parent:*p=%d,var=%d\n",*p,var);
/*回收子进程*/
wait(NULL);
/*归还映射区给内存池*/
ret=munmap(p,4);
if(ret==-1)
perr_exit("munmap error");
}
return 0;
}
120-无血缘关系进程间MMAP通信
先来认识一个内存操作函数:
void* memcpy(void* dest, const void* src, size_t n);
写进程:
int main(int argc,char* argv[]){
struct student stu={1,"xiaming",18};
struct student* p;
int fd=0;
/*打开或创建一个文件*/
fd=open("temp",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd==-1)
perr_exit("open error");
ftruncate(fd,sizeof(stu));
/*建立内存映射区*/
p=(struct student*)mmap(NULL,sizeof(stu),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(p==MAP_FAILED)
perr_exit("mmap error");
/*循环使用memcpy向内存映射区中写入数据,并修改stu的id值*/
while(1){
memcpy(p,&stu,sizeof(stu));
stu.id++;
sleep(1);
}
/*归还内存映射区给内存池*/
munmap(p,sizeof(stu));
close(fd);
return 0;
}
读进程:
int main(int argc,char* argv[]){
struct student stu;
struct student* p;
int fd=0;
fd=open("temp",O_RDONLY);
if(fd==-1)
perr_exit("open error");
/*建立内存映射区*/
p=(struct student*)mmap(NULL,sizeof(stu),PROT_READ,MAP_SHARED,fd,0);
if(p==MAP_FAILED)
perr_exit("mmap error");
/*循环读出内存映射区中的数据*/
while(1){
printf("id=%d,name=%s,age=%d\n",p->id,p->name,p->age);
sleep(1);
}
/*归还内存映射区给内存池*/
munmap(p,sizeof(stu));
close(fd);
return 0;
}
121-MMAP总结
mmap相当于文件, 所以可以反复读取, 不像FIFO
;
122-MMAP匿名映射区
int main(int argc,char* argv[]){
pid_t pid=0;
int* p=NULL;
int ret=0;
/*创建匿名映射区*/
p=(int*)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
if(p==MAP_FAILED)
perr_exit("mmap error");
pid=fork();
if(pid==-1)
perr_exit("fork error");
if(pid==0){
/*子进程向映射区中写*/
*p=9527;
var=200;
printf("I'm child,*p=%d,var=%d\n",*p,var);
}else if(pid>0){
/*父进程从映射区中读,然后回收子进程*/
sleep(1);
printf("I'm parent,*p=%d,var=%d\n",*p,var);
wait(NULL);
/*父进程归还映射区*/
ret=munmap(p,4);
if(ret==-1)
perr_exit("munmap error");
}
return 0;
}
/dev/zero
-文件白洞, 里面有无限量的’\0’, 要多少有多少;
/dev/null
-文件黑洞, 可以写入任意量的数据;
所以在创建映射区时可以用zero文件, 就不用自己创建文件然后拓展大小了;
无血缘关系进程间通信, 不能用匿名映射;
123-总结
124-复习
/dev/zero
文件也不能用于无血缘关系进程间通信;
125-信号的概念和机制
信号的共性:
- 简单;
- 不能携带大量信息;
- 满足特性条件才能发送;
信号的特质:
所有信号的产生和处理, 都是由内核完成的;
126-与信号相关的概念
产生信号:
- 按键产生:Ctrl+c, Ctrl+z, Ctrl+;
- 系统调用产生:kill, raise, abort;
- 软件条件产生:定时器alarm;
- 硬件异常产生:非法访问内存(段错误), 除0(浮点数例外), 内存对齐错误(总线错误);
- 命令产生:kill命令;
递达:内核发出的信号递送并且到达进程;
未决:产生和递达之间的状态, 主要由于阻塞(屏蔽)导致该状态;
信号的处理方式:
- 执行默认动作;
- 丢弃(忽略);
- 捕捉(调用户处理函数);
127-信号屏蔽字和未决信号集
两者都是位图;
阻塞信号集(信号屏蔽字):将某些信号加入集合, 对他们设置屏蔽, 当屏蔽x信号后, 再收到该信号, 该信号的处理将推后(直到解除屏蔽后);
未决信号集:
- 信号产生后, 未决信号集中描述该信号的位立刻翻转为1, 表示信号处于未决状态, 当信号被处理后, 对应位翻转回0, 这一时刻非常短暂;
- 信号产生后由于某些原因(主要是阻塞)不能抵达, 这类信号的集合称为未决信号集, 在屏蔽解除前, 信号一直处于未决状态;
128-信号四要素和常规信号一览
前31个位常规信号, 有默认事件和处理动作. 后面的是实时信号, 没有默认事件和处理动作;
信号四要素:
- 编号;
- 名称;
- 事件;
- 默认处理动作
后面有多个值的信号是因为不同的操作系统(处理器架构不同);
常规信号一览:
信号的默认处理动作:
- Term: 终止进程;
- Ign: 忽略信号(默认即时对该种信号忽略操作)
- Core: 终止进程, 生成Core文件(查验进程死亡原因, 用于gdb调试);
- Stop: 暂停进程;
- Cont: 继续运行进程;
SIGKILL(9)
和SIGSTOP(19)
, 不允许忽略和捕捉, 只能执行默认动作, 甚至不能将其设置为阻塞;
只有每个信号所对应的事件发生了, 该信号才会被递送(但不一定递达), 不应该乱发信号;
129-kill函数和kill命令
int kill(pid_t pid, int sig); //send signal to a process
一个弑父的例子:
int main(int argc,char* argv[]){
pid_t pid=fork();
if(pid>0){
printf("I'm parent,pid=%d\n",getpid());
while(1);
}else if(pid==0){
printf("I'm child,pid=%d,ppid=%d\n",getpid(),getppid());
sleep(2);
kill(getppid(),SIGKILL);
}
return 0;
}
pid的不同取值:
kill -9 -10698
-杀死10698进程组的所有进程;
关于发送权限:发送者实际有效的用户ID==接收者实际有效的用户ID;
如果你想杀死1号进程, 是不允许的;
130-alarm函数
unsigned int alarm(unsigned int seconds);
测试一秒钟数多少个数:
int main(int argc,char* argv[]){
int i=0;
int j=0;
/*设置闹钟为1s,1s后自动结束进程*/
alarm(1);
/*死循环打印计数*/
while(1){
printf("i=%d,j=%d\n",i,j);
++i;
j++;
}
return 0;
}
使用time
命令查看程序执行时间占用情况:
程 序 实 际 执 行 时 间 = 系 统 时 间 + 用 户 时 间 + 等 待 时 间 程序实际执行时间=系统时间+用户时间+等待时间 程序实际执行时间=系统时间+用户时间+等待时间
向屏幕打印的情况下进程的时间占用情况:
重定向到文件, 进程的时间占用情况:
程序运行的瓶颈在于IO, 要优化程序, 首选优化IO;
131-settimer函数
int setitimer(int which, const struct itimerval* new_value, struct itimerval* old_value);
struct itimerval {
struct timeval it_interval; /* Interval for periodic timer */
struct timeval it_value; /* Time until next expiration */
};
/*精确到us的时间结构体*/
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
成功返回0, 失败返回-1并设置errno
;
参1which
指定定时方式:
- 自然定时:ITIMER_REAL->SIGALRM;
- 用户空间计时(只计算进程占用CPU的时间):ITIMER_VIRTUAL->SIGVTALARM;
- 运行时计时(用户+内核):ITIMER_PROF->SIGPROF;
参2是传入参数;
参3是传出参数;
it_interval
:设定两次定时任务之间的时间间隔;
it_value
:定时的时长(等it_value
秒后触发闹钟, 以后每隔it_interval
触发一次);
/*信号捕捉回调函数*/
void myfunc(int signo){
printf("fuckyou\n");
return;
}
int main(int argc,char* argv[]){
/*注册信号捕捉函数*/
signal(SIGALRM,myfunc);
int ret=0;
/*这是一个传出参数*/
struct itimerval oldit;
/*传入参数,进行初始化*/
struct itimerval it;
it.it_value.tv_sec=2;
it.it_value.tv_usec=0;
it.it_interval.tv_sec=5;
it.it_interval.tv_usec=0;
/*设置闹钟*/
ret=setitimer(ITIMER_REAL,&it,&oldit);
if(ret==-1)
perr_exit("setitimer error");
/*手动让程序阻塞*/
while(1);
return 0;
}
132-午后回顾
133-信号集操作函数
/*自定义信号集*/
sigset_t set;
/*全部清空*/
int sigemptyset(sigset_t* set);
/*全部置1*/
int sigfillset(sigset_t* set);
/*将一个信号添加到集合当中*/
int sigaddset(sigset_t* set, int signum);
/*将一个信号从集合中移除*/
int sigdelset(sigset_t* set, int signum);
/*判断某一信号是否在集合当中*/
int sigismember(const sigset_t* set, int signum);
sigpromask
函数:
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
用于屏蔽信号或解除屏蔽, 本质是读取或修改进程PCB中的信号屏蔽字;
屏蔽信号, 只是将信号处理延后执行(延至解除屏蔽);而忽略表示将该信号丢弃处理;
how:
- SIG_BLOCK:设置阻塞, set表示需要屏蔽的信号;
- SIG_UNBLOCK:设置非阻塞, set表示需要解除屏蔽的信号;
- SIG_SETMASK:用set替换原始屏蔽集;
set
:传入参数, 是一个位图, set中哪位置1, 就表示当前进程屏蔽哪个信号;
oldset
:传出参数, 保存旧的信号屏蔽集;
sigpending
函数:读取当前进程的未决信号集;
int sigpending(sigset_t* set);
set
是传出参数;
返回值: 成功返回0, 失败返回-1并设置errno
;
134-信号集操作函数使用原理分析
操作信号集的若干步骤:
/*创建一个自定义信号集*/
sigset_t set;
/*清空自定义信号集*/
sigemptyset(&set);
/*向自定义信号集添加信号*/
sigaddset(&set,SIGINT);
/*用自定义信号集操作内核信号集*/
sigprocmask(SIG_BLOCK,&set);
/*查看未决信号集*/
sigpending(&myset);
135-信号集操作函数练习
Ctrl+D
是向终端中写入一个EOF
;
void print_set(sigset_t* set){
int i=0;
for(i=1;i<32;++i){
if(sigismember(set,i))
putchar('1');
else
putchar('0');
}
printf("\n");
}
int main(int argc,char* argv[]){
sigset_t set,oldset,pedset;
int ret=0;
sigemptyset(&set);
/*屏蔽掉Ctrl+c*/
sigaddset(&set,SIGINT);
/*屏蔽掉Ctrl+\*/
sigaddset(&set,SIGQUIT);
/*屏蔽掉SIGBUS*/
sigaddset(&set,SIGBUS);
/*屏蔽掉SIGKILL-不灵*/
sigaddset(&set,SIGKILL);
/*设置到内核*/
sigprocmask(SIG_BLOCK,&set,&oldset);
if(ret==-1)
perr_exit("sigpending error");
while(1){
ret=sigpending(&pedset);
if(ret==-1)
perr_exit("sigpending error");
print_set(&pedset);
sleep(1);
}
return 0;
}
注意点: 对于SIGKILL
信号, 即使设置了信号屏蔽, 依然能kill;
136-signal实现信号捕捉
/*定义回调函数类型,很不幸,函数类型限制死了*/
typedef void (*sighandler_t)(int);
/*注册信号捕捉函数*/
sighandler_t signal(int signum, sighandler_t handler);
该函数由ANSI定义, 由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为, 因此应尽量避免使用它, 取而代之使用sigaction
函数;
void catch_signal(int signum){
printf("Catch you!!!%d\n",signum);
}
int main(int argc,char* argv[]){
/*注册信号捕捉函数*/
signal(SIGINT,catch_signal);
/*让程序阻塞*/
while(1);
return 0;
}
137-sigaction实现信号捕捉
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_t sa_mask; //只工作于信号捕捉函数执行期间,相当于中断屏蔽
int sa_flags; //本信号默认屏蔽
void (*sa_restorer)(void); //废弃
};
一个Demo:
void catch_signal(int signum){
if(signum==SIGINT)
printf("Catch you SIGINT:%d\n",signum);
else if(signum==SIGQUIT)
printf("Catch you SIGQUIT:%d\n",signum);
}
int main(int argc,char* argv[]){
int ret=0;
/*前者传入,后者传出*/
struct sigaction act,oldact;
/*设置回调函数*/
act.sa_handler=catch_signal;
/*设置回调函数的信号屏蔽字*/
sigemptyset(&act.sa_mask);
act.sa_flags=0;
/*捕捉SIGINT信号*/
ret=sigaction(SIGINT,&act,&oldact);
if(ret==-1)
perr_exit("sigaction error");
/*捕捉SIGQUIT信号*/
ret=sigaction(SIGQUIT,&act,&oldact);
if(ret==-1)
perr_exit("sigaction error");
while(1);
return 0;
}
138-信号捕捉的特性
- 捕捉函数执行期间, 信号屏蔽字由
mask
变为sigaction
结构体中的sa_mask
, 捕捉函数执行结束后, 恢复回mask
; - 捕捉函数执行期间, 本信号自动被屏蔽
(sa_flags=0)
; - 捕捉函数执行期间, 若被屏蔽信号多次发送, 解除屏蔽后只响应一次;
139-内核实现信号捕捉简析
为什么执行完信号处理函数后要再次进入内核?
因为信号处理函数是内核调用的, 函数执行完毕后要返回给调用者;
140-借助信号捕捉回收子进程
void catch_child(int signum){
pid_t wpid;
/*
*这里要用循环回收多个子进程:
*如果只简单的调用一次wait,会造成当多个子进程同时发信号时,父进程只能响应一次回收过程,
*而设置了while相当于自动提醒父进程要检查还有没有未回收的子进程,知道都回收完了,返回-1.
*/
while((wpid=wait(NULL))!=-1){
printf("Catch child:%d\n",wpid);
}
return;
}
int main(int argc,char* argv[]){
pid_t pid;
int i=0;
/*
*那么如何解决父进程注册信号捕捉函数时子进程无法回收的问题呢(下面sleep(1)的问题)?
*先设置信号屏蔽,将子进程发来的信号屏蔽在未决信号集上,
*后面一旦信号集被打开,父进程就会处理未决信号集中的信号,
*进入到信号捕捉函数,while(wait(NULL))自动提醒父进程检查回收子进程
*/
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_BLOCK,&set,NULL);
/*循环创建15个子进程*/
for(i=0;i<15;++i)
if((pid=fork())==0)
break;
if(i==15){
struct sigaction act,oldact;
act.sa_handler=catch_child;
sigemptyset(&(act.sa_mask));
act.sa_flags=0;
/*
*这里设置sleep(1)只是为了模拟如果父进程注册信号的时间很长,
*在这期间子进程已经死亡而发送的信号没有被父进程响应.
*/
sleep(1);
sigaction(SIGCHLD,&act,&oldact);
/*此处将阻塞解除,父进程能拿到SIGCHLD信号*/
sigprocmask(SIG_UNBLOCK,&set,NULL);
printf("I'm parent,pid=%d\n",getpid());
while(1);
}
else
printf("I'm child,pid=%d\n",getpid());
return 0;
}
要注意的点已经写在注释里了, 如果有的地方不小心有纰漏, 很可能会造成产生僵尸进程;
141-慢速系统调用中断
142-总结
setitimer可以实现高精度定时;
143-复习子进程借助信号回收
None;
144-会话
会话的概念: 多个进程组的集合;
setsid
函数:
创建一个会话, 并以自己的ID设置进程组ID, 同时也是新会话的ID;
pid_t setsid(void);
成功返回调用进程的会话ID, 失败返回-1并设置errno
;
调用了setsid
函数的进程, 既是新的会长, 也是新的组长;
145-守护进程创建步骤分析
- 创建子进程, 父进程退出: 所有工作在子进程中形式上脱离了控制终端;
- 在子进程中创建新会话: setsid()函数, 使子进程完全独立出来, 脱离控制;
- 改变当前工作目录位置: chdir()函数, 防止占用可卸载的文件系统;
- 重设文件权限掩码: umask()函数, 防止继承的文件创建屏蔽字拒绝某些权限;
- 关闭文件描述符: 继承的打开文件不会用到, 浪费系统资源, 无法卸载;
- 开始执行守护进程核心工作;
146-守护进程创建
int main(int argc,char* argv[]){
pid_t pid=0;
int ret=0;
int fd=0;
pid=fork();
if(pid>0)
exit(0);
/*创建新会话*/
pid=setsid();
if(pid==-1)
perr_exit("setsid error");
ret=chdir("/home/daniel");
if(ret==-1)
perr_exit("chdir error");
/*重设文件权限掩码*/
umask(0022);
/*关闭标准输入*/
close(STDIN_FILENO);
/*将标准输出和标准出错重定向到文件黑洞*/
fd=open("/dev/null",O_RDWR);
if(fd==-1)
perr_exit("open error");
dup2(fd,STDOUT_FILENO);
dup2(fd,STDERR_FILENO);
/*模拟业务逻辑*/
while(1);
return 0;
}
147-线程概念
什么是线程:
LWP:轻量级进程, 本质仍然是进程;
进程: 有独立的地址空间, 有PCB;
线程: 有独立的PCB, 但是没有独立的地址空间(共享);
所以二者区别就在于是否共享地址空间;
线程: 最小的执行单位;
进程: 最小分配资源的单位, 可以看作是只有一个线程的进程;
ps -Lf pid
查看一个进程开的线程个数;
148-三级映射
PCB中持有当前进程的页目录表的指针, 页目录表中每一项指向一个个页表, 用页表检索物理内存页面;
149-线程共享和非共享
线程之间共享的资源:
- 文件描述符表;
- 每种信号的处理方式;
- 当前工作目录位置;
- 用户ID和组ID;
- 内存地址空间(.text/.data/.bss/.heap/共享库);
线程非共享资源:
- 线程id;
- 处理器现场和栈指针(内核栈);
- 独立的栈空间(用户空间栈);
- errno变量;
- 信号屏蔽字;
- 调度优先级;
150-中午复习
线程号不是线程ID;
151-创建线程
获取线程id:
pthread_t pthread_self(void);
创建线程:
int pthread_create(pthread_t* thread,const pthread_attr_t* attr,void* (*start_routine)(void* ),void* arg);
成功返回0, 失败返回errno
;
一个例子:
void* tfn(void* arg){
printf("tfn:pid=%d,tid=%lu\n",getpid(),pthread_self());
return NULL;
}
int main(int argc,char* argv[]){
int ret=0;
pthread_t tid=0;
printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
ret=pthread_create(&tid,NULL,tfn,NULL);
if(ret!=0)
perr_exit("pthread_create error");
/*父进程等待1秒,否则父进程一旦退出,地址空间被释放,子线程没机会执行*/
sleep(1);
return 0;
}
152-循环创建多个子线程
void* tfn(void* arg){
int i=(int)arg;
printf("I'm %dth thread,pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
sleep(i);
return NULL;
}
int main(int argc,char* argv[]){
int i=0;
int ret=0;
pthread_t tid=0;
/*循环创建多个子线程*/
for(i=0;i<5;++i){
ret=pthread_create(&tid,NULL,tfn,(void*)i);
if(ret!=0)
perr_exit("pthread_create error");
}
sleep(i);
return 0;
}
注意参数传递方式, 先将int
型的i
强转成void*
传入, 用到时再强转回int
型;
153-错误分析
如果不用强转, 看似规规矩矩的传地址再解引用, 会出现问题:
/*这是一个出错的版本*/
void* tfn(void* arg){
int i=*((int*)arg);
printf("I'm %dth thread,pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
sleep(i);
return NULL;
}
int main(int argc,char* argv[]){
int i=0;
int ret=0;
pthread_t tid=0;
for(i=0;i<5;++i){
ret=pthread_create(&tid,NULL,tfn,(void*)&i);
if(ret!=0)
perr_exit("pthread_create error");
}
sleep(i);
return 0;
}
错误分析:
main
中给tfn
传入的是他的函数栈帧中局部变量i的地址, 这样tfn
能随时访问到i的值, 考虑到线程之间是并发执行的, 每次中main
中固定的地址中拿数据, 相当于各个线程共享了这块地址, 由于访问时刻随机, 所以访问到的各个值也是很随机的;
使用强转可以保证变量i的实时性(C语言值传递的特性);
154-线程间全局变量共享
线程默认共享数据段, 代码段等地址空间, 常用的是全局变量, 而进程不共享全局变量, 只能借助mmap
;
155-pthrea_exit退出
void pthread_exit(void* retval);
retval
表示退出状态, 通常传NULL
;
exit()
函数用来退出当前进程, 不可以用在线程中, 否则直接一锅端了;
pthread_exit()
函数才是用来将单个的线程退出;
pthread_exit
或者return
返回的指针所指向的内存单元必须是全局的或者malloc分配的, 不能在线程函数的栈上分配, 因为其他线程得到这个返回指针时线程函数已经退出了;
void* tfn(void* arg){
int i=(int)arg;
/*当i循环到2,退出当前线程*/
if(i==2)
pthread_exit(NULL);
printf("I'm %dth thread,pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
sleep(i);
return NULL;
}
156-pthread_join函数
int pthread_join(pthread_t thread, void** retval);
成功返回0, 失败返回errno
线程的退出状态是void*
, 回收时传的就是void**
;
struct thrd{
int var;
char str[256];
};
void* tfn(void* arg){
/*在堆区创建一个结构体*/
struct thrd* tval;
tval=(struct thrd*)malloc(sizeof(struct thrd));
/*给结构体赋值*/
tval->var=100;
strcpy(tval->str,"fuckyou");
return (void*)tval;
}
int main(int argc,char* argv[]){
pthread_t tid=0;
struct thrd* retval;
int ret=0;
ret=pthread_create(&tid,NULL,tfn,NULL);
if(ret==-1)
perr_exit("pthread_create error");
/*pthread_join回收子线程*/
ret=pthread_join(tid,(void**)&retval);
if(ret==-1)
perr_exit("pthread_join error");
/*打印结构体中的信息*/
printf("child thread exit with var=%d,str=%s\n",retval->var,retval->str);
pthread_exit(NULL);
return 0;
}
注意一个错误的写法:
void* tfn(void* arg){
/*在堆区创建一个结构体*/
struct thrd tval;
/*给结构体赋值*/
tval.var=100;
strcpy(tval.str,"fuckyou");
return (void*)&tval;
}
不能将子线程的回调函数的局部变量返回, 由于该函数执行完毕返回后, 其栈帧消失, 栈上的局部变量也就消失, 返回的是无意义的;
当然, 可以在main
函数中创建局部变量;
157-pthread_join作业
使用pthread_join
函数将循环创建的多个子线程回收;
定义一个tid
数组, 保存不同子线程的tid
;
158-线程分离pthread_detach
int pthread_detach(pthread_t thread);
子线程分离后不能再调用join
回收了:
void* tfn(void* arg){
printf("tfn:pid=%d,tid=%lu\n",getpid(),pthread_self());
return NULL;
}
int main(int argc,char* argv[]){
int ret=0;
pthread_t tid=0;
ret=pthread_create(&tid,NULL,tfn,NULL);
if(ret!=0){
fprintf(stderr,"pthread_create error%s\n",strerror(ret));
exit(1);
}
/*设置线程分离*/
ret=pthread_detach(tid);
if(ret!=0){
fprintf(stderr,"pthread_detach error%s\n",strerror(ret));
exit(1);
}
sleep(1);
/*这里回出错:不能对一个已经分离出去的子线程回收*/
ret=pthread_join(tid,NULL);
if(ret!=0){
fprintf(stderr,"pthread_join error%s\n",strerror(ret));
exit(1);
}
printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
pthread_exit(NULL);
return 0;
}
159-检查出错返回
detach
: 设置线程分离, 线程终止会自动清理pcb, 无需回收;
detach
相当于自动回收, join
相当于手动回收;
注意检查出错方式的变化(失败会直接返回errno
);
if(ret!=0){
fprintf(stderr,"pthread_detach error%s\n",strerror(ret));
exit(1);
}
160-pthread-cancel函数
函数原型:
int pthread_cancel(pthread_t thread);
应用:
void* tfn(void* arg){
while(1){
printf("tfn:pid=%d,tid=%lu\n",getpid(),pthread_self());
sleep(1);
}
return NULL;
}
int main(int argc,char* argv[]){
int ret=0;
pthread_t tid=0;
printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
ret=pthread_create(&tid,NULL,tfn,NULL);
if(ret!=0)
perr_exit("pthread_create error",ret);
/*等待5秒之后杀死子线程*/
sleep(5);
ret=pthread_cancel(tid);
if(ret!=0)
perr_exit("pthread_cancel error",ret);
while(1);
pthread_exit(NULL);
return 0;
}
cancel
必须要等待取消点(进入内核的契机), 所以如果一个线程一直使用系统调用(一直不进内核), cancel
就无法杀死该线程;
可以手动添加一个取消点:
pthread_testcancel();
161-进程和线程控制原语对比
162-线程属性设置分离线程
先初始化线程属性, 再pthread_create
创建线程;
/*初始化线程属性:成功返回0,失败返回errno*/
int pthread_attr_init(pthread_attr_t* attr);
/*销毁线程属性所占用的资源:成功返回0,失败返回errno*/
int pthread_attr_destroy(pthread_attr_t* attr);
线程分离状态函数:
/*设置线程属性:分离或非分离*/
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate);
/*获取线程属性*/
int pthread_attr_getdetachstate(const pthread_attr_t* attr, int* detachstate);
detachstate
取值:
PTHREAD_CREATE_DETACHED
PTHREAD_CREATE_JOINABLE
一个例子:
int main(int argc,char* argv[]){
int ret=0;
pthread_t tid=0;
pthread_attr_t attr;
/*初始化属性结构体*/
ret=pthread_attr_init(&attr);
if(ret!=0)
perr_exit("pthread_attr_init error",ret);
/*给属性结构体添加分离属性*/
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
if(ret!=0)
perr_exit("pthread_attr_setdetachstate error",ret);
printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
/*创建子线程*/
ret=pthread_create(&tid,&attr,tfn,NULL);
if(ret!=0)
perr_exit("pthread_create error",ret);
/*join一下试试,由于线程已经分离了,会出错*/
ret=pthread_join(tid,NULL);
if(ret!=0)
perr_exit("pthread_join error",ret);
/*销毁线程属性结构体*/
ret=pthread_attr_destroy(&attr);
if(ret!=0)
perr_exit("pthread_attr_destory error",ret);
pthread_exit(NULL);
return 0;
}
如果为了突出逻辑, 将所有错误检查都去掉, 还是比较简单的:
int main(int argc,char* argv[]){
int ret=0;
pthread_t tid=0;
pthread_attr_t attr;
/*初始化属性结构体*/
ret=pthread_attr_init(&attr);
/*给属性结构体添加分离属性*/
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
/*创建子线程*/
ret=pthread_create(&tid,&attr,tfn,NULL);
/*销毁线程属性结构体*/
ret=pthread_attr_destroy(&attr);
pthread_exit(NULL);
return 0;
}
各个子线程会均分进程的栈空间, 但是线程的栈空间大小是可以调整的;
163-线程使用注意事项
164-总结
165-线程同步概念
线程同步: 一个线程发出某一功能调用时, 再没有得到结果之前, 该调用不返回. 同时其他线程为保证数据的一致性, 不能调用该功能;
避免产生与时间有关的错误;
166-锁使用注意事项
Linux提供的