课程链接:https://www.bilibili.com/video/BV1KE411q7ee

课程视频资源和笔记: 链接:https://pan.baidu.com/s/150gSAFxTGBaBF-wb6yZfhQ 提取码:unix

练习代码:https://github.com/Daniel741312/linux-system-programming

Linux命令基础

$ echo $SHELL            #查看当前正在使用的命令解析器
$ cat /etc/shells       #查看支持的所有shell
$ ls -l file            #查看file的详细信息
Ctrl+h  #BackSpace
Ctrl+a #回到命令首
Ctrl+e #回到命令尾
Ctrl+u #删除所有内容

类Unix系统目录:

  • bin-存放可执行文件
  • boot-存放OS启动例程
  • dev-存放设备文件(Linux系统中所见皆文件)
  • etc-存放用户信息, 如密码等, 来源于拉丁语全称etcetera, “等等”的意思
  • home-用户的家目录
  • lib-库目录
  • media和mnt-挂载磁盘的设备文件
  • opt和proc-进程相关
  • usr-Unix Software Resource
  • root-管理员宿主目录(家目录)

目录和文件操作

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-拷贝目录

more-分屏显示文件内容, 空格翻页

less同理

软连接和硬连接

ln -s hello.c hello.c.s-创建软连接


软连接中存的就是文件的路径, 路径有几个字符就占几个字节, 所以建议用绝对路径创建软连接

另外注意文件的权限, 软连接的权限代表其本身的权限, 与指向的目的文件无关

ln hello.c hello.c.h-创建硬链接

创建硬链接会增加文件FCB的硬链接计数

这些硬链接指向同一个文件, 修改一个其余的会同步变化

查看文件状态:


所有的硬链接有相同的Inode(文件统一id)

删除只是把硬链接计数-1

创建修改用户和用户组

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-删除用户组

find命令

目录紧跟在find之后

find ./ -type 'l'找当前目录下的软连接, 子目录会递归进入

find ./ -name '*.jpg'-找当前目录下的jpg文件, 子目录会递归进入

find ./ -maxdepth 3 -name '*.jpg'-指定目录层级深度为3层

find ./ -size +20M -size -50M-指定大小范围

ls -h-以人类可读的方式显示结果

man手册中反斜杠/可以用于查找关键字, 来自于vim

关于size的单位(find默认用b)


按时间查找:

  • -atime(access访问时间)
  • -ctime(change更改时间)
  • -mtime(modify改动时间)

find ./ -ctime 3查找三天内被改动的文件

find /usr/ -name "\*temp\*" -exec ls -l {} \;

大括号表示前面命令返回的结果集, 对其指定-exec后面的命令, \;是转义后的;

find ~/ -type f -ok rm -r {} \;

exec的缓冲版, 操作前会询问, 保证安全性;

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

-execxargs的区别:前者会将结果不论多少一股脑的交给-exec, 而xargs会做分片处理(效率更高)

创建名字中有空格的文件:

$ touch abc\ def
$ touch "abc def"

由于xargs会将文件名中的空格误认为是分隔符, 解决方式: 控制分隔符:

find /usr/ -maxdepth 3 -type f -print0 | xargs -0 ls -l

软件安装

sudo vim /etc/apt/sources.list   #更新源服务器列表
sudo apt-get update     #更新源
sudo apt-get install tree   #安装软件
sudo apt-get remove tree    #卸载软件
sudo dpkg [-i|--install] xxx.deb    #通过deb包安装

源码安装三部曲

cd dir
./configure
sudo make && make install

文件压缩

压缩文件:

tar -zcvf test.tar.gz 039_serverMultiProcess.c hello.c repository

系统中真正进行压缩的是gzip, 解压缩是gunzip, 但是gzip只能一对一压缩

tar命令实际上是用于打包的, 参数z就是用gzip进行压缩, ccreate的意思, v表示可见, f表示生成文件

file是文件照妖镜, 看文件属性

bzip2gzip类似, 都是单个文件所用, 如果使用bzip2进行压缩

tar -jcvf test2.tar.gz 039_serverMutiProcess.c hello.c repository

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

cat &让cat去后台运行

则用jobs可以将其拿出来查看(查看OS后面用户的作业)

fgbg前后台切换

env查看环境变量

env | grep SHELL

top是调出任务管理器

sudo passwd daniel改密码

ifconfig查看网卡信息

man手册:


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–

free -m查看空闲内存

一个目录所占的磁盘大小为4K

vim

命令模式下:

I->光标到行首, 插入

A->光标到行尾, 插入

S->直接干掉整行, 切换到文本模式书写

末行模式下直接输入行号就可以跳转到指定行

命令模式下的%可以跳转到匹配的括号

d0->删到行首

找设想内容: 命令模式下输入斜杠/, 然后输入查找的内容, 进行查找

回车后按n找到下一个

找看到的内容: 在一定范围内检索单词: 在单词名字上星号(向后)*, 或者井号(向前)#

替换: 在本行的末行模式下: :s /printf/println

通篇替换: 只会替换每一行的首个: :%s /printf/println

如果想要把每行后面的也替换, 加参数g

局部替换: :30,37 s /int/unsigned int/g

Ctrl+r反撤销;

:sp水平分屏;

[d查看宏定义;

! gcc hello.c -o hello在文件中执行命令;

vim配置文件路径: ~/.vimrc

C程序的编译与调试

gcc

预处理:gcc -E hello.c -o hello.i
编译:gcc -S hello.c -o hello.S
汇编:gcc -c hello.c -o hello.o
链接:ld hello.o

指定头文件位置: gcc hello.c -I ./headers -o hello

向程序中动态注册一个宏-D:gcc hello.c -o hello -D HELLO, 这种宏定义常可以做开关使用

静态库和动态库

因为每一个可执行文件都要包含进静态库的内容,所以静态库会大量占用存储空间

而对于动态库,内存中只需要保留一份库的备份,其他进程需要时转入执行即可

二者的适合场景:

  • 静态库: 对空间要求较低, 对时间要求较高
  • 动态库: 对时间要求较低, 对空间要求较高

静态库制作

ar rcs libMyLib.a add.o sub.o div1.o

先用gcc的-c参数将源文件编译成二进制文件, 再用ar命令封装静态库

如果直接编译, collect2是链接器, 报错了


说明链接阶段出错;

将库直接加入编译的源文件中就可以了:gcc test.c libMyMath.a -o test1

隐式声明: 编译过程中没有遇到函数定义和函数声明, 编译器会帮你做隐式声明

但是这种隐式声明只能对于返回值为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

动态库制作:

将源文件.c编译为目标文件.o, 生成与位置无关的代码, 借助参数-fPIC

编译生成hello.o的时候, 各个函数的地址还是相对于main的地址, 链接阶段填入main的地址

由于动态库的函数在库里, 不能像程序内部的函数一样直接填入main的地址, 动态函数在a.out中没有位置, 依赖于@plt, 进行延迟绑定

查看二进制文件的反汇编代码:objdump -dS test

输出重定向:objdump -dS test > test.s

动态库制作过程:

$ tree
.
├── demo.c
├── math.c
├── math.h#将math制作成动态库
$ gcc -c -fPIC math.c   #将.c文件生成.o文件(生成与位置无关的代码-fPIC)
$ gcc -shared math.o -o libmymath.so    #使用gcc -shared制作动态库
$ gcc demo.c -lmymath -L./      #-l 指定库名, -L 指定库路径
$ ./a.out
./a.out: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory  #编译通过,执行出错

首先要清楚两个没有任何关系的链接器:

  • 链接器:工作于链接阶段, 工作时需要指定-l和-L参数, 上面已经指定了;
  • 动态链接器:工作于程序运行阶段, 工作时需要提供动态库所在目录;

动态链接器工作时要根据环境变量LD_LIBRARY_PATH寻找动态库

临时设置环境变量export LD_LIBRARY_PATH=./lib,但是bash重启后这个设置就会失效

要想永久指定, 需要将其写入bashrc配置文件,然后重启bash或source ~/.bashrc

ldd a.out可以查看程序运行所需要的动态库

$ ldd a.outlinux-vdso.so.1 (0x00007ffe94bfc000)libmymath.so => ./libmymath.so (0x00007f1672067000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1671c76000)/lib64/ld-linux-x86-64.so.2 (0x00007f167246b000)

最后一种方法:修改配置文件法: sudo vim /etc/ld.so.conf, 写入动态库绝对路径, 保存

sudo ldconfig -v, 使配置文件生效

数据段合并:

为了节省内存, 将只读的.rodata和只读的.text段合并到一页内存

同样的也将.bss.data合并到一页内存

gdb调试

gdb a.out        #开始调试
list 1          #从第一行开始显示源码, 后面再展开用l
layout src|asm|split    #显示源码|汇编|都显示
break 52        #在第52行设置断点
run             #开始执行, 到断点暂停
start           #运行程序并在main函数自动暂停
next            #下一个, 转到下一条语句或函数
ni              #asm时下一条指令
step            #单步, 进入函数, 单步执行, 注意系统函数只能用n, 不要用s进入
print i         #打印变量i的值
continue        #继续执行断点后续指令
finish          #结束当前函数调用, 返回调用点
set args aa bb cc   #给函数添加参数, 或者run aa bb cc
info b          #查看断点信息
info thread     #显示线程信息
delete n        #删除n号断点
display j       #一直显示j变量
undisplay num   #取消监视
wa flag         #flag变化时trap
b 20 if i=5        #设置条件断点
ptype arr       #查看变量类型
backtrace       #简称bt查看函数调用的栈帧和层级关系
frame 1         #切换函数栈帧

用gdb调试段错误: 直接run, 程序停止的位置就是出段错误的位置

makefile

makefile的名字只能是makefile或Makefile

  • 1个规则:

    目标:依赖条件

    ​ 命令(前面是一个Tab缩进)

  • 2个函数

  • 3个自动变量

若想生成目标, 检查规则中的依赖条件是否存在, 如果不存在, 则寻找是否有规则用来生成该依赖文件

检查规则中的目标是否需要被更新, 必须先检查他的所有依赖, 依赖中有任何一个被更新, 则目标必须被更新

  • 分析各个目标和依赖之间的关系;
  • 根据依赖关系自底向上执行命令;
  • 根据修改时间比目标新旧与否确定更新;
  • 如果目标不依赖任何条件, 则执行对应命令, 以示更新;
/*一个最简单的makefile*/
hello:hello.cgcc hello.c -o hello

考虑中间步骤:

hello:hello.ogcc hello.o -o hello
hello.o:hello.cgcc -c hello.c -o hello.o

多文件联编:

hello:hello.cgcc hello.c add.c sub.c div1.c -o hello

考虑到多文件编译的时间成本, 应该先将各个模块编译成.o目标文件, 由目标文件链接成可执行文件

这样, 只有改动了的模块会被再次编译, 其他的保持不变

hello:hello.o add.o sub.o div1.ogcc hello.o add.o sub.o div1.o -o hellohello.o:hello.cgcc -c hello.c -o hello.oadd.o:add.cgcc -c add.c -o add.osub.o:sub.cgcc -c sub.c -o sub.odiv1.o:div1.cgcc -c div1.c -o div1.o

当依赖条件的时间比目标的时间还晚, 说明目标该更新了

依赖条件如果不存在, 找寻新的规则去产生依赖

make只会认为第一行是自己的最终目标, 如果最终目标没有写在第一行, 通过ALL来指定

ALL:hellohello.o:hello.cgcc -c hello.c -o hello.o
add.o:add.cgcc -c add.c -o add.o
sub.o:sub.cgcc -c sub.c -o sub.o
div1.o:div1.cgcc -c div1.c -o div1.ohello:hello.o add.o sub.o div1.ogcc hello.o add.o sub.o div1.o -o hello

makefile的2个函数和clean

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 hellohello.o:hello.cgcc -c hello.c -o hello.o
add.o:add.cgcc -c add.c -o add.o
sub.o:sub.cgcc -c sub.c -o sub.o
div1.o:div1.cgcc -c div1.c -o div1.oclean:-rm -rf $(obj) hello

执行make clean时必须加上-n参数检查, 否则可能会把源码误删

clean相当于一个没有依赖条件的规则

rm前面的横杠表示出错(文件不存在)仍然执行

makefile的3个自动变量和模式规则

三个自动变量:

  • $@:在规则的命令中, 表示规则中的目标;
  • $^:在规则的命令中, 表示所有依赖条件;
  • $<:在规则的命令中, 表示第一个依赖条件;
src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))ALL:hello
hello:$(obj)gcc $^ -o $@           #目标依赖于所有依赖条件hello.o:hello.cgcc -c $< -o $@          #目标依赖于第一个(唯一一个)依赖条件
add.o:add.cgcc -c $< -o $@          #目标依赖于第一个(唯一一个)依赖条件
sub.o:sub.cgcc -c $< -o $@          #目标依赖于第一个(唯一一个)依赖条件
div1.o:div1.cgcc -c $< -o $@            #目标依赖于第一个(唯一一个)依赖条件clean:-rm -rf $(obj) hello

