文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-09-06 15:32

关注

0613

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

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并实现 免密登录

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

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

安装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-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 -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.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 = 3666,7
799,3
3850,0
3902,0
c = 300,t = 306775,499
6439,732
38040,0
38086,0
c = 800,t = 3786,12
851,0
4026,0
3771,0
c = 800,t = 307533,1195
7315,1574
34609,0
39094,0
c = 3000,t = 31227,0
776,68
2619,0
2068,0
c = 3000,t = 308354,807
8219,708
3390,1
2605,0
c = 5000,t = 3939,287
1025,290
1249,0
1297,0
c = 5000,t = 309146,419
7633,974
1640,1
1827,0
c = 8000,t = 3835,61
1016,443
1223,0
1482,0
c = 8000,t = 307830,1235
8460,1230
1453,0
1384,0
c = 10000,t = 3844,166
521,201
Resource temporarily unavailable
c = 10000,t = 307675,1926
5305,1513
c = 11000,t = 3fork failed.: Resource temporarily unavailable
c = 10100,t = 3fork 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*平均响应时间
在这里插入图片描述

主要看参考链接0qps多少才算高并发_一文搞懂高并发性能指标: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程序,gccg++都可以;
对于cpp程序,在编译阶段用gcc,链接阶段用g++(因为gcc不能自动和 C++ 程序使用的库进行链接),因此为了方便,对cpp程序的编译和链接过程,直接都用g++,所以就给人一种错觉,好像 cpp 程序只能用 g++ 。
在这里插入图片描述

误区2:gcc 不会定义 __cplusplus 宏,而 g++ 会

误区3:编译只能用 gcc,链接只能用 g++

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

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

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

变量 --> Makefile3

在这里插入图片描述
在这里插入图片描述

模式匹配 --> Makefile4

在这里插入图片描述

函数 --> Makefile5

在这里插入图片描述
在这里插入图片描述

伪目标:

.PHONY:cleanclean: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.代码保密 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的时候出错了,提示找不到动态库文件,所以需要学习下动态库的工作原理。

静态库和动态库的区别:

动态库的工作原理:

在终端输入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

在这里插入图片描述
环境变量是个键值对,一个键可以对应多个值,用冒号隔开。

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

  1. 把绝对路径放到环境变量LD_LIBRARY_PATH中;
  2. 把路径放到/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.二者的区别

相同点:

不同点:

在这里插入图片描述

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

参数:

返回值:返回一个新的文件描述符,如果调用失败,返回-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);

参数:

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

参数:

返回值:

write()函数:

ssize_t write(int fd, const void *buf, size_t count);

参数:

返回值:

示例:

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

参数:

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

作用:

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

作用:获取一个文件相关的一些信息
参数:

返回值:

lstat()函数 :

int lstat(const char *pathname, struct stat *statbuf);

参数:

返回值:

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

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

返回值:成功返回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);

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

返回值:成功返回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);

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

返回值:成功返回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);

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

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

示例:

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

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

返回值:
成功返回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, ...  );

参数:

阻塞和非阻塞:描述的是函数调用的行为

示例:

#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.程序、进程、线程

简单来说:
程序就是代码,占用磁盘空间,不占用内存空间
进程是运行中的程序,占用内存空间

程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程:

进程正在运行的程序的实例,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。在引入线程之后,进程资源分配的基本单位线程处理机调度的基本单位

可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号(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 结构体定义。

其内部成员有很多,我们只需要掌握以下部分即可:

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 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。

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

  1. fork()函数的返回值不同
    父进程中: >0 返回的子进程的ID
    子进程中: =0
  2. 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后面的字母含义:

例如:

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, ...);

参数:

返回值:

示例:
原本是父子进程,另外还有一个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函数会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。

参数:

返回值:

示例:
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);

功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源
参数:

返回值:

调用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);

功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:

返回值:

示例:
创建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 options0表示阻塞;WNOHANG表示非阻塞
阻塞?阻塞可以设置是否阻塞
作用回收子进程回收指定子进程

退出信息相关宏函数

WIFEXITED(status)0,进程正常退出WEXITSTATUS(status) 如果上面的宏为真,获取进程退出的状态(exit的参数)WIFSIGNALED(status)0,进程异常终止WTERMSIG(status) 如果上面的宏为真,获取使进程终止的信号编号WIFSTOPPED(status)0,进程处于暂停状态WSTOPSIG(status) 如果上面的宏为真,获取使进程暂停的信号的编号WIFCONTINUED(status)0,进程暂停后已经继续运行

10.进程间通信☆☆☆

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

来源地址:https://blog.csdn.net/weixin_38665351/article/details/125254289

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-人工智能
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