0613

第4章 项目制作与技能提升

  • 4.0 视频课链接
  • 4.1 项目介绍与环境搭建
    • 4.1.1 项目介绍
    • 4.1.2 开发环境搭建
      • ①安装Linux系统、XSHELL、XFTP、Visual Studio Code并实现 免密登录
      • ②ubuntu中安装MySQL
      • ③项目启动
      • ④单元测试
      • ⑤压力测试
        • 压力测试结果统计
      • 补充:什么叫高并发?怎么衡量?
  • 4.2 Linux系统编程1、4.3 Linux系统编程2
    • 4.2.1 GCC
      • gcc的底层都实现了什么?
      • gcc 和 g++ 的区别
    • 4.2.2 Makefile
      • 什么是Makefile?(Makefile是个文件)
      • makefile文件命名和规则
      • 基本原理 --> Makefile1、Makefile2
      • 变量 --> Makefile3
      • 模式匹配 --> Makefile4
      • 函数 --> Makefile5
    • 4.2.3 GDB调试
      • 什么是GDB?
      • 常用的GDB命令:
    • 4.2.4 静态库
      • 静态库的制作
      • 静态库的使用
    • 4.2.5 动态库
      • 动态库的制作和使用
      • 动态库加载失败的原因
      • 解决动态库加载失败的问题☆☆☆
    • 4.2.6 静态库和动态库的对比☆☆☆
      • 0.二者的命名
      • 1.二者的区别
      • 2.二者的制作过程
      • 3.二者的优缺点
    • 4.2.7 文件IO(操作系统笔记、第5章 高频考点与真题精讲笔记)
      • 标准C库IO函数
      • 标准C库IO函数和Linux系统IO函数对比
      • 虚拟地址空间(虚拟内存空间)
      • 文件描述符fd
      • 前4小节的补充:
      • Linux系统IO函数(Linux系统api一般也称为系统调用) --- man 2 系统函数、man 3 标准C库函数
        • ☆☆☆open()函数 ---(打开一个已经存在的文件、创建一个新文件)
        • ☆☆☆read()函数、write()函数
        • lseek()函数 ---(重定位文件偏移量offset)
        • stat()函数、lstat()函数 --- (获取一个文件相关的一些信息)
      • 文件属性操作函数
        • access()函数 ---(查看权限)
        • chmod()函数 ---(修改权限)
        • truncate()函数 ---(缩减或者扩展文件的尺寸至指定的大小)
      • 目录操作函数
        • chdir()函数(修改进程的工作目录)、getcwd()函数(获取当前工作目录)
        • mkdir()函数 ---(创建一个目录)
        • rename()函数 ---(重命名)
      • 目录遍历函数(opendir()函数、readdir()函数、closedir()函数)
        • 补充:dirent 结构体和 d_type
        • 示例:获取目录下所有普通文件的个数
      • dup、dup2 函数 ---(复制、重定向 文件描述符)
        • dup()函数
        • dup2()函数
      • ☆☆☆fcntl 函数 ---(file control 复制文件描述符、设置/获取文件的状态标志)
      • 模拟实现 ls -l 指令
  • 4.4 多进程(操作系统笔记、Linux简明教程笔记、C++工程师第五章笔记)
    • 1.程序、进程、线程
    • 2.单道程序、多道程序、并行并发
    • 3.进程控制快(PCB)
    • 4.进程的状态(三状态、五状态、七状态)
    • 5.进程相关指令(Linux学习笔记)
      • 查看进程 ps aux / ps ajx
      • 实时显示进程动态 top
      • 杀死进程(kill名并不是去杀死一个进程,而是给进程发送某个<信号>)
    • 6.进程号相关函数 --- getpid() getppid() getpgid()
    • 7.进程创建 ---fork()函数
      • 特点:读时共享,写时拷贝 (copy- on-write)☆☆☆
      • 示例:
    • 8.exec 函数族(在进程内部执行一个可执行文件,用它来取代进程原本要执行的内容)
      • execl()函数(用自己写的可执行程序进行替换)
      • execlp()函数(用系统的shell命令进行替换)
    • 9.进程控制☆☆☆
      • 进程退出 --- exit()函数
      • 孤儿进程orphan(不危险)
      • 僵尸进程zombie(危险,解决办法)
      • 进程回收 --- wait()函数、waitpid()函数
        • 阻塞 & 非阻塞
        • wait()函数 --- 回收任意子进程的资源(返回被回收的子进程id)
        • waitpid()函数 --- 回收指定进程号的子进程
        • wait()函数 和 waitpid()函数 区别
        • 退出信息相关宏函数
    • 10.进程间通信☆☆☆

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:5501:20:15

4.1.2 开发环境搭建

①安装Linux系统、XSHELL、XFTP、Visual Studio Code并实现 免密登录

1.安装Linux系统(虚拟机安装、云服务器)
https://releases.ubuntu.com/bionic/

2.安装XSHELL、XFTP
https://www.netsarang.com/zh/free-for-home-school/

3.安装Visual Studio Code
https://code.visualstudio.com/

课程内容:

  1. 通过XShellxftp远程连接Linux服务器:通过SSH协议进程远程连接。
  2. 通过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-dev
sudo 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# ls
bin  build  code  LICENSE  log  Makefile  readme.assest  readme.md  resources  test  webbench-1.5

就在这个目录项执行下面的两行指令:

make
./bin/server

就把这个服务器运行起来了。

④单元测试

(这个没试)

cd test
make
./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之后,生成两个文件:可执行文件webbenchwebbench.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.5
Copyright (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.5
Copyright (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.5
Copyright (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的底层都实现了什么?

1.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程序,gccg++都可以;
对于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

1.命令在执行之前,需要先检查规则中的依赖是否存在:

  • a.如果存在,执行命令;
  • b.如果不存在,向下检查其它的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令

2.检测更新,在执行规则中的命令时,会比较目标依赖文件的时间

  • a.如果依赖的时间比目标的时间晚,需要重新生成目标
  • b.如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行

变量 --> Makefile3


模式匹配 --> Makefile4

函数 --> Makefile5


伪目标:

.PHONY:clean
clean:rm $(objs) -f//删除所有.o文件

4.2.3 GDB调试

什么是GDB?

GDB 是由 GNU 软件系统社区提供的调试工具,同 GCC 配套组成了一套完整的开发环境,GDB 是 Linux 和许多类 Unix 系统中的标准开发环境。

一般来说,GDB 主要帮助你完成下面四个方面的功能:

  1. 启动程序,可以按照自定义的要求随心所欲的运行程序;
  2. 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式);
  3. 当程序被停住时,可以检查此时程序中所发生的事;
  4. 可以改变程序,将一个 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

2.将.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

2.得到动态库

gcc -shared a.o b.o -o libcals.so

示例:(动态库的制作与使用)
在路径 /home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用 下,有以下文件:

(和静态库的制作和使用一样,视频课中也是在/calc目录下制作动态库,然后在/library目录下又制作了一遍动态库,然后再使用动态库,所以直接在/library目录下一次性完成制作加使用动态库

1.制作动态库,并放到/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 foundlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fce25c3a000)/lib64/ld-linux-x86-64.so.2 (0x00007fce25e3c000)

解决动态库加载失败的问题☆☆☆

环境变量:

env


环境变量是个键值对,一个键可以对应多个值,用冒号隔开。

那么如何配置动态库的绝对路径呢?
课里讲了两种方式:

  1. 把绝对路径放到环境变量LD_LIBRARY_PATH中;
  2. 把路径放到/etc/ld.so.cache中。


(视频课中从07:40开始)

方式1:把绝对路径放到环境变量LD_LIBRARY_PATH中;
刚开始讲了一种方式,是在终端上配置的,但把终端关闭后,就又找不到动态库了,这种配置是临时的
具体实现如下:
1.先把动态库的绝对路径复制下来

pwd
/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library/lib

2.配置环境变量

export LD_LIBRARY_PATH=$LD_LIBRARY:/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library/lib

3.查看环境变量

echo $LD_LIBRARY_PATH
:/home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library/lib

4.退到上一层目录,通过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)

5.最后运行可执行文件

./a.out a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
root@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文件生效:(两种方式都行)

. .bashrc
source .bashrc

然后再查看动态库的依赖关系:

cd /home/reus/牛客网-Linux高并发服务器开发/1.6动态库的制作和使用/library
ldd 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动态库的制作和使用/library
ldd 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动态库的制作和使用/library
ldd 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

2.将.o文件打包,使用ar工具(archive)

ar rcs libxxx.a a.o b.o


动态库的制作过程:
1.得到和位置无关的目标代码.o文件;(记得加-fpic

gcc -c -fpic a.c b.c

2.得到动态库

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);//返回文件描述符fd
int 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);//返回文件描述符fd
int open(const char *pathname, int flags, mode_t mode);
int close(int fd);

头文件:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

打开一个已经存在的文件

int open(const char *pathname, int flags);

参数:

  • pathname:要打开的文件路径
  • flags:对文件的操作权限设置还有其他的设置
  • O_RDONLY, O_WRONLY, O_RDWR 这三个设置是互斥的

返回值:返回一个新的文件描述符,如果调用失败,返回-1

errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号。

#include <stdio.h>
void perror(const char *s);作用:打印errno对应的错误描述

s参数:用户描述,比如hello,最终输出的内容是 hello:xxx(实际的错误描述)

示例1:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>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 <unistd.h>

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 <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>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 <stdio.h>
int fseek(FILE *stream, long offset, int whence);

Linux系统函数:

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

参数:

  • fd:文件描述符,通过open得到的,通过这个fd操作某个文件
  • offset:偏移量
  • whence:
    SEEK_SET:设置文件指针的偏移量
    SEEK_CUR:设置偏移量:当前位置 + 第二个参数offset的值
    SEEK_END:设置偏移量:文件大小 + 第二个参数offset的值

返回值:返回文件指针的位置

作用:

  1. 移动文件指针到文件头
lseek(fd, 0, SEEK_SET);
  1. 获取当前文件指针的位置
lseek(fd, 0, SEEK_CUR);
  1. 获取文件长度(文件大小)
lseek(fd, 0, SEEK_END);
  1. 拓展文件的长度,当前文件10b, 110b, 增加了100个字节
lseek(fd, 100, SEEK_END)
  • 注意:需要写一次数据

示例:拓展文件的长度

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>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 <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

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 <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>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 <unistd.h>
int access(const char *pathname, int mode);

作用:判断某个文件是否有某个权限,或者判断文件是否存在
参数:

  • pathname: 判断的文件路径
  • mode:
    R_OK: 判断是否有读权限
    W_OK: 判断是否有写权限
    X_OK: 判断是否有执行权限
    F_OK: 判断文件是否存在

返回值:成功返回0, 失败返回-1

示例:

#include <unistd.h>
#include <stdio.h>int main() {int ret = access("a.txt", F_OK);if(ret == -1) {perror("access");}printf("文件存在!!!\n");return 0;
}

chmod()函数 —(修改权限)

#include <sys/stat.h>
int chmod(const char *filename, int mode);

作用:修改文件的权限
参数:

  • pathname: 需要修改的文件的路径
  • mode:需要修改的权限值,八进制的数

返回值:成功返回0,失败返回-1

示例:

#include <sys/stat.h>
#include <stdio.h>
int main() {int ret = chmod("a.txt", 0777);if(ret == -1) {perror("chmod");return -1;}return 0;
}

truncate()函数 —(缩减或者扩展文件的尺寸至指定的大小)

#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);

作用:缩减或者扩展文件的尺寸至指定的大小
参数:

  • path: 需要修改的文件的路径
  • length: 需要最终文件变成的大小

返回值:成功返回0, 失败返回-1

示例:

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>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 directory
char *getcwd(char *buf, size_t size); //get
int mkdir(const char *pathname, mode_t mode); //make directory
int rmdir(const char *pathname);  //remove directory

chdir()函数(修改进程的工作目录)、getcwd()函数(获取当前工作目录)

头文件:

#include <unistd.h>

chdir()函数:

#include <unistd.h>
int chdir(const char *path);

作用:修改进程的工作目录
比如在/home/nowcoder 启动了一个可执行程序a.out, 进程的工作目录 /home/nowcoder
参数:
path : 需要修改的工作目录

getcwd()函数:

#include <unistd.h>
char *getcwd(char *buf, size_t size);

作用:获取当前工作目录
参数:

  • buf : 存储的路径,指向的是一个数组(传出参数)
  • size: 数组的大小

返回值:
返回的指向的一块内存,这个数据就是第一个参数

示例:

#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>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 <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);

作用:创建一个目录
参数:

  • pathname: 创建的目录的路径
  • mode: 权限,八进制的数

返回值:
成功返回0, 失败返回-1

示例:

#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>int main() {int ret = mkdir("aaa", 0777);if(ret == -1) {perror("mkdir");return -1;}return 0;
}

rename()函数 —(重命名)

    #include <stdio.h>int rename(const char *oldpath, const char *newpath);

示例:

#include <stdio.h>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 <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);

参数:
name: 需要打开的目录的名称
返回值:
DIR * 类型,理解为目录流;错误返回NULL

读取目录中的数据:

#include <dirent.h>
struct dirent *readdir(DIR *dirp);

参数:dirp是opendir返回的结果
返回值:
struct dirent,代表读取到的文件的信息
读取到了末尾或者失败了,返回NULL

关闭目录:

#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);

补充:dirent 结构体和 d_type

struct dirent
{// 此目录进入点的inode
ino_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 <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>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 <unistd.h>
int dup(int oldfd);

作用:复制一个新的文件描述符
示例:fd=3; int fd1 = dup(fd); //fd指向的是a.txt, fd1也是指向a.txt

从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符

示例:

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>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 <unistd.h>
int dup2(int oldfd, int newfd);

作用:重定向文件描述符
oldfd 指向 a.txt, newfd 指向 b.txt
调用函数成功后:newfd 和 b.txt 做close, newfd 指向了 a.txt
oldfd 必须是一个有效的文件描述符
oldfd和newfd值相同,相当于什么都没有做

示例:

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>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.txtchar * 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 <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

参数:

  • 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 <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>int main() {// 1.复制文件描述符// int fd = open("1.txt", O_RDONLY);// int ret = fcntl(fd, F_DUPFD);// 2.修改或者获取文件状态flagint fd = open("1.txt", O_RDWR);if(fd == -1) {perror("open");return -1;}// 获取文件描述符状态flagint 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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <string.h>// 模拟实现 ls -l 指令
// -rw-rw-r-- 1 nowcoder nowcoder 12 12月  3 15:48 a.txt
int 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 进程的挂起态和七状态模型

在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态
在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态

  1. 运行态:进程占有处理器正在运行;
  2. 就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列
  3. 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成;
  4. 新建态:进程刚被创建时的状态,尚未进入就绪队列
  5. 终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。

注意:
阻塞态无法直接到运行态,必须先转换成就绪态;
运行态–>阻塞态:是主动行为;
阻塞态–>就绪态:是被动行为;

三状态模型:

五状态模型:

七状态模型:
暂时调到外存等待的进程状态称为挂起态,suspend;

5.进程相关指令(Linux学习笔记)

Linux学习笔记4—第9、10、11章中的 10.2 显示系统执行的进程-ps10.3 终止进程—kill和killall10.6 监控 1. 动态监控进程—top

查看进程 ps aux / ps ajx

ps aux
ps ajx
a:显示终端上的所有进程,包括其他用户的进程
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 进程ID
kill -9 进程ID
killall 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 <sys/types.h>
#include <unistd.h>pid_t fork(void);

函数的作用:用于创建子进程。
返回值:
fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。
在父进程中返回创建的子进程的ID
在子进程中返回0
如果返回-1,表示创建子进程失败

失败的两个主要原因:
①当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN
系统内存不足,这时 errno 的值被设置为 ENOMEM

如何区分父进程和子进程?
通过fork的返回值
如果返回值为0,则表示子进程;
如果返回值大于0,表示父进程;
在父进程中返回-1,表示创建子进程失败,并且设置errno。

父子进程之间的关系:
区别:

  1. fork()函数的返回值不同
    父进程中: >0 返回的子进程的ID
    子进程中: =0
  2. pcb中的一些数据
    当前的进程的id pid
    当前的进程的父进程的id ppid
    信号集

共同点:
某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作

  • 用户区的数据
  • 文件描述符表

特点:读时共享,写时拷贝 (copy- on-write)☆☆☆

父子进程对变量是不是共享的?

  • 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了
  • 读时共享(子进程被创建,两个进程没有做任何的写的操作);写时拷贝。

实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现的。

写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。

注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。

示例:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>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 : 2043827
child num : 10
child num += 100 : 110
i : 0 , pid : 2043839
i am parent process, pid : 2043827, ppid : 2043804
parent num : 10
parent num += 10 : 20
i : 0 , pid : 2043827
i : 1 , pid : 2043839
i : 1 , pid : 2043827
i : 2 , pid : 2043839
i : 2 , pid : 2043827

8.exec 函数族(在进程内部执行一个可执行文件,用它来取代进程原本要执行的内容)

exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件

exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。
只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行

int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
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 <unistd.h>
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 <stdio.h>
int main() {    printf("hello, world\n");return 0;
}

execl.c

#include <unistd.h>
#include <stdio.h>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 <unistd.h>
int execlp(const char *file, const char *arg, ... );

execlp函数会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。

参数:

  • file:需要执行的可执行文件的文件名
    例如:
    a.out
    ps

  • arg:是执行可执行文件所需要的参数列表
    第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
    从第二个参数开始往后,就是程序执行所需要的的参数列表。
    参数最后需要以NULL结束(哨兵)

返回值:

  • 只有当调用失败,才会有返回值,返回-1,并且设置errno
  • 如果调用成功,没有返回值。

示例:
execlp函数会到环境变量中查找指定的可执行文件ps,找到了就将子进程要执行的进行替换,找不到就执行子进程原本的内容。

execlp.c

#include <unistd.h>
#include <stdio.h>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 <stdlib.h>
void exit(int status);//标准C库函数#include <unistd.h>
void _exit(int status);//Linux的系统调用

其中status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。

如果调用标准C库函数exit(),会刷新I/O缓冲(回车符是行缓冲的标志)。

示例1:Linux系统调用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main() {printf("hello\n");//回车符是行缓冲的标志printf("world");//这个没有回车符// exit(0);//标准C库函数(会刷新I/O缓冲)_exit(0);//Linux系统调用return 0;
}

结果:

示例2:标准C库函数(会刷新I/O缓冲)—推荐

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>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 <sys/types.h>
#include <unistd.h>
#include <stdio.h>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 <sys/types.h>
#include <unistd.h>
#include <stdio.h>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 <sys/types.h>
#include <sys/wait.h>
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 <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>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 <sys/types.h>
#include <sys/wait.h>
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 <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>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.进程间通信☆☆☆

见Linux高并发服务器开发—笔记2

Linux高并发服务器开发---笔记1(环境搭建、系统编程、多进程)相关推荐

  1. Linux高并发服务器开发---笔记2(多进程)

    0630 第4章 项目制作与技能提升 4.0 视频课链接 4.1 项目介绍与环境搭建 4.2 Linux系统编程1.4.3 Linux系统编程2 4.4 多进程 1-9 10.进程间通信☆☆☆ 进程间 ...

  2. Linux高并发服务器开发---笔记4(网络编程)

    0705 第4章 项目制作与技能提升 4.0 视频课链接 4.1 项目介绍与环境搭建 4.2 Linux系统编程1.4.3 Linux系统编程2 4.4 多进程 1-9 10.进程间通信☆☆☆ 4.5 ...

  3. Linux 高并发服务器开发

    该文章是通过观看牛客网的视频整理所得,以及在实践过程中遇到的问题及解决方案的整理总结. Linux 高并发服务器开发 linux 系统编程 linux 环境的搭建 环境搭建需要的软件 虚拟机中安装 u ...

  4. Linux高并发服务器开发—项目实战

    1.阻塞/非阻塞.同步/异步(网络IO) 典型的一次IO的两个阶段是什么?数据就绪 和 数据读写 数据就绪:根据系统IO操作的就绪状态 阻塞--非阻塞 数据读写:根据应用程序和内核的交互方式 同步-- ...

  5. Linux中的进程控制:进程退出、孤儿进程、僵尸进程 概念及代码示例 [Linux高并发服务器开发]

    目录 一.进程退出 二.孤儿进程 三.僵尸进程 一.进程退出 #include <stdlib.h> void  exit ( int status ); #include <uni ...

  6. [Linux 高并发服务器]网络基础

    [Linux 高并发服务器]网络基础 文章概述 该博客为牛客网C++项目课:Linux高并发服务器 个人笔记 作者信息 NEFU 2020级 zsl ID:fishingrod/鱼竿钓鱼干 Email ...

  7. Linux 高并发服务器实战 - 2 Linux多进程开发

    Linux 高并发服务器实战 - 2 Linux多进程开发 进程概述 概念1: 概念2: 微观而言,单CPU任意时刻只能运行一个程序 并发:两个队列交替使用一台咖啡机 并行:两个队列同时使用两台咖啡机 ...

  8. [Linux 高并发服务器]GDB调试

    [Linux 高并发服务器]GDB调试 [Linux 高并发服务器]GDB调试 [Linux 高并发服务器]GDB调试 GDB是什么 预先准备 基本命令 例子 进入和退出gdb 获取帮助 查看文件代码 ...

  9. Linux 高并发服务器实战 - 1 Linux系统编程入门

    Linux 高并发服务器实战-1Linux系统编程入门 在本机和服务器端设置公共密钥(配置免密登录) 在本机cmd里输入 ssh-keygen -t rsa,生成本机的公密钥 在服务器端里也配置 ss ...

最新文章

  1. TCP/IP协议的SYN攻击
  2. WEB前端 从原生JavaScript到MVVM
  3. 扫雷游戏(洛谷P2670题题解,Java语言描述)
  4. oracle 层次查询判断叶子和根节点
  5. 前端校验rules写法:
  6. math.floor实现四舍五入
  7. Shell.FlyoutHeader
  8. 图像分类算法DenseNet论文解读
  9. IIC详解之AT24C08
  10. FS4066耐高压1到4节内置MOS的锂电池充电管理芯片
  11. SECS/GSM 测试工具
  12. VScode提交Git代码总是要输入账号和密码?
  13. WHOIS查询检索,域名信息查询工具软件
  14. C# Word脚注和交叉引用功能
  15. redis基数树rax源码分析(1)
  16. matlab wolfe准则,[原创]用“人话”解释不精确线搜索中的Armijo-Goldstein准则及Wolfe-Powell准则...
  17. php 去除中英文空格,php去除字符串首尾中英文空格程序
  18. 【转】BYR论坛-浅谈TD-SCDMA目前的主要问题
  19. 禁用计算机上的所有鼠标加速,cs go鼠标加速命令_基本计算机知识_IT /计算机_信息...
  20. java 计算工作日工具类

热门文章

  1. [Spring手撸专栏学习笔记]——把AOP动态代理,融入到Bean的生命周期
  2. 太戈编程 1072. 优惠促销 答案及讲解
  3. 【精品盘点】2020年最受欢迎的6个知识库整理软件!
  4. perp系列之三:perp版本变化和作者联系方式
  5. C语言中不同类型的运算和比较问题
  6. C语言之联合体通用变量类型之妙用
  7. 计算机表格按性别排列,excel表格数据男女分类-Excel 按性别(男女)排序
  8. 多少人败给了一个字:等……
  9. win10资源保护无法启动修复服务器,为你设置win10系统“sfc /scannow”修复系统提示Windows资源保护无法启动修复服务的处理技巧...
  10. 5个一见钟情的手机APP软件 使用过后必须赞赞赞