模式规则:

鉴于上面的都是某个.o文件依赖于某个.c文件的形式, 可以将其总结为一个模式规则:

%.o:%.cgcc -c $< -o $@

关于$<:如果将该变量应用在模式规则中, 它可将依赖条件列表中的依赖项依次取出, 套用模式规则

src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))ALL:hello
hello:$(obj)gcc $^ -o $@%.o:%.cgcc -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 -gALL:hello
hello:$(obj)gcc $^ -o $@ $(myArgs)%.o:%.cgcc -c $< -o $@ $(myArgs)
clean:-rm -rf $(obj) hello.PHONY:clean ALL

考虑工程目录结构:

祖传的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 cleanclean:-rm -rf ./obj/*.o hello

文件操作

系统调用: 内核提供的函数: 由操作系统实现并提供给外部应用程序的编程接口, 是应用程序同操作系统之间交互数据的桥梁;

为了保证系统的安全性, manPage中的系统调用都是对系统调用的一次浅封装, 比如open对应的是sys_open…

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

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
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)

makefile作业

将当前目录下的所有C程序编译成可执行文件:

src=$(wildcard ./*.c)
target=$(patsubst %.c,%,$(src))ALL:$(target)myArgs=-Wall -g%:%.cgcc $< -o $@ $(myArgs)clean:-rm -rf $(target)
.PHONY:ALL clean

read和write函数

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

使用read()write()实现cp命令:

//mycp.c
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>const int BS = 1024;int main(int argc, char* argv[]) {if (argc != 3) {printf("format: ./mycp a b\n");exit(1);}int fd1 = open(argv[1], O_RDONLY);int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0644);if (fd1 == -1 || fd2 == -1) {perror("open error");exit(1);}char buf[BS];ssize_t s;while ((s = read(fd1, buf, BS)) > 0) {ssize_t ret = write(fd2, buf, s);if (ret != s) {perror("write error");exit(1);}}if (s < 0) {perror("read error");exit(1);}close(fd1);close(fd2);return 0;
}

系统调用和库函数比较:预读入和缓输出机制

strace-跟踪一个程序执行时所需要的系统调用

如果规定逐字节的进行拷贝, 用库函数会比用系统调用快很多, 因为其有预读入和缓输出机制


OS绝不会让你逐字节的向Disk上写数据, 实际上它维护了一个系统级缓冲, 只有当从用户空间过来的数据在该缓冲上写满时, 他才会一次性将数据冲刷到Disk上;

当使用系统调用的方法时, 要不断的在用户空间和内核空间进行来回切换, 这会消耗大量时间;

而使用fputc(库函数)时, 他在设计之初自己在用户空间维护了一个缓冲, 这样在用户空间把自己的缓冲写满, 再一次性写入内核缓冲(写入了内核缓冲就认为写到了磁盘上了), 可见这样大大减少了在用户空间和内核空间来回切换的次数;

readwrite函数常被称为UnbufferedIO, 指无用户级缓冲区, 但不保证不使用内核缓冲区

文件描述符

PCB中有一根指针, 指向了该进程的文件描述符表, 每个表项都是一个键值对, 其中的value是指向文件结构体的指针, 其中的索引是fd, fd是OS暴露给用户的唯一操作文件的依据

新打开的文件描述符一定是所有文件描述符表中可用的, 最小的那个文件描述符;

文件描述符最大1023, 说明一个进程最多能打开1024个文件;

文件结构体:

阻塞和非阻塞


一个自己的echo程序:

//myecho.c
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>const int BS = 1024;int main(int argc, char* argv[]) {char buf[BS];int ret = read(STDIN_FILENO, buf, BS);if (ret < 0) {perror("read error");exit(1);}ret = write(STDOUT_FILENO, buf, ret);if (ret < 0) {perror("read error");exit(1);}return 0;
}

当不敲入换行符时, read会一直阻塞等待用户输入

阻塞是设备文件和网络文件的属性

当然也可以设置以非阻塞方式从tty中读数据

//echo-nonblock.c
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>const int BS = 128;int main(int argc, char* argv[]) {int fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);if (fd < 0) {perror("open /dev/tty error");exit(1);}char buf[BS];memset(buf, 0, BS);ssize_t n;while ((n = read(fd, buf, BS)) < 0) {if (errno != EWOULDBLOCK) {perror("read /dev/tty error");exit(1);} else {printf("didn't get input, try again\n");sleep(1);}}write(STDOUT_FILENO, buf, n);close(fd);return 0;
}

read函数返回-1, 并且errno=EAGAINEWOULDBLOCK, 说明不是read失败, 而是read在以非阻塞方式读一个设备文件网络文件, 而文件中无数据

阻塞方式存在的问题也正是后来网络IO中select, pollepoll函数存在的原因

fcntl函数

fcntl函数:改变一个已经打开文件的访问控制属性

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

将fd设置为非阻塞模式的核心调用:

int flags=fcntl(fd, F_GETFL);
flags|= O_NONBLOCK;
int ret=fcntl(fd, F_SETLF, flags);

fcntl改写上面的程序, 不用重新打开文件:

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>const int BS = 128;int main(int argc, char* argv[]) {int flag = fcntl(STDIN_FILENO, F_GETFL);if (flag < 0) {perror("fcntl get error");}flag |= O_NONBLOCK;flag = fcntl(STDIN_FILENO, F_SETFL, flag);if (flag < 0) {perror("fcntl set error");}ssize_t n;char buf[BS];while ((n = read(STDIN_FILENO, buf, BS)) < 0) {if (errno != EWOULDBLOCK) {perror("read /dev/tty error");exit(1);} else {printf("didn't get input, try again\n");sleep(1);}}write(STDOUT_FILENO, buf, n);return 0;
}

文件的flags是一个位图, 每一位代表不同属性的真假值;

lseek函数

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

举例:

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int main(int argc, char* argv[]) {int fd = open("./lseek.txt", O_CREAT | O_RDWR, 0644);if (fd < 0) {perror("open ./lseek.txt error");exit(1);}const char* str = "hello, lseek test\n";ssize_t ret = write(fd, str, strlen(str));if (ret < 0) {perror("write error");exit(1);}//如果这里不将读写指针归零,下面的read读不到任何东西lseek(fd, 0, SEEK_SET);char c;while ((ret = read(fd, &c, 1))) {if (ret < 0) {perror("read error");}putchar(c);}close(fd);return 0;
}

lseek获取文件大小:

int main(int argc, char* argv[]) {int fd = open(argv[1], O_RDONLY);  //省略了错误处理off_t s = lseek(fd, 0, SEEK_END);printf("%s size = %ld\n", argv[1], s);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);

C语言中指针作为传入传出参数:

传入参数:

  1. 指针作为函数参数
  2. 同时有const关键字修饰
  3. 指针指向有效区域, 在函数内部做读操作

传出参数:

  1. 指针作为函数参数
  2. 在函数调用前, 指针指向的空间可以无意义, 但必须有效
  3. 在函数内部做写操作
  4. 函数调用结束后充当函数返回值

传入传出参数:

  1. 指针作为函数参数
  2. 在函数调用前, 指针指向的空间有实际意义
  3. 在函数内部, 先做读操作, 再做写操作
  4. 函数调用结束后, 充当函数返回值

目录项和inode


增加文件的硬链接只是增加dentry, 指向相同的inode

同样, 删除硬链接也只是删除dentry, 要注意删除文件并不会让数据在磁盘消失, 只是OS丢失了inode, 磁盘只能覆盖, 不能擦除

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:文件路径
  • statbuf(传出参数)存放文件属性

返回值: 成功返回0, 失败返回-1并设置errno

利用stat获取文件大小:

//stat-get-file-size.c
int main(int argc, char* argv[]) {struct stat st;int ret = stat(argv[1], &st); //忽略了错误处理printf("%s size = %ld\n", argv[1], st.st_size);return 0;
}

使用宏函数获取文件属性: S_ISDIR(sbuf.st_mode)-> bool

//stat-get-file-type.c
int main(int argc, char* argv[]) {struct stat st;stat(argv[1], &st);if (S_ISDIR(st.st_mode)) {printf("%s is dir\n", argv[1]);} else if (S_ISREG(st.st_mode)) {printf("%s is regular\n", argv[1]);} else if (S_ISFIFO(st.st_mode)) {printf("%s is fifo\n", argv[1]);} else if (S_ISBLK(st.st_mode)) {printf("%s is block\n", argv[1]);} else {printf("others\n");}return 0;
}

ln -s makefile makefile.soft: 创建软连接

mkfifo f1: 创建管道文件

stat穿透: 当用stat获取软连接的文件属性时, 会穿透符号连接直接返回软连接指向的本尊的文件属性, vim, cat等命令也有穿透作用

解决方法: 换lstat函数

S_IFMT是一个文件类型掩码(文件类型那四位全1), st_mode与它位与后就可以提取出文件类型(后面的权限位被归零);

switch (sb.st_mode & S_IFMT) {case S_IFBLK:  printf("block device\n");            break;case S_IFCHR:  printf("character device\n");        break;case S_IFDIR:  printf("directory\n");               break;case S_IFIFO:  printf("FIFO/pipe\n");               break;case S_IFLNK:  printf("symlink\n");                 break;case S_IFREG:  printf("regular file\n");            break;case S_IFSOCK: printf("socket\n");                  break;default:       printf("unknown?\n");                break;
}

link和unlink函数

link函数:可以为已经存在的文件创建目录项(硬链接)

ln makefile makefile.hard:为makefile创建硬连接

int link(const char *oldpath, const char *newpath);

使用linkunlink函数实现mv命令:

int main(int argc, char* argv[]) {int 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:

//unlink-demo.c
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");p[3] = 'a';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中写, 仍可正常写入

隐式回收:当进程运行结束时, 所有该进程打开的文件会被关闭, 申请的内存空间会被释放, 系统的这一特性称为隐式回收系统资源

目录操作

readlink m1.soft查看软连接的内容:

$ ln -s /home/daniel/Linux_System/test/makefile m.soft
$ ll m.soft
lrwxrwxrwx 1 daniel daniel 39 Jun  6 20:59 m.soft -> /home/daniel/Linux_System/test/makefile
$ readlink m.soft
/home/daniel/Linux_System/test/makefile

Linux下所见皆文件,我们也可以用vim打开一个目录


文件名不能超过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的功能:

//readdir-myls.c
int main(int argc, char* argv[]) {DIR* dirp = opendir(argv[1]);    //忽略了错误处理struct dirent* sdp;while ((sdp = readdir(dirp)) != NULL) {if (strcmp(sdp->d_name, ".") == 0 || strcmp(sdp->d_name, "..") == 0) {continue;} else {printf("%s\n", sdp->d_name);}}closedir(dirp);return 0;
}

核心调用:

DIR* dp=opendir(dirpath);
struct dirent* sdp=readdir(dp);
printf("%s\n",sdp->d_name);

Linux下文件存储原理:

递归遍历目录

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();

代码实现:

/*参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;
}

dup函数

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.

传入已有的文件描述符, 返回一个新的文件描述符;

//dup-demo.c
#define CHECK_NEG(x, str) \if ((x) < 0) {        \perror((str));    \exit(1);          \}int main(int argc, char* argv[]) {int fd1 = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0644);CHECK_NEG(fd1, "open error");int fd2 = dup(fd1);CHECK_NEG(fd2, "dup error");printf("fd1 = %d, fd2 = %d\n", fd1, fd2);const char* str = "fuckyou";write(fd2, str, strlen(str));close(fd2);return 0;
}

dup的返回值fd2相当于fd1的副本, 拿着它也可以操作fd1

//dup2-demo.c
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);int ret = dup2(fd1, fd2);printf("ret = %d, fd1 = %d, fd2 = %d\n", ret, fd1, fd2);const char* str = "aloha\n";write(fd2, str, strlen(str));dup2(fd1, STDOUT_FILENO);    //标准输出指向fd1printf("hallo");close(fd1);close(fd2);return 0;
}


总之, dup2是后面的指向前面的

除了dup函数,还可以使用fcntl实现dup描述符

int main(int argc, char* argv[]) {int fd1 = open(argv[1], O_RDWR | O_CREAT, 0644);int fd2 = fcntl(fd1, F_DUPFD, 0);    //F_DUPFD命令复制文件描述符int fd3 = fcntl(fd1, F_DUPFD, 8);printf("fd1 = %d, fd2 = %d, fd3 = %d\n", fd1, fd2, fd3);  // fd1 = 3, fd2 = 4, fd3 = 8const char* str = "abcdefg";write(fd3, str, strlen(str));close(fd3);return 0;
}

参3传0, 则从0开始向下寻找可用的文件描述符返回给newfd1

参3传8, 则从8开始向下寻找可用的文件描述符返回给newfd2

dup2的newfd比dup的灵活一些: 他能打破可用最小的文件描述符限制

多进程编程

内存映射

对于一个32位的机器来说, 每个进程都能看到4GB的虚拟地址空间, 且他们的3G~4G的位置都是kernel(每个进程都有kernel区);

从虚拟内存到物理内存的映射由MMU完成, 不同进程的用户空间被映射到物理内存的不同位置, 而不同进程的kernel空间被映射到物理内存的相同位置, 对于物理内存来用户空间和内核空间有不同的特权级, 从用户空间到内核空间的转换实质上是特权级的切换;

PCB进程控制块

每个进程在内核中都有一个PCB来维护进程相关信息, Linux内核的进程控制块是task_struct类型的结构体

着重掌握的:

  • 进程id
  • 文件描述符表
  • 信号相关的信息资源
  • 进程状态: 初始态, 就绪态, 运行态, 挂起态, 终止态
  • 进程工作目录位置
  • 用户id和组id

环境变量

  • PATH: 存放可执行程序的目录位置
  • SHELL: 当前使用的命令解析器
  • TERM: 查看终端类型
  • LANG: 语言和编码
  • env: 查看所有环境变量

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

fork创建子进程:

//fork-demo.c
int main(int argc, char* argv[]) {printf("before fork1\n");printf("before fork2\n");printf("before fork2\n");pid_t pid = fork();if (pid < 0) {perr_exit("fork error");} else if (pid == 0) {printf("I'm child, my pid = %d, my parent pid = %d\n", getpid(), getppid());usleep(10);} else {printf("I'm parent, my pid = %d\n", getpid());pid_t ret = wait(NULL);printf("wait %d\n", ret);}printf("end fork\n");return 0;
}

执行结果:

before fork1
before fork2
before fork2
I'm parent, my pid = 4586
I'm child, my pid = 4587, my parent pid = 4586
end fork
wait 4587
end fork

父进程的父进程是bash

思考如何循环创建n个子进程:


循环创建多个子进程:

int main(int argc, char* argv[]) {int i = 0;for (; i < 5; ++i) {pid_t pid = fork();if (pid == 0) {break;}}if (i < 5) {printf("I'm %d child, my pid = %d, my parent pid = %d\n", i, getpid(), getppid());} else {sleep(3);printf("I'm parent, my pid = %d\n", getpid());}return 0;
}
$ ./a.out
I'm 0 child, my pid = 5721, my parent pid = 5720
I'm 4 child, my pid = 5725, my parent pid = 5720
I'm 1 child, my pid = 5722, my parent pid = 5720
I'm 2 child, my pid = 5723, my parent pid = 5720
I'm 3 child, my pid = 5724, my parent pid = 5720
I'm parent, my pid = 5720

乱序输出反映了了操作系统对进程调度的无序性,加上了sleep后就能控制输出顺序

父子进程共享的内容:

刚fork之后:

父子的相同之处: 全局变量, .data段, .text段, 栈, 堆, 环境变量, 用户ID, 宿主目录, 进程工作目录, 信号处理方式…;

父子进程的不同之处: 进程ID, fork返回值, 父进程ID, 进程运行时间, 闹钟(定时器), 未决信号集;

但是子进程并不是把父进程0~3G地址空间完全copy一份, 然后映射到物理内存

父子进程间遵循读时共享, 写时复制的原则, 这样设计, 无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销

//fork-shared.c
int var = 100;int main(int argc, char* argv[]) {pid_t pid = fork();if (pid < 0) {perr_exit("fork error");} else if (pid == 0) {printf("beofre write: var = %d,&var = %p, I'm child, my pid = %d, my parent pid = %d\n", var, &var, getpid(), getppid());var = 200;printf("after write: var = %d, &var = %p, I'm child, my pid = %d, my parent pid = %d\n", var, &var, getpid(), getppid());} else {printf("before write: var = %d, &var = %p, I'm parent, my pid = %d\n", var, &var, getpid());var = 300;printf("after write: var = %d, &var = %p, I'm parent, my pid = %d\n", var, &var, getpid());}return 0;
}

输出结果:

$ ./a.out
before write: var = 100, &var = 0x55ff40929010, I'm parent, my pid = 6610
after write: var = 300, &var = 0x55ff40929010, I'm parent, my pid = 6610
beofre write: var = 100,&var = 0x55ff40929010, I'm child, my pid = 6611, my parent pid = 6610
after write: var = 200, &var = 0x55ff40929010, I'm child, my pid = 6611, my parent pid = 1
$ ./a.out
before write: var = 100, &var = 0x5627e4f5c010, I'm parent, my pid = 6638
after write: var = 300, &var = 0x5627e4f5c010, I'm parent, my pid = 6638
beofre write: var = 100,&var = 0x5627e4f5c010, I'm child, my pid = 6639, my parent pid = 6638
after write: var = 200, &var = 0x5627e4f5c010, I'm child, my pid = 6639, my parent pid = 6638

父子进程之间不共享全局变量(线程之间是共享全局变量的)

重点要掌握的共享的内容: 文件描述符, mmap建立的映射区

使用gdb调试多进程程序的时候, gdb只能跟踪一个进程, 可以在fork函数调用之前通过指令设置gdb跟踪父进程还是子进程:

set follow-fork-mode child

set follow-fork-mode parent

exec函数族

int execlp(const char* file, const char* arg, ... /* (char  *) NULL */);

fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支), 子进程往往要执行一种exec函数以执行另一个程序;

当进程调用一种exec函数时, 该进程的用户空间代码和数据完全被新程序替换, 从新程序的启动例程开始执行;

调用exec函数并不会创建新的进程, 所以调用exec前后该进程的id并未改变;

将当前进程的.text和.data替换为所加载程序的.text和.data, 然后进程从新的.text的第一条指令开始执行, 但进程id不变, 换核不换壳,exec函数不会返回任何值给任何人;

execlp函数:

int execlp(const char* file, const char* arg, ... /* (char  *) NULL */);
int execl(const char *path, const char *arg, .../* (char  *) NULL */);
int execvp(const char *file, char *const argv[]);

execlp中的p表示环境变量, 所以该函数通常用来调用系统程序

execvp的v是vector的意思,就是将execlp中的参数组织成字符串数组传入(或许你也可以传入从main函数中传来的参数)

/*execlp("ls","ls","-l","-R","-h",NULL)的等效形式*/
char* argv[]={"ls","-l","-R","-h",NULL};
execvp("ls",argv);

exec函数族的一般规律:调用成功立即执行新的程序, 不会返回, 只有调用失败才会返回-1

注意结尾加上NULL指定变参结束, printf函数也是变参, 结尾也要加上NULL作为哨兵

//exec-demo.c
int main(int argc, char* argv[]) {int i = 0;pid_t pid;for (; i < 3; ++i) {pid = fork();if (pid == 0) {break;}}if (i == 3) {sleep(3);} else if (i == 2) {//执行系统程序execlp("ls", "ls", "-l", "-R", "-h", NULL);perr_exit("execlp error");} else if (i == 1) {//执行自己的程序execl("hello", "hello", NULL);perr_exit("execl error");} else if (i == 0) {//以字符串数组的形式传递参数execvp("ls", argv);perr_exit("execvp error");}printf("parent finished\n");return 0;
}

fork, 再exec, 这就是bash的大概原理

ps aux的输出打印到文件当中:

//execlp-ps.c
int main(int argc, char* argv[]) {int fd = open("ps.log", O_RDWR | O_CREAT | O_TRUNC, 0644);if (fd < 0) {perr_exit("open error");}int ret = dup2(fd, STDOUT_FILENO);if (ret < 0) {perr_exit("dup2 error");}execlp("ps", "ps", "-aux", NULL);perr_exit("execlp error");return 0;
}

孤儿进程和僵尸进程

孤儿进程: 父进程先于子进程结束, 子进程的父进程变为init进程, init进程又称为进程孤儿院, 专门收养孤儿进程(为了回收)

僵尸进程: 进程终止, 父进程尚未回收子进程残留在内核的资源(PCB), 变为僵尸(defunct)进程(每一个进程都会经历僵尸态)

ps ajx:查看进程ID和父进程ID

kill -9 pid:杀死进程, 但是杀不死僵尸进程, 杀僵尸只能杀死他父亲

wait函数

父进程调用wait函数可以回收子进程终止信息, 该函数有三个功能:

  • 阻塞等待子进程退出
  • 回收子进程残留资源
  • 获取子进程结束状态(退出原因)
pid_t wait(int* wstatus);

成功返回清理掉的子进程ID, 失败返回-1

通过调用宏函数获取子进程退出状态:

//wait-demo.c
int main(int argc, char* argv[]) {pid_t p1 = fork();if (p1 == 0) {printf("I'm child, I'm going to sleep 20s\n");sleep(20);printf("I'm child, I'm going to die\n");} else if (p1 > 0) {int status;printf("I'm parent\n");pid_t p2 = wait(&status);if (p2 < 0) {perr_exit("wait error");}if (WIFEXITED(status)) {printf("my child exited with %d\n", WEXITSTATUS(status));} else if (WIFSIGNALED(status)) {printf("my child was killed by %d\n", WTERMSIG(status));}printf("I'm parent, wait %d == %d finished\n", p1, p2);} else {perr_exit("fork error");}return 0;
}

核心调用:

int status;
pid_t wpid=wait(&status);  //阻塞等待子进程退出
WIFEXITED(status);          //判断是否正常退出
WIFSIGNALED(status);        //判断是否被信号终止
WEXITSTATUS(status)         //获取退出值
WTERMSIG(status)            //获取凶手信号值

子进程被信号杀死:

$ ./a.out
I'm parent
I'm child, I'm going to sleep 10s
my child was killed by 9
I'm parent, wait 23663 == 23663 finished

子进程正常终止:

$ ./a.out
I'm parent
I'm child, I'm going to sleep 10s
I'm child, I'm going to die
my child exited with 0
I'm parent, wait 23651 == 23651 finished

各种信号的宏值:

$ kill -l1) SIGHUP    2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP6) SIGABRT    7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10    45) SIGRTMIN+11    46) SIGRTMIN+12    47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15    50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

程序所有异常终止的原因都是因为信号

waitpid函数

waitpid可以指定某一个子进程进行回收

一次waitwaitpid函数调用, 只能回收一个子进程, 如果你循环创建了多个子进程, 那么就碰到哪个算哪个

pid_t waitpid(pid_t pid, int* wstatus, int options);

参1传要回收的pid, 传-1表示回收任意子进程, 传0表示回收同一组的所有子进程;

参2传进程结束状态, 如果不关心直接传NULL(传出参数);

参3传回收方式:WNOHANG(非阻塞);

waitpid的参1传进程组号取反, 表示回收指定进程组的任意子进程

//waitpid-demo.c
int main(int argc, char* argv[]) {int i = 0;pid_t p2;for (; i < 5; ++i) {pid_t p = fork();if (p < 0) {  //错误perr_exit("fork error");} else if (p > 0) {   //父进程中if (i == 2) {p2 = p;}} else {  //子进程中break;}}if (i == 5) {// sleep(2);pid_t wpid = waitpid(p2, NULL, 0);if (wpid < 0) {perr_exit("waitpid error");} else {printf("waitpid a child %d\n", wpid);}} else {sleep(1);printf("I'm %d child, mypid = %d\n", i, getpid());}return 0;
}

waitpid回收多个子进程: 用while循环

//waitpid-while.c
int main(int argc, char* argv[]) {int i = 0;for (i = 0; i < 5; ++i) {pid_t pid = fork();if (pid < 0) {perr_exit("fork error");} else if (pid == 0) {break;}}pid_t wpid;if (i == 5) {while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) {if (wpid == 0) {sleep(1);continue;} else if (wpid > 0) {printf("catch child %d\n", wpid);}}} else {sleep(1);printf("I'm %d child, mypid = %d\n", i, getpid());}return 0;
}

输出信息:

$ ./a.out
I'm 0 child, mypid = 3135
I'm 1 child, mypid = 3136
I'm 2 child, mypid = 3137
I'm 3 child, mypid = 3138
I'm 4 child, mypid = 3139
catch child 3135
catch child 3136
catch child 3137
catch child 3138
catch child 3139

wait和waitpid总结


waitpid(-1,&status,0)==wait(&status);。注意wait/waitpid只能回收子进程, 爷孙的也不行

进程间通信

  1. 管道(使用最简单)
  2. 信号(开销最小)
  3. 共享映射区(可以用于无血缘关系的进程之间)
  4. 本地套接字(最稳定)

匿名管道pipe

管道是一种最基本的IPC机制, 作用于有血缘关系的进程之间, 完成数据传递. 调用pipe系统函数即可创建一个管道, 有如下特质:

  1. 其本质是一个伪文件(实为内核缓冲区)
  2. 有两个文件描述符引用, 一个表示读端, 一个表示写端
  3. 规定数据从管道的写端流入管道, 从读端流出, 只能单向流动

管道的原理: 管道实际为内核使用环形队列机制, 借助内核缓冲区(4k)实现

管道的局限性:

  • 数据不能进程自己写, 自己读
  • 管道中的数据不可反复读取, 一旦读走, 管道中不再存在
  • 采用半双工通信方式, 数据只能在单方向上流动
  • 只能在有公共祖先的进程之间使用管道

使用pipe函数创建并打开管道

int pipe(int pipefd[2]);

pipefd[0]-读端

pipefd[1]-写端

成功返回0, 失败返回-1并设置errno

fork完成时:

则父进程关闭写端, 子进程关闭读端, 数据就能在pipe中单向流动, 父子进程能够完成通信

//pipe-demo.c
int main(int argc, char* argv[]) {int pfd[2];int ret = pipe(pfd);if (ret < 0) {perr_exit("pipe error");}const char* str = "hello, world\n";pid_t p = fork();if (p < 0) {perr_exit("fork error");} else if (p == 0) {  // childclose(pfd[0]);write(pfd[1], str, strlen(str));close(pfd[1]);} else {  // parentclose(pfd[1]);char buf[128];memset(buf, 0, sizeof(buf));read(pfd[0], buf, sizeof(buf));printf("%s", buf);close(pfd[0]);}return 0;
}

管道读写行为:

读管道:

1.管道中有数据, read返回实际读到的字节数

2.管道中无数据:

​若管道写端被全部关闭, 则read返回0

​若写端没有被全部关闭, 则read阻塞等待(不久的将来可能会有数据抵达, 此时会让出CPU)

写管道:

1.管道读端全部被关闭, 进程异常终止(也可以捕捉SIGPIPE信号, 使进程不终止)

2.管道读端没有全部关闭:

若​管道已满, 则write阻塞

若管道未满, 则write将数据写入, 并返回实际写入的字节数

总结:



父子进程借助管道实现ls | wc -l统计行数的功能:

需要使用的函数:

  • exec()
  • dup2()
  • pipe()
int main(int argc, char* argv[]) {int pfd[2];int ret = pipe(pfd);if (ret < 0) {perr_exit("pipe error");}pid_t pid = fork();if (pid < 0) {perr_exit("fork error");} else if (pid > 0) {close(pfd[0]);dup2(pfd[1], STDOUT_FILENO);execlp("ls", "ls", NULL);perr_exit("execlp error");} else {close(pfd[1]);dup2(pfd[0], STDIN_FILENO);execlp("wc", "wc", "-l", NULL);perr_exit("execlp error");}return 0;
}

上面的内容如果用兄弟进程间通信来做:

int main(int argc, char* argv[]) {int pfd[2];int ret = pipe(pfd);if (ret < 0) {perr_exit("pipe error");}int i = 0;for (; i < 2; ++i) {pid_t pid = fork();if (pid < 0) {perr_exit("fork error");} else if (pid == 0) {break;}}if (i == 0) {close(pfd[0]);dup2(pfd[1], STDOUT_FILENO);execlp("ls", "ls", NULL);perr_exit("execlp error");} else if (i == 1) {close(pfd[1]);dup2(pfd[0], STDIN_FILENO);execlp("wc", "wc", "-l", NULL);perr_exit("execlp error");} else {  // parentclose(pfd[0]), close(pfd[1]);wait(NULL), wait(NULL);}return 0;
}

注意创建完进程后, 父进程要将管道的读写两端全部关闭

管道可以一个读端, 多个写端, 但是不建议这样做

默认管道的大小是4k

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 15435
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1048576
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 15435
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

命名管道fifo

匿名管道pipe的优缺点:

为区分pipe, 将FIFO称为命名管道

FIFO可以用于不相关的进程间交换数据

FIFO是Linux基础文件类型中的一种, 但是FIFO文件在磁盘上没有数据块, 仅仅用来标识内核中的一条通道, 各进程可以打开这个文件进行read/write, 实际上是在读写内核通道, 这样就实现了进程间通信

创建方式:

int mkfifo(const char* pathname, mode_t mode);

成功返回0, 失败返回-1并设置errno;

使用FIFO实现非血缘关系进程间通信:用FIFO进行通信几乎只有文件读写操作, 比较简单

写进程

//fifo-w.c
int main(int argc, char* argv[]) {if (argc < 2) {printf("format: ./a.out fifoname\n");exit(1);}int fd = open(argv[1], O_WRONLY);if (fd < 0) {perr_exit("open error");}char buf[128];memset(buf, 0, sizeof(buf));int i = 0;while (1) {sprintf(buf, "hello, world: %d\n", i++);write(fd, buf, strlen(buf));sleep(1);}close(fd);return 0;
}

读进程

//fifo-r.c
int main(int argc, char* argv[]) {if (argc < 2) {printf("format: ./a.out fifoname\n");exit(1);}int fd = open(argv[1], O_RDONLY);if (fd < 0) {perr_exit("open error");}char buf[128];while (1) {read(fd, buf, sizeof(buf));printf("%s", buf);}close(fd);return 0;
}

文件

读普通文件不会造成read阻塞, 如果子进程睡1秒再写, 父进程由于刚开始读不到数据read直接返回0

没有血缘关系的进程也可以用文件进行进程间通信

mmap

存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射, 于是当从缓冲区中取数据, 就相当于读文件中的相应字节

于此类似, 将数据存入缓冲区, 则相应的字节就自动写入文件, 这样就可以在不使用read和write函数的情况下, 使用指针完成I/O操作

使用这种方法, 首先应通知内核, 将一个文件映射到存储区域中, 这个映射工作可以通过mmap函数来实现

void* mmap(void* addr, size_t length, int prot, int flags,int fd, off_t offset); //创建映射区
int munmap(void* addr, size_t length);  //删除映射区

参数:

  • ​ 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

MMAP建立映射区:

//mmap-demo.c
int main(int argc, char* argv[]) {int fd = open("mmaptext", O_RDWR | O_CREAT | O_TRUNC, 0644);if (fd < 0) {perr_exit("open error");}//扩展文件大小到20Bint ret = ftruncate(fd, 20);if (ret < 0) {perr_exit("ftruncate error");}//获取文件大小off_t len = lseek(fd, 0, SEEK_END);char* p = mmap(NULL, len, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);if (p == MAP_FAILED) {perr_exit("mmap error");}strcpy(p, "hello, world\n");printf("%s", p);munmap(p, len);return 0;
}

od -tcx filename:以16进制查看文件

MMAP使用注意事项:

1.可以, 但是要拓展文件大小, 否则会出现总线错误. 当然, 如果破罐子破摔, mmap时指定size=0, mmap会报错

2.mmap会报错: 无效参数(注意ftruncte()函数需要写权限, 否则无法拓展文件大小). 如果都用只读权限, 不会出错. 要创建映射区, 文件必须有读权限

3.没有影响, 建立完映射区后fd就能关闭

4.mmap报错: 无效参数, 偏移量必须是4k的整数倍(因为MMU映射的最小单位就是4k)

5.小范围的越界问题不大, 但是最好不要这么做(操纵不安全的内存, 操作系统不给你保障)

6.不能成功. 与malloc一样, 释放的内存的指针必须是申请得来的初始的指针, 如果要改变指针的值, 拷贝一份用

7.除了第一个参数, 后面的参数都可能导致失败

8.会死的很难看

所以MMAP的保险调用方式:

fd=open("filename",O_RDWR);

mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAX_SHARED,fd,0);

MMAP总结:

父子进程间MMAP通信:必须指定内存映射区为shared属性, 如果指定了private属性, 内核只会给子进程mmap的拷贝, 不会给他真正的mmap

//mmap-fork.c
int main(int argc, char* argv[]) {int fd = open("temp", O_RDWR | O_TRUNC | O_CREAT, 0644);if (fd < 0) {perr_exit("open error");}ftruncate(fd, 4);int* p = (int*)mmap(NULL, fd, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (p == MAP_FAILED) {perr_exit("mmap error");}close(fd);int var = 100;pid_t pid = fork();if (pid < 0) {perr_exit("fork error");} else if (pid == 0) {printf("child before write: *p = %d, var = %d\n", *p, var);*p = 9527;var = 200;printf("child after write: *p = %d, var = %d\n", *p, var);} else {sleep(1);wait(NULL);printf("parent: *p = %d, var = %d\n", *p, var);munmap(p, 4);}return 0;
}

无血缘关系进程间MMAP通信:

先来认识一个内存操作函数

void* memcpy(void* dest, const void* src, size_t n);

写进程:

int main(int argc, char* argv[]) {struct Student stu = {1, "daniel", 22};int fd = open("temp", O_RDWR | O_TRUNC | O_CREAT, 0644);if (fd < 0) {perr_exit("open error");}ftruncate(fd, sizeof(stu));struct Student* ps = (struct Student*)mmap(NULL, sizeof(stu), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (ps == MAP_FAILED) {perr_exit("mmap error");}while (1) {memcpy(ps, &stu, sizeof(stu));stu.id++;sleep(1);}munmap(ps, sizeof(stu));close(fd);return 0;
}

读进程:

int main(int argc, char* argv[]) {int fd = open("temp", O_RDONLY);if (fd < 0) {perr_exit("open error");}struct Student* ps = (struct Student*)mmap(NULL, sizeof(struct Student), PROT_READ, MAP_SHARED, fd, 0);if (ps == MAP_FAILED) {perr_exit("mmap error");}while (1) {printf("stu id = %d, name = %s, age = %d\n", ps->id, ps->name, ps->age);sleep(1);}munmap(ps, sizeof(struct Student));close(fd);return 0;
}

mmap相当于文件, 所以可以反复读取, 不像FIFO


MMAP匿名映射区:

//mmap-anonymous.c
int main(int argc, char* argv[]) {int* p = (int*)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);if (p == MAP_FAILED)perr_exit("mmap error");pid_t 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);munmap(p, 4);}return 0;
}

/dev/zero-文件白洞, 里面有无限量的’\0’, 要多少有多少

/dev/null-文件黑洞, 可以写入任意量的数据

所以在创建映射区时可以用zero文件, 就不用自己创建文件然后拓展大小了

但是注意无血缘关系进程间通信, 不能用匿名映射

总结:

复习:


/dev/zero文件也不能用于无血缘关系进程间通信

信号

信号的特性:

  • 简单
  • 不能携带大量信息
  • 满足特定条件才能发送

信号的特质:

所有信号的产生和处理, 都是由内核完成的

如何产生信号:

  1. 按键产生:Ctrl+c, Ctrl+z, Ctrl+\
  2. 系统调用产生:kill, raise, abort
  3. 软件条件产生:定时器alarm
  4. 硬件异常产生:非法访问内存(段错误), 除0(浮点数例外), 内存对齐错误(总线错误)
  5. 命令产生:kill命令

递达:内核发出的信号递送并且到达进程;

未决:产生和递达之间的状态, 主要由于阻塞(屏蔽)导致该状态

信号的处理方式:

  1. 执行默认动作
  2. 丢弃(忽略)
  3. 捕捉(回调用户处理函数)

信号屏蔽字和未决信号集:两者都是位图

阻塞信号集(信号屏蔽字):将某些信号加入集合, 对他们设置屏蔽, 当屏蔽x信号后, 再收到该信号, 该信号的处理将推后(直到解除屏蔽后)

未决信号集:

  1. 信号产生后, 未决信号集中描述该信号的位立刻翻转为1, 表示信号处于未决状态, 当信号被处理后, 对应位翻转回0, 这一时刻非常短暂
  2. 信号产生后由于某些原因(主要是阻塞)不能抵达, 这类信号的集合称为未决信号集, 在屏蔽解除前, 信号一直处于未决状态

常规信号一览:

$ kill -l1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

前31个位常规信号, 有默认事件和处理动作. 后面的是实时信号, 没有默认事件和处理动作

信号四要素:编号,名称,触发事件,默认处理动作

后面有多个值的信号是因为不同的操作系统的处理器架构不同

常规信号一览:


信号的默认处理动作:

  • Term: 终止进程
  • Ign: 忽略信号(默认即时对该种信号忽略操作)
  • Core: 终止进程, 生成Core文件(查验进程死亡原因, 用于gdb调试)
  • Stop: 暂停进程
  • Cont: 继续运行进程

SIGKILL(9)SIGSTOP(19), 不允许忽略和捕捉, 只能执行默认动作, 甚至不能将其设置为阻塞

只有每个信号所对应的事件发生了, 该信号才会被递送(但不一定递达), 不应该乱发信号

kill函数

int kill(pid_t pid, int sig);        //send signal to a process

一个弑父的例子:

//kill-demo.c
int main(int argc, char* argv[]) {pid_t pid = fork();if (pid < 0) {perr_exit("fork error");} else if (pid == 0) {sleep(1);kill(getppid(), SIGKILL);} else {while (1) {printf("I'm parent\n");     //疯狂输出}}return 0;
}

pid的不同取值:

kill -9 -10698-杀死10698进程组的所有进程

关于发送权限:发送者实际有效的用户ID==接收者实际有效的用户ID

如果你想杀死1号进程, 是不允许的

alarm函数

unsigned int alarm(unsigned int seconds);

测试一秒钟数多少个数:

int main(int argc, char* argv[]) {int i = 0;int j = 0;alarm(1);while (1) {printf("i = %d, j = %d\n", i, j);i++;++j;}return 0;
}

使用time命令查看程序执行时间占用情况:

程序实际执行时间=系统时间+用户时间+等待时间程序实际执行时间=系统时间+用户时间+等待时间程序实际执行时间=系统时间+用户时间+等待时间

real    0m1.003s
user    0m0.010s
sys     0m0.202s

程序运行的瓶颈在于IO, 要优化程序, 首选优化IO

setitimer函数

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("hello, world\n");return;
}int main(int argc, char* argv[]) {//为SIGALRM注册回调函数signal(SIGALRM, myfunc);// 5s后触发,然后每隔1s周期性触发一次struct itimerval it = {{1, 0}, {5, 0}};struct itimerval oldit;int ret = setitimer(ITIMER_REAL, &it, &oldit);if (ret < 0) {perr_exit("setitimer error");}while (1);return 0;
}

信号集操作函数

/*自定义信号集*/
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);

sigprocmask函数:

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;

操作信号集的若干步骤:

/*创建一个自定义信号集*/
sigset_t set;
/*清空自定义信号集*/
sigemptyset(&set);
/*向自定义信号集添加信号*/
sigaddset(&set,SIGINT);
/*用自定义信号集操作内核信号集*/
sigprocmask(SIG_BLOCK,&set);
/*查看未决信号集*/
sigpending(&myset);

Ctrl+D是向终端中写入一个EOF

//sigset-demo.c
void print_sigset(sigset_t* set) {for (int i = 1; i < 32; ++i) {if (sigismember(set, i)) {printf("1");} else {printf("0");}}printf("\n");
}int main(int argc, char* argv[]) {sigset_t new_sigset, old_sigset, ped_sigset;sigemptyset(&new_sigset);sigaddset(&new_sigset, SIGINT);   //屏蔽Ctrl+csigaddset(&new_sigset, SIGQUIT);  //屏蔽Ctrl+'\'sigaddset(&new_sigset, SIGBUS);     //屏蔽SIGBUSsigaddset(&new_sigset, SIGKILL);  //屏蔽SIGKILL,但是无效sigprocmask(SIG_BLOCK, &new_sigset, &old_sigset);while (1) {int ret = sigpending(&ped_sigset);    //读取未决信号集if (ret == -1) {perr_exit("sigpending error");}print_sigset(&ped_sigset);sleep(1);}return 0;
}

注意点: 对于SIGKILL信号, 即使设置了信号屏蔽, 依然能kill

signal函数

/*定义回调函数类型,很不幸,函数类型限制死了*/
typedef void (*sighandler_t)(int);
/*注册信号捕捉函数*/
sighandler_t signal(int signum, sighandler_t handler);

该函数由ANSI定义, 由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为, 因此应尽量避免使用它, 取而代之使用sigaction函数;

//signal-demo.c
void func(int signum) {printf("catch you %d\n", signum);
}int main(int argc, char* argv[]) {signal(SIGINT, func);while (1);return 0;
}

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 SIGINT\n");} else if (signum == SIGQUIT) {printf("catch SIGQUIT\n");}
}int main(int argc, char* argv[]) {struct sigaction act, oldact;act.sa_handler = catch_signal;act.sa_flags = 0;sigemptyset(&act.sa_mask);sigaction(SIGINT, &act, &oldact);sigaction(SIGQUIT, &act, &oldact);while (1);return 0;
}

信号捕捉的特性:

  1. 捕捉函数执行期间, 信号屏蔽字由mask变为sigaction结构体中的sa_mask, 捕捉函数执行结束后, 恢复回mask
  2. 捕捉函数执行期间, 本信号自动被屏蔽(sa_flags=0)
  3. 捕捉函数执行期间, 若被屏蔽信号多次发送, 解除屏蔽后只响应一次

内核实现信号捕捉简析:

为什么执行完信号处理函数后要再次进入内核?

因为信号处理函数是内核调用的, 函数执行完毕后要返回给调用者

借助信号捕捉回收子进程:

//sigaction-catch-child.c
void catch_child(int signum) {pid_t wpid;//使用while循环,当多个child同时结束时能够完全回收while ((wpid = wait(NULL)) != -1) {printf("catch child %d\n", wpid);}
}int main(int argc, char* argv[]) {sigset_t set;sigemptyset(&set);sigaddset(&set, SIGCHLD);sigprocmask(SIG_BLOCK, &set, NULL);   //将SIGCHLD信号屏蔽掉int i = 0;for (; i < 15; ++i) {pid_t pid = fork();if (pid == 0) {break;}}if (i == 15) {struct sigaction act, oldact;act.sa_handler = catch_child;  //注册信号捕捉函数sigemptyset(&(act.sa_mask));act.sa_flags = 0;sleep(1);  //模拟sigaction调用很长时间sigaction(SIGCHLD, &act, &oldact);sigprocmask(SIG_UNBLOCK, &set, NULL);  //解除SIGCHLD的屏蔽,开始回收子进程printf("I'm parent\n");while (1);} else {printf("I'm %d child\n", i);}return 0;
}

要注意的点已经写在注释里了, 如果有的地方不小心有纰漏, 很可能会造成产生僵尸进程

慢速系统调用中断:

总结:setitimer可以实现高精度定时


会话

会话的概念: 多个进程组的集合

setsid函数:

创建一个会话, 并以自己的ID设置进程组ID, 同时也是新会话的ID

pid_t setsid(void);

成功返回调用进程的会话ID, 失败返回-1并设置errno

调用了setsid函数的进程, 既是新的会长, 也是新的组长

守护进程

  1. 创建子进程, 父进程退出: 所有工作在子进程中形式上脱离了控制终端
  2. 在子进程中创建新会话: setsid()函数, 使子进程完全独立出来, 脱离控制
  3. 改变当前工作目录位置: chdir()函数, 防止占用可卸载的文件系统
  4. 重设文件权限掩码: umask()函数, 防止继承的文件创建屏蔽字拒绝某些权限
  5. 关闭文件描述符: 继承的打开文件不会用到, 浪费系统资源, 无法卸载
  6. 开始执行守护进程核心工作

守护进程创建:

//deamon.c
int main(int argc, char* argv[]) {//创建子进程,关闭父进程pid_t pid = fork();if (pid != 0) {exit(0);}//创建新会话int ret = setsid();//切换工作目录,防止当前目录被卸载chdir("/home/daniel");umask(0022);close(STDIN_FILENO);  //关闭标准输入int fd = open("/dev/null", O_RDWR);if (fd < 0) {perr_exit("open error");}//将标准输出和标准错误重定向到/dev/nulldup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);while (1);return 0;
}

多线程编程

LWP:轻量级进程, 本质仍然是进程

进程: 有独立的地址空间, 有PCB

线程: 有独立的PCB, 但是没有独立的地址空间(共享)

所以二者区别就在于是否共享地址空间

线程: 最小的执行单位

进程: 最小分配资源的单位, 可以看作是只有一个线程的进程

ps -Lf pid查看一个进程开的线程个数


PCB中持有当前进程的页目录表的指针, 页目录表中每一项指向一个个页表, 用页表检索物理内存页面

线程之间共享的资源:

  1. 文件描述符表
  2. 每种信号的处理方式
  3. 当前工作目录位置
  4. 用户ID和组ID
  5. 内存地址空间(.text/.data/.bss/.heap/共享库)

线程非共享资源:

  1. 线程id
  2. 处理器现场和栈指针(内核栈)
  3. 独立的栈空间(用户空间栈)
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级

pthread_create函数

int pthread_create(pthread_t* thread,const pthread_attr_t* attr,void* (*start_routine)(void* ),void* arg);

获取线程id:

pthread_t pthread_self(void);

成功返回0, 失败返回errno

//pthread_create-demo.c
void* tfn(void* arg) {printf("tfn:pid=%d,tid=%lu\n", getpid(), pthread_self());return NULL;
}int main(int argc, char* argv[]) {printf("main:pid=%d,tid=%lu\n", getpid(), pthread_self());pthread_t tid = 0;int ret = pthread_create(&tid, NULL, tfn, NULL);if (ret != 0)perr_exit("pthread_create error");/*父进程等待1秒,否则父进程一旦退出,地址空间被释放,子线程没机会执行*/sleep(1);return 0;
}

循环创建多个子线程

//pthreads.c
void* tfn(void* arg) {long i = (long)arg;sleep(i);printf("I'm %ld thread, pid = %d, tid = %lu\n", i, getpid(), pthread_self());return NULL;
}int main(int argc, char* argv[]) {for (long i = 0; i < 5; ++i) {pthread_t tid;int ret = pthread_create(&tid, NULL, tfn, (void*)i);if (ret < 0) {perr_exit("pthread_create error");}}sleep(5);return 0;
}

注意参数传递方式, 先将int型的i强转成void*传入, 用到时再强转回int

一个有错误的版本:如果不用强转, 看似规规矩矩的传地址再解引用, 会出现问题

/*这是一个出错的版本*/
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语言值传递的特性)

线程默认共享数据段, 代码段等地址空间, 常用的是全局变量, 而进程不共享全局变量, 只能借助mmap

void pthread_exit(void* retval);

retval表示退出状态, 通常传NULL

exit()函数用来退出当前进程, 不可以用在线程中, 否则直接一锅端了

pthread_exit()函数才是用来将单个的线程退出

pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者malloc分配的, 不能在线程函数的栈上分配, 因为其他线程得到这个返回指针时线程函数已经退出了

//pthread_exit-demo.c
void* tfn(void* arg) {long i = (long)arg;if (i == 2)pthread_exit(NULL);printf("I'm %ld thread,pid=%d,tid=%lu\n", i, getpid(), pthread_self());sleep(i);return NULL;
}

pthread_join函数

int pthread_join(pthread_t thread, void** retval);

成功返回0, 失败返回errno

线程的退出状态是void*, 回收时传的就是void**

//pthread_join-demo.c
struct thrd {int var;char str[256];
};void* tfn(void* arg) {struct thrd* pt = (struct thrd*)malloc(sizeof(struct thrd));pt->var = 9527;strcpy(pt->str, "hello, world");return (void*)pt;
}int main(int argc, char* argv[]) {pthread_t tid;pthread_create(&tid, NULL, tfn, NULL);struct thrd* pt;pthread_join(tid, (void**)&pt);printf("thread returns pt->var = %d, pt->str = %s\n", pt->var, pt->str);free(pt);return 0;
}

注意一个错误的写法:

void* tfn(void* arg){/*在栈区创建一个结构体*/struct thrd tval;/*给结构体赋值*/tval.var=100;strcpy(tval.str,"fuckyou");return (void*)&tval;
}

不能将子线程的回调函数的局部变量返回, 由于该函数执行完毕返回后, 其栈帧消失, 栈上的局部变量也就消失, 返回的是无意义的

当然, 可以在main函数中创建局部变量

使用pthread_join函数将循环创建的多个子线程回收:定义一个tid数组, 保存不同子线程的tid

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[]) {pthread_t tid;pthread_create(&tid, NULL, tfn, NULL);pthread_detach(tid);  //设置线程分离sleep(1);int ret = pthread_join(tid, NULL);   //这里会出错,不能对一个已经分离出去的子线程回收if (ret != 0) {printf("pthrad_join error: %s\n", strerror(ret));exit(1);}return 0;
}

detach: 设置线程分离, 线程终止会自动清理pcb, 无需回收

detach相当于自动回收, join相当于手动回收

注意检查出错方式的变化(失败会直接返回errno)

if (ret != 0) {printf("pthrad_join error: %s\n", strerror(ret));exit(1);
}

pthread_cancel函数

 int pthread_cancel(pthread_t thread);
DESCRIPTIONThe  pthread_cancel() function sends a cancellation request to the thread thread.  Whether and when the target thread reacts to the cancellation request depends on two attributesthat are under the control of that thread: its cancelability state and type.A thread's cancelability state, determined by pthread_setcancelstate(3), can be enabled (the default for new threads) or disabled.  If a thread has disabled cancellation, then  acancellation request remains queued until the thread enables cancellation.  If a thread has enabled cancellation, then its cancelability type determines when cancellation occurs.A thread's cancellation type, determined by pthread_setcanceltype(3), may be either asynchronous or deferred (the default for new threads).  Asynchronous cancelability means thatthe thread can be canceled at any time (usually immediately, but the system does not guarantee this).  Deferred cancelability means that cancellation will be  delayed  until  thethread next calls a function that is a cancellation point.  A list of functions that are or may be cancellation points is provided in pthreads(7).When a cancellation requested is acted on, the following steps occur for thread (in this order):1. Cancellation clean-up handlers are popped (in the reverse of the order in which they were pushed) and called.  (See pthread_cleanup_push(3).)2. Thread-specific data destructors are called, in an unspecified order.  (See pthread_key_create(3).)3. The thread is terminated.  (See pthread_exit(3).)The  above steps happen asynchronously with respect to the pthread_cancel() call; the return status of pthread_cancel() merely informs the caller whether the cancellation requestwas successfully queued.After a canceled thread has terminated, a join with that thread using pthread_join(3) obtains PTHREAD_CANCELED as the thread's exit status.  (Joining with a thread  is  the  onlyway to know that cancellation has completed.)RETURN VALUEOn success, pthread_cancel() returns 0; on error, it returns a nonzero error number.ERRORSESRCH  No thread with the ID thread could be found.ATTRIBUTESFor an explanation of the terms used in this section, see attributes(7).┌─────────────────┬───────────────┬─────────┐│Interface        │ Attribute     │ Value   │├─────────────────┼───────────────┼─────────┤│pthread_cancel() │ Thread safety │ MT-Safe │└─────────────────┴───────────────┴─────────┘

应用:

//pthread_cancel-demo.c
void* tfn(void* arg) {while (1) {printf("pid = %d,tid = %lu\n", getpid(), pthread_self());sleep(1);}return NULL;
}int main(int argc, char* argv[]) {pthread_t tid;pthread_create(&tid, NULL, tfn, NULL);//等待5s后杀死该线程sleep(5);int ret = pthread_cancel(tid);if (ret != 0) {perr_exit("pthread_cancel error", ret);}return 0;
}

cancel必须要等待取消点(进入内核的契机), 所以如果一个线程一直使用系统调用(一直不进内核), cancel就无法杀死该线程

可以手动添加一个取消点pthread_testcancel()


进程和线程控制原语对比:

线程控制原语 进程控制原语
pthread_create() fork()
pthread_self() getpid()
pthread_exit() exit()
pthread_join() wait()/waitpid()
pthread_cancel() kill()
pthread_detach() -

线程属性

先初始化线程属性, 再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_DETACHEDPTHREAD_CREATE_JOINABLE

一个例子:

void perr_exit(const char* str, int ret) {fprintf(stderr, "%s:%s\n", str, strerror(ret));pthread_exit(NULL);    //为了不至于使子线程退出,主线程应调用pthread_exit()而非exit()
}void* tfn(void* arg) {while (1) {printf("pid = %d,tid = %lu\n", getpid(), pthread_self());sleep(1);}return NULL;
}int main(int argc, char* argv[]) {pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);pthread_t tid;pthread_create(&tid, &attr, tfn, NULL);int ret = pthread_join(tid, NULL);    //尝试回收,但是会失败,因为前面已经设置了线程分离属性pthread_attr_destroy(&attr);if (ret != 0) {perr_exit("pthread_join error", ret);}pthread_exit(NULL);    //为了不至于使子线程退出,主线程应调用pthread_exit()而非exit()
}

各个子线程会均分进程的栈空间, 但是线程的栈空间大小是可以调整的


复习:

线程同步

线程同步: 一个线程发出某一功能调用时, 再没有得到结果之前, 该调用不返回. 同时其他线程为保证数据的一致性, 不能调用该功能, 避免产生与时间有关的错误

锁使用注意事项:Linux提供的

Linux系统编程学习笔记相关推荐

  1. Linux系统编程学习笔记(九)进程间通信IPC

    进程间通信IPC: 我们以前介绍过进程控制原语,看到怎么创建多个进程.但是进程之间交互信息的方式只介绍了通过fork或者exec继承父进程的打开文件或者通过文件系统. 经典的进程通信方式有:管道.FI ...

  2. linux线程并不真正并行,Linux系统编程学习札记(十二)线程1

    Linux系统编程学习笔记(十二)线程1 线程1: 线程和进程类似,但是线程之间能够共享更多的信息.一个进程中的所有线程可以共享进程文件描述符和内存. 有了多线程控制,我们可以把我们的程序设计成为在一 ...

  3. 【Linux系统编程学习】Linux进程控制原语(fork、exec函数族、wait)

    此为牛客Linux C++和黑马Linux系统编程课程笔记. 1. fork函数 1.1 fork创建单个子进程 #include<unistd.h> pid_t fork(void); ...

  4. 《Linux Shell编程学习笔记之一》

    <Linux Shell编程学习笔记之一> 前言 由于自己一直在Windows上面编程,用linux用的比较少,学习linux还是本科大二学的一点点知识.因此自己就准备花点时间来熟悉下li ...

  5. Linux Shell编程学习笔记(4)

    Linux Shell编程学习笔记(2015-7-20) 分类:linux shell   今天学习了Linux Shell中的控制结构.其实大多数语言的控制结构这一块都是基本类似的,有了C语言的基础 ...

  6. Linux Shell编程学习笔记(3)

    Linux Shell编程学习笔记(2015-7-19) 分类:linux shell 一:位置参数和特殊变量   什么是位置参数?   位置参数也叫位置变量,是运行shell脚本程序时,命令行she ...

  7. 【Linux系统编程学习】信号、信号集以其相关函数

    此为牛客Linux C++和黑马Linux系统编程课程笔记. 文章目录 0. 信号的概念 1. Linux信号一览表 2. 信号相关函数 3. kill函数 4. raise函数 5. abort函数 ...

  8. 【Linux系统编程学习】匿名管道pipe与有名管道fifo

    此为牛客Linux C++和黑马Linux系统编程课程笔记. 0. 关于进程通信 Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间.任何一个进程的全局变量在另一个进程中都看不到 ...

  9. 嵌入式Linux系统编程学习之二常用命令

    嵌入式Linux系统编程学习之二常用命令 文章目录 嵌入式Linux系统编程学习之二常用命令 前言 一.常用命令 1.su(用户切换) 2.useradd(添加用户) 3.passwd(修改密码) 4 ...

  10. 嵌入式Linux系统编程学习之一目录结构

    嵌入式Linux系统编程学习之一目录结构 文章目录 嵌入式Linux系统编程学习之一目录结构 前言 一.Linux目录结构 前言 Linux目录结构 一.Linux目录结构 /bin:存放Linux的 ...

最新文章

  1. 对于量子计算来说,99%的准确度足够吗?
  2. as 关联 android源码,android studio 2.x以上关联源码
  3. 课堂练习--最大子数组和 环
  4. 两种方式:mysql查看正在执行的sql语句
  5. Ubuntu 下搭建 NFS 服务
  6. 让其他CMS黯然失色的we7CMS
  7. html跳动的小球,canvas绘制跳动的小球
  8. 文字识别(三)--文字定位与切割
  9. centos 7 修改ip
  10. chemdraw怎么画拐弯的箭头_教你画系列,像金属一样有复杂反光的漆皮材质
  11. 微信二次开发html,基于mvc的微信二次开发c#源代码
  12. 计算机组成原理框架结构图
  13. 微信小程序-图片等比例显示不变形
  14. 《一周搞定模电》—功率放大器
  15. diy无感无刷电机霍尔安装_无刷直流电机霍尔传感器安装方法研究
  16. 你用什么软件做笔记?
  17. Supermap iDesktop处理导入CAD文件存在线型风格显示缺失问题
  18. mysql用了索引为什么还是查询很慢?
  19. 快速理解简单的短链接生成原理
  20. 使用 Stunnel 建立加密隧道

热门文章

  1. 疯狂python讲义豆瓣评分_书榜 | 计算机书籍(9.16-9.22)销售排行榜
  2. 常见问题之Golang——verifying github.com/go-playground/assert/v2@v2.0.1/go.mod: checksum mismatch错误...
  3. yxy小蒟蒻的201117总结
  4. Samsung 6818平台首次编译遇到的问题
  5. upc 卡德加的兔子 线段树 + 矩阵快速幂
  6. 6.0系统xposed框架安装流程
  7. c语言程序设计教程答案王晓云,【单选题】华人图灵奖获得者是( ) A. 吴恩达 B. 王小云 C. 姚期智 D. 杨振宁...
  8. IP定位如何揪出SEM、百度竞价的“头号天敌——恶意点击”
  9. 印度人为什么立刀切菜_印度人的刀功有多厉害?看到一刀刮掉的鱼鳞和碎洋葱,我彻底服了...
  10. Win10 快速检查修复系统方法