(提取码:unix)
$ echo $SHELL #查看当前正在使用的命令解析器
$ cat /etc/shells #查看支持的所有shell
$ ls -l file #查看file的详细信息
Ctrl+h #BackSpace
Ctrl+a #回到命令首
Ctrl+e #回到命令尾
Ctrl+u #删除所有内容
daniel@u22:/$ ls
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run
etcetera
,“等等”的意思$ ls -l dir #查看dir目录下文件的详细信息
$ ls -ld dir #查看dir目录本身的详细信息
$ ls -R #递归进入子目录
Linux文件类型:
查看可执行文件的路径:
daniel@ubuntu:~$ which date
/bin/date
more
:分屏显示文件内容,空格翻页。less
与之同理
创建软连接:
$ ln -s hello.c hello.c.s
$ ll
total 8.0K
drwxrwxr-x 2 daniel daniel 4.0K 2月 29 20:26 ./
drwxr-x--- 38 daniel daniel 4.0K 2月 29 20:25 ../
-rw-rw-r-- 1 daniel daniel 0 2月 29 20:25 hello.c
lrwxrwxrwx 1 daniel daniel 7 2月 29 20:26 hello.c.s -> hello.c
软连接中存的就是文件的路径,路径有几个字符就占几个字节,所以建议用绝对路径创建软连接
另外注意文件的权限,软连接的权限代表其本身的权限,与指向的目的文件无关
创建硬链接:
$ ln hello.c hello.c.h
$ ll
total 8.0K
drwxrwxr-x 2 daniel daniel 4.0K 2月 29 20:26 ./
drwxr-x--- 38 daniel daniel 4.0K 2月 29 20:25 ../
-rw-rw-r-- 2 daniel daniel 0 2月 29 20:25 hello.c
-rw-rw-r-- 2 daniel daniel 0 2月 29 20:25 hello.c.h
lrwxrwxrwx 1 daniel daniel 7 2月 29 20:26 hello.c.s -> hello.c
创建硬链接会增加文件FCB的硬链接计数
这些硬链接指向同一个文件,修改一个其余的会同步变化
查看文件状态:
$ stat hello.c
File: hello.c
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 803h/2051d Inode: 1723215 Links: 2
Access: (0664/-rw-rw-r--) Uid: ( 1000/ daniel) Gid: ( 1000/ daniel)
Access: 2024-02-29 20:25:48.052639591 +0800
Modify: 2024-02-29 20:25:48.052639591 +0800
Change: 2024-02-29 20:26:49.372963401 +0800
Birth: 2024-02-29 20:25:48.052639591 +0800
所有的硬链接有相同的Inode(文件统一id)。删除只是把硬链接计数-1
chmod:
文字设定法:
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 ./ -size +20M -size -50M
:指定大小范围
ls -h
-以人类可读的方式显示结果
man手册中反斜杠/
可以用于查找关键字,来自于vim
关于size的单位(find默认用b)
find ./ -ctime 3
:查找三天内被改动的文件
find /usr/ -name "\*temp\*" -exec ls -l {} \;
大括号表示前面命令返回的结果集,对其指定-exec
后面的命令,\;
是转义后的;
find ~/ -type f -ok rm -r {} \;
exec的缓冲版,操作前会询问,保证安全性
grep:按文件内容搜索"return"关键字:
grep -rn "return" ./
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
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
进行压缩,c
是create
的意思,v
表示可见,f
表示生成文件
file
是文件照妖镜,看文件属性
bzip2
和gzip
类似,都是单个文件所用,如果使用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后面用户的作业)
fg
和bg
用于前后台切换
env
查看环境变量:
env | grep SHELL
top
是调出任务管理器
sudo passwd daniel
改密码
ifconfig
查看网卡信息
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
查看空闲内存
命令模式下:
I:光标到行首, 插入
A:光标到行尾,插入
S:直接干掉整行,切换到文本模式书写
末行模式下直接输入行号就可以跳转到指定行
命令模式下的%
可以跳转到匹配的括号
d0
:删到行首
找设想内容:命令模式下输入斜杠/
,然后输入查找的内容,进行查找
回车后按n找到下一个
找看到的内容:在一定范围内检索单词:在单词名字上星号(向后)*
,或者井号(向前)#
替换:在本行的末行模式下::s /printf/println
通篇替换:只会替换每一行的首个::%s /printf/println
如果想要把每行后面的也替换:加参数g
局部替换::30,37 s/int/unsigned int/g
(有点像sed的语法)
Ctrl+r
反撤销
:sp
水平分屏
[d
查看宏定义
! gcc hello.c -o hello
在文件中执行命令
vim配置文件路径:~/.vimrc
GNU Compiler Collection
预处理: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
选项生成与位置无关的代码
查看二进制文件的反汇编代码: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 #编译通过,执行出错
首先要清楚两个没有任何关系的链接器:
动态链接器工作时要根据环境变量LD_LIBRARY_PATH
寻找动态库
临时设置环境变量export LD_LIBRARY_PATH=./lib
,但是bash重启后这个设置就会失效
要想永久指定,需要将其写入bashrc配置文件,然后重启bash或source ~/.bashrc
ldd a.out
可以查看程序运行所需要的动态库
$ ldd a.out
linux-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 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只需掌握以下几点即可:
目标:依赖条件
命令(前面是一个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
多文件联编:
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
makefile的2个函数和clean
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前面的横杠表示出错(文件不存在)仍然执行
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 $@
.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
daniel@u22:~/gnumake$ tree
.
├── include
│ └── math.h
├── makefile
├── obj
└── src
├── add.c
├── main.c
├── mul.c
└── sub.c
3 directories, 6 files
祖传的makefile:
src=$(wildcard src/*.c)
obj=$(patsubst src/%.c,obj/%.o,${src})
FALGS=-Wall -g
INLUCDE_PATH=include
ALL:main
main:${obj}
gcc $^ -o $@ ${FALGS}
${obj}:obj/%.o:src/%.c
gcc -c $< -o $@ ${FALGS} -I${INLUCDE_PATH}
clean:
-rm -rf obj/* main
run:
./main
.PHONY: ALL clean
系统调用:内核提供的函数:由操作系统实现并提供给外部应用程序的编程接口,是应用程序同操作系统之间交互数据的桥梁
为了保证系统的安全性,manPage中的系统调用都是对系统调用的一次浅封装,比如open()
对应的是sys_open()
int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode); //mode_t是一个8进制整型,指定文件权限,只有当参2指定了CREAT才有用
参数:
成功返回文件描述符,失败返回-1并设置errno
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.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)
src=$(wildcard ./*.c)
target=$(patsubst %.c,%,$(src))
ALL:$(target)
myArgs=-Wall -g
%:%.c
gcc $< -o $@ $(myArgs)
clean:
-rm -rf $(target)
.PHONY:ALL clean
read:从文件中读出数据写到缓冲区
ssize_t read(int fd, void* buf, size_t count);
参3是缓冲区的大小
成功返回实际读到的字节数,返回0时意味着读到了文件末尾,失败返回-1并设置errno
wirte:从缓冲区中读出数据写到文件
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;
}
核心代码:
while ((s = read(fd1, buf, BS)) > 0)
ssize_t ret = write(fd2, buf, s);
系统调用和库函数比较:库函数具有预读入和缓输出机制
strace:跟踪一个程序执行时所需要的系统调用
如果规定逐字节的进行拷贝,用库函数会比用系统调用快很多,因为其有预读入和缓输出机制
OS绝不会让你逐字节的向Disk上写数据,实际上它维护了一个系统级缓冲,只有当从用户空间过来的数据在该缓冲上写满时,他才会一次性将数据冲刷到Disk上
当使用系统调用的方法时,要不断的在用户空间和内核空间进行来回切换,这会消耗大量时间
而使用fputc()
时,他在设计之初自己在用户空间维护了一个用户级缓冲,这样在用户空间把自己的缓冲写满,再一次性写入内核缓冲(写入了内核缓冲就认为写到了磁盘上了),这样大大减少了在用户空间和内核空间来回切换的次数
read()
和write()
函数常被称为UnbufferedIO
,指无用户级缓冲区,但不保证不使用内核缓冲区
PCB
中有一个指针,指向了该进程的文件描述符表,每个表项都是一个键值对,其中的value是指向文件结构体的指针,其索引是fd,fd是OS暴露给用户的唯一操作文件的依据
新打开的文件描述符一定是所有文件描述符表中可用的, 最小的那个文件描述符
文件描述符最大1023,说明一个进程最多能打开1024个文件
文件结构体:
struct file {
文件偏移量
文件访问权限
文件打开标志
文件内核缓冲区首地址
struct operations* f_op
}
一个自己的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
会一直阻塞等待用户输入(进程 sleep)
阻塞是设备文件和网络文件的属性
当然也可以设置 O_NONBLOCK
以非阻塞方式从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=EAGAIN
或EWOULDBLOCK
,说明不是read
失败,而是read
在以非阻塞方式读一个设备文件或网络文件,而文件中无数据
阻塞方式存在的问题也正是后来网络IO中select
,poll
和epoll
函数存在的原因
fcntl
函数:改变一个已经打开文件的访问控制属性
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
是一个位图,每一位代表不同属性的真假值
off_t lseek(int fd, off_t offset, int whence);
举例:
//lseek-demo.c
#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
获取文件大小:
//lseek-get-file-size.c
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操作
//lseek-extend-file.c
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语言中指针作为传入传出参数:
传入参数:
传出参数:
传入传出参数:
增加文件的硬链接只是增加dentry
,指向相同的inode
同样,删除硬链接也只是删除dentry
,要注意删除文件并不会让数据在磁盘消失,只是OS丢失了inode
,磁盘只能覆盖,不能擦除
作用:获取文件属性(从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
};
参数:
返回值:成功返回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;
}
ln makefile makefile.hard
:为makefile创建硬连接
int link(const char *oldpath, const char *newpath);
使用link
和unlink
函数实现mv
命令:
// link-mymv.c
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;
}
因此我们删除文件,从某种意义上来说只是让文件具备了被删除的条件
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
文件名不能超过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 */
};
//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下文件存储原理:
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;
}
duplicate:复制,副本
cat makefile > m1
:将cat的结果重定向到m1(此时m1与makefile内容相同)
cat makefile >> m1
:将cat的结果重定向并追加到m1后面(此时m1是双份的makefile)
int dup(int oldfd);
int dup2(int oldfd, int newfd);
传入已有的文件描述符,返回一个新的文件描述符
//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); //标准输出指向fd1
printf("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 = 8
const char* str = "abcdefg";
write(fd3, str, strlen(str));
close(fd3);
return 0;
}
参3传0,则从0开始向下寻找可用的文件描述符返回给newfd1
参3传8,则从8开始向下寻找可用的文件描述符返回给newfd2
dup2
的newfd比dup
的灵活一些:他能打破可用最小的文件描述符限制
从虚拟内存到物理内存的映射由MMU完成,不同进程的用户空间被映射到物理内存的不同位置,而不同进程的kernel空间被映射到物理内存的相同位置,对于物理内存来用户空间和内核空间有不同的特权级,从用户空间到内核空间的转换实质上是特权级的切换
每个进程在内核中都有一个PCB来维护进程相关信息,Linux内核的进程控制块是task_struct
类型的结构体
着重掌握的:
$ echo $PATH
/home/daniel/.local/bin:/home/daniel/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/local/go/bin
$ echo $SHELL
/bin/bash
$ echo $TERM
xterm-256color
$ echo $LANG
en_US.UTF-8
pid_t fork(); /*函数原型相当简单:空参,返回一个整数pid*/
成功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,因为命令行中的程序都由 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之后:
父子进程的不同之处: 进程ID, fork返回值, 父进程ID, 进程运行时间, 闹钟(定时器), 未决信号集
父子进程间遵循读时共享, 写时复制的原则,这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销
//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
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
函数可以回收子进程终止信息,该函数有三个功能:
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 -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) 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
可以指定某一个子进程进行回收
一次wait
或waitpid
函数调用, 只能回收一个子进程, 如果你循环创建了多个子进程, 那么就碰到哪个算哪个
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
只能回收子进程,爷孙的也不行
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe
系统函数即可创建一个管道,有如下特质:
管道的原理:管道实际为内核使用环形队列机制,借助内核缓冲区(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) { // child
close(pfd[0]);
write(pfd[1], str, strlen(str));
close(pfd[1]);
} else { // parent
close(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
信号(也可以捕捉SIGPIPE
信号,使进程不终止)
2.管道读端没有全部关闭:
若管道已满,则write
阻塞
若管道未满,则write
将数据写入,并返回实际写入的字节数
总结:
父子进程借助管道实现ls | wc -l
统计行数的功能:
需要使用的函数:
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 { // parent
close(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
匿名管道pipe
的优缺点:
优点:简单,相比信号,套接字实现进程间通信,简单很多
缺点:只能单向通信,双向通信需建立两个管道;只能用于父子、兄弟进程(有共同祖先)间通信,该问题后来使用 fifo 有名管道解决
为区分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
没有血缘关系的进程也可以用文件进行进程间通信
存储映射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); //删除映射区
参数:
返回值:
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");
}
//扩展文件大小到20B
int 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使用注意事项:
所以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匿名映射区:
//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
文件也不能用于无血缘关系进程间通信
信号的特性:
信号的特质:
A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行
与硬件中断类似:异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理
如何产生信号:
递达:内核发出的信号递送并且到达进程
未决:产生和递达之间的状态,主要由于阻塞(屏蔽)导致该状态
信号的处理方式:
信号屏蔽字和未决信号集:两者都是位图
阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(直到解除屏蔽后)
未决信号集:
常规信号一览:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) 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个位常规信号,有默认事件和处理动作。后面的是实时信号,没有默认事件和处理动作
信号四要素:编号,名称,触发事件,默认处理动作
后面有多个值的信号是因为不同的操作系统的处理器架构不同
常规信号一览:
信号的默认处理动作:
SIGKILL(9)
和SIGSTOP(19)
,不允许忽略和捕捉,只能执行默认动作,甚至不能将其设置为阻塞
只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应该乱发信号
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号进程,是不允许的
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
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
指定定时方式:
参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:
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+c
sigaddset(&new_sigset, SIGQUIT); //屏蔽Ctrl+'\'
sigaddset(&new_sigset, SIGBUS); //屏蔽SIGBUS
sigaddset(&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
/*定义回调函数类型,很不幸,函数类型限制死了*/
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;
}
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;
}
信号捕捉的特性:
mask
变为sigaction
结构体中的sa_mask
,捕捉函数执行结束后,恢复回mask
(sa_flags=0)
内核实现信号捕捉简析:
为什么执行完信号处理函数后要再次进入内核?
因为信号处理函数是内核调用的,函数执行完毕后要返回给调用者
借助信号捕捉回收子进程:
//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
函数的进程,既是新的会长,也是新的组长
Daemon 进程,是 Linux 中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字
Linux 后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录,注销的影响,一直在运行着
创建守护进程,最关键的一步是调用 setsid 函数创建一个新的 Session,并成为 Session Leader
守护进程创建:
//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/null
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
while (1)
;
return 0;
}
LWP:轻量级进程,本质仍然是进程
线程是最小的执行单位;而进程是最小分配资源的单位,可以看作是只有一个线程的进程
ps -Lf pid
:查看一个进程开的线程个数
线程之间共享的资源:
线程非共享资源:
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;
}
错误分析:
使用强转可以保证变量i
的实时性(C语言值传递的特性)
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;
}
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
int pthread_detach(pthread_t thread);
子线程分离后不能再调用join
回收了:
//pthread_detach-demo
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
相当于手动回收
注意检查出错方式的变化,要用strerror()
(失败会直接返回errno
)
int ret = pthread_join(tid, NULL);
if (ret != 0) {
printf("pthrad_join error: %s\n", strerror(ret));
exit(1);
}
类似于kill()
,用于杀死线程,但是他只是向一个指定的线程发送取消请求,发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出
int pthread_cancel(pthread_t thread);
应用:
//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_cancel-endof3.c
void* tfn1(void* arg) {
printf("thread1: returning\n");
return (void*)111;
}
void* tfn2(void* arg) {
printf("thread2: exiting\n");
pthread_exit((void*)222);
}
void* tfn3(void* arg) {
while(1) {
//下面的两条语句都会陷入内核,从而处理calcel“信号”,杀死线程
printf("thread3: going to die in 3 seconds\n");
sleep(1);
//pthread_testcancel(); //手动添加取消点
}
return (void*)333;
}
int main() {
pthread_t tid;
void* ret;
pthread_create(&tid, NULL, tfn1, NULL);
pthread_join(tid, &ret);
printf("thread1 exit code = %ld\n", (long)ret);
pthread_create(&tid, NULL, tfn2, NULL);
pthread_join(tid, &ret);
printf("thread2 exit code = %ld\n", (long)ret);
pthread_create(&tid, NULL, tfn3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
printf("thread3 exit code = %ld\n", (long)ret);
return 0;
}
进程和线程控制原语对比:
线程控制原语 | 进程控制原语 |
---|---|
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);
线程的分离状态:
线程的分离状态决定一个线程以什么样的方式来终止自己
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当 pthread_join()
函数返回时,创建的线程才算终止,才能释放自己占用的系统资源
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态
设置线程分离状态函数:
/*设置线程属性:分离或非分离*/
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
一个例子:
//pthread_attr_t-demo.c
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 中提供一把互斥锁 mutex(也称之为互斥量)
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁
资源还是共享的,线程间也还是竞争的,但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了
先来看一个C的关键字:restrict
:用来限定指针变量,被该关键字限定的指针变量所指向的内存操作,必须由本指针完成:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
使用锁的一般流程:
pthread_mutex_t lock; //创建
pthread_mutex_init(); //初始化
pthread_mutex_lock(); //加锁
visit(); //访问数据
pthread_mutex_unlock(); //解锁
pthread_mutex_destory(); //销毁锁
//lock-demo.c
/*创建一把全局锁*/
pthread_mutex_t mutex;
void* tfn(void* arg) {
srand(time(NULL));
while (1) {
pthread_mutex_lock(&mutex);
printf("hello, ");
sleep(rand() % 4);
printf("world\n");
pthread_mutex_unlock(&mutex);
sleep(rand() % 4);
}
return NULL;
}
int main(int argc, char* argv[]) {
pthread_mutex_init(&mutex, NULL);
srand(time(NULL));
pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
while (1) {
pthread_mutex_lock(&mutex);
printf("HELLO, ");
sleep(rand() % 4);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);
sleep(rand() % 4);
}
pthread_mutex_destroy(&mutex);
pthread_join(tid, NULL);
return 0;
}
互斥锁使用技巧:锁的粒度越小越好,访问前加锁,访问结束后立即解锁
可以将mutex
想象为一个整数:
try锁:try锁会尝试加锁,成功mutex--
,失败返回错误号,而lock如果加锁失败会阻塞,等待锁释放
两种死锁:
mutex
反复加锁读写锁与互斥量类似,但读写锁允许更高的并行性,其特性为
读写锁只有一把,但其具备两种状态:
当读线程远大于写线程,读写锁可以调高访问效率
读写锁操作函数:
/*定义一个读写锁变量*/
pthread_rwlock_t rwlock;
int pthread_rwlock_init(&rwlock,NULL);
int pthread_rwlock_destory(&rwlock);
int pthread_rwlock_rdlock(&rwlock);
int pthread_rwlock_wrlock(&rwlock);
int pthread_rwlock_tryrdlock(&rwlock);
int pthread_rwlock_trywrlock(&rwlock);
int pthread_rwlock_unlock(&rwlock);
都是成功返回0,失败直接返回错误号
读写锁代码示例:
//rwlock-demo.c
int cnt = 0;
pthread_rwlock_t rwlock;
void* writer(void* arg) {
long i = (long)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock);
usleep(10000);
int t = cnt;
printf("I'm writer %ld, cnt = %d, ++cnt = %d\n", i, t, ++cnt);
pthread_rwlock_unlock(&rwlock);
usleep(100000);
}
return NULL;
}
void* reader(void* arg) {
long i = (long)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock);
printf("I'm reader %ld, cnt = %d\n", i, cnt);
pthread_rwlock_unlock(&rwlock);
usleep(20000);
}
return NULL;
}
int main() {
pthread_rwlock_init(&rwlock, NULL);
pthread_t tid[8];
long i = 0;
for (; i < 3; ++i) {
pthread_create(tid + i, NULL, writer, (void*)i);
}
for (; i < 8; ++i) {
pthread_create(tid + 3 + i, NULL, reader, (void*)i);
}
for (i = 0; i < 8; ++i) {
pthread_join(tid[i], NULL);
}
pthread_rwlock_destroy(&rwlock);
}
现象:
I'm writer 0, cnt = 0, ++cnt = 1
I'm reader 3, cnt = 1
I'm reader 4, cnt = 1
I'm reader 5, cnt = 1
I'm reader 6, cnt = 1
I'm reader 7, cnt = 1
I'm writer 1, cnt = 1, ++cnt = 2
I'm writer 2, cnt = 2, ++cnt = 3
I'm reader 6, cnt = 3
I'm reader 3, cnt = 3
I'm reader 5, cnt = 3
I'm reader 4, cnt = 3
I'm reader 7, cnt = 3
I'm reader 3, cnt = 3
I'm reader 5, cnt = 3
I'm reader 6, cnt = 3
I'm reader 4, cnt = 3
I'm reader 7, cnt = 3
I'm reader 6, cnt = 3
I'm reader 5, cnt = 3
I'm reader 4, cnt = 3
I'm reader 3, cnt = 3
I'm reader 7, cnt = 3
I'm reader 4, cnt = 3
I'm reader 5, cnt = 3
I'm reader 3, cnt = 3
I'm reader 6, cnt = 3
I'm reader 7, cnt = 3
I'm writer 0, cnt = 3, ++cnt = 4
I'm reader 5, cnt = 4
I'm reader 3, cnt = 4
I'm reader 4, cnt = 4
I'm reader 6, cnt = 4
I'm reader 7, cnt = 4
I'm writer 1, cnt = 4, ++cnt = 5
I'm writer 2, cnt = 5, ++cnt = 6
I'm reader 5, cnt = 6
I'm reader 3, cnt = 6
I'm reader 4, cnt = 6
I'm reader 6, cnt = 6
I'm reader 7, cnt = 6
I'm reader 4, cnt = 6
I'm reader 5, cnt = 6
I'm reader 3, cnt = 6
条件变量本身不是锁,但它也可以造成线程阻塞。通常与互斥锁配合使用,给多线程提供一个会和的场所
主要应用函数:
/*定义一个条件变量:静态初始化*/
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
int pthread_cond_init(&cond,NULL);
int pthread_cond_destory(&cond);
int pthread_cond_wait(&cond,&mutex);
int pthread_cond_timewait();
int pthread_cond_signal(); //通知一个线程
int pthread_cond_broadcast(); //通知所有线程
互斥量也可以进行静态初始化:
pthread_mutex_t cond=PTHREAD_MUTEX_INITIALIZER;
wait函数,阻塞等待一个条件变量:
int pthread_cond_wait(pthread_cond_t* restrict cond,pthread_mutex_t* restrict mutex);
函数作用:
pthread_mutex_unlock(&mutex)
,与第一步在一块形成一个原子操作pthread_cond_wait()
函数返回时,解除阻塞并重新申请互斥锁pthread_mutex_lock(&mutex)
生产者:
pthread_mutex_lock(&mutex)
pthread_mutex_unlock(&mutex)
pthread_cond_signal()
或pthread_cond_broadcast()
消费者:
pthread_mutex_t mutex
pthread_mutex_init(&mutex,NULL)
pthread_mutex_lock(&mutex)
pthread_cond_wait(&cond,&mutex)
,首先解锁并阻塞等待条件变量,然后加锁代码实现:
//producer-consumers.c
struct Node {
int val;
struct Node* next;
};
typedef struct Node Node;
Node* head = NULL;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_producted = PTHREAD_COND_INITIALIZER;
void* consumer(void* arg) {
while (1) {
pthread_mutex_lock(&lock);
while (head == NULL) {
pthread_cond_wait(&has_producted, &lock);
}
Node* p = head;
head = head->next;
pthread_mutex_unlock(&lock);
printf("consume %d\n", p->val);
free(p);
sleep(rand() % 8);
}
return NULL;
}
void* producer(void* arg) {
while (1) {
Node* n = (Node*)malloc(sizeof(Node));
n->val = rand() % 1000;
printf("produce %d\n", n->val);
pthread_mutex_lock(&lock);
n->next = head;
head = n;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_producted);
sleep(rand() % 2);
}
return NULL;
}
int main(int argc, char* argv[]) {
srand(time(NULL));
pthread_t pid, cid[3];
pthread_create(&pid, NULL, producer, NULL);
for (int i = 0; i < 3; ++i) {
pthread_create(cid + i, NULL, consumer, NULL);
}
pthread_join(pid, NULL);
for (int i = 0; i < 3; ++i) {
pthread_join(cid[i], NULL);
}
return 0;
}
/*当条件变量不满足时,解锁,并死循环等待*/
while(head==NULL)
pthread_cond_wait(&has_product,&lock);
注意这里不能用if
,否则当多个消费者其中一个抢到了锁把数据读走后,其他消费者由于阻塞在了锁上,会尝试去缓冲区拿数据,而此时缓冲区并没有数据,所以应该用while
循环回来,重新检查条件变量
pthread_cond_signal()
唤醒阻塞在条件变量上的至少一个线程
pthread_cond_broadcast()
唤醒阻塞在条件变量上的所有线程
相当于初始化值为N的互斥量,N值表示可以同时访问共享区域的线程数。类似于操作系统理论课中的PV操作
信号量操作函数:
/*定义一个信号量*/
sem_t sem;
/*信号量操作函数*/
int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destroy(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_trywait(sem_t* sem);
int sem_timedwait(sem_t* sem, const struct timespec* abs_timeout);
int sem_post(sem_t *sem);
sem_wait()
:P 操作,如果信号量>0,则信号量–;如果信号量=0,造成线程阻塞
sem_post()
:V 操作,将信号量++,同时唤醒阻塞在信号量上的线程
当然,sem_t
的实现对用户隐藏,所以所谓的++
和--
只能通过函数来实现,不能直接用++
和--
符号
信号量的初值,决定了占用信号量的线程个数
信号量实现的生产者消费者:
//producer-consumer-sem_t.c
#define NUM 5
int arr[NUM];
sem_t empty, full;
void* producer(void* arg) {
int i = 0;
while (1) {
int t = rand() % 1000;
printf("produce %d\n", t);
sem_wait(&empty);
arr[i] = t;
sem_post(&full);
i = (i + 1) % NUM;
sleep(rand() % 3);
}
return NULL;
}
void* consumer(void* arg) {
int i = 0;
while (1) {
sem_wait(&full);
printf("consume %d\n", arr[i]);
arr[i] = -1;
sem_post(&empty);
i = (i + 1) % NUM;
sleep(rand() % 3);
}
return NULL;
}
__attribute__((constructor)) void begin() {
sem_init(&empty, 0, NUM);
sem_init(&full, 0, 0);
}
int main() {
srand(time(NULL));
pthread_t pid, cid;
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
__attribute__((destructor)) void end() {
sem_destroy(&empty);
sem_destroy(&full);
}