0613
第4章 项目制作与技能提升
- 4.0 视频课链接
- 4.1 项目介绍与环境搭建
- 4.2 Linux系统编程1、4.3 Linux系统编程2
- 4.2.1 GCC
- 4.2.2 Makefile
- 4.2.3 GDB调试
- 4.2.4 静态库
- 4.2.5 动态库
- 4.2.6 静态库和动态库的对比☆☆☆
- 4.2.7 文件IO(操作系统笔记、第5章 高频考点与真题精讲笔记)
- 4.4 多进程(操作系统笔记、Linux简明教程笔记、C++工程师第五章笔记)
4.0 视频课链接
首先这整个系列笔记属于笔记①:牛客校招冲刺集训营—C++工程师中的第四章笔记。
视频课链接:
视频1:Linux高并发服务器开发(40h);
视频2:第4章 项目制作与技能提升(录播)(26h30min);
视频课3:
第5章 高频考点与真题精讲(录播)中的5.10-5.13 项目回顾
有个学生的评论:
上个月做了也是这个老师讲得一个2400分钟的webserver课程,但只能实现访问服务器上图片的功能,也没有登录,数据库日志相关的东西,现在又看到这个课程,人麻了,先做这个多好。。。
所以直接看这个26h30min的视频课2;如果有什么地方不清楚的可以看视频课1,那里面讲的细。
(原本视频课3中的内容也不用看了,直接看视频课2,里面都包含了)
看完之后再看下面这个**7个小时**的课,照着写的笔记快速回顾一遍。**项目回顾**的[视频课](https://www.nowcoder.com/study/live/690/5/10)从**01:15:00**开始(7个小时);笔记链接:[笔记③:牛客校招冲刺集训营---C++工程师](https://blog.csdn.net/weixin_38665351/article/details/125450578)中的**5.10-5.13 项目回顾**。
GitHub链接:
拓兄给的:https://github.com/markparticle/WebServer
拓兄的:https://github.com/Arthur940621/myWebServer
牛客老师的:https://github.com/gaojingcome/WebServer
第一个和第三个好像是完全一样,第二个是拓兄自己写的。
4.1 项目介绍与环境搭建
4.1.1 项目介绍
整个项目程序的介绍:视频课中从01:10:55到01:20:15。
4.1.2 开发环境搭建
①安装Linux系统、XSHELL、XFTP、Visual Studio Code并实现 免密登录
安装Linux系统(虚拟机安装、云服务器)
https://releases.ubuntu.com/bionic/
安装XSHELL、XFTP
https://www.netsarang.com/zh/free-for-home-school/
安装Visual Studio Code
https://code.visualstudio.com/
课程内容:
- 通过XShell和xftp远程连接Linux服务器:通过
SSH协议
进程远程连接。 - 通过vscode远程连接Linux服务器:
安装几个插件(Chinese Language,Remote Development,C/C++);
也是通过SSH协议
进程远程连接;
并且实现免密连接(生成密钥公钥)。
②ubuntu中安装MySQL
参考链接0:https://segmentfault.com/a/1190000023081074
参考链接1:Ubuntu18.04 安装 MySQL8.0 详细步骤 以及 彻底卸载方法
参考链接2:Ubuntu18.04安装MySQL数据库
①彻底卸载MySQL安装历史
# 首先用以下命令查看自己的mysql有哪些依赖包dpkg --list | grep mysql# 先依次执行以下命令sudo apt-get remove mysql-commonsudo apt-get autoremove --purge mysql-server-5.0 # 卸载 MySQL 5.x 使用, 非5.x版本可跳过该步骤sudo apt-get autoremove --purge mysql-server# 然后再用 dpkg --list | grep mysql 查看一下依赖包# 最后用下面命令清除残留数据dpkg -l |grep ^rc|awk '{print $2}' |sudo xargs dpkg -P# 查看从MySQL APT安装的软件列表, 执行后没有显示列表, 证明MySQL服务已完全卸载dpkg -l | grep mysql | grep i
②安装MySQL
sudo apt-get install mysql-server //安装 MySQL 服务端、核心程序sudo apt-get install mysql-client //安装 MySQL 客户端//sudo apt-get install libmysqlclient-devsudo ps -ef | grep mysql //安装结束后,用命令验证是否安装并启动成功打开终端,运行以下命令:mysql -u root//此时无密码,直接Enter就可以进入(quit,退出mysql服务)mysql -u root -p//修改密码(quit,退出mysql服务)mysql -u root -p123456//就可以使用密码登陆了mysql -u root//直接回车
③其他操作
sudo service mysql start //启动服务ps ajx|grep mysql //查看进程中是否存在mysql服务sudo service mysql stop //停止服务sudo service mysql restart //重启服务
0720更新:
之前一直出现
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corre
找了很久多的博客都没解决,今天看到一篇博客:修改mysql的密码时遇到问题ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corre,好像是语法不对,最终通过
flush privileges;ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';
就OK了,就可以通过
mysql -u root -p123456
来登录mysql了。
在这之前修改了密码的强度,见博客:mysql 降低密码_关于mysql8权限赋予及降低密码强度问题
mysql> set global validate_password.policy=LOW;mysql> set global validate_password.length=6;
(0720更新到此结束)
③项目启动
需要先配置好对应的数据库(按照上面的步骤),然后进入数据库之后执行以下语句:
// 建立yourdb库create database webserver;// 创建user表USE webserver;CREATE TABLE user( username char(50) NULL, password char(50) NULL)ENGINE=InnoDB;// 添加数据INSERT INTO user(username, password) VALUES('nowcoder', 'nowcoder');//退出数据库:quit;
然后进入到项目文件所在的目录下,我这边是/home/reus/WebServer/WebServer-master
,它里面的内容如下:
root@VM-16-2-ubuntu:/home/reus/WebServer/WebServer-master# lsbin build code LICENSE log Makefile readme.assest readme.md resources test webbench-1.5
就在这个目录项执行下面的两行指令:
make./bin/server
就把这个服务器运行起来了。
④单元测试
(这个没试)
cd testmake./test
⑤压力测试
Webbench 是 Linux 上一款知名的、优秀的 web 性能压力测试工具。它是由Lionbridge公司开发。
- 测试处在相同硬件上,不同服务的性能以及不同硬件上同一个服务的运行状况。
展示服务器的两项内容:每秒钟响应请求数和每秒钟传输数据量。 - 基本原理:Webbench 首先 fork 出多个子进程,每个子进程都循环做 web 访问测试。子进程把访问的结果通过pipe 告诉父进程,父进程做最终的统计结果。
测试示例:
webbench -c 1000 -t 30 http://192.168.110.129:10000/index.html参数:-c 表示客户端数-t 表示时间
在一个终端运行服务器,在另一个终端运行webbench
,输入:
cd webbench-1.5/make./webbench -c 500 -t 5 http://124.221.96.249:1317/
刚开始文件夹webbench-1.5/
中只有三个文件:Makefile socket.c webbench.c,输入make
之后,生成两个文件:可执行文件webbench
和webbench.o
;然后执行可执行文件,就可模拟高并发。
运行结果1:5698
susceed, 2214
failed.
root@VM-16-2-ubuntu:/home/reus/123/web_tuo0720/webbench-1.5# ./webbench -c 9800 -t 30 http://124.221.96.249:1317/Webbench - Simple Web Benchmark 1.5Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.Benchmarking: GET http://124.221.96.249:1317/9800 clients, running 30 sec.Speed=15824 pages/min, 265962 bytes/sec.Requests: 5698 susceed, 2214 failed.root@VM-16-2-ubuntu:/home/reus/123/web_tuo0720/webbench-1.5#
运行结果2:7555
susceed, 724
failed.
root@VM-16-2-ubuntu:/home/reus/123/web_tuo0720/webbench-1.5# ./webbench -c 800 -t 30 http://124.221.96.249:1317/Webbench - Simple Web Benchmark 1.5Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.Benchmarking: GET http://124.221.96.249:1317/800 clients, running 30 sec.Speed=16558 pages/min, 490905 bytes/sec.Requests: 7555 susceed, 724 failed.root@VM-16-2-ubuntu:/home/reus/123/web_tuo0720/webbench-1.5#
运行结果3:907
susceed, 59
failed.
root@VM-16-2-ubuntu:/home/reus/123/web_tuo0720/webbench-1.5# ./webbench -c 800 -t 3 http://124.221.96.249:1317/Webbench - Simple Web Benchmark 1.5Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.Benchmarking: GET http://124.221.96.249:1317/800 clients, running 3 sec.Speed=19320 pages/min, 592989 bytes/sec.Requests: 907 susceed, 59 failed.root@VM-16-2-ubuntu:/home/reus/123/web_tuo0720/webbench-1.5#
压力测试结果统计
分别在服务器和虚拟机上做压力测试;
服务器的配置:操作系统:Ubuntu 20.04;CPU: 2核;内存: 4GB;
虚拟机的配置:处理器内核总数:4 内存: 4GB 硬盘:20GB
./webbench -c 500 -t 5 c表示客户端数,t表示访问时间 | 服务器 xxx susceed, xxx failed. | 虚拟机 xxx susceed, xxx failed. |
---|---|---|
c = 300,t = 3 | 666,7 799,3 | 3850,0 3902,0 |
c = 300,t = 30 | 6775,499 6439,732 | 38040,0 38086,0 |
c = 800,t = 3 | 786,12 851,0 | 4026,0 3771,0 |
c = 800,t = 30 | 7533,1195 7315,1574 | 34609,0 39094,0 |
c = 3000,t = 3 | 1227,0 776,68 | 2619,0 2068,0 |
c = 3000,t = 30 | 8354,807 8219,708 | 3390,1 2605,0 |
c = 5000,t = 3 | 939,287 1025,290 | 1249,0 1297,0 |
c = 5000,t = 30 | 9146,419 7633,974 | 1640,1 1827,0 |
c = 8000,t = 3 | 835,61 1016,443 | 1223,0 1482,0 |
c = 8000,t = 30 | 7830,1235 8460,1230 | 1453,0 1384,0 |
c = 10000,t = 3 | 844,166 521,201 | Resource temporarily unavailable |
c = 10000,t = 30 | 7675,1926 5305,1513 | |
c = 11000,t = 3 | fork failed.: Resource temporarily unavailable | |
c = 10100,t = 3 | fork failed.: Resource temporarily unavailable |
结论:QPS 10000+
补充:
QPS(Queries-per-second),即每秒查询率,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。
补充:什么叫高并发?怎么衡量?
PV,page view,指页面被浏览的次数,比如你打开一网页,那么这个网站的pv就算加了一次;
TPS,transactions per second,指每秒内的事务数,比如执行了dml操作,那么相应的tps会增加;
QPS,queries per second,指每秒内查询次数,比如执行了select操作,相应的qps会增加;
RPS,requests per second,RPS=并发数/平均响应时间;
RT,响应时间
并发数: 系统同时处理的request/事务数
响应时间: 一般取平均响应时间
QPS = 总请求数 / ( 进程总数 * 请求时间 )
QPS(TPS)= 并发数/平均响应时间
并发数 = QPS*平均响应时间
主要看参考链接0:qps多少才算高并发_一文搞懂高并发性能指标:QPS、TPS、RT、吞吐量
参考链接1:https://blog.csdn.net/qq_15037231/article/details/80085368
参考链接2:https://blog.csdn.net/weixin_42483745/article/details/123954673
参考链接3:一直再说高并发,多少QPS才算高并发?
4.2 Linux系统编程1、4.3 Linux系统编程2
4.2.1 GCC
gcc的底层都实现了什么?
GCC的安装和版本
2.编程语言的发展:
3.GCC工作流程
GCC常用参数选项:
示例:(课程里的)
示例2:(自己写的)
①g++ 1.cpp -E
对源代码(文件后缀.h .c .cpp)进行预处理,生成预处理后的代码(文件后缀.i
)
问:预处理都预处理了什么?
答:
1.导入头文件:将头文件的内容复制到源代码中;
2.删除注释;
3.对宏定义的内容进行宏替换。
②g++ 1.cpp -S
对源代码进行预处理+编译,生成汇编代码(文件后缀.s
);
③g++ 1.cpp -c
对源代码进行预处理+编译+汇编,生成目标代码(文件后缀.o
);
④g++ 1.cpp -o app
对源代码进行预处理+编译+汇编+链接,生成可执行代码app
;
⑤g++ 1.cpp
对源代码进行预处理+编译+汇编+链接,生成可执行代码a.out
;
结论:
gcc/g++的底层完成了预处理+编译+汇编+链接等过程,最终生成可执行代码。
gcc 和 g++ 的区别
首先gcc 和 g++都是GNU(组织)的一个编译器。
误区1:gcc 只能编译 c 代码,g++ 只能编译 c++ 代码?
解释:
- 后缀为
.c
的,gcc 把它当作是 C 程序,而 g++ 当作是 c++ 程序; - 后缀为
.cpp
的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些 - 编译阶段,g++ 会调用 gcc,对于 C++ 代码,两者是等价的,但是因为 gcc
命令不能自动和 C++ 程序使用的库联接,所以通常用 g++ 来完成链接,为了统
一起见,干脆编译/链接统统用 g++ 了,这就给人一种错觉,好像 cpp 程序只
能用 g++ 似的
总结:
对于c
程序,gcc
和g++
都可以;
对于cpp
程序,在编译阶段用gcc,链接阶段用g++(因为gcc不能自动和 C++ 程序使用的库进行链接),因此为了方便,对cpp程序的编译和链接过程,直接都用g++,所以就给人一种错觉,好像 cpp 程序只能用 g++ 。
误区2:gcc 不会定义 __cplusplus
宏,而 g++ 会
- 实际上,这个宏只是标志着编译器将会把代码按 C 还是 C++ 语法来解释;
- 如上所述,如果后缀为 .c,并且采用 gcc 编译器,则该宏就是未定义的,否则,
就是已定义。
误区3:编译只能用 gcc,链接只能用 g++
- 严格来说,这句话不算错误,但是它混淆了概念,应该这样说:
编译可以用gcc/g++
,
链接可以用g++
或者gcc -lstdc++
; - gcc 命令不能自动和C++程序使用的库联接,所以通常使用 g++ 来完成联接;
但在编译阶段,g++ 会自动调用 gcc,二者等价。
GCC常用参数选项:
4.2.2 Makefile
什么是Makefile?(Makefile是个文件)
一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,Makefile 文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 Makefile 文件就像一个 Shell 脚本一样,也可以执行操作系统的命令。
Makefile 带来的好处就是“自动化编译” ,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。make 是一个命令工具,是一个解释 Makefile 文件中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,比如 Delphi 的 make,Visual C++ 的 nmake
,Linux 下 GNU 的 make
。
makefile文件命名和规则
文件命名:makefile或者Makefile(只能是这两种命名方式)
Makefile规则:一个Makefile文件中可以有一个或者多个规则
目标... : 依赖...命令(shell命令)...
目标:最终要生成的文件;
依赖:生成目标所需要的文件或者目标;
命令:通过执行命令对依赖操作生成目标(命令前必须Tab缩进)
Makefile中的其他规则一般都是为第一条规则服务的。
基本原理 --> Makefile1、Makefile2
命令在执行之前,需要先检查规则中的依赖是否存在:
- a.如果存在,执行命令;
- b.如果不存在,向下检查其它的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令
检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间
- a.如果依赖的时间比目标的时间晚,需要重新生成目标
- b.如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行
变量 --> Makefile3
模式匹配 --> Makefile4
函数 --> Makefile5
伪目标:
.PHONY:cleanclean:rm $(objs) -f//删除所有.o文件
4.2.3 GDB调试
什么是GDB?
GDB 是由 GNU 软件系统社区提供的调试工具,同 GCC 配套组成了一套完整的开发环境,GDB 是 Linux 和许多类 Unix 系统中的标准开发环境。
一般来说,GDB 主要帮助你完成下面四个方面的功能:
- 启动程序,可以按照自定义的要求随心所欲的运行程序;
- 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式);
- 当程序被停住时,可以检查此时程序中所发生的事;
- 可以改变程序,将一个 BUG 产生的影响修正从而测试其他 BUG。
GDB说白了就是断点调试,排除开发过程中出现的bug。
通常,在为调试而编译时,我们会()关掉编译器的优化选项( -O
), 并打开调试选项( -g
)。
另外, -Wall
在尽量不影响程序行为的情况下选项打开所有 warning,也可以发现许多问题,避免一些不必要的 BUG。
gcc -g -Wall program.c -o program
-g
选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证 gdb 能找到源文件。
常用的GDB命令:
4.2.4 静态库
静态库的制作
什么是库?
库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类。
库的特点:
库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行。
库的分类:库文件有两种,静态库和动态库(共享库);
- 静态库在程序的链接阶段被复制到了程序中;
- 动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。
- (具体的原理和区别见1.7 动态库加载失败的原因)
库的好处:
1.代码保密 2.方便部署和分发。
命名规则:
(注意区分库文件的名字和库的名字)
libxxx.a //库文件的名字lib:前缀(固定)xxx:库的名字(自己起).a:后缀(固定)
静态库的制作:
1.获得目标代码.o
文件;
gcc -c a.c b.c
将.o
文件打包,使用ar
工具(archive)
ar rcs libxxx.a a.o b.o
示例:(静态库的制作)
在/calc
目录下,最开始有一个头文件,四个**.c文件**,和一个main.c文件
通过gcc -c
生成目标代码 xxx.o
;
再通过ar
指令制作静态库libcalc.a
。
静态库的使用
(视频课中是在/calc
目录下制作静态库,然后在/library
目录下又制作了一遍静态库,然后再使用静态库,所以直接在/library
目录下一次性完成制作+使用
静态库)
分发静态库的时候一定要把头文件和库文件一起分发出去,
(下面的示例的视频课链接:12:20开始)
示例:(静态库的制作和使用)
在/library
目录下,刚开始有3个文件夹,6个文件:
示例中会用到下面的几个参数:
-I 路径
指定include包含文件的搜索目录;
-l 路径
程序编译的时候,指定要使用的库的名称(即要使用哪个库);
-L 路径
指定库的路径。
第1步:生成目标代码.o
提示找不到头文件,所以要指明去哪里搜索头文件:上级目录的/include文件夹下,即../include/
(注意是两个.
)
第2步:制作静态库libsuanshu.a
然后把静态库移动到上级目录下的/lib
文件夹下,即../lib/
第3步:使用静态库对main.c文件进行编译
编译的时候也是提示找不到头文件,所以要指明去哪里搜索头文件:当前目录的/include
文件夹下,即../include/
(注意是两个.
);
然后main函数中的函数未定义,而函数的定义在库文件中,所以要指定库文件的路径和要用哪个库;
最后生成可执行文件a.out
,运行即可。
注意:-l
后面加的是库的名称,而不是库文件的名称。
4.2.5 动态库
动态库的制作和使用
动态库的命名规则:
动态库的制作:
1.得到和位置无关的目标代码.o
文件;
(记得加-fpic
)
gcc -c -fpic a.c b.c
得到动态库
gcc -shared a.o b.o -o libcals.so
示例:(动态库的制作与使用)
在路径 /home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用 下,有以下文件:
(和静态库的制作和使用一样,视频课中也是在/calc
目录下制作动态库,然后在/library
目录下又制作了一遍动态库,然后再使用动态库,所以直接在/library目录下一次性完成制作加使用动态库)
制作动态库,并放到/lib
目录下
2.使用动态库
和静态库的操作一样,但最后运行可执行文件的时候出错了(提示找不到动态库文件),具体见下一节。
./a.out: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
动态库加载失败的原因
上一节中最后运行可执行文件a.out
的时候出错了,提示找不到动态库文件,所以需要学习下动态库的工作原理。
静态库和动态库的区别:
- 静态库:GCC 进行链接时,会把静态库中代码打包到可执行程序
a.out
中; - 动态库:GCC 进行链接时,动态库的代码不会被打包到可执行程序
a.out
中,而是在程序启动之后,将动态库动态地加载到内存中;
动态库的工作原理:
- 程序启动之后,动态库会被动态加载到内存中,通过
ldd
(list dynamic dependencies)命令检查动态库依赖关系; - 如何定位共享库文件呢?
当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径,此时就需要系统的动态载入器来获取该绝对路径。
对于elf
格式的可执行程序,是由ld-linux.so
来完成的,它先后搜索elf
文件的DT_RPATH
段 ——> 环境变量LD_LIBRARY_PATH
——>/etc/ld.so.cache
文件列表 ——>/lib/
,/usr/lib
目录,找到库文件后将其载入内存。
在终端输入ldd a.out
,可以看出libcalc.so => not found
创建的动态库libcalc.so
未找到,具体的解决方法见下一节。
oot@VM-16-2-ubuntu:/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library# ldd a.out linux-vdso.so.1 (0x00007fff6b7b4000) libcalc.so => not found libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fce25c3a000) /lib64/ld-linux-x86-64.so.2 (0x00007fce25e3c000)
解决动态库加载失败的问题☆☆☆
环境变量:
env
环境变量是个键值对,一个键可以对应多个值,用冒号隔开。
那么如何配置动态库的绝对路径呢?
课里讲了两种方式:
- 把绝对路径放到环境变量
LD_LIBRARY_PATH
中; - 把路径放到
/etc/ld.so.cache
中。
(视频课中从07:40开始)
方式1:把绝对路径放到环境变量LD_LIBRARY_PATH
中;
刚开始讲了一种方式,是在终端上配置的,但把终端关闭后,就又找不到动态库了,这种配置是临时的。
具体实现如下:
1.先把动态库的绝对路径复制下来
pwd/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library/lib
配置环境变量
export LD_LIBRARY_PATH=$LD_LIBRARY:/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library/lib
查看环境变量
echo $LD_LIBRARY_PATH:/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library/lib
退到上一层目录,通过ldd
命令检查动态库的依赖关系,不再是not found
cd ..ldd a.out linux-vdso.so.1 (0x00007fff1933c000) libcalc.so => /home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library/lib/libcalc.so (0x00007efe29609000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efe2940e000) /lib64/ld-linux-x86-64.so.2 (0x00007efe29615000)
最后运行可执行文件
./a.out a = 20, b = 12a + b = 32a - b = 8a * b = 240a / b = 1.666667root@VM-16-2-ubuntu:/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library#
(自己尝试)
上面的配置方法是临时的,终端关闭之后,动态库的依赖关系又会变成not found
,所以要把这种配置设置成永久的。
把动态库的绝对路径永久配置到环境变量分为两种:用户级别的配置、系统级别的配置。
①用户级别的配置:(推荐)
先找到.bashrc
文件:
cdll
然后编辑.bashrc
文件:
vim .bashrc
把配置环境变量的代码加到文件的最后一行:
export LD_LIBRARY_PATH=$LD_LIBRARY:/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library/lib
保存:
:wq
然后让.bashrc
文件生效:(两种方式都行)
. .bashrcsource .bashrc
然后再查看动态库的依赖关系:
cd /home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/libraryldd a.out
最后运行可执行文件:
./a.out
(自己尝试)
②系统级别的配置:
编辑/etc/profile
文件:
sudo vim /etc/profile
把配置环境变量的代码加到文件的最后一行:
export LD_LIBRARY_PATH=$LD_LIBRARY:/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library/lib
保存:
:wq
然后让该文件生效:
source /etc/profile
然后再查看动态库的依赖关系:
cd /home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/libraryldd a.out
最后运行可执行文件:
./a.out
(自己尝试)
方式2:把路径放到/etc/ld.so.cache
中
编辑/etc/ld.so.cache
文件:
sudo vim /etc/ld.so.conf
把动态库的路径加到文件的最后一行:
/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library/lib
保存:
:wq
然后让该文件生效:
sudo ldconfig
然后再查看动态库的依赖关系:
cd /home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/libraryldd a.out
最后运行可执行文件:
./a.out
(自己尝试)
4.2.6 静态库和动态库的对比☆☆☆
0.二者的命名
静态库:
libxxx.a
动态库:
libxxx.so
1.二者的区别
相同点:
- 静态库和动态库都是在链接阶段处理的:
不同点:
- 静态库:GCC 进行链接时,会把静态库中代码打包到可执行程序
a.out
中; - 动态库:GCC 进行链接时,动态库的代码不会被打包到可执行程序a.out中,而是在程序运行时由系统动态加载到内存中供程序调用。
2.二者的制作过程
静态库的制作过程:
1.获得目标代码.o
文件;
gcc -c a.c b.c
将.o
文件打包,使用ar
工具(archive)
ar rcs libxxx.a a.o b.o
动态库的制作过程:
1.得到和位置无关的目标代码.o
文件;(记得加-fpic
)
gcc -c -fpic a.c b.c
得到动态库
gcc -shared a.o b.o -o libcals.so
3.二者的优缺点
一般来说,库比较小的话建议用静态库;库比较大的话用动态库。
静态库 | 动态库 | |
---|---|---|
优点1 | 静态库被打包到应用程序中加载速度快 | 可以实现进程间资源共享(共享库) |
优点2 | 发布程序无需提供静态库,移植方便 | 更新、部署、发布简单 |
优点3 | 可以控制何时加载动态库 | |
缺点1 | 消耗系统资源,浪费内存 | 加载速度比静态库慢 |
缺点2 | 更新、部署、发布麻烦 | 发布程序时需要提供依赖的动态库 |
4.2.7 文件IO(操作系统笔记、第5章 高频考点与真题精讲笔记)
计算机操作系统笔记 和 笔记②:牛客校招冲刺集训营—C++工程师(面向对象(友元、运算符重载、继承、多态) – 内存管理 – 名称空间、模板(类模板/函数模板) – STL) 中有说到下面的部分内容。
文件IO:(站在内存的角度)
cout是输出;—写操作(从内存往外存的文件中写)
cin是输入/读入;—读操作(把外存的文件内容读到内存中)
标准C库IO函数
有缓冲区
什么时候把把数据从内存刷新到磁盘(外存)?
1.执行刷新缓冲区的操作fflush;
2.缓冲区已满;
3.正常关闭文件(fclose、return、exit等)
标准C库IO函数和Linux系统IO函数对比
虚拟地址空间(虚拟内存空间)
链接②Linux简明系统编程(嵌入式公众号的课)—总课时12h中的六、相关链接 — 博客2:六种进程间通信方式 — ③共享内存shared memory;
计算机操作系统笔记 和
笔记②:牛客校招冲刺集训营—C++工程师(面向对象(友元、运算符重载、继承、多态) – 内存管理 – 名称空间、模板(类模板/函数模板) – STL) 中有讲到虚拟内存的知识。
文件描述符fd
文件描述符表
前4小节的补充:
有几个参考链接:
链接1:C语言文件操作标准库函数与Linux系统函数效率比较
(下面的评论:
按理说linux系统调用是比c库函数效率高的,缓冲io和直接io的区别。之所以结论相反是因为你这里每次写的数据量太小,系统调用每次都要进行io操作,而库函数会将小量数据转存缓冲区从而大量减少io次数,所以这里最好的优化策略是每次写之前检查数据量大小,如果数据量较大则直接io,如果缓冲区较小则缓冲io)
链接2:文件操作——C库调用与Linux系统调用区别
(结论:
【区别】
1、本质:缓冲与非缓冲;
2、操作:系统调用通过文件描述符fd
来操作文件,而库函数通过文件指针FILE*
操作文件;
系统调用只能以二进制的形式读写文件,而库函数可以以二进制、字符、字符串、格式化数据读写文件;
3、效率:系统调用效率更高。)
链接3:【Linux】文件描述符和FILE结构体
文件描述符的优缺点
优点:文件描述符是许多Linux/Unix系统进行系统调用的接口
缺点:不可移植性,不能移植到Unix系统之外的其他系统
链接4:Linux(三)文件描述符和FILE结构体
①文件描述符的分配规则:在file_struct数组当中,找到当前没有被使用的最小的一个下标做为新的文件描述符;
②fwrite库函数会自带缓冲区,而write系统调用没有带缓冲区。我们所说的缓冲区都是用户级缓冲区,这个缓冲区是由C标准库提供的。
链接5:FILE结构体(文件描述符及缓冲区)
①fopen、fread…为库函数;open、read、write为系用调用。
②缓冲方式通常有行缓冲、无缓冲、全缓冲三种,在往显示器内写入通常为行缓冲模式,再往文件内写入时通常用全缓冲,无缓冲暂且不加讨论。
------------------------(视频课里的部分内容,没听得特别细:)------------------------
标准C库IO函数是跨平台的,在Windows系统可以用,在Linux系统也能用,Qt也是跨平台的。
标准C库函数的效率比较高,因为它带有缓冲区;
缓冲区的作用就是先把要写的内容放到缓冲区(默认是8KB),等缓冲区满的时候再把数据从内存刷新到磁盘中,因为往磁盘中写东西是和硬件打交道,所以效率不高,因此如果没有这个缓冲区,直接频繁地和硬件打交道,这样的话效率就会很低。
而Linux系统IO函数就是每次读写都直接跟磁盘进行交互,这样做也有它的好处,就是可以保证实时性,因此在网络通信中就用Linux系统IO函数。
标准C库IO函数和Linux系统IO函数之间是调用和被调用的关系:
前者在执行的时候会调用后者;—>fopen、fread…
为库函数;
在Windows系统中就会调用Windows的API,在Linux系统中就调用的Linux的API。—>open、read、write
为系统调用。
文件描述符fd
,是int类型,是Linux系统中的一个概念,在Windows中叫句柄。
文件描述符表,是个数组(默认是1024),文件描述符fd相当于数组下标,
程序就是代码,占用磁盘空间,不占用内存空间;
而进程是运行中的程序,占用内存空间。
man 2 xxx //Linux系统函数man 2 openman 3 xxx //标准C库的函数man 3 fopen
Linux系统IO函数(Linux系统api一般也称为系统调用) — man 2 系统函数、man 3 标准C库函数
man 2 xxx //Linux系统函数man 2 openman 3 xxx //标准C库的函数man 3 fopen
int open(const char *pathname, int flags);//返回文件描述符fdint open(const char *pathname, int flags, mode_t mode);int close(int fd);ssize_t read(int fd, void *buf, size_t count);ssize_t write(int fd, const void *buf, size_t count);off_t lseek(int fd, off_t offset, int whence);int stat(const char *pathname, struct stat *statbuf);int lstat(const char *pathname, struct stat *statbuf);perror("aaa"); "aaa":XXXX //错误信息
☆☆☆open()函数 —(打开一个已经存在的文件、创建一个新文件)
int open(const char *pathname, int flags);//返回文件描述符fdint open(const char *pathname, int flags, mode_t mode);int close(int fd);
头文件:
#include #include #include
打开一个已经存在的文件:
int open(const char *pathname, int flags);
参数:
- pathname:要打开的文件路径
- flags:对文件的操作权限设置还有其他的设置
- O_RDONLY, O_WRONLY, O_RDWR 这三个设置是互斥的
返回值:返回一个新的文件描述符,如果调用失败,返回-1
errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号。
#include void perror(const char *s);作用:打印errno对应的错误描述
s参数:用户描述,比如hello,最终输出的内容是 hello:xxx(实际的错误描述)
示例1:
#include #include #include #include #include int main() { // 打开一个文件 int fd = open("a.txt", O_RDONLY); if(fd == -1) { perror("open"); // open:XXXXX } // 读操作:只读权限,所以只能进行读操作 ... // 关闭 close(fd); return 0;}
创建一个新的文件:
int open(const char *pathname, int flags, mode_t mode);
参数:
- pathname:要创建的文件的路径
- flags:对文件的操作权限和其他的设置
必选项:O_RDONLY, O_WRONLY, O_RDWR 这三个之间是互斥的
可选项:O_CREAT 文件不存在,创建新文件
flags参数是一个int类型的数据,占4个字节,32位。
flags 32个位,每一位就是一个标志位。 - mode:八进制的数,表示创建出的新的文件的操作权限,比如:0775
最终的权限是:mode & ~umask
0777 -> 111111111& 0775 -> 111111101---------------------------- 111111101
按位与:0和任何数都为0
umask的作用就是抹去某些权限。
示例2:
#include #include #include #include #include int main() { // 创建一个新的文件 int fd = open("create.txt", O_RDWR | O_CREAT, 0777); if(fd == -1) { perror("open"); } // 关闭 close(fd); return 0;}
☆☆☆read()函数、write()函数
ssize_t read(int fd, void *buf, size_t count);ssize_t write(int fd, const void *buf, size_t count);
头文件:
#include
read()函数:
ssize_t read(int fd, void *buf, size_t count);
参数:
- fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf:需要读取数据存放的地方,数组的地址(传出参数)
- count:指定的数组的大小
返回值:
- 成功:
>0: 返回实际的读取到的字节数
=0:文件已经读取完了 - 失败:-1 ,并且设置errno
write()函数:
ssize_t write(int fd, const void *buf, size_t count);
参数:
- fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf:要往磁盘写入的数据,数据
- count:要写的数据的实际的大小
返回值:
- 成功:实际写入的字节数
- 失败:返回-1,并设置errno
示例:
#include #include #include #include #include int main() { // 1.通过open打开english.txt文件 int srcfd = open("english.txt", O_RDONLY); if(srcfd == -1) { perror("open"); return -1; } // 2.创建一个新的文件(拷贝文件) int destfd = open("cpy.txt", O_WRONLY | O_CREAT, 0664); if(destfd == -1) { perror("open"); return -1; } // 3.频繁的读写操作 char buf[1024] = {0}; int len = 0; while((len = read(srcfd, buf, sizeof(buf))) > 0) { write(destfd, buf, len); } // 4.关闭文件描述符 close(destfd); close(srcfd); return 0;}
lseek()函数 —(重定位文件偏移量offset)
reposition read/write file offset
off_t lseek(int fd, off_t offset, int whence);
标准C库的函数:
#include int fseek(FILE *stream, long offset, int whence);
Linux系统函数:
#include #include off_t lseek(int fd, off_t offset, int whence);
参数:
- fd:文件描述符,通过open得到的,通过这个fd操作某个文件
- offset:偏移量
- whence:
SEEK_SET
:设置文件指针的偏移量
SEEK_CUR
:设置偏移量:当前位置 + 第二个参数offset的值
SEEK_END
:设置偏移量:文件大小 + 第二个参数offset的值
返回值:返回文件指针的位置
作用:
- 移动文件指针到文件头
lseek(fd, 0, SEEK_SET);
- 获取当前文件指针的位置
lseek(fd, 0, SEEK_CUR);
- 获取文件长度(文件大小)
lseek(fd, 0, SEEK_END);
- 拓展文件的长度,当前文件10b, 110b, 增加了100个字节
lseek(fd, 100, SEEK_END)
- 注意:需要写一次数据
示例:拓展文件的长度
#include #include #include #include #include int main() {//打开文件: int fd = open("hello.txt", O_RDWR); if(fd == -1) { perror("open"); return -1; } // 扩展文件的长度 int ret = lseek(fd, 100, SEEK_END); if(ret == -1) { perror("lseek"); return -1; } // 写入一个空数据 write(fd, " ", 1); // 关闭文件 close(fd); return 0;}
stat()函数、lstat()函数 — (获取一个文件相关的一些信息)
int stat(const char *pathname, struct stat *statbuf);int lstat(const char *pathname, struct stat *statbuf);
头文件:
#include #include #include
stat()函数:
int stat(const char *pathname, struct stat *statbuf);
作用:获取一个文件相关的一些信息
参数:
- pathname:操作的文件的路径
- statbuf:结构体变量,传出参数,用于保存获取到的文件的信息
返回值:
- 成功:返回0
- 失败:返回-1 设置errno
lstat()函数 :
int lstat(const char *pathname, struct stat *statbuf);
参数:
- pathname:操作的文件的路径
- statbuf:结构体变量,传出参数,用于保存获取到的文件的信息
返回值:
- 成功:返回0
- 失败:返回-1 设置errno
stat 结构体:
struct stat {dev_t st_dev; // 文件的设备编号ino_t st_ino; // 节点mode_t st_mode; // 文件的类型和存取的权限nlink_t st_nlink; // 连到该文件的硬连接数目uid_t st_uid; // 用户IDgid_t st_gid; // 组IDdev_t st_rdev; // 设备文件的设备编号off_t st_size; // 文件字节数(文件大小)blksize_t st_blksize; // 块大小blkcnt_t st_blocks; // 块数time_t st_atime; // 最后一次访问时间time_t st_mtime; // 最后一次修改时间time_t st_ctime; // 最后一次改变时间(指属性)};
其中st_mode
变量:(文件的类型和存取的权限)
示例:获取文件的大小
#include #include #include #include int main() { struct stat statbuf;//结构体变量 int ret = stat("a.txt", &statbuf); if(ret == -1) { perror("stat"); return -1; } printf("size: %ld\n", statbuf.st_size); return 0;}
文件属性操作函数
int access(const char *pathname, int mode);int chmod(const char *filename, int mode);int chown(const char *path, uid_t owner, gid_t group);int truncate(const char *path, off_t length);
access()函数 —(查看权限)
#include int access(const char *pathname, int mode);
作用:判断某个文件是否有某个权限,或者判断文件是否存在
参数:
- pathname: 判断的文件路径
- mode:
R_OK: 判断是否有读权限
W_OK: 判断是否有写权限
X_OK: 判断是否有执行权限
F_OK: 判断文件是否存在
返回值:成功返回0, 失败返回-1
示例:
#include #include int main() { int ret = access("a.txt", F_OK); if(ret == -1) { perror("access"); } printf("文件存在!!!\n"); return 0;}
chmod()函数 —(修改权限)
#include int chmod(const char *filename, int mode);
作用:修改文件的权限
参数:
- pathname: 需要修改的文件的路径
- mode:需要修改的权限值,八进制的数
返回值:成功返回0,失败返回-1
示例:
#include #include int main() { int ret = chmod("a.txt", 0777); if(ret == -1) { perror("chmod"); return -1; } return 0;}
truncate()函数 —(缩减或者扩展文件的尺寸至指定的大小)
#include #include int truncate(const char *path, off_t length);
作用:缩减或者扩展文件的尺寸至指定的大小
参数:
- path: 需要修改的文件的路径
- length: 需要最终文件变成的大小
返回值:成功返回0, 失败返回-1
示例:
#include #include #include int main() { int ret = truncate("b.txt", 5); if(ret == -1) { perror("truncate"); return -1; } return 0;}
目录操作函数
int rename(const char *oldpath, const char *newpath);int chdir(const char *path);//change directorychar *getcwd(char *buf, size_t size); //get int mkdir(const char *pathname, mode_t mode); //make directoryint rmdir(const char *pathname); //remove directory
chdir()函数(修改进程的工作目录)、getcwd()函数(获取当前工作目录)
头文件:
#include
chdir()函数:
#include int chdir(const char *path);
作用:修改进程的工作目录
比如在/home/nowcoder 启动了一个可执行程序a.out, 进程的工作目录 /home/nowcoder
参数:
path : 需要修改的工作目录
getcwd()函数:
#include char *getcwd(char *buf, size_t size);
作用:获取当前工作目录
参数:
- buf : 存储的路径,指向的是一个数组(传出参数)
- size: 数组的大小
返回值:
返回的指向的一块内存,这个数据就是第一个参数
示例:
#include #include #include #include #include int main() { // 获取当前的工作目录 char buf[128]; getcwd(buf, sizeof(buf)); printf("当前的工作目录是:%s\n", buf); // 修改工作目录 int ret = chdir("/home/nowcoder/Linux/lesson13"); if(ret == -1) { perror("chdir"); return -1; } // 创建一个新的文件 int fd = open("chdir.txt", O_CREAT | O_RDWR, 0664); if(fd == -1) { perror("open"); return -1; } close(fd); // 获取当前的工作目录 char buf1[128]; getcwd(buf1, sizeof(buf1)); printf("当前的工作目录是:%s\n", buf1); return 0;}
mkdir()函数 —(创建一个目录)
#include #include int mkdir(const char *pathname, mode_t mode);
作用:创建一个目录
参数:
- pathname: 创建的目录的路径
- mode: 权限,八进制的数
返回值:
成功返回0, 失败返回-1
示例:
#include #include #include int main() { int ret = mkdir("aaa", 0777); if(ret == -1) { perror("mkdir"); return -1; } return 0;}
rename()函数 —(重命名)
#include int rename(const char *oldpath, const char *newpath);
示例:
#include int main() { int ret = rename("aaa", "bbb"); if(ret == -1) { perror("rename"); return -1; } return 0;}
目录遍历函数(opendir()函数、readdir()函数、closedir()函数)
DIR *opendir(const char *name);struct dirent *readdir(DIR *dirp);int closedir(DIR *dirp);
①打开一个目录:
#include #include DIR *opendir(const char *name);
参数:
name: 需要打开的目录的名称
返回值:
DIR * 类型,理解为目录流;错误返回NULL
②读取目录中的数据:
#include struct dirent *readdir(DIR *dirp);
参数:dirp是opendir返回的结果
返回值:
struct dirent,代表读取到的文件的信息
读取到了末尾或者失败了,返回NULL
③关闭目录:
#include #include int closedir(DIR *dirp);
补充:dirent 结构体和 d_type
struct dirent{// 此目录进入点的inodeino_t d_ino;// 目录文件开头至此目录进入点的位移off_t d_off;// d_name 的长度, 不包含NULL字符unsigned short int d_reclen;// d_name 所指的文件类型unsigned char d_type;// 文件名char d_name[256];};
其中d_type:
d_typeDT_BLK - 块设备DT_CHR - 字符设备DT_DIR - 目录DT_LNK - 软连接DT_FIFO - 管道DT_REG - 普通文件DT_SOCK - 套接字DT_UNKNOWN - 未知
示例:获取目录下所有普通文件的个数
#include #include #include #include #include int getFileNum(const char * path);// 读取某个目录下所有的普通文件的个数int main(int argc, char * argv[]) { if(argc < 2) { printf("%s path\n", argv[0]); return -1; } int num = getFileNum(argv[1]); printf("普通文件的个数为:%d\n", num); return 0;}// 用于获取目录下所有普通文件的个数int getFileNum(const char * path) { // 1.打开目录 DIR * dir = opendir(path); if(dir == NULL) { perror("opendir"); exit(0); } struct dirent *ptr; // 记录普通文件的个数 int total = 0; while((ptr = readdir(dir)) != NULL) { // 获取名称 char * dname = ptr->d_name; // 忽略掉. 和.. if(strcmp(dname, ".") == 0 || strcmp(dname, "..") == 0) { continue; } // 判断是否是普通文件还是目录 if(ptr->d_type == DT_DIR) { // 目录,需要继续读取这个目录 char newpath[256]; sprintf(newpath, "%s/%s", path, dname); total += getFileNum(newpath); } if(ptr->d_type == DT_REG) { // 普通文件 total++; } } // 关闭目录 closedir(dir); return total;}
dup、dup2 函数 —(复制、重定向 文件描述符)
dup()函数
#include int dup(int oldfd);
作用:复制一个新的文件描述符
示例:fd=3; int fd1 = dup(fd);
//fd指向的是a.txt, fd1也是指向a.txt
从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符
示例:
#include #include #include #include #include #include int main() { int fd = open("a.txt", O_RDWR | O_CREAT, 0664); int fd1 = dup(fd); if(fd1 == -1) { perror("dup"); return -1; } printf("fd : %d , fd1 : %d\n", fd, fd1); close(fd); char * str = "hello,world"; int ret = write(fd1, str, strlen(str)); if(ret == -1) { perror("write"); return -1; } close(fd1); return 0;}
dup2()函数
#include int dup2(int oldfd, int newfd);
作用:重定向文件描述符
oldfd 指向 a.txt, newfd 指向 b.txt
调用函数成功后:newfd 和 b.txt 做close, newfd 指向了 a.txt
oldfd 必须是一个有效的文件描述符
oldfd和newfd值相同,相当于什么都没有做
示例:
#include #include #include #include #include #include int main() { int fd = open("1.txt", O_RDWR | O_CREAT, 0664); if(fd == -1) { perror("open"); return -1; } int fd1 = open("2.txt", O_RDWR | O_CREAT, 0664); if(fd1 == -1) { perror("open"); return -1; } printf("fd : %d, fd1 : %d\n", fd, fd1); int fd2 = dup2(fd, fd1); if(fd2 == -1) { perror("dup2"); return -1; } // 通过fd1去写数据,实际操作的是1.txt,而不是2.txt char * str = "hello, dup2"; int len = write(fd1, str, strlen(str)); if(len == -1) { perror("write"); return -1; } printf("fd : %d, fd1 : %d, fd2 : %d\n", fd, fd1, fd2); close(fd); close(fd1); return 0;}
☆☆☆fcntl 函数 —(file control 复制文件描述符、设置/获取文件的状态标志)
复制文件描述符、设置/获取文件的状态标志
#include #include int fcntl(int fd, int cmd, ... );
参数:
- fd : 表示需要操作的文件描述符
- cmd: 表示对文件描述符进行如何操作
F_DUPFD
: 复制文件描述符,复制的是第一个参数fd,得到一个新的文件描述符(返回值),例如int ret = fcntl(fd, F_DUPFD);
F_GETFL
: 获取指定的文件描述符文件状态flag,获取的flag和我们通过open函数传递的flag是一个东西。
F_SETFL
: 设置文件描述符文件状态flag
必选项:O_RDONLY
,O_WRONLY
,O_RDWR
不可以被修改
可选性:O_APPEND
,O_NONBLOCK
(其中O_APPEND
表示追加数据,O_NONBLOK
设置成非阻塞)
阻塞和非阻塞:描述的是函数调用的行为。
示例:
#include #include #include #include int main() { // 1.复制文件描述符 // int fd = open("1.txt", O_RDONLY); // int ret = fcntl(fd, F_DUPFD); // 2.修改或者获取文件状态flag int fd = open("1.txt", O_RDWR); if(fd == -1) { perror("open"); return -1; } // 获取文件描述符状态flag int flag = fcntl(fd, F_GETFL); if(flag == -1) { perror("fcntl"); return -1; } flag |= O_APPEND; // flag = flag | O_APPEND // 修改文件描述符状态的flag,给flag加入O_APPEND这个标记 int ret = fcntl(fd, F_SETFL, flag); if(ret == -1) { perror("fcntl"); return -1; } char * str = "nihao"; write(fd, str, strlen(str)); close(fd); return 0;}
模拟实现 ls -l 指令
#include #include #include #include #include #include #include #include // 模拟实现 ls -l 指令// -rw-rw-r-- 1 nowcoder nowcoder 12 12月 3 15:48 a.txtint main(int argc, char * argv[]) { // 判断输入的参数是否正确 if(argc < 2) { printf("%s filename\n", argv[0]); return -1; } // 通过stat函数获取用户传入的文件的信息 struct stat st; int ret = stat(argv[1], &st); if(ret == -1) { perror("stat"); return -1; } // 获取文件类型和文件权限 char perms[11] = {0}; // 用于保存文件类型和文件权限的字符串 switch(st.st_mode & S_IFMT) { case S_IFLNK: perms[0] = 'l'; break; case S_IFDIR: perms[0] = 'd'; break; case S_IFREG: perms[0] = '-'; break; case S_IFBLK: perms[0] = 'b'; break; case S_IFCHR: perms[0] = 'c'; break; case S_IFSOCK: perms[0] = 's'; break; case S_IFIFO: perms[0] = 'p'; break; default: perms[0] = '?'; break; } // 判断文件的访问权限 // 文件所有者 perms[1] = (st.st_mode & S_IRUSR) ? 'r' : '-'; perms[2] = (st.st_mode & S_IWUSR) ? 'w' : '-'; perms[3] = (st.st_mode & S_IXUSR) ? 'x' : '-'; // 文件所在组 perms[4] = (st.st_mode & S_IRGRP) ? 'r' : '-'; perms[5] = (st.st_mode & S_IWGRP) ? 'w' : '-'; perms[6] = (st.st_mode & S_IXGRP) ? 'x' : '-'; // 其他人 perms[7] = (st.st_mode & S_IROTH) ? 'r' : '-'; perms[8] = (st.st_mode & S_IWOTH) ? 'w' : '-'; perms[9] = (st.st_mode & S_IXOTH) ? 'x' : '-'; // 硬连接数 int linkNum = st.st_nlink; // 文件所有者 char * fileUser = getpwuid(st.st_uid)->pw_name; // 文件所在组 char * fileGrp = getgrgid(st.st_gid)->gr_name; // 文件大小 long int fileSize = st.st_size; // 获取修改的时间 char * time = ctime(&st.st_mtime); char mtime[512] = {0}; strncpy(mtime, time, strlen(time) - 1); char buf[1024]; sprintf(buf, "%s %d %s %s %ld %s %s", perms, linkNum, fileUser, fileGrp, fileSize, mtime, argv[1]); printf("%s\n", buf); return 0;}
4.4 多进程(操作系统笔记、Linux简明教程笔记、C++工程师第五章笔记)
参考笔记1:②Linux简明系统编程(嵌入式公众号的课)—总课时12h;
参考笔记2:计算机操作系统笔记 ;
参考笔记3:笔记②:牛客校招冲刺集训营—C++工程师(面向对象(友元、运算符重载、继承、多态) – 内存管理 – 名称空间、模板(类模板/函数模板) – STL)
1.程序、进程、线程
简单来说:
程序就是代码,占用磁盘空间,不占用内存空间;
而进程是运行中的程序,占用内存空间。
程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程:
- 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息。内核利用此信息来解释文件中的其他信息。(ELF可执行连接格式)
- 机器语言指令:对程序算法进行编码。
- 程序入口地址:标识程序开始执行时的起始指令位置。
- 数据:程序文件包含的变量初始值和程序使用的字面量值(比如字符串)。
- 符号表及重定位表:描述程序中函数和变量的位置及名称。这些表格有多重用途,其中包括调试和运行时的符号解析(动态链接)。
- 共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态连接器的路径名。
- 其他信息:程序文件还包含许多其他信息,用以描述如何创建进程。
进程是正在运行的程序的实例,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。在引入线程之后,进程是资源分配的基本单位;线程是处理机调度的基本单位。
可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。
2.单道程序、多道程序、并行并发
单道程序,即在计算机内存中只允许一个程序运行;
多道程序设计技术是在计算机内存中同时存放几道相互独立的程序;
在多道程序设计模型中,多个进程轮流使用 CPU。而当下常见 CPU 为纳秒级,1秒可以执行大约10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。
对于一个单CPU 系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻,CPU 上运行的程序只有一个。
并行:parallel
并发:concurrency
3.进程控制快(PCB)
为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。
内核为每个进程分配一个PCB(Processing Control Block)进程控制块,维护进程相关的信息;
Linux 内核的进程控制块是task_struct
结构体。
在 /usr/src/linux-headers-xxx/include/linux/sched.h
文件中可以查看 struct task_struct
结构体定义。
其内部成员有很多,我们只需要掌握以下部分即可:
- 进程id:系统中每个进程有唯一的 id,用 pid_t 类型表示,其实就是一个非负整数
- 进程的状态:有就绪、运行、挂起、停止等状态
- 进程切换时需要保存和恢复的一些CPU寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录(Current Working Directory)
- umask 掩码(抹去一些权限)
- 文件描述符表
fd
,包含很多指向 file 结构体的指针 - 和信号相关的信息
- 用户 id 和组 id
- 会话(Session)和进程组
- 进程可以使用的资源上限(Resource Limit)(指令:
ulimit -a
)
4.进程的状态(三状态、五状态、七状态)
操作系统笔记中的2.1.2 进程的状态 及 状态间的转换(五状态模型) 和 2.2.1 进程的挂起态和七状态模型。
在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。
在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。
- 运行态:进程占有处理器正在运行;
- 就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列;
- 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成;
- 新建态:进程刚被创建时的状态,尚未进入就绪队列;
- 终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。
注意:
阻塞态无法直接到运行态,必须先转换成就绪态;
运行态–>阻塞态:是主动行为;
阻塞态–>就绪态:是被动行为;
三状态模型:
五状态模型:
七状态模型:
暂时调到外存等待的进程状态称为挂起态,suspend;
5.进程相关指令(Linux学习笔记)
Linux学习笔记4—第9、10、11章中的 10.2 显示系统执行的进程-ps 和 10.3 终止进程—kill和killall 和 10.6 监控 1. 动态监控进程—top 和
查看进程 ps aux / ps ajx
ps aux ps ajxa:显示终端上的所有进程,包括其他用户的进程u:显示进程的详细信息x:显示没有控制终端的进程j:列出与作业控制相关的信息
PID表示进程号;
PPID表示父进程号;
STAT表示状态:
D 不可中断 Uninterruptible(usually IO)R 正在运行,或在队列中的进程S 处于休眠状态T 停止或被追踪Z 僵尸进程 //zombie僵尸的单词首字母W 进入内存交换(从内核2.6开始无效)X 死掉的进程< 高优先级N 低优先级s 包含子进程+ 位于前台的进程组
实时显示进程动态 top
top
可以在使用 top
命令时加上 -d
来指定显示信息更新的时间间隔;
top -d 5 //每5秒更新一次
在 top
命令执行后,可以按以下按键对显示的结果进行排序:
(直接在键盘上输入下面的大写字母,就可以按照不同的规则对进程进行排序)
M 根据内存使用量排序P 根据 CPU 占有率排序T 根据进程运行时间长短排序U 根据用户名来筛选进程K 输入指定的 PID 杀死进程
杀死进程(kill名并不是去杀死一个进程,而是给进程发送某个<信号>)
kill [-signal] pid //给进程号为pid的进程发送signal信号kill –l 列出所有信号kill –SIGKILL 进程IDkill -9 进程IDkillall name 根据进程名杀死进程
6.进程号相关函数 — getpid() getppid() getpgid()
每个进程都由进程号来标识,其类型为 pid_t
(整型),进程号的范围:0~32767。
进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
任何进程(除 init
进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID
)。默认情况下,当前的进程号会当做当前的进程组号。
pid_t getpid(void);pid_t getppid(void);pid_t getpgid(pid_t pid);
7.进程创建 —fork()函数
①Linux简明系统编程(嵌入式公众号的课)—总课时12h 中的 第2、3节课:进程创建函数fork()
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。
#include #include pid_t fork(void);
函数的作用:用于创建子进程。
返回值:
fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。
在父进程中返回创建的子进程的ID;
在子进程中返回0;
如果返回-1
,表示创建子进程失败。
失败的两个主要原因:
①当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN
②系统内存不足,这时 errno 的值被设置为 ENOMEM
如何区分父进程和子进程?
通过fork的返回值:
如果返回值为0,则表示子进程;
如果返回值大于0,表示父进程;
在父进程中返回-1
,表示创建子进程失败,并且设置errno。
父子进程之间的关系:
①区别:
- fork()函数的返回值不同
父进程中:>0
返回的子进程的ID
子进程中:=0
- pcb中的一些数据
当前的进程的id pid
当前的进程的父进程的id ppid
信号集
②共同点:
某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作
- 用户区的数据
- 文件描述符表
特点:读时共享,写时拷贝 (copy- on-write)☆☆☆
父子进程对变量是不是共享的?
- 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。
- 读时共享(子进程被创建,两个进程没有做任何的写的操作);写时拷贝。
实际上,更准确来说,Linux 的 fork()
使用是通过写时拷贝 (copy- on-write) 实现的。
写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
示例:
#include #include #include int main() { int num = 10; // 创建子进程 pid_t pid = fork(); // 判断是父进程还是子进程 if(pid > 0) { // printf("pid : %d\n", pid); // 如果大于0,返回的是创建的子进程的进程号,当前是父进程 printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid()); printf("parent num : %d\n", num); num += 10; printf("parent num += 10 : %d\n", num); } else if(pid == 0) { // 当前是子进程 printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid()); printf("child num : %d\n", num); num += 100; printf("child num += 100 : %d\n", num); } // for循环 for(int i = 0; i < 3; i++) { printf("i : %d , pid : %d\n", i , getpid()); sleep(1); } return 0;}
结果:
i am child process, pid : 2043839, ppid : 2043827child num : 10child num += 100 : 110i : 0 , pid : 2043839i am parent process, pid : 2043827, ppid : 2043804parent num : 10parent num += 10 : 20i : 0 , pid : 2043827i : 1 , pid : 2043839i : 1 , pid : 2043827i : 2 , pid : 2043839i : 2 , pid : 2043827
8.exec 函数族(在进程内部执行一个可执行文件,用它来取代进程原本要执行的内容)
exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。
只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。
int execl(const char *path, const char *arg, ...);int execlp(const char *file, const char *arg, ... );int execle(const char *path, const char *arg, ...);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[], char *const envp[]);int execve(const char *filename, char *const argv[], char *const envp[]);
其中,exec
后面的字母含义:
l
(list) 参数地址列表,以空指针结尾v
(vector) 存有各参数地址的指针数组的地址p
(path) 按 PATH 环境变量指定的目录搜索可执行文件e
(environment) 存有环境变量字符串地址的指针数组的地址
例如:
int execv(const char *path, char *const argv[]);
其中argv
是需要的参数的一个字符串数组,
例如:
char * argv[] = {"ps", "aux", NULL};execv("/bin/ps", argv);
execl()函数(用自己写的可执行程序进行替换)
#include int execl(const char *path, const char *arg, ...);
参数:
path
:需要指定的执行的文件的路径或者名称
例如:
a.out
//相对路径
/home/nowcoder/a.out
//绝对路径(推荐)
./a.out hello world
//arg
:是要执行的可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数列表最后需要以NULL
结束(哨兵)
返回值:
- 只有当调用失败,才会有返回值,返回
-1
,并且设置errno - 如果调用成功,没有返回值。
示例:
原本是父子进程,另外还有一个hello.c
程序,对它生成一个可执行程序hello
;然后用execl()
函数将子进程原本要执行的内容替换成可执行程序hello
的内容。
hello.c
#include int main() { printf("hello, world\n"); return 0;}
execl.c
#include #include int main() { // 创建一个子进程,在子进程中执行exec函数族中的函数 pid_t pid = fork(); if(pid > 0) { // 父进程 printf("i am parent process, pid : %d\n",getpid()); sleep(1); }else if(pid == 0) { // 子进程 execl("hello","hello",NULL);//用自己写的一个可执行程序替换子进程原本要执行的内容 // execl("/bin/ps", "ps", "aux", NULL);//用系统shell命令替换子进程原本要执行的内容,注意:系统shell命令必须写绝对路径 // execl("ps", "ps", "aux", NULL);//这样写并不会替换,因为找不到ps命令,所以还是执行子进程原来的内容 //perror("execl"); printf("i am child process, pid : %d\n", getpid()); } for(int i = 0; i < 3; i++) { printf("i = %d, pid = %d\n", i, getpid()); } return 0;}
编译运行:
没加execl()
函数之前的结果:
加了execl()
函数之后的结果:
系统shell命令必须写绝对路径,否则找不到ps
命令,所以还是执行子进程原来的内容:
execlp()函数(用系统的shell命令进行替换)
#include int execlp(const char *file, const char *arg, ... );
execlp
函数会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
参数:
-
file
:需要执行的可执行文件的文件名
例如:
a.out
ps
-
arg
:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)
返回值:
- 只有当调用失败,才会有返回值,返回-1,并且设置errno
- 如果调用成功,没有返回值。
示例:
execlp
函数会到环境变量中查找指定的可执行文件ps
,找到了就将子进程要执行的进行替换,找不到就执行子进程原本的内容。
execlp.c
#include #include int main() { // 创建一个子进程,在子进程中执行exec函数族中的函数 pid_t pid = fork(); if(pid > 0) { // 父进程 printf("i am parent process, pid : %d\n",getpid()); sleep(1); }else if(pid == 0) { // 子进程 execlp("ps", "ps", "aux", NULL);//ok //execlp("/bin/ps", "ps", "aux", NULL);//ok //execlp("hello", "hello", NULL);//可执行程序hello没在环境变量里,所以就不会替换,依然执行子进程原来的内容 printf("i am child process, pid : %d\n", getpid()); } for(int i = 0; i < 3; i++) { printf("i = %d, pid = %d\n", i, getpid()); } return 0;}
编译运行:
可执行程序hello
没在环境变量里,所以就不会替换,依然执行子进程原来的内容:
9.进程控制☆☆☆
进程退出 — exit()函数
#include void exit(int status);//标准C库函数#include void _exit(int status);//Linux的系统调用
其中status
参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
如果调用标准C库函数exit()
,会刷新I/O缓冲(回车符是行缓冲的标志)。
示例1:Linux系统调用
#include #include #include int main() { printf("hello\n");//回车符是行缓冲的标志 printf("world");//这个没有回车符 // exit(0);//标准C库函数(会刷新I/O缓冲) _exit(0);//Linux系统调用 return 0;}
结果:
示例2:标准C库函数(会刷新I/O缓冲)—推荐
#include #include #include int main() { printf("hello\n");//回车符是行缓冲的标志 printf("world");//这个没有回车符 exit(0);//标准C库函数(会刷新I/O缓冲) // _exit(0);//Linux系统调用 return 0;}
结果:
刷新I/O缓冲之后,"world"就会被打印出来
孤儿进程orphan(不危险)
父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(OrphanProcess)。
孤儿进程会被init进程接管回收,没啥危害。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init
(pid为1的进程 ),而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
示例:
(父进程先执行,子进程等待10s
后再执行,父进程执行完之后就结束了,然后子进程就成孤儿进程了,随后会被init进程接管)
#include #include #include int main() { // 创建子进程 pid_t pid = fork(); // 判断是父进程还是子进程 if(pid > 0) { printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid()); } else if(pid == 0) { sleep(10); // 当前是子进程 printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid()); } //for循环 for(int i = 0; i < 3; i++) { printf("i : %d , pid : %d\n", i , getpid()); } return 0;}
结果:
僵尸进程zombie(危险,解决办法)
每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程
去释放。
如果子进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
僵尸进程不能被 kill -9
杀死,这样就会导致一个问题,如果父进程不调用 wait()
或 waitpid()
的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免僵尸进程。
示例:
(父进程处在一个死循环里,没空回收子进程,子进程执行完就成了僵尸进程,一直占用着一个进程号,需要父进程通过调用 wait() 或 waitpid() 来将其回收)
#include #include #include int main() { // 创建子进程 pid_t pid = fork(); // 判断是父进程还是子进程 if(pid > 0) { while(1) { printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid()); sleep(3); } } else if(pid == 0) { // 当前是子进程 printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid()); } // for循环 for(int i = 0; i < 3; i++) { printf("i : %d , pid : %d\n", i , getpid()); } return 0;}
编译运行:
此时在另一个终端输入ps aux
,查看进程的状态:
发现子进程(pid = 2428593)的状态是Z
,表示zombie
,僵尸进程的意思。
那么怎么解决呢?两种方法:
一是杀死父进程,让init
进程接管这个僵尸进程(因为这个僵尸进程其实已经执行结束了,只是父进程没有对其进行回收,所以它还占着进程号,但也只是占着进程号,一旦将父进程杀死,那么这对父子进程就都结束了);
二是尝试通过kill
指令杀死僵尸进程;
方法一:
方法二:
所以说不能通过kill
指令来杀死僵尸进程,只能通过杀死父进程或者让父进程循环调用 wait()
或 waitpid()
函数(见下一节)来彻底结束僵尸进程。
(在下一篇笔记的最后,还有更好的方法:捕捉SIGCHLD信号来处理僵尸进程
,具体见Linux高并发服务器开发—笔记2(多进程)补充:SIGCHLD信号(解决僵尸进程的问题)。)
进程回收 — wait()函数、waitpid()函数
可以参考①Linux简明系统编程(嵌入式公众号的课)—总课时12h中的第4节课:监控子进程函数wait()。
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。
父进程可以通过调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程。
wait() 和 waitpid() 函数的功能一样,区别在于:
wait() 函数会阻塞(处在阻塞态,一旦有子进程结束,就将其回收);
waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。
注意:一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。
阻塞 & 非阻塞
阻塞:就是一直等着,一旦有子进程结束,wait进程将被唤醒;(类似于猎人在兔子窝口放了个陷阱,有兔子出来掉进陷阱,猎人就被吵醒了,然后就抓到它了)
非阻塞:wait进程一直执行,有子进程结束了就可以知道。(类似于猎人24小时在盯着兔子窝,有兔子出来就能抓到它)
可以看看下面的 waitpid()函数中的示例 或者 Linux高并发服务器开发—笔记2中的示例:(将管道设置为非阻塞)
wait()函数 — 回收任意子进程的资源(返回被回收的子进程id)
#include #include pid_t wait(int *wstatus);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。
参数:
int *wstatus
:进程退出时的状态信息,传入的是一个int类型的地址(int*
),这个参数是一个传出参数。也可以直接写NULL
;
返回值:
- 成功:返回被回收的子进程的id
- 失败:返回
-1
(所有的子进程都结束,调用函数失败)
调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行);
如果没有子进程了,函数立刻返回,返回-1
;
如果子进程都已经结束了,也会立即返回,返回-1
。
示例:
创建5个子进程,每个子进程在一个死循环中每隔10s打印一次自己的pid;
父进程也在一个死循环中执行wait()
函数等待子进程结束,然后将其回收;当没有子进程结束,父进程进一直处在阻塞状态,因此说wait
函数会阻塞;
在当前终端运行生成的可执行程序wait
,会看到父进程和子进程的id号;
在另一个终端通过kill -9 子进程id
指令给某个子进程发送信号让它结束,然后父进程这边就会被唤醒,将此进程回收,并且返回此进程的id;然后执行下一次wait
函数,直到所有子进程都结束了,wait
函数就会返回-1
,然后执行break;
退出循环,然后父进程就结束了。
wait.c
#include #include #include #include #include int main() { // 有一个父进程,创建5个子进程(兄弟) pid_t pid; // 创建5个子进程 for(int i = 0; i < 5; i++) { pid = fork(); if(pid == 0) { break; } } if(pid > 0) { // 父进程 while(1) { printf("parent, pid = %d\n", getpid()); int ret = wait(NULL); // int st; // int ret = wait(&st); if(ret == -1) {//子进程都结束了,返回-1,然后就跳出循环,结束父进程 break; } // if(WIFEXITED(st)) { // // 是不是正常退出 // printf("退出的状态码:%d\n", WEXITSTATUS(st)); // } // if(WIFSIGNALED(st)) { // // 是不是异常终止 // printf("被哪个信号干掉了:%d\n", WTERMSIG(st)); // } printf("child die, pid = %d\n", ret); sleep(1); } printf("parent die, pid = %d\n", getpid()); } else if (pid == 0){ // 子进程 while(1) { printf("child, pid = %d\n",getpid()); sleep(10); } exit(0); } return 0; // exit(0)}
waitpid()函数 — 回收指定进程号的子进程
#include #include pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:
- pid:
pid > 0
: 回收某个子进程的pid
pid = 0 : 回收当前进程组的所有子进程
pid = -1
: 回收所有的子进程,相当于 wait() (最常用)
pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程 - wstatus:
跟wait()函数中的参数一样,是一个int类型的地址,即(int*
),也可以直接写NULL
; - options:设置阻塞或者非阻塞
0
: 阻塞;返回值只会出现>0
和=-1
的情况;
WNOHANG
: 是一个宏值,表示非阻塞;返回值可能会返回0
,表示还有子进程活着。
返回值:
- >0:返回被回收的子进程id
- =0 : 当options设置为
WNOHANG
时才有可能返回0, 表示还有子进程活着;
因为把options设置为WNOHANG
时表示非阻塞,父进程会继续运行,循环判断是否还有子进程活着;
而如果把options设置为0
时表示阻塞,父进程就不动了,无法判断是否还有子进程活着,所以只会返回>0
和=-1
的两种情况; - = -1 :错误,或者没有子进程了。
示例:
创建5个子进程,每个子进程在一个死循环中每隔10s打印一次自己的pid;
父进程也在一个死循环中执行waitpid()
函数等待子进程结束,然后将其回收;这里可以将waitpid()
函数设置为阻塞态活着非阻塞态;
int ret = waitpid(-1, NULL, 0);//设置成阻塞int ret = waitpid(-1, NULL, WNOHANG);//设置成非阻塞
在当前终端运行生成的可执行程序wait
,会看到父进程和子进程的id号;
在另一个终端通过kill -9 子进程id
指令给某个子进程发送信号让它结束,然后父进程会将此进程回收,并且返回此进程的id;直到所有子进程都结束了,wait
函数就会返回-1
,然后执行break;
退出循环,然后父进程就结束了。
waitpid.c
#include #include #include #include #include int main() { // 有一个父进程,创建5个子进程(兄弟) pid_t pid; // 创建5个子进程 for(int i = 0; i < 5; i++) { pid = fork(); if(pid == 0) { break; } } if(pid > 0) { // 父进程 while(1) { printf("parent, pid = %d\n", getpid()); sleep(3); int ret = waitpid(-1, NULL, 0);//设置成阻塞 //int ret = waitpid(-1, NULL, WNOHANG);//设置成非阻塞 //int st; //int ret = waitpid(-1, &st, 0);//设置成阻塞 // int ret = waitpid(-1, &st, WNOHANG);//设置成非阻塞 if(ret == -1) { break; } else if(ret == 0) {//只有当第三个参数设置为WNOHANG时,才有可能返回0,表示还有子进程存在 continue; } else if(ret > 0) {//有进程结束 // if(WIFEXITED(st)) { // // 是不是正常退出 // printf("退出的状态码:%d\n", WEXITSTATUS(st)); // } // if(WIFSIGNALED(st)) { // // 是不是异常终止 // printf("被哪个信号干掉了:%d\n", WTERMSIG(st)); // } printf("child die, pid = %d\n", ret); } } printf("parent die, pid = %d\n", getpid()); } else if (pid == 0){ // 子进程 while(1) { printf("child, pid = %d\n",getpid()); sleep(10); } exit(0); } return 0; }
编译运行:
int ret = waitpid(-1, NULL, WNOHANG);
//设置成非阻塞:
int ret = waitpid(-1, NULL, 0);
//设置成阻塞:
wait()函数 和 waitpid()函数 区别
wait()函数 | waitpid()函数 | |
---|---|---|
参数: | ||
pid_t pid | 无 | 可以指定要回收的子进程id;如果写-1,就是回收所有子进程 |
int *wstatus | 可以写NULL | 可以写NULL |
int options | 无 | 0表示阻塞;WNOHANG表示非阻塞 |
阻塞? | 阻塞 | 可以设置是否阻塞 |
作用 | 回收子进程 | 回收指定子进程 |
退出信息相关宏函数
WIFEXITED(status) 非0,进程正常退出WEXITSTATUS(status) 如果上面的宏为真,获取进程退出的状态(exit的参数)WIFSIGNALED(status) 非0,进程异常终止WTERMSIG(status) 如果上面的宏为真,获取使进程终止的信号编号WIFSTOPPED(status) 非0,进程处于暂停状态WSTOPSIG(status) 如果上面的宏为真,获取使进程暂停的信号的编号WIFCONTINUED(status) 非0,进程暂停后已经继续运行
10.进程间通信☆☆☆
来源地址:https://blog.csdn.net/weixin_38665351/article/details/125254289