Linux高性能服务器编程 黑马程序员linux系统编程
linux系统编程
一、基本命令
Linux常见目录介绍
- /:根目录
- /bin:可执行的二进制文件
- /boot:放置linux系统启动时用到的一些文件
- /dev:存放linux系统下的设备文件
- /etc:系统配置文件存放的目录
- /home:系统默认的家目录
- /lib:系统使用的函数库目录
- /media和/mnt:光盘默认挂载点
- /opt:安装第三方软件所需的默认目录
- /proc:此目录的数据都在内存中(不占用磁盘空间),如系统核心、外部设备等
- /root:系统管理员的家目录
- /sbin:放置给系统管理员root使用的命令
- /srv:服务器启动之后需要访问的数据目录,如www服务需要访问的网页数据存放在/srv/www内
- /var:放置系统执行过程中经常变化的文件,如随时更改的日志文件、邮件;
Bash解析器常用快捷键
- Tab键:补齐命令、补齐路径、显示当前目录下的所有目录
- 中断进程:ctrl+c
- 光标瞬移:ctrl+a移动到光标头部,ctrl+e移动到光标尾部
- 字符删除:ctrl+u删除光标前所有内容,ctrl+k删除光标后所有内容
终端相关快捷键
- Ctrl+Shift+N:新建一个终端
- Ctrl+Shift+T:在终端里新建一个标签
- Ctrl+Shift+W:关闭标签页
- Ctrl+Shift+Q:关闭窗口
- Ctrl+Shift+C:复制
- Ctrl+Shift+V:粘贴
- Alt+[1-9]:终端标签页的切换
- Ctrl+D:关闭一个终端
- Alt+F4:关闭全部终端
相对路径和绝对路径
绝对路径:
- 绝对路径是从目录树的树根“/”目录开始往下直至到达文件所经过的所有节点目录
- 下级目录接在上级目录后面用“/”隔开
- 绝对路径都是从“/”开始的,所以第一个字符一定是“/”;
相对路径:
- 相对路径是指目标目录相对于当前目录的位置
- 目录 . 指向当前目录,而 .. 指向当前目录的上一级
目录相关的命令
pwd
打印当前所在目录的名字
cd
mkdir
mkdir用于创建一个空目录(不是新建普通文件)
用法:mkdir+选项+目录
rmdir
删除指定的空目录,而且必须离开当前的目录;
Linux文件类型
在Linux系统中,文件类型可以分为以下几类:
- 普通文件 (regular file):包括文本文件、二进制文件和数据文件。
- 目录 (directory):用于组织文件和子目录。
- 符号链接 (symbolic link):指向另一个文件的链接。
- 块设备文件 (block special file):与设备进行交互,如硬盘和 CD-ROM。
- 字符设备文件 (character special file):使用字符流与设备进行交互,如键盘和鼠标。
- 套接字 (socket):用于网络编程。
- 管道 (named pipe):用于进程间通信。
使用命令“ls -l”可以查看文件类型。输出的第一列中,第一个字符表示文件类型,例如“-”表示普通文件,“d”表示目录,“l”表示符号链接,“b”表示块设备文件,“c”表示字符设备文件,“s”表示套接字,“p”表示管道。
文件相关命令
ls
ls是英文单词list的简写,其功能为列出目录的内容
touch
用于创建普通文件
- 如果文件不存在则创建普通文件
- 如果文件存在,则更新文件时间
cp(拷贝)
rm
可以用于删除文件或目录
mv
用于移动文件或目录,也可以给文件或目录重命名
文件内容查看命令
cat
cat将文件内容一次性输出到终端
缺点是当文件太长则无法显示全部
less
less命令将文件内容分页显示到终端,可以自由地上下切换
head
head -n[行数] 文件名
head命令从文件头部开始查看前n行的内容
如果没有指定行数,默认显示前10行的内容
tail
- 从文件尾部向上查看最后n行的内容
- 使用方式:tail -n[行数] 文件名
- 如果没有指定行数 默认显示最后10行的内容
du和df
du -sh 文件名:查看文件所占磁盘空间大小
df -h :查看所有磁盘分区占用情况
查找相关命令
find
通常用于特定的目录下搜索符合条件的文件,也可以用来搜索特定用户属主的文件
按文件名查询:
命令:find + 路径 + -name + “文件名”
示例:find ./ -name test //在当前目录寻找test文件
按文件大小查询:
命令:find + 路径 + -size + 范围(+是大于,-是小于)
示例:find ./ -size +100k //在当前目录寻找大于100k的文件
按文件类型查询:
命令:find + 路径 + -type + 类型
d:目录
b:块
l:链接
p:管道
-:普通文件(用f表示,而不是 - )
c:设备文件
s:socker,网络套接字
grep
find主要用于查找文件,而grep用于在某个位置查找指定的字符
语法:grep -r “字符”路径
压缩包管理
tar打包
tar是Linux中最常用的备份工具,此命令可以把一系列文件归档到一个大文件中,也可以把归档文件解开已恢复数据
语法:tar [参数] 打包后的文件名 要打包的文件
示例:tar -cvf testfile test1.txt test2.txt test3.txt; 将这三个文本文件生成一个归档文件testfile
常用:
tar -cvf:创建归档文件并显示进度
tar -xvf:接触归档文件(还原)并显示进度
tar -tvf:查看档案中包含的文件
gzip压缩
- tar与gzip结合使用才能实现打包、压缩
- tar只负责打包文件,但不压缩,用gzip打包后的文件 其扩展名一般用xxx.tar.gz
- 语法:gizp [选项] 被压缩文件
在Linux中,gzip命令可以压缩文件和目录,但它只能压缩目录中的文件,并不能直接压缩整个目录。当你使用gzip压缩目录时,它会一次性压缩目录中的所有文件,并将它们存储到单个压缩文件中。
如果你想要压缩整个目录(包括子目录),可以考虑使用tar命令将目录打包,然后再使用gzip进行压缩。具体操作如下:
压缩整个目录:
1 | tar cvzf compressed.tar.gz directory/ |
解压目录:
1 | tar xvzf compressed.tar.gz |
解压到指定目录:
1 | tar xvzf compressed.tar.gz -C /temp |
上述命令中,tar
命令用于打包文件和目录,z
选项指定压缩格式为gzip,c
选项表示压缩,x
选项表示解压缩,v
选项表示在打包或解包的过程中显示进度和详细信息。
需要注意的是,使用gzip
命令压缩大型目录时可能会导致压缩时间较长,并且会给系统带来一定的负担。因此,在压缩较大的目录时,最好使用tar
命令打包后再进行压缩。
bzip2
bzip2与gzip功能相同,都是与tar结合使用;
bizp是-j,gzip是-z
小文件用gzip,大文件用bzip
zip和unzip
压缩:zip huifile hui1.txt hui2.txt hui3.txt
解压:unzip huifile
三种压缩方式的选择
gzip
,zip
和bzip2
都是在Linux系统中常见的文件压缩格式,它们使用不同的算法和方法压缩文件,选择哪种格式取决于你想要实现的目标,如压缩比、速度和系统资源的占用等。以下是对三种格式的简介和比较:
gzip
:使用Lempel-Ziv算法和哈夫曼编码算法,压缩速度快,压缩比较高,适用于较小的文件或数据流。由于压缩和解压缩速度较快,因此经常被用作文件传输和备份。文件扩展名为.gz
zip
:使用LZ77算法和哈夫曼编码算法,适用于较大或结构复杂的文件和目录,压缩速度较慢,但压缩比较好。在Windows和Mac OS X系统中,zip格式已成为标准压缩格式。文件扩展名为.zip
bzip2
:使用Burrows-Wheeler变换和霍夫曼编码算法,具有更高的压缩比,但是压缩速度比gzip
和zip
慢。适用于需要更高压缩比的大型文件和数据集的压缩和备份。文件扩展名为.bz2
文件权限管理
访问用户分类
chmod
示例1:给其他用户添加了可写权限(chmod o+w 文件名)
示例2:撤销用户所有组的可写权限(chmod g-w 文件名)
示例3:将所有者的权限设为读写,所有组和其他用户只能读
软连接和硬连接
二、Linux下编写程序
vim编辑器工作模式
命令模式
任何时候,不管用户处于何种模式,只要按下ESC键 即可进入命令模式;我们在shell环境下输入vim命令进入编辑器时,也是处于该模式下
编辑模式
在命令模式下输入命令i(I)、附加命令a(A)、打开命令o(O)、替换命令s(S)都可以进入文本输入模式,此时vi窗口的最后一行会显示”插入”;
末行模式
末行模式下,用户可以对文件进行一些附加处理,如:字符串查找、替换、显示行号等;
在命令模式下输入冒号即可进入末行模式,用户输完命令后按回车执行,之后vi编辑器又自动返回到命令模式下;
vim实用操作
命令模式下的操作
- 切换到编辑模式
- 光标移动
- 复制粘贴
- 删除
- 撤销恢复
、
- 可视模式
末行模式下的操作
- 保存退出
- 替换
- 分屏
- 其他用法
GCC编译器
上面所将的编辑器(vi、vim、记事本)是指我们用来写程序的,而我们写好的程序是需要使用编译器去运行;
gcc编译步骤
具体操作:
- 预处理
解释:首先编写好了代码文件1hello.c,然后gcc -E进行预处理,-o的意思是将输出结果到1hello.i中,
此时生成了预处理文件1hello.i
- 编译
解释:将预处理文件1hello.i编译成了1hello.s汇编文件
- 汇编
解释:将1hello.s汇编文件转为了机器语言(二进制)1hello.o
- 链接
解释:生成可执行文件
- 执行文件
简化操作:
直接一步到位,从预处理直接到可执行文件,相当于执行了上面四步操作;
gcc 代码文件 -o 生成的执行文件名
(如果没有指定生成的执行文件名,会使用默认名a.out)
静态链接和动态链接
静态链接
由链接器在链接时将库的内容加入到可执行程序中
优点:
- 对运行环境的依赖性较小,具有较好的兼容性
缺点:
- 生成的程序比较大,需要更多的系统资源,在装入内存时会消耗更多的时间;
- 库函数有了更新,必须重新编译应用程序
动态链接
连接器在链接时仅仅建立于所需库函数之间的链接关系,在程序运行时才将所需资源调入可执行程序;
优点:
- 在需要的时候才会调入对应的资源
- 简化程序的升级,有着较小的程序体积
- 实现进程之间的资源共享
缺点:
- 依赖动态库,不能独立运行
- 动态库依赖版本问题严重
对比
在gcc编译的最后一步(链接)加上-static,就可以把生成的可执行文件变为静态的
静态的是将库的内容直接加入到可执行文件中;
动态的是将库和可执行文件直接建立了链接,当要执行文件时,才将内容加进去
GDB调试器
GDB简介
GNU工具集中的调试器是GDB,该程序是一个交互式工具,工作在字符模式;
GDB主要帮忙完成下面四个方面的功能:
- 启动程序,可以按照你的自定义的要求随心所欲的运行程序
- 可让被调试的程序在你所指定的调置的断点处停住
- 当程序被停住时,可以检查此时你的程序中所发生的事
- 动态的改变你程序的执行环境
生成调试信息
a.out是一个可执行文件,然后使用命令 gcc -g gdbtest.c 后 它生成的可执行文件就具备了调试信息;
启动GDB
- 启动gdb
语法:gdb target;
这里的target就是你具备调试信息的可执行文件;
- 设置参数
1 | set args 可指定运行时参数,(如:set args 10 20 30 40) |
- 启动程序
1 | run:运行整个程序,如果程序有断点,则在第一个断点停住 |
显示源代码
用list代码打印程序的源代码,默认打印十行
- list linenum:打印第linenum行的上下文内容
- list function:打印函数名为function的函数的源程序
- list:打印当前行后面的十行源代码
- list-:打印当前行前面的十行源代码
- set listsize count:设置一次显示源代码的行数
- show listsize:查看当前listsize的设置
- l 1:从第一行开始打印
断点操作
- 简单断点
- break 设置断点,可以简写为b
- b 10,在源代码第十行设置断点
- b func,在func函数入口处设置断点
- info break || i b,查询所有断点
- 多文件设置断点
条件断点
1 | b test.c:23 if i == 5 |
如果该程序23行中的i等于5,那么程序就在这里停住
维护断点
调试代码
数据查看
print 打印变量、字符串、表达式的值,可简写为p
p count 打印count的值
查看和修改变量的值
1 | ptype width,查看变量width的属性 |
三、Makefile
makefile简介
makefile定义了一系列的规则来指定如何编译文件,makefile带来的好处就是–“自动化编译”,一旦写好 只需要一个make命令,整个工程就完全自动化编译,极大的提高了软件开发的效率;
make主要解决了两个问题:
- 大量代码的关系维护
- 大项目中源代码比较多,手工维护、编译时间长而且编译命令复杂,难以记忆及维护;
- 而将代码维护命令和编译命令写入makefile中,然后在用make工具解析写入的命令,就可实现代码的合理编译
- 减少重复编译时间
- 在改动其中一个文件的时候,能判断哪些文件被修改过,可以只对该文件进行重新编译 节省编译的时间;
makefile语法规则
一条规则:
1 | 目标:依赖文件列表 |
例:1.mk文件,如果文件后缀名是.mk,那么就需要使用-f来指定文件
例:Makefile文件,如果文件名为Makefile或者makefile,那么就可以直接使用make命令执行
makefile加减乘除示例
代码首先执行第一行(依赖文件列表),然后依次去执行 依赖文件中的代码,最后生成目标可执行文件;
makefile中的变量
在makefile中使用变量有点类似于C语言中的宏定义,使用变量来保存命令或者依赖文件,使用时直接引用变量即可;
自定义变量:
- 定义变量:变量名=变量值
- 引用变量:$(变量名)或者${变量名}
makefile内置变量:
$@:表示目标;
$^:表示所有的依赖;
$<:表示第一个依赖;
最终精简版本:
makefile中的函数
四、文件IO
文件和系统调用
系统调用简介和实现
什么是系统调用?
系统调用,顾名思义,说的是操作系统提供给用户程序调用的一组特殊接口,用户程序可以通过这个特殊接口来获得操作系统内核提供的服务
系统调用的实现
系统调用是属于操作系统内核的一部分,必须已某种方式提供给进程让它们去调用,相应的操作系统也有不同的运行级别 用户态和内核态,内核态可以毫无限制的访问各种资源,而用户态下的用户进程的各种操作都有限制,显然 属于内核的系统调用是运行在内核态下,那么如何切换到内核态呢?
答案是软件中断,操作系统一般是通过软件中断从用户态切换到内核态
系统调用和库函数的区别
库函数主要由两类函数组成:
- 不需要调用系统调用
不需要切换到内核空间即可完成函数全部功能,如strcpy、bzero等字符串操作函数
- 需要调用系统调用
需要切换到内核空间,这类函数通过封装系统调用去实现相应的功能,如print、fread等
错误处理函数
errno用于记录系统的最后一次错误代码,返回一个int值(错误码),在errno.h中定义,不同的错误码表示不同的含义
也可以使用perror函数来直接获取错误原因:perror(“xxxx”)
虚拟地址空间
文件描述符:
- 当我们打开文件或者新建文件时,系统会返回一个文件描述符用来指定已打开的文件,这个文件描述符相当于这个已打开文件的标号,操作这个文件描述符就相当于操作这个描述符所指定的文件;
- 程序运行起来后每个进程都有一张文件描述符的表,标准输入、输出,标准错误输出设备文件被打开,对应的文件描述符0、1、2就记录在表中,程序运行起来后这三个文件描述符是默认打开的
常用文件IO函数
open函数
基本语法:
1 |
|
close函数
基本语法:
1 |
|
write函数
1 |
|
代码解释:
- 首先以可写的方式打开一个文件,如果没有该文件则新建;
- 写入文件,参数分别是:文件描述符、数据首地址、写入的字符长度
- 关闭文件
- gcc 成功执行后,就会将数据写入文件中
read函数
1 |
|
lseek函数
文件的读和写使用的是同一偏移位置;
使用lseek获取文件大小:
文件描述符复制
概述
dup()和dup2()是两个非常有用的系统调用,都是用来复制一个文件的描述符,使新的文件描述符也标识旧的文件描述符所标识的文件;
这个过程类似于现实生活中的配钥匙,一把钥匙对应一把锁,然后我们又去配了一把新钥匙,此时两把钥匙都可以打开锁,而dup()和dup2()也一样,原来的文件描述符和新复制出来的文件描述符都指向同一个文件,我们操作这两个文件描述符的任何一个 都能操作它所对应的文件
dup函数
运行后输出:ABCDEFG1234567;因为它俩是共享一个偏移量,因此不会覆盖
dup2函数
与dup函数的区别:可以指定文件描述符(第二个参数 newfd);
头文件
fcntl函数
功能一:复制文件描述符(与dup相同)
fcntl的第三个参数0 表示返回一个最小的可用的文件描述符,并且大于或等于0;
执行后输出123abc表示复制成功,fd和newfd指向了同一个文件,共用一个偏移量
目录相关操作
getcwd函数
chdir函数
opendir函数
closedir函数
readdir函数
五、进程
进程和程序
我们平时写好的代码,通过编译后生成的可执行文件就是一个程序,当这个程序被运行起来后(没结束之前) 它就成为了一个进程。
程序是静态的,进程是动态的(创建、调度、消亡)
进程的状态
在三态模型中,进程状态分为三个基本状态,即:运行态、就绪态、阻塞态;
在五态模型中,进程状态分为五个基本状态,即:新建态、终止态、运行态、就绪态、阻塞态;
ps命令
ps命令可以查看进程的详细状况,常用选项如下:
top命令
top命令用来动态显示运行中的进程
kill命令
使用方法:
- 让进程在后台睡眠
- ps -a,找到该进程的PID
- kill PID,即可杀死此进程
但是有些进程我们不能直接杀死,这时候我们需要加上参数-9,强制结束
killall可以通过进程名来杀死进程
进程号和相关函数
每个进程都由一个进程号来标识,其类型为pid,进程号的范围:0~32767,进程号总是唯一的,但进程号可以重用,当一个进程终止后 其进程号就可以再次使用;
接下来在给大家介绍三种进程号:
进程号(PID):
标识进程的一个非负整形数
父进程号(PPID):
任何进程都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID),如 A进程创建了B进程,A的进程号就是B进程的父进程号
进程组号(PGID):
进程组是一个或多个进程的集合,它们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID),默认情况下 当前的进程号会当作当前的进程组号
getpid
1 |
|
getppid
1 |
|
getpgid
该函数必须传入一个参数>>要获取哪个进程的进程组号
1 |
|
进程的创建
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型
示例代码:
1 |
|
因为我们复制出来的子进程就相当于父进程的克隆体,而它们的PC指针指向了下一条执行的语句,所以打印两次
我们说进程调用 fork ,当控制转移到内核的 fork 代码后,内核会:
- 将父进程部分数据拷贝给子进程;
- 分配新的内存块和数据结构给子进程;
- 子进程添加到进程列表;
- fork 返回,开始调度
因此 fork 之前父进程独立执行,fork 后父子分别执行,但是谁先谁后完全取决于调度器。
fork 之后,共享的只有 fork 后的代码吗?一般情况下,是父子共享所有代码,虽然子进程执行后续代码,它是相当于共享了所有代码但是子进程只能从某个地方开始执行!什么原理呢?很简单,在 CPU 里面有一种寄存器能保存当前的执行进度,它叫 eip,普遍喜欢叫它程序计数器或者 pc 指针,通过保存当前正在执行指令的下一条指令,拷贝给子进程。
循环创建进程
父子进程关系
- 使用fork()函数得到的子进程是父进程的一个复制品,它从父进程继承了整个进程的地址空间:包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。
- 但是子进程也有它独有的进程号,计时器等。
- linux内核引入了 读时共享,写时拷贝;如果父子进程都是只读的话 那么它们就共享同一个地址空间(避免造成资源浪费),只有写入的时候才会复制地址空间,那么此时它俩都拥有各自的空间。
- fork之后的父子进程共享文件偏移量。
区分父子进程
通过fork的返回值来区分,fork函数被调用一次但返回两次,两次返回的区别是:子进程的返回值是0,而父进程的返回值则是子进程的PID;
1 | int main(void) |
父子进程地址空间
代码解释:定义了一个变量var=88,然后让子进程睡眠1s,此时父进程肯定先执行 然后将var++,那么子进程睡醒之后去获取var的值依然是88,遵循读时共享,写时拷贝;
如果在堆区开辟了空间,那么释放的时候要释放两次(子进程和父进程),否则会造成内存泄漏
进程退出函数
等待子进程退出的函数★
概述
在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件、占用的内存等。但是仍然为其保留了一定的信息,这些信息主要是指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。
父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
wait()和waitpid()的功能一样,区别在于wait()会阻塞(如果子进程没结束,那么就一直等待不做其他事),waitpid()可以设置不阻塞,waitpid()还可以指定等待哪个子进程结束
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环
wait函数
示例一:
1 |
|
使用宏函数来获取进程退出时的状态:
1 | //使用宏函数来获取进程退出时的状态信息 |
第一组:正常结束进程,然后获取退出状态(exit的参数)
第二组:异常终止,然后获得使进程终止的信号的编号
- 此进程的PID是121114
- 杀死进程
- 获得终止信号的编号
第三组:进程暂停,取得使进程暂停的那个信号的编号
- 此进程的PID是121422
- 使用 kill -19暂停进程
- 此时进程暂停了
- 使用 kill -18开始进程
- 进程正常开始运行
waitpid函数
此状态下为不阻塞,即不会等待子进程执行,直接去执行自己的任务
wait和waitpid只能回收子进程,并且调用一次只能清理一个,如需清理多个得使用循环;
僵尸进程&孤儿进程
孤儿进程:父进程结束了,但是子进程还在运行,此时内核的init进程会等待该子进程
1 |
|
僵尸进程:
进程替换
Linux进程替换指的是将当前正在运行的一个进程的代码和数据替换成另一个可执行文件的代码和数据,同时保留其原有的进程ID和其他系统资源(如文件描述符、信号处理程序等),从而实现了进程的动态更新。这种技术通常用于实现程序的热更新、回滚、重新加载等功能,同时也可以用于实现不同版本之间的平滑升级或回滚。在UNIX和Linux系统中,常用的进程替换函数包括exec()系列函数、system()函数、popen()函数等。
exec函数族
fork创建子进程后执行的是和父进程相同的程序,我们可以使用exec函数族来让它执行另一个程序,但进程PID不变,换核不换壳
execlp
该函数通常用来调用系统的可执行程序,如:cat、ls、date
语法:int execlp(const char *file,const char *arg,…)
参数一:要执行的文件名;参数二:文件名;参数三:要执行的文件的参数(可以是多个);最后要加上NULL,因为它是变参
成功的话无返回值,失败的话返回-1
1 |
|
execl
该函数通常用来执行自己的可执行文件;
语法:int execl(const char *path,const char *arg,…);
参数一:自己要执行文件的路径;参数二:文件名;参数三:要执行的文件的参数(可以是多个,也可以为空);最后要加上NULL,因为它是变参
需求:在execl中fork子进程去执行该目录下的wait可执行文件
1 |
|
六、进程间通信★
概念
无名管道PIPE ★
管道的概念
无名管道是作用于有血缘关系的进程之间完成数据传递,调用pipe系统函数即可创建一个管道
实现原理:内核借助环形队列机制,使用内核缓冲区实现
管道有如下特质:
- 其本质是一个伪文件(实为内核缓冲区)
- 由两个文件描述符引用,一个表示读端,一个表示写端
- 规定数据从管道的写端流入管道,从读端流出
管道的局限性:
- 数据不能进程自己写,自己读
- 管道中数据不可反复读取,一旦读走 管道中将不存在此数据
- 采用半双工通信方式,数据只能在单方向上流动
- 只能在有公共祖先的进程间使用管道
pipe函数★
用于创建并打开管道;
语法:int pipe(int pipefd[2]); fd[0]是读端,fd[1]是写端
返回值:成功:0,失败:-1并设置perror
- 父进程调用了pipe(),相当于创建了一个管道并打开了读端和写端,pipefd[0]是读端,pipefd[1]是写端
- 父进程fork出一个子进程,此时子进程也掌握着父进程管道的读端和写端
- 父进程写入数据(读端关闭),子进程读取数据(写端关闭)
- 最后就实现了单通道传递数据
1 |
|
管道的读写行为
读管道:
- 管道有数据:返回实际读到的字节数
- 管道无数据:(1)无写端,read返回0; (2)有写端,read阻塞等待
写管道:
- 无读端:异常终止
- 有读端:(1)管道已满,阻塞等待;(2)管道未满,返回写出的字节个数
兄弟进程间通信
1 |
|
有名管道FIFO
概念
fifo可以实现没有血缘关系进程间的通信;
创建方式:
- 命令:mkfifo 管道名
- 库函数:int mkfifo(const char *pathname,mode_t mode);成功0,失败-1
- 参数解析:参数1是管道名,参数二是权限0664;
1 | int main(void) |
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的I/O函数都可以用于FIFO,如:open,close….
实现非血缘关系的通信
argc和argv是程序的输入参数。
argc是一个整数类型的变量,代表了传递给程序的参数个数,其中argc至少为1,因为第一个参数总是程序本身的名称。
argv是一个字符指针数组,每个元素指向一个传递给程序的参数字符串,包括程序名称。数组的第一个元素argv[0]通常为程序名称,那么argv[1]就是该程序后面接的文件,例如:writefifo myfifo,myfifo管道文件就是argv[1];
1 |
|
一个写,一个读
存储映射I/O ★
存储映射使一个磁盘文件与存储空间中的一个缓冲区相映射,这个映射工作可以通过mmap函数来实现
扩展内存
ftruncate(fd,10);
mmap函数
munmap(p,len); 释放
1 |
|
mmap注意事项
问题:
- 可以open的时候O_CREAT一个新文件来创建映射区吗?
- 如果open时 O_RDONLY, mmap时PROT参数指定PROT_READ|PROT_WRITE会怎样?
- 文件描述符先关闭,对mmap映射有没有影响?
- 如果文件偏移量为1000会怎么样?
- 对mem越界操作会怎样?
- 如果mem++,munmap可否成功?
- mmap什么情况下会调用失败?
- 如果不检测mmap的返回值会怎样?
父子进程间mmap通信
- 父进程先创建映射区,open(O_RDWR) mmap(MAP_SHARED);
- fork创建子进程
- 一个进程读,另外一个进程写
1 |
|
无关系进程间mmap通信★
重点:
写入时直接使用memcpy函数:参数一是指针,参数二是数据,参数三是字节数
读出时直接打印指针:printf(p)
1 |
|
七、信号★
信号的概念
共性:简单、不能携带大量信息、满足条件才发送;
信号的机制
A给B发送信号,B收到信号之前在执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行去处理信号,处理完毕再去继续执行自己的代码,与硬件中断类似——异步模式。
每个进程收到的所有信号,都是由内核负责发送的,内核处理;
与信号相关的事件和状态
产生信号:
- 按键产生:如 Ctrl+c、Ctrl+z、Ctrl+\
- 系统调用产生:如 kill、raise、abort
- 软件条件产生:如 定时器alarm
- 硬件异常产生:如 非法访问内存、内存对齐出错
- 命令产生:如 kill命令
递达:
递送并且到达进程
未决:
产生和递达之间的状态,主要由于阻塞导致该状态
信号的处理方式:
- 执行默认动作
- 忽略(丢弃)
- 捕捉(调用户处理函数)
阻塞信号集(信号屏蔽字):
将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后),
一旦被屏蔽的信号,再解除屏蔽前 一直处于未决态
未决信号集:
信号的编号
kill -l 获取全部信号
\1) SIGHUP
本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也能继续下载。
此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
\2) SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
\3) SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
\4) SIGILL
执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
\5) SIGTRAP
由断点指令或其它trap指令产生. 由debugger使用。
\6) SIGABRT
调用abort函数生成的信号。
\7) SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
\8) SIGFPE
在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
\9) SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
\10) SIGUSR1
留给用户使用
\11) SIGSEGV
试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
\12) SIGUSR2
留给用户使用
\13) SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
\14) SIGALRM
时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
\15) SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
\17) SIGCHLD
子进程结束时, 父进程会收到这个信号。
如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。
\18) SIGCONT
让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符
\19) SIGSTOP
停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.
\20) SIGTSTP
停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
\21) SIGTTIN
当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
\22) SIGTTOU
类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
\23) SIGURG
有”紧急”数据或out-of-band数据到达socket时产生.
\24) SIGXCPU
超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。
\25) SIGXFSZ
当进程企图扩大文件以至于超过文件大小资源限制。
\26) SIGVTALRM
虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
\27) SIGPROF
类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.
\28) SIGWINCH
窗口大小改变时发出.
\29) SIGIO
文件描述符准备就绪, 可以开始进行输入/输出操作.
\30) SIGPWR
Power failure
\31) SIGSYS
非法的系统调用。
在以上列出的信号中,
程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:SIGILL,SIGTRAP
默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
信号的四要素
信号使用前应确定它的四要素
- 编号
- 名称
- 事件
- 默认处理动作
kill函数/命令产生信号
int kill(pid_t pid, int sig)
成功:0,失败:-1;参数一是进程pid,参数二是信号名
pid>0:发送信号给指定的进程
pid=0:发送信号给与调用kill函数进程属于同一进程组的所有进程
pid<0:取绝对值,杀死该绝对值所对应的进程组的所有组员
pid=-1:发送给进程给有权限发送的系统中所有进程
代码解释:让父进程进入死循环,子进程睡2秒(确保父进程先执行),然后再子进程中使用kill杀死父进程
kill -9 10698:杀死PID为10698的这个进程
kill -9 -10698:杀死进程组为10698的所有进程
alarm函数
返回值是它上次定时所剩余的秒数
setitimer函数
setitimer函数
设置闹钟,可以替代alarm函数,精度微秒us,可以实现周期定时
1 | int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); |
参数:
which: ITIMER_REAL: 采用自然计时。 ——> SIGALRM
ITIMER_VIRTUAL: 采用用户空间计时 —> SIGVTALRM
ITIMER_PROF: 采用内核+用户空间计时 —> SIGPROF
new_value:定时秒数
old_value:传出参数,上次定时剩余时间。
返回值:
成功: 0
失败: -1 errno
类型
struct itimerval
{
struct timeval
{
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
}it_interval;—> 用于设定两个定时任务之间的间隔时间
struct timeval
{
time_t tv_sec;
suseconds_t tv_usec;
}it_value; —> 第一次定时秒数
};
可以理解为有2个定时器
- 一个用于第一个闹钟什么时候触发打印
- 一个用于之后间隔多少时间再次触发闹钟。
例子
使用setitimer定时,向屏幕打印信息:
第一次信息打印是两秒间隔,之后都是5秒间隔打印一次
信号集操作函数
内核通过读取未决信号集来判断信号是否应该被处理,信号屏蔽字mask可以影响未决信号集 而我们可以在应用程序中自定义set来改变mask,已达到屏蔽指定信号的目的
信号集设定
由于我们不能直接操作内核里的阻塞信号集,因此我们要自己设定一个 然后用函数对它操作,可以将我们自己设定的信号集与系统内核的信号集进行位或&位与&替换;
自己写的叫set,系统内核PCB里的叫mask,视频P134里解释原理
信号集函数练习
1 |
|
代码解释:
- 定义自己的信号集set;
- 清空信号集,使用sigaddset()加指定的信号加入信号集内
- 使用sigprocmask()将系统的信号集替换为自己的,它的第一个参数是设置条件(阻塞,取消阻塞)
- 使用sigpending()查看未决信号集
- 封装一个sigismember函数,来打印sigpending传出的信号集
开始执行:
按下Ctrl C之后,信号2 从0变成了1,一直处于了未决状态,如果没设置阻塞,它会从0变为1然后迅速变为0
信号捕捉
捕捉(调用户处理函数),捕捉到信号后 用户自定义让它去做什么
signal函数
注册一个信号捕捉函数;
signal(要捕捉的信号,要执行的函数)
1 |
|
因为代码中有循环,所以程序停在了这里;
按下Ctrl C后,执行参数二的函数,2表示这个信号的编号
sigaction函数★
注册一个信号捕捉函数;
1 | int sigaction(int signum,const struct sigaction *act, struct sigaction *oldact); |
参数一:要捕捉的信号,参数二:一个结构体 新的处理状态,参数三:保存它旧有的对这个信号的处理状态;
1 |
|
信号捕捉的特性
SIGCHLD信号
SIGCHLD产生的条件
- 只要子进程的状态发生变化就会产生SIGCHLD信号;
借助SIGCHLD回收子进程
核心思路:
- 循环创建多个子进程
- 在注册信号捕捉前加上信号屏蔽集去屏蔽SIGCHLD信号,以免信号捕捉未注册完 子进程就死亡了
- 父进程中注册sigaction信号捕捉(回调函数中循环回收子进程)
- 取消屏蔽SIGCHLD信号
1 |
|
八、守护进程★
概念和特性
多个进程就是一个进程组,而多个进程组就是一个会话
创建会话
组长进程不能创建会话
getsid函数:
获取进程所属的会话ID
1 | pid_t getsid(pid_t pid); |
成功返回所属会话ID,失败返回-1并设置error
setsid函数:
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID;
1 | pid_t setsid(void); |
成功返回会话ID,失败返回-1并设置error
调用了setsid函数的进程,既是新的会长,也是新的组长
守护进程
守护进程创建步骤:
- fork子进程,让父进程终止。
- 子进程调用 setsid() 创建新会话
- 改变工作目录位置 chdir(), 防止目录被卸载。
- 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限。
- 关闭/重定向 文件描述符
- 守护进程 业务逻辑。while()
1 |
|
执行文件后即可在后台发现此进程(不受用户登录、注销的影响,一直运行着),可以使用kill -9 PID 强制结束
九、线程★
线程的概念
进程:有独立的进程地址空间,有独立的pcb
线程:有独立的pcb,没有独立的进程地址空间
ps -Lf 进程id —> 线程号 LWP —>cpu执行的最小单位
如果一个进程创建了多个线程,那么它就能更快的去争夺CPU,更快的执行
线程共享:
独享 栈空间(内核栈、用户栈)
共享 ./text./data ./rodataa ./bsss heap —> 共享【全局变量】(errno不共享)
线程函数
pthread_self
1 | pthread_t pthread_self(); |
获取线程id,返回值就是本线程的id
pthread_create
创建线程,对应fork
1 | int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); |
参数一:传出参数,表示新创建的子线程 id,pthread_t 类型的&;
参数二:线程属性,传NULL表示使用默认属性
参数三:子线程的回调函数 void *tfn(void *arg),创建成功 pthread_create函数返回时,该函数会被自动调用
参数四:参数三函数的参数,如果没有则传NULL
1 |
|
循环创建线程
1 |
|
编译时会出现类型强转的警告,指针4字节转int的8字节,不过不存在精度损失,忽略就行。
pthread_exit
将当前线程退出
pthread_join
阻塞等待线程退出,获取线程退出状态,其作用对应进程中的waitpid()函数;
1 | int pthread_join(pthread_t thread, void **retval); |
thread 参数用于指定接收哪个线程的返回值;retval 参数表示接收到的返回值,而线程退出的时候返回值
*void *类型的,所以我们接收的时候应该使用void *来解引用,如果 thread 线程没有返回值,又或者我们不需要接收 thread 线程的返回值,可以将 retval 参数置为 NULL。
1 |
|
pthread_cancel
1 | int pthread_cancel(pthread_t thread); |
杀死(取消)线程,其作用对应进程中的kill函数;
1 |
|
pthread_detach
1 | int pthread_detach(pthread_t thread); |
实现线程分离,线程终止后会自动清理PCB,无需回收;
thread:待分离的线程id
在线程中perror无效,可以使用:
1 | fprintf(stderr,"xxx error:%s\n", strerror(ret)); |
线程使用注意事项
线程同步
线程同步概念
协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。
数据混乱的原因:
- 资源共享(独享资源则不会)
- 调度随机(意味着数据访问会出现竞争)
- 线程间缺乏必要同步机制
互斥锁 mutex
Liunx中提供一把互斥锁 mutex(也称之为互斥量)
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束后解锁
资源还是共享的,线程间也还是竞争的
但通过”锁”就将资源的访问变成互斥操作 而后与时间有关的错误也不会在产生了
建议锁!对公共数据进行保护,所以线程应该在访问公共数据前先拿锁在访问,但是锁本身不具备强制性
下面是一个小例子,数据共享未加锁导致的混乱:
因为主线程和子线程打印输出一句话后就sleep了,那么cpu此时挂起,等睡眠结束后 谁抢夺到cpu就去打印谁
1 |
|
互斥锁的主要函数:
pthread_mutex_init 函数
pthread_mutex_destory 函数
pthread_mutex_lock 函数
pthread_mutex_trylock 函数
pthread_mutex_unlock 函数
pthread_mutex_t 类型
以上5个函数的返回值都是:成功返回0,失败返回错误号;
加锁步骤:
- pthread_mutex_t lock;创建锁
- pthread_mutex_init;初始化
- pthread_mutex_lock;加锁
- 访问共享数据
- pthread_mutex_unlock;解锁
- pthread_mutex_destroy;销毁锁
1 |
|
读写锁 rwlock
读写锁与互斥锁类似,但读写锁允许更高的并行性,其特性为:写独占 读共享。写锁优先级高
锁只有一把,以读方式给数据加锁–读锁,以写方式给数据加锁–写锁,
函数:
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
pthread_rwlock_rdlock(&rwlock); try
pthread_rwlock_wrlock(&rwlock); try
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);
1 | 1. /* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */ |
十、条件变量
条件变量本身不是锁,但它也可以造成线程阻塞,通常与互斥锁配合使用,给多线程提供一个会合的场所。
主要应用函数
wait
阻塞等待一个条件变量;
pthread_cond_wait(&cond, &mutex);
作用:
- 阻塞等待条件变量满足
- 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex)),1 2两步为一个原子操作(一起执行,不可分割)
- 当条件满足,函数返回时,解除阻塞并重新申请获取互斥锁。重新加锁信号量 (相当于pthread_mutex_lock(&mutex);)
条件变量的生产者消费者模型分析
条件变量的生产者消费者代码实现
1 | 1. |