cthub技能树_web_RCE之命令注入

[toc]

命令注入

image-20200730205313234

进入题目,题目中给出了源码:

image-20200730205758156

我们随便输入一个ip看看输出结果:

image-20200730205859577

可以看到返回了执行ping -c 4 127.0.0.1的结果。同时发现这个网页使用的是get请求:

image-20200730210440289

那如果我们可以让服务器不执行ping命令,而执行我们需要的命令让服务器将结果回显,不就相当于我们有了一个webshell吗!这就是命令注入的意思。我们可以通过管道符来实现。

linux支持多种管道符:

管道符用法
;执行完前面语句再执行后面的。如ping 127.0.0.1; ls
|显示后面语句的执行结果。如ping 127.0.0.1 | ls
||前面语句出错时执行后面语句。如ping 127.0.0.1 || ls
&前面语句为假则执行后面语句。如ping 127.0.0.1 & ls
&&前面语句为假则报错,为真则执行后面语句。如ping 127.0.0.1 && ls

我们使用 |进行注入,可以看到返回了ls的结果:

image-20200730210948859

我们看看那个奇怪的php文件,发现没有回显,看看源码(这个地方坑了我很长时间,后面的题最终都需要查看源码),得到flag:

image-20200730211234543

过滤cat

image-20200730211439573

对于命令注入的题目,主要考察的就是各种过滤的绕过,这道题就是考察cat过滤的绕过。

首先查看flag文件名:

image-20200730221846743

读取文件可以使用more、head等命令。关于linux的文本读取命令,可以看看这篇文章:Linux读取文本常用命令。本题我使用more命令,得到flag

image-20200730222246812

由于后面的题目均考察不同内容的过滤,所以非重点的图片等就不再放上来了。

过滤空格

这次我们要绕过空格,绕过空格有很多种方法。我们可以使用我们可以使用${IFS}来表示空格,IFS是shell中的一个变量,关于IFS的资料,可以看这篇文章(强烈推荐看看):详细解析Shell中的IFS变量

我们在命令中就用IFS来替换空格:

image-20200730223159084

得到flag:

image-20200730223441299

过滤目录分隔符

目录分隔符/我们可以使用$HOME代替,HOME也是shell中的一个环境变量,表示当前用户的根目录,我们可以看看当前用户的HOME值是什么

image-20200731173205285

可以看到当前用户的根目录是/home/www-data,我们只需要/,所以我们可以用${HOME:0:1}来实现

image-20200731181313922

首先查看flag位置:

image-20200731172616367

image-20200731172934380

读取flag:

image-20200731181418868

image-20200731181428086

过滤运算符

这道题过滤了 | 和 &,我们可以使用 ; 进行注入:

image-20200731181801768

直接cat得到flag:

image-20200731181837629

综合过滤练习

首先看一下过滤的符号有哪些:

image-20200731181939756

过滤了 | & ; 空格 / cat flag ctfhub这些符号

我们可以使用%0a(换行符的url编码)来绕过运算符:

image-20200731182638296

字符串的绕过我们可以使用反斜杠 \,如flag变成fl\ag:

image-20200731182804769

读取flag:

image-20200731183029022

image-20200731183037672

至此,我们就完成了命令注入的技能树,可以看到命令注入主要考察的就是各种绕过姿势,本文主要针对题目来讲,这里再放一些命令注入绕过姿势的总结文章,供大家参考和学习:

命令注入绕过姿势

命令执行绕过小技巧

命令注入绕过技巧总结

关于命令执行/注入 以及常见的一些绕过过滤的方法

ctfhub技能树_web_RCE之eval、文件包含

[toc]

这次我们来看看RCE(远程代码/命令执行)吧。由于篇幅限制,这篇文章不包含命令注入,命令注入我会在另一篇文章中详细的记录。

eval执行

image-20200730155557512

打开题目,可以看到网页的源代码

image-20200728222754175

通过代码可以看到,这就是典型的web后门,配置中国蚁剑

image-20200730153921884

进入后台,获取flag

image-20200730153821908

文件包含

image-20200728223933664

看看代码:

image-20200728223945677

strpos(string,find,start) 函数查找字符串在另一字符串中第一次出现的位置

参数描述
string必需。规定要搜索的字符串。
find必需。规定要查找的字符串。
start可选。规定在何处开始搜索。
返回值:返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE。
注释:字符串位置从 0 开始,不是从 1 开始。

可以看到如果get传入参数file的值开头为xflag(x为任意字符),则执行include()函数。

include (或 require)语句会获取指定文件中存在的所有文本/代码/标记,并复制到使用 include 语句的文件中。

服务器后台有一句话木马shell.txt,因此我们的目标就是让服务器执行shell.txt中的语句。所以file的值为shell.txt

看看shell:

image-20200728224421429

则shell要传的参数是ctfhub:

image-20200730154818635

查找flag:

find / -name flag*

image-20200730154859368

得到flag:

image-20200730154918100

php://input

image-20200728230112933

首先看代码:

image-20200728230147871

可以看到如果get的参数file前六个字符为”php://“则执行include函数

看看phpinfo

image-20200728230338863

可以看到allow_url_include是On,说明可以使用php://input伪协议。

php://input 是个可以访问请求的原始数据的只读流。其实说白了它就是用来获取post内容的,但是其实只要你把内容写到请求包中,post和get都能获取。

那我们就通过这个伪协议和精心构造的请求包来获取我们想要的信息(注意此时虽然我是get请求而不是post,但由于我的包中有内容,所以伪协议依然是接收到了):

image-20200728231301739

这样我们就可以来找我们要的flag了

image-20200728231554765

有了flag的路径,就可以得到flag了。

image-20200728231744426

远程包含

image-20200728231924149

看代码:

image-20200728232001224

这个题的解法和上一题php://input一模一样,不再赘述。

image-20200728232316912

读取源代码

image-20200728232832725

代码审计:

image-20200728232902587

首先尝试php://input,发现没有返回结果

image-20200728233102208

测试了好多遍都无果,那看来是没法用input了。再看题目,题目告诉我们了flag的路径,于是我们可以用另一条伪指令php://filter来进行读取

php://filter是一种元封装器, 设计用于数据流打开时的筛选过滤应用。简单理解就是个可以读取数据的过滤器。我们可以用它选择想要进行操作并读取的内容。

php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。

名称描述
resource=<要过滤的数据流>这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表>任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。

关于php://filter更多的妙用可以看这个大佬的文章

知道了php://filter的用法,我们就可以读取flag了。

image-20200728232928462

关于伪协议部分,这里是官方的文档

至此,我们初步了解了文件包含和eval执行,解锁了对应的技能树,下一步,我们就来看看命令注入吧。

ctfhub技能树_web_web前置技能之http协议

[toc]

[begin]这[/begin]篇文章是ctfhub write up系列的第一篇,ctfhub这个平台在知名的ctf练习平台中算是相对比较基础的,我觉得它很好的一点是它的技能树由浅入深(只是除了web其他几乎都没几道题啊……),非常适合刚入门的ctfer。这个系列呢就来刷一下ctfhub技能树,wp也会按照技能树的节点来整理。当然,篇幅限制,这个系列不会详细的解释题目背后的基础原理。好了,废话不多说,让我们首先从web开始吧!

初见web,当然要了解web中常用的前置技能中最基础之HTTP协议。

请求方式

image-20200728214356549

打开题目,题目描述如下:

image-20200728214449865

让我们用CTF**B,这一猜就是CTFHUB嘛。(我不会告诉你我试了很多遍CTF**B)

burp抓包把报头的GET改成CTFHUB再发送,得到flag:

image-20200728214927838

302跳转

image-20200728215000780

打开题目

image-20200728215119561

点一下give me flag还是这个界面,题目提示我们302跳转,那点击按钮的时候用burp抓包看看:

image-20200728215219830

可以看到有一个“一闪而过”的302跳转界面,而这里就有我们要的flag

Cookie

image-20200728215341660

进入题目:

image-20200728215415635

所以我们要伪造admin身份,burp抓包看看:

image-20200728215513014

cookie的值本来是admin=0,我们改成1再请求下就可以得到flag

基础认证

image-20200728215735900

进入题目,点击按钮,发现让我们输入用户名和密码,注意验证中的“Do u know admin ?”

image-20200728215702867

题目中还有个附件,我们下下来看看,发现是个字典

image-20200728215909065

那看来让我们爆破了,题目中有“Do u know admin ?”,那我们就猜用户名是admin,用burp开始爆破吧,首先burp抓包看看(用户名随便填的wwww,密码随便填的ww):

image-20200728220401932

发现有base64加密,解密后发现是wwww:ww,那我们就知道了请求的格式了:“用户名:密码”。

准备爆破,注意设置规则,先添加前缀admin:,再进行base64加密得到payload,设置完成后用题目给的字典开始爆破。

image-20200728220822212

爆破结果:

image-20200728221049282

可以看到有一次的结果和其他结果返回的数据包长度不一样,打开响应包,得到flag

响应包源代码

image-20200728221211252

打开是个贪吃蛇游戏,不过我们不管他,看看网页的源码:

直接得到flag

至此,http协议的技能树我们就全解锁啦,继续向青草更青处漫溯吧!

攻防世界web新手训练

攻防世界web新手

[toc]

view_source

F12看源码

image-20200717105543297

get_post

进入链接后提示如下:

image-20200717105042516

构造url

image-20200717105235135

提交后提示如下:

image-20200717105348157

再次构造url

image-20200717105410671

提交得到flag

image-20200717105433487

robots

进入链接后什么都没有

看看robots.txt

image-20200717105816695

可以看到disallow的位置就是我们要找的flag

image-20200717105903647

backup

image-20200717110737642

看到index.php,我们看看

image-20200717110832971

发现没有什么变化,那么根据提示,我们看看备份文件,一般备份文件的后缀是.bak

image-20200717110927693

当我们输入index.php.bak时可以下载该文件,下载后打开,得到flag

image-20200717111007209

cookie

image-20200717111232613

抓包看看

image-20200717111309876

那我们就看看cookie.php

image-20200717111344601

提示我们看看响应包,得到flag

image-20200717111438468

disabled_button

image-20200717113546553

看看这个按钮

image-20200717113711469

可以看到这个按钮的作用实际上就是post传递一个auth=flag,那么我们自己传这样一个参数,得到flag

image-20200717113819628

weak_auth

进入后是一个登陆界面,随便输一个用户名密码

image-20200717114325671

说明用户是admin

image-20200717114352984

可以看到这个题就是暴力破解,那么放到burp suite里跑个字典

image-20200717114500113

可以看到密码就是123456

image-20200717114523421

得到flag

simple_php

image-20200717114651376

看php代码说明我们要构造a和b,让a和b的值都满足条件,就可以获得flag

那么首先看a,要a=0为真,a为真,所以a不可能等于0,要想解决这个问题,首先我们要知道php的特性。

由于php为弱类型语言,因此当不同类型的值进行==比较的时候会发生类型转换。正常情况下不同类型的值是不能比较的,php 为了比较进行了数据类型转换。把不同类型的值转换为相同类型后再比较。

具体转换规则可以看这篇文章《彻底解决php判断a==0为真引发的问题-类型转换》,这里不再赘述。

知道了转换规则,那么我们就可以构造了,得到flag

image-20200717115934924

xff_referer

进入后提示如下:

那么我们就在请求包中通过x-forwarded-for伪造ip

image-20200717162612014

注意xff加入的位置,我之前加入到末尾发现页面一直在加载没有响应,我想xff写在请求头中间就好了。

然后发现提示变化:

image-20200717161539728

那么在伪造referer

image-20200717162711288

得到flag

image-20200717162734141

webshell

image-20200717170244887

php的一句话木马,使用中国菜刀连接,中国菜刀可以从这个地方下载,

image-20200717170319974

即可进入服务器

image-20200717170345624

获得flag

image-20200717170404876

当然,如果没有工具,那就手动构造请求post包

一句话木马中的变量是shell,所以我们传的变量就是shell

image-20200717170710704

可以看到返回了当前路径的所有文件,看到了flag.txt

image-20200717170754480

得到flag

command_execution

image-20200717172147730

尝试ping一下本机127.0.0.1

image-20200717172220593

可以看到ping的结果返回到了页面,那么尝试命令注入

命令注入有很多种方法,可以参考这两篇文章:文章1文章2,这里不再赘述

这道题比较简单,没有waf,不需要绕过,我使用 | 进行注入

command 1 | command 2 只执行command2

image-20200717172637290

可以看到执行了ls命令,现在找flag

image-20200717172921373

得到flag

image-20200717172957802

simple_js

image-20200717181134028

随便输一个

image-20200717181148627

看看源码,调试一下js

image-20200717181218694

发现不论输入的密码是什么,最后都会跳到假密码FAUX PASSWORD HAHA

而真密码是初始的pass_enc

image-20200717181342602

那么就把pass的值改为pass_enc,执行一下,注意14行的tab2下标要改成10,因为默认的pass长度为18,而修改后的pass长度为11,所以要修改,否则最后执行的时候p加入的就不是pass的最后一个值了(这个地方坑了我好久)

image-20200717181443038

把获取到的字符串加上题目要求的flag格式即可得到flag

sqli-labs Less1-5题 Write Up

[toc]

Less-1

image-20200713154808415

输入一个id看看

image-20200713154939518

题目是单引号,因此id加一个单引号看看

image-20200713155040120

可以看到报错了。说明单引号被成功解析,那么我们就通过闭合单引号来进行注入。

首先通过order by判断字段:

image-20200713155238806

不断提高order by后面的数值

image-20200713155311592

可以看到order by到4的时候报错了,说明字段只有3个。

接下来通过union看看页面显示的内容对应的位置

image-20200713155813890

页面成功执行,但是返回的内容没有变化,说明页面只会返回第一条结果(id=1),因此我们可以把id变成系统中没有的(id=-1),让服务器返回select的结果

image-20200713155932512

可以看到页面返回了2和3,说明页面返回的是select的第二个和第三个地方,所以我们注入的地方就是2和3

接下来查询当前网站使用的数据库 database()、当前Mysql的版本 version()、当前Mysql的用户 user()

CONCAT_WS(separator,str1,str2,…),连接str1、str2等字符串,并在各字符串中以separator分隔

image-20200713160539637

接下来先查询服务器一共有多少个数据库

Mysql中默认存放一个information_schema的数据库,该库中,有三个重要的表,分别是schemata, tables和columns

schemata储存该用户创建的所有数据库的库名,库名字段为schema_name

tables储存该用户创建的所有数据库的库名和每个库中的表名,库名字段是table_schema,表名字段是table_name

columns储存该用户创建的所有数据库的库名、每个库的表名和每个库中每个表中的字段名,库名字段是table_schema,表名字段是table_name,字段名是column_name

image-20200713162304922

可以看到第一个表为information_schema,修改limit 0,1中0的值,遍历

image-20200713162401207

最终得到所有的库名。

然后查询数据库中的表名,以security为例

image-20200713163201802

遍历查询,就得到所有的表名。

然后获取表的字段名,以emails为例

image-20200713164353082

遍历可以得到emails的所有字段id,email_id

然后获取字段内容

image-20200713165034010

遍历

image-20200713165402331

可以看到一共有8条数据

image-20200713165624149

通过这样我们就可以把整个服务器的数据库down下来。

补充:

我们可以使用group_concat将信息输出到一行,这样更为方便。

image-20200713170544482

Less-2

image-20200713171603173

注入单引号,根据报错信息可以看出id在sql中是整型

image-20200713171622650

通过布尔条件测试判断能否注入

image-20200713171850699

image-20200713171924317

得出结论存在注入,使用union注入

image-20200713172017996

查询数据库名

image-20200713172117403

注入成功,后续爆库步骤省略。

Less-3

单引号测试,查看报错信息

image-20200713172910823

猜测sql语句形式为

SELECT * FROM * WHERE id=('$id') LIMIT 0,1

布尔测试

image-20200713173111249

image-20200713173120963

可以注入,注意构造的形式

image-20200713173536293

爆库

image-20200713173648836

Less-4

双引号测试

image-20200713174125565

猜测sql语句

SELECT * FROM * WHERE id=("$id") LIMIT 0,1

布尔测试

image-20200713174333657

union注入:

image-20200713174422863

爆库

image-20200713174656504

Less-5

单引号测试

image-20200713174921832

布尔测试

image-20200713174941031

image-20200713174949880

可以看到存在注入,但是当布尔值为真时页面没有回显,因此考虑布尔注入

首先判断该数据表的字段

image-20200713175252435

image-20200713175301836

可以看到一共有三个字段。

判断数据库名字的长度

使用length()函数

可以看到长度大于等于1

image-20200713175615810

小于15

image-20200713175702619

多次测试后发现数据库名长度为8

再按位测试数据库每一位的值

使用substr(str,a,b)函数,a是从第几个字符开始(注意:字符从1开始,不是从0开始),b是每次返回几个字符

image-20200713180012852

当测试到s时返回真,则数据库名的第一个字符为s

image-20200713180044267

通过这种方式可以进行爆库。

xsser.me平台搭建

注意:xsser.me平台不支持PHP 7, 请使用PHP 5环境搭建

首先下载源码,我是在ms08067平台下载的,链接:Xss测试平台(解压密码:ms08067.com)

下载之后得到XSS文件夹,放在/var/www/html下,文件目录如下:/var/www/html/XSS

image-20200713140254469

然后用建立一个新的数据库xssplatform,我用的phpmyadmin

然后修改config.php里面数据库连接字段:数据库地址、数据库的用户、密码、数据库(xssplatform)

初次安装将注册配置中的模式从invite改为normal

url配置中localhost修改为自己的域名,XSS改为自己的文件夹名,我的就是默认解压出来的XSS,所以就是http://localhost/XSS。

image-20200713140721146

再修改authtest.php中的域名位置:

在XSS文件夹中有个xssplatform.sql,在phpmyadmin中创建好数据库后导入库。

执行之后有9张表:

image-20200713141845337

进入数据库后执行sql语句把域名修改为自己的域名(http://localhost/XSS):

UPDATE oc_module SET
code=REPLACE(code, 'http://xsser.me', 'http://localhost/XSS')

然后在XSS文件夹内建立.htaccess伪静态文件,注意文件中的XSS要换成自己的文件夹名

我使用的是Apache:

RewriteEngine On
RewriteRule ^([0-9a-zA-Z]{6})/XSS/index.php?do=code&urlKey=1 [L]
RewriteRule ^do/auth/(\w+?)(/domain/([\w\.]+?))?/XSS/index.php?do=do&auth=1&domain=3 [L]
RewriteRule ^register/(.*?) /XSS/index.php?do=register&key=1 [L]
RewriteRule ^register-validate/(.*?) /XSS/index.php?do=register&act=validate&key=1 [L]
RewriteRule ^login /XSS/index.php?do=login [L]

然后进去注册,注册完了将config.php中的模式再从normal改成invite。

PDF如何添加目录

[toc]

[begin]不[/begin]知道大伙有没有遇到这样的问题:在网上找的很多PDF都没有目录,尤其是一些扫描版pdf,或者是有目录但是页码错误等等。这样非常不方便我们查阅和学习,为了解决这些问题,我们可以自己手动给PDF添加目录,添加的过程很简单,下面就教大家如何添加目录。

修改目录我们需要FreePic2Pdf(下载链接在后文)和一个文本编辑器(这里我使用的是sublime text)

查找目录

以《Orange’s 一个操作系统的实现》为例。这本书是扫描版的没有目录,如图:

image-20200131142740876

那么我们开始制作书签。首先我们要获得书签的文本。由于是扫描版,目录文本无法选中,因此我们在百度上搜索这本书的目录,一般这本书的百度百科都有目录。

image-20200131143106649

但是这个目录没有页码,所以我们再找其他的(当然你也可以手动输入目录)

找到一个合适的:

image-20200131143421497

将目录复制粘贴到文本编辑器中(这里我用的是sublime),我们需要修改文本的格式,PDF书签需要特定格式才能正确添加到PDF中。

修改目录

修改页码

首先需要注意的一点是页码。这本书的页码是从正文开始算的,也就是说前面序言目录等等都不算在页码的范围内,但是添加的目录中的页码需要算上序言,也就是说在这本书中第一章不是从第2页开始的,而是从22页开始的,所以页码需要进行修改。

修改页码有两种方式,一种是批量修改每个条目的页码,是比较麻烦的。另一种就需要用到FreePic2Pdf这个软件了。这里先介绍第一种。

image-20200131144118440

批量修改页码,我们使用excel进行修改。直接复制粘贴无法将目录正确的复制进表格中,文本只会出现在第一列,而我们的想法是让页码单独一页,这样我们就能批量修改了,那么我们首先修改目录文本的格式使之适配excel。

首先看我的原本的文本是这样的

image-20200131144817437

可以看到页码和标题间隔着一个空格(sublime中一个空格显示为一个 · )那么就可以通过一个可区分和定位的特征(数字前有一个空格)对这个位置的空格进行批量操作,将这一个空格替换成英文逗号。

ctrl+h进行替换。这里为了准确定位需要使用正则表达式进行替换(只要掌握基础的正则表达式语法即可,可以看看这篇教程)。\d代表一个数字,+表示匹配前面的字表达是一次或多次(也就是说\d可以是一个或多个)。我们要修改的是数字前面的一个空格,而数字要保留,所以我们输入空格+(\d+)()中括到的是我们要保存的,如图:

image-20200131152054846

可以看到sublime自动识别出了定位的范围,最好这时候再检查一下,看看有没有不是页码的地方也符合定位条件(数字前面一个空格),有时候标题也会出现匹配到的情况,这里检查之后都正常,只有页码匹配到了,非常好,接下来就将空格修改成逗号即可。表示取出第一个括号匹配的内容(就是第二个括号,这里就这一个括号)

所以替换的语句的意思就是替换成,+\d+

全部替换后如图:

image-20200131152727154

可以看到已经全部替换完成了。

保存成.csv文件,然后新建一个excel的空白文件,打开空白文件,点击数据->自文本

image-20200131160138386

然后选中csv文件,点击下一步

image-20200131160241926

分隔符号选逗号然后点下一步->完成-> 确认。

image-20200131160323494

这样就得到了待修改的目录。

image-20200131160422795

批量修改页码,不会改的看这里

然后复制粘贴到sublime。这样我们的页码就修改好了。同时在粘贴到sublime之后我们会发现页码前已经变成了制表符,节省了我们再批量修改成制表符的麻烦(sublime中制表符是以横杠:image-20200131162804210的形式表示)

修改章节分级

页码修改好后,我们还要修改章节的分级,例如1.1在第一章下,1.1.1在1.1下,如果你认为目录不用折叠也无所谓,那你可以不做修改,但是,如果你想要目录可以分级折叠,例如下图,那么我们还需要修改目录的格式。

image-20200131163225180

修改的方式是添加缩进,一级标题不添加缩进,二级标题加一个制表符,三级缩进加两个制表符,以此推类。

修改还是通过正则表达式进行修改。当然手改也可以。

本书中,上下篇是一级目录,所以不用修改,第一章第二章是二级标题,所以在开头加一个缩进。\t表示一个制表符。

image-20200131163748622

1.1是三级标题,所以有两个制表符,^表示字符串的开始,也就是说要匹配的是开头一个或多个数字,然后一个点然后接着一个或多个数字然后一个空格,也就是我们要找的三级标题。注意最后有个空格来限定是1.1而不是1.1.1。

image-20200131164158103

2.1.1是四级标题也就是三个制表符,不赘述了,如图:

image-20200131164600065

最终得到的目录如图:

image-20200131164635505

这样一个符合格式的目录就做好了。接下来就是添加目录了。

添加目录

添加目录需要用到FreePic2Pdf(链接:https://pan.baidu.com/s/1GCMIlwLSL1Z4eI64XrZeTQ 提取码:146l )

软件界面如图

image-20200131165159064

点击右下角更改PDF,选择从PDF取书签,把pdf拖进去点击开始,然后在你pdf的目录下会生成一个同名的文件夹,打开,有这两个文件。

image-20200131165420390

这个txt就是你的目录了,由于开始时没有目录,所以这是个空文件,将我们刚才修改好的目录复制进去并保存。然后再次使用FreePic2Pdf,选择往PDF挂书签,它默认就是刚才的文件,所以直接点击开始即可

image-20200131165725870

编辑完成即成功。打开PDF文件看看,成功挂上了书签!

image-20200131165831626


使用FreePic2Pdf解决页码偏移问题

这个方法在当时我写这篇博客的时候还不知道,现在补上。

FreePic2Pdf生成的文件夹中,除了有txt的目录以外,还有一个itf的配置文件。

使用sublime打开如下:

basepage表示目录中的第一页代表实际页面的数值,例如这里为4就说明目录的第一页对应pdf的第4页。

通过修改这个basepage就可以很方便的解决页码偏移问题。

Win10(家庭版)修改中文用户名为英文

[toc]

[begin]今[/begin]天用pip安装包的时候,pip提示我编码错误,上网查了之后发现是我的中文用户名导致的{{tuxue}}嗨呀那叫一个气啊,网上的方法试了一下没有解决,加上我之前也遇到过因为这个中文用户名而出现的莫名其妙的问题。所以干脆就一次性解决,直接把我的用户名改成英文的。网上有很多教程,但是我还是遇到一些奇奇怪怪的问题,所以把自己的修改过程记录下来,如果大家遇到和我一样的问题就可以参考参考。

注意:由于要修改注册表,加上很多人都是一个账户,因此修改用户名有一定风险,请谨慎修改。

修改步骤

0x01 创建Administrator账户

我们当前的账户是没有权限修改用户名的,如图所示,可以看到名字那里是灰色的:

image-20191119185124086

因此我们需要使用Administrator账户修改用户名。首先我们进入命令提示符(CMD),注意一定要以管理员身份运行

image-20191119183455906

然后输入如下命令:

net user administrator /active:yes

这样我们就创建了Administrator账户。你可以在windows菜单中点击自己的账户图标就可以看到多了一个administrator账户,如下图所示:

image-20191119183833169

然后重新启动电脑,使用Administrator账户登录,这时候会初始化,可能需要几分钟,耐心等待就好。

0x02 修改用户名

进入账户之后就可以修改用户名了,改成你想改的名字。然后按win+r,输入regedit然后回车打开注册表编辑器,找到 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Profilelist,在该目录下的子目录中一个一个找,找到你要修改的那个用户,然后双击ProfileImagePath修改数据为你刚才修改的用户名,保存并退出。如图:

image-20191119185424746

修改完成后再按win+r,输入control userpasswords2:

image-20191119190219469

打开用户账户:

image-20191119190255465

点击你要修改的用户,修改用户名为你要修改的用户名,修改完成后保存关闭窗口。到此我们的用户名就已经修改完毕了,但是很多程序还不能正常运行,这是因为我们还没有修改环境变量。

0x03 修改环境变量

打开控制面板(不知道怎么打开就直接win+s然后搜索控制面板)

控制面板->系统->高级系统设置->环境变量,修改环境变量,把有旧用户名的值都改成新用户名即可。

重新启动电脑,修改完毕。

0x04 关闭Administrator账户

Administrator账户可以关闭也可以保留,如果要关闭那么我们再次打开管理员CMD,然后把上面的yes改成no运行即可。

避免采坑

0x01 输入命令提示错误

输入命令时提示“发生系统错误 5。拒绝访问。”这就是因为CMD没有足够的权限执行命令,需要以管理员身份运行CMD。

0x02 在另一程序中打开,请重试

登陆之后,修改用户名,系统提示文件夹或文件已经在另一程序中打开,请重试。这个问题我在注销原账户登陆administrator账户时遇到了,我的解决方法是直接重启,重启后直接登陆administrator账户,不登录原账户,这样就解决了问题。

0x03 右键崩溃

修改完后发现我在文件资源管理器中点击右键文件资源管理器就崩溃了,这时候使用火绒的右键菜单管理(其他杀毒软件应该也有)逐个排查之后发现是格式工厂的问题,关闭这个右键菜单项就好了。

0x04 环境变量修改后又恢复原值

我修改完环境变量之后发现我关闭再次打开环境变量,环境变量又恢复原值。后来经过上网查找发现是我没有修改用户账户的名字(就是通过control userpasswords2修改的那个),修改之后就好了。

OS实验之玩转linux内核(持续更新)

[toc]

[begin]虽[/begin]然用过linux系统,但是内核却一直没接触过。正好这学期的操作系统有实验课,要玩内核,就通过这门课好好学习一下内核相关的知识。在这里记录一下自己的linux内核学习过程和遇到的各种坑。如果写的有什么问题欢迎各位大佬指正。我会把源代码放到GitHub上,有些代码(例如内存分配)比较长,因此可以直接从GitHub上下载下来,便于阅读。如果这篇博客帮助到你,可以在文章末尾点一个喜欢或者分享给他人,也可以给我的项目点一个star哦~

[github repo=”winny1001/Operating-System-Experiment”]

linux内核的编译初体验~

编译实验第一个题目是要添加一个系统调用。但是不管什么题目,都得重新编译内核,所以首先我得学习如何编译linux内核。

实验环境准备

我用的环境是VM Ware + Ubuntu Server18.04.3 LTS(没有图形化界面,操作方便且占用内存较小)。给的配置为4GB内存,50GB硬盘,4核处理器。

声明:我所有的命令没有特殊说明都是在root用户下执行的,因此没有sudo,如果你在非root用户下执行可能需要sudo

首先查看当前使用的linux版本:

uname -r

1570889446612

可以看到我初始的版本是4.15.0-66,然后下载你想要的内核,在 https://mirrors.edge.kernel.org/pub/linux/kernel/ 上找到你想要的内核并下载,例如我下载的是linux-4.16.10.tar.xz:

wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.16.10.tar.xz

下载完成后解压到linux-4.16.10文件夹:

tar xvJf linux-4.16.10.tar.xz

解压完成后就是修改并编译linux啦。

编译linux内核

编译linux内核前首先我们要配置相应的环境。要求至少给虚拟机50G的磁盘空间(没有50G也尽量越多越好),不然编四五个小时最后提示你没有空间了要重新编译那直接原地裂开。

然后安装相应的依赖包和软件工具:

apt-get install libncurses5-dev libssl-dev zlibc minizip build-essential openssl libidn11-dev libidn11 
apt-get install git fakeroot ncurses-dev xz-utils bc flex libelf-dev bison

安装完之后cd linux-4.16.10进入文件夹内,如果你是第一次编译,那么使用:

make menuconfig

进行配置,这个命令打开一个配置工具,允许我们定制自己的内核。

1571221714061

如果不是第一次编译,那么首先要清除之前编译产生的中间文件。输入以下指令:

make mrproper

mrproper是清除编译过程中产生的所有文件、配置和备份文件(Remove all generated files + config + various backup files)或者你也可以用:

make clean

这是清除编译过程中产生的大多数文件,但会保留内核的配置文件,同时还有足够的编译支持来建立扩展模块( Remove most generated files but keep the config and enough build support to build external modules)。

由于是第一次并且是以学习编译为目的,因此我们使用默认配置就好。直接save->ok->exit->exit。

配置完了之后就可以开始编译了

直接输入make就可以开始编译。当然,你可以采用多线程编译,这样速度会快一点。命令是:

make -j8

-jx中的x是make并行编译的线程数,给多少随意,一般这个数值为内核数*2比较合适,比如说我给了虚拟机4核,那么我就-j8。

但是需要注意的是:如果你修改了内核,而且不知道对不对,那么最好不要多线程编译。因为如果一个线程出错了的话,编译不会停止,其他线程会继续编译,其他线程编译的内容就会把出错的信息刷掉,这样一来你就无法知道自己编译的是否正确以及出错的位置在哪(血的教训)。

由于是编译整个内核,因此编译时间很长,接下来就是耐心的等待了。

编译过程如图:

1571218157936

编译完成后开始安装之前启用的模块:

make modules_install

安装完成之后接着安装内核:

make install

安装完内核之后我们需要启用编译好的内核,打开/etc/default/grub

vim /etc/default/grub

i进行编辑,找到并注释掉GRUB_TIMEOUT_STYLE=hidden和GRUB_TIMEOUT=0,如图所示:

1570874109940

修改完成后先按esc退出编辑模式,再按:wq保存退出,然后更新配置并重新启动:

update-grub
reboot

重启后出现菜单:

1570874224027

直接进入就启用了我们编译好的内核了。如果想要切换内核,就选Advanced options for Ubuntu,这一栏就可以选择想要使用的内核了:

1571228835055

避免采坑

正常操作到这就完成了。但是这时候我遇到了问题,我在编译完成之后发现无法使用新内核进入系统,如图:

1570874893440

后来在网上查找发现造成这种错误的一个可能的原因是内存太小,我本来的配置是2GB(我寻思也不小啊),改成4GB之后就能正常启动了。

打开之后用uname -r看看内核版本:

1571229039863

是4.16.10,我们成功了!至此,我们第一个内核就算正是编译完成了,撒花!

下面我们就可以对新鲜出炉的内核动手动脚啦hiahiahia{{xieyanxiao}}。

设计一个系统调用!

光会编译内核,还不能算入门,只能算看到了门。真正想要“玩”内核,就必须要学会修改内核,让内核变得更加个性化。一开始我们举一个最简单的例子,也是编译实验原理课的第一个实验,设计一个系统调用!

啥叫系统调用

系统调用,听着很高大上,但千万不要被它的名字吓到,其实简单理解它就是一内核调用的函数,而不是用户调用的。

维基百科是这样定义的:系统调用(英语:system call),指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。

举个简单的例子,我去别人家做客,我想玩他家的乐低,但是乐高在人家卧室里,我没有权限进去,那咋办嘞?我就给主人说,“我想玩你的乐低,请帮我拿一下。”主人说没问题,起身进去帮我拿了出来,然后我开心的玩起了乐低。这时候,“我”就是用户,“主人”就是内核,而“玩乐低”是需要更高权限运行的服务,“帮我拿乐低”就是一个接口,由于这个动作是“主人”发出的(这个函数是内核调用的),那么这就是一个系统调用。

加入自己的Hello World系统调用

参考《操作系统作业:给linux系统增加一个系统调用》。

首先在系统调用表linux-4.16.10/arch/x86/entry/syscalls/syscall_64_tbl中定义自己的系统调用的调用号和系统调用函数的映射。

1571234059525

然后在头文件linux-4.16.10/arch/x86/include/asm/syscalls.h中声明自己的系统调用

1571234171381

然后在把具体调用的实现函数写在linux-4.16.10/kernel/sys.c里:

1571322664930

然后重新编译内核,不要忘了重启。编译好了之后我们写一个测试代码test.c来看看能否成功执行我们的系统调用:

//test.c

#include <stdio.h>

int main()
{
    long a = syscall(333);  //333是我们自己的系统调用的调用号
    printf("System call sys_helloworld return %ld\n",a);
    return 0;
}

gcc -o test test.c来编译,编译完成后用./test来运行:

1571235696127

可以看到输出了22,证明我们的系统调用成功的被执行了!

但是为什么没有“Hello World!!!”呢?是因为printk函数输出的信息具有日志级别,简单来说就是printk输出的内容输出到了内核日志里,我们可以通过dmesg命令查看内核日志:

1571239942149

可以看到最后一行就是我们输出的信息“Hello World!!!”,也同样证明我们的系统调用成功实现了!

那如果我们就想让它输出到终端可以吗?也是可以的,这就涉及到printk的知识点了。

内核通过 printk() 输出的信息具有日志级别,内核中共提供了八种不同的日志级别,在 linux/kernel.h 中有相应的宏对应。

#define KERN_EMERG   "<0>"   /* system is unusable */
#define KERN_ALERT   "<1>"   /* action must be taken immediately */
#define KERN_CRIT   "<2>"   /* critical conditions */
#define KERN_ERR   "<3>"   /* error conditions */
#define KERN_WARNING "<4>"   /* warning conditions */
#define KERN_NOTICE  "<5>"   /* normal but significant */
#define KERN_INFO   "<6>"   /* informational */
#define KERN_DEBUG  "<7>"   /* debug-level messages */

未指定日志级别的 printk() 采用的默认级别是 DEFAULT_MESSAGE_LOGLEVEL,这个宏在 kernel/printk.c 中被定义为整数 4,即对应KERN_WARNING。

当printk中指定的级别(级别越小优先级越高)小于当前控制台日志级别时,printk的信息就会在控制台上显示。

所以如果我们想把Hello World打印到控制台上,可以在输出的文本前加上KERN_ALERT(其他也可以只要优先级大于控制台日志级别):

printk(KERN_ALERT "Hello World!\n");

但是这种方法要求每次输出前都要加上,比较麻烦。还有一种一劳永逸的方法,输入以下命令:

echo 4 3 > /proc/sys/kernel/printk

如果你不是root用户,那么使用如下命令:

echo 4 3 | sudo dd of=/proc/sys/kernel/printk

/proc/sys/kernel/printk中会显示4个数值(可由 echo 修改),分别表示:

  • 当前控制台日志级别;
  • 未明确指定日志级别的默认消息日志级别;
  • 最小(最高)允许设置的控制台日志级别;
  • 引导时默认的日志级别;

上述命令就将默认消息日志级别改为了3,而控制台日志级别为4,这样就可以输出到屏幕了。

1571273096443

注意:修改用echo修改,不能用vim(但好像个别可以),否则会报Fsync failed错。因为vim编辑文件是首先创建该文件的一个副本,当保存的时候就用这个副本替换掉原文件。而proc文件系统下的文件并不是真的文件,都是内存中的影像,因此不支持这种编辑方式,所以不能用vim。

避免采坑

我的实验是基于4.16.10版本的。内核版本不同,系统调用的修改可能也略有不同。

缩进最好使用tab键(就是键盘q左边的那个),不要用空格,否则可能有不可预料的错误。

设计一个动态调用模块!

相信很多初学者和我一样,每次修改一下系统调用就要重新编译整个内核,这样做既耗费时间,效率也非常非常低。所以我们现在来一起学习一下动态模块,这样以后我们想加什么功能就直接以动态模块的形式加入到内核,修改之后只用编译我们这个模块就行了。

啥叫动态模块

维基百科是这样定义的:可加载内核模块(英语:Loadable kernel module,缩写为 LKM),又译为加载式核心模块、可装载模块、可加载内核模块,或直接称为内核模块,是一种目标文件(object file),在其中包含了能在操作系统内核空间运行的代码。它们运行在核心基底(base kernel),通常是用来支持新的硬件,新的文件系统,或是新增的系统调用(system calls)。当不需要时,它们也能从存储器中被卸载,清出可用的存储器空间。

由于 Linux属于单内核,单内核扩展性与维护性都很差(大家编译了这么多次内核应该已经深有体会{{xiaoku}}),所以就引入了这么个动态模块,这样一来就大大方便我们添加和修改自己想要的功能。一般动态模块主要是用来写驱动的。

加入自己的Hello Module动态模块

那么废话少说,现在就让我们来写一个动态模块吧。

首先mkdir mod_hello新建一个文件夹,然后在里面创建我们的动态模块源码mod_a.c:

//mod_a.c
//动态模块必须要的三个头文件
#include<linux/init.h>
#include<linux/kernel.h>
#include<linux/module.h>

int init_mymod(void)    //声明是一个模块以及加载时初始化的动作
{
    printk("Hello Module!\n");
    return 0;
}

void exit_mymod(void)   //卸载模块时的动作
{
    printk("Goodbye Module!\n:");
}

module_init(init_mymod); 
module_exit(exit_mymod);

//声明模块相关的信息,第一条声明模块的许可证是必要的,2.4.10之后的版本必须声明
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Test");
MODULE_AUTHOR("WPX");

现在我们就写好了一个模块。这个模块的功能就是在安装的时候输出“Hello Module!”,卸载的时候输出“Goodbye Module!”。下面我们开始写Makefile:

CONFIG_MODULE_SIG=n

ifneq ((KERNELRELEASE),)
        obj-m :=mod_a.o
else
        KERNELDIR := /lib/modules/(shell uname -r)/build
PWD := (shell pwd)
modules:(MAKE) -C (KERNELDIR) M=(PWD) modules
clean:
        (MAKE) -C(KERNELDIR) M=$(PWD) clean
endif

关于Makefile的语法这里就不细讲了,有兴趣的同学可以自己学习一下,不难。写好了之后就可以make编译啦:

1571292905251

生成mod_a.ko文件就说明成功啦。下面就是如何装载和卸载模块了

insmod mod_a.ko //装载模块

rmmod mod_a //卸载模块

装载模块之后我们可以通过lsmod查看系统已装载的模块:

1571293144531

可以看到我们的模块已经加载到了系统中。现在看看系统日志测试一下我们的语句有没有输出:

1571322903655

可以看到已经成功输出了,说明我们成功的完成了动态模块的加载!

如果你在加载模块的时候出现以下错误:

1571294764926

那是因为你之前已经装载过了该模块,所以先卸载这个模块再重新装载就可以了。

避免采坑

模块签名

我刚开始测试的时候,发现日志中报错,然后没有输出:

1571290497284

这个报错是因为在3.7版本后内核有了模块签名机制,如果模块没有签名在加载的时候就会报这个错。这个有两种解决办法,一种是给模块签名,一种是关掉签名检查机制。这里给出第二种。

关掉签名检查机制一种说法是在Makefile的开头加上CONFIG_MODULE_SIG=n,但是在我测试的时候发现好像没啥用。第二种方法是直接修改内核配置文件,在make menuconfig后修改linux-4.16.10/.config(这是一个隐藏文件):

1571324205319

将CONFIG_MODULE_SIG=y、CONFIG_MODULE_SIG_ALL=y和CONFIG_MODULE_SIG_SHA512=y的y都改成n,然后重新编译内核,经过测试这种方法可以解决问题。

但是经过测试我发现这个签名的报错好像也没什么影响,还是可以正常的输出“Hello Module!”……有点迷,不是很懂。

\n!\n!\n!

在我还没有解决上面报错的问题时,我又发现一个很奇怪的现象:

1571320179038

我加载内核的时候输出的是Goodbye我卸载内核的时候输出的是Hello。

我百思不得其解,查了大量的资料,都没找到答案。后来发现……

原因竟然是的因为我printk中没有换行符\n所以实际上输出了但是没有显示在日志里,所以看不见。等到我再加载的时候hello就把goodbye顶上来了所以我只能看见hello。{{tuxue}}{{tuxue}}{{tuxue}}

我就说为啥测试的时候第一遍加载没有输出,卸载输出hello,然后加载输出goodbye,卸载输出hello开始循环。我在这个\n上吃了不少苦头,在上面设计系统调用的时候我一开始也没有加\n,上面那个图加了换行符是我做到这才反应过来换上去的,实际上上面那个图中输出的“Hello World!!!”是我在截图之前测试了两次,把第一次的顶上来了,我当时以为是输出了,写的时候我还纳闷怎么突然又输出来了{{tuxue}}

这个换行符坑了我不知道多少时间,来来回回我因为测试结果有问题又重新编译了好几次内核{{outu}}

实验一——设计一个系统调用

知道了如何设计系统调用,我们就可以开始做第一个实验了。

实验内容

设计一个系统调用,功能是将系统的相关信息(CPU型号、操作系统的版本号、系统中的进程等类似于Windows的任务管理器的信息)以文本形式列表显示于屏幕,并编写用户程序予以验证。

前导知识

要输出CPU型号,操作系统的版本号和系统中的进程,首先得知道我们去哪里找这些信息。

CPU型号和操作系统我们可以在/proc文件夹下找到:

cat /proc/cpuinfo   #cpu信息

cat /proc/version   #系统版本信息

关于/proc文件夹,在“加入自己的Hello Module动态模块”的最后我提到了一下。实际上/proc是一个位于内存的伪文件系统,通过/proc我们可以运行时访问内核内部数据结构、改变内核设置的机制。用户和应用程序可以通过/proc得到系统的信息,并可以改变内核的某些参数。由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取/proc文件时,/proc文件系统是动态从系统内核读出所需信息并提交的。 注意/proc里面的文件不是真的存在于硬盘中的文件,它们只是内存中的映像。

系统中的进程相关信息记录在task_struct结构体中。task_struct是Linux内核的一种数据结构,它会被装载到RAM中并且包含着进程的信息。每个进程都把它的信息放在 task_struct这个数据结构体。具体使用在代码中就可以看到。

这两部分内容这里都只是简单提了一下,如果要是深入讲解那又可以写两篇文章了,秉着知识屏蔽的原则,这里不展开讲了。各位读者感兴趣可以自学一下。

实验思路

我们要设计一个系统调用,如果直接修改内核添加系统调用然后编译内核进行调试,那效率实在是太低(一次写成的大佬当我没说),因此我首先用动态模块把程序写好,然后再添加到系统调用里并重新编译,这样就大大提高了效率。

实际上,也可以用动态模块直接写一个钩子来修改系统调用(注意是修改不是添加,动态模块没法添加系统调用,但是可以修改现有的系统调用),但是由于要利用sys_call_table表,而在实验过程中发现4.16.10版本中获取sys_call_table表的地址存在许多问题,由于我目前水平太菜还没有解决,所以这种方法先暂时不用,以后应该会更新这个方法。

这个实验思路很直接,就是找到所需要的信息并打印出来。

代码实现

知道了信息的位置,那剩下的就是代码实现了。这里给出我的动态模块源代码:

#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include<linux/sched/signal.h>

static char buf[41];
static char buf1[1024];

int init_mymod(void)    //声明是一个模块以及加载时初始化的动作
{
    struct task_struct *p;
    struct file *fp;
    mm_segment_t fs;
    loff_t pos;

    printk(KERN_ALERT "Hello Module!\n");

    //准备打印CPU型号
    printk("/***************cpu info****************/\n");

    fp = filp_open("/proc/cpuinfo",O_RDONLY,0);//打开文件并存到结构体中,准备进行后续操作

    if(IS_ERR(fp)) //判断文件是否正常打开
    {
        printk(KERN_ALERT "create file error\n");
        return -1;
    }

    fs = get_fs();
    set_fs(KERNEL_DS);

    pos = 79;   //文件操作的起始位置
    kernel_read(fp,buf,sizeof(buf),&pos);
    printk(KERN_ALERT "%s\n",buf);

    filp_close(fp,NULL);
    set_fs(fs);

    //准备打印系统版本信息
    printk(KERN_ALERT "/***************system version****************/\n");

    fp = filp_open("/proc/version",O_RDONLY,0);//open file

    if(IS_ERR(fp)) 
    {
        printk(KERN_ALERT "create file error\n");
        return -1;
    }

    fs = get_fs();
    set_fs(KERNEL_DS);

    pos = 0;
    kernel_read(fp,buf1,sizeof(buf1),&pos);
    printk(KERN_ALERT "%s\n",buf1);

    filp_close(fp,NULL);
    set_fs(fs);

    //准备打印进程信息
    printk(KERN_ALERT "/*************processes information**************/\n");
    printk(KERN_ALERT "%-20s%-10s%-15s%-15s%-10s\n","name","pid","time(userM)","time(kernelM)","state");
    for_each_process(p)
    {
        printk(KERN_ALERT "%-20s%-10d%-15lld%-15lld%-5ld\n",p->comm,p->pid,(p->utime)/60,(p->stime)/60,p->state);
    }
    return 0;
}

void exit_mymod(void)   //卸载模块时的动作
{
    printk(KERN_ALERT "Goodbye Module!\n");
}

module_init(init_mymod);
module_exit(exit_mymod);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Test");
MODULE_AUTHOR("WPX");

需要注意的是第一个输出CPU型号的buf和pos需要自己设置成适合自己的,因为每个人的CPU型号不同,我设置测试的最终结果如图,正好把我的型号输出出来:

1571817038619

现在加载我们的模块看看效果(长图预警):

2019-10-23_172003

看上去还不错!那么现在我们就可以把它添加到系统调用里去了,添加方法上文已经说了。(修改sys.c的时候不要忘了添加头文件哦!)

最终添加的系统调用号为334,调用具体代码如下:

asmlinkage long sys_getinfo(void)
{
    static char buf[41];
    static char buf1[1024];
    struct task_struct *p;
    struct file *fp;
    mm_segment_t fs;
    loff_t pos;

    //准备打印CPU型号
    printk("/***************cpu info****************/\n");

    fp = filp_open("/proc/cpuinfo",O_RDONLY,0);//打开文件并存到结构体中,准备进行后续操作

    if(IS_ERR(fp)) //判断文件是否正常打开
    {
        printk(KERN_ALERT "create file error\n");
        return -1;
    }

    fs = get_fs();
    set_fs(KERNEL_DS);

    pos = 79;   //文件操作的起始位置
    kernel_read(fp,buf,sizeof(buf),&pos);
    printk(KERN_ALERT "%s\n",buf);

    filp_close(fp,NULL);
    set_fs(fs);

    //准备打印系统版本信息
    printk(KERN_ALERT "/***************system version****************/\n");

    fp = filp_open("/proc/version",O_RDONLY,0);//open file

    if(IS_ERR(fp)) 
    {
        printk(KERN_ALERT "create file error\n");
        return -1;
    }

    fs = get_fs();
    set_fs(KERNEL_DS);

    pos = 0;
    kernel_read(fp,buf1,sizeof(buf1),&pos);
    printk(KERN_ALERT "%s\n",buf1);

    filp_close(fp,NULL);
    set_fs(fs);

    //准备打印进程信息
    printk(KERN_ALERT "/*************processes information**************/\n");
    printk(KERN_ALERT "%-20s%-10s%-15s%-15s%-10s\n","name","pid","time(userM)","time(kernelM)","state");
    for_each_process(p)
    {
        printk(KERN_ALERT "%-20s%-10d%-15lld%-15lld%-5ld\n",p->comm,p->pid,(p->utime)/60,(p->stime)/60,p->state);
    }
    return 334;
}

用上次测试系统调用的程序测试一下:

1571822845749

输出到了控制台中,系统调用成功!

注意:我使用的是ubuntu server,使用的终端是物理终端,而printk输出的位置就是物理终端,如果你使用的是图形化界面,那么你可能会发现信息没有输出到你的终端上,因为图形化界面使用的不是物理终端,需要用户更改为物理终端,更改方法可以在网上查找资料。

避免采坑

vfs_read……

我看网上说内核用vfs_read、vfs_write等函数来操作文件,但是我写的时候报错说未定义的函数。我找了半天最后发现是在4.14版本以后,内核不再支持vfs_read、vfs_write等函数,而是改用kernel_read、kernel_write等函数。如果使用vfs_read、vfs_write等函数会报错。把vfs改成kernel就好了。

用户空间和系统空间

在对文件进行操作的时候,我直接使用了kernel_read函数,然后报错了。后来发现是因为kernel_read等函数它们默认的参数(buf)是指向用户空间的内存地址,而现在buf在内核空间,因此会出错。这个可以通过get_fs和set_fs修改。简单来说就是这样:

mm_segment_t fs = get_fs();
set_fs(KERNEL_DS);

//然后开始文件操作例如kernel_read()

filp_close(fp,NULL);
set_fs(fs);

具体的知识点这里不讲了,有兴趣的可以自行上网学习,这里给一篇参考文章:《在linux内核中读写文件》

sys_call_table地址

本来我想直接用动态模块写个钩子修改系统调用,结果获得地址之后运行发现永远不对,后来发现在linux2.6版本之后,出于保护系统的目的,不能直接导出sys_call_table的地址,因此使用如下命令得到的地址不是真实的物理地址,不能直接使用:

1571498488155

否则会报错,而且模块无法通过rmmod命令卸载,会显示正在使用,非常棘手(有解决办法,但很麻烦,所以我每次都是直接恢复快照)。

获取文件大小

在写代码的时候因为cpuinfo太长了打印不完,我一开始想动态获取文件大小然后再打印整个文件。在网上找了很多办法,看到别人用f->f_dentry->d_inode,但是我每次都报错,后来发现是在3.19版本之后内核不支持f->f_dentry->d_inode了,需要使用file_inode(f)替换掉f->f_dentry->d_inode。具体如下:

Linux 3.19 compat: file_inode was added

struct access f->f_dentry->d_inode was replaced by accessor function
file_inode(f)

Signed-off-by: Joerg Thalheim <joerg@higgsboson.tk>
Signed-off-by: Brian Behlendorf <behlendorf1@llnl.gov>

修改之后编译成功了发现获取的大小是0,后来才反应过来/proc中的文件不是真的文件因此没有大小可言……再后来反应过来要求不是要CPU型号嘛,那我直接光输出个型号不就可以了吗{{xieyanxiao}}

for_each_process

在使用 for_each_process时,有很多资料显示需要添加的头文件为linux/sched.h,但我编译的时候发现报错:

1571806269316

后来发现在 4.11以后,该方法都放在了include/linux/sched/signal.h中 ,因此需要修改头文件,修改之后就好了。

实验2-1——进程的软中断通信

实验内容

编制实现软中断通信的程序。使用系统调用fork()创建两个子进程,再用系统调用signal()让父进程捕捉键盘上发出的中断信号(即按delete键),当父进程接收到这两个软中断的某一个后,父进程用系统调用kill()向两个子进程分别发出整数值为16和17软中断信号,子进程获得对应软中断信号,然后分别输出下列信息后终止:

  • Child process 1 is killed by parent !!
  • Child process 2 is killed by parent !!

父进程调用wait()函数等待两个子进程终止后,输出以下信息,结束进程执行:

  • Parent process is killed!!

多运行几次编写的程序,简略分析出现不同结果的原因。

前导知识

fork()

这里简单说一下,fork()的作用就是创建一个子进程,fork把父进程复制一份给子进程,需要注意的是fork返回值是两个。在子进程中返回0,父进程中返回子进程的pid,还有一种情况是创建失败时会返回-1。

如果想深入了解推荐一个大佬的博客,写的有多好呢?这么说吧,我第一次见到CSDN光评论就几百条的(可能是我见识太少):《linux中fork()函数详解(原创!!实例讲解)》

signal()

捕捉中断信号sig后执行function规定的操作。就有点像if语句,捕获到信号之后就调用指定的函数。

用法如下:

#include <signal.h>    //头文件

int sig;
void function();

signal(sig,function)    //参数定义

sig一个有19个值:

名字说明
01SIGHUP挂起
02SIGINT中断,当用户从键盘键入“del”键时
03SIGQUIT退出,当用户从键盘键入“quit”键时
04SIGILL非法指令
05SIGTRAP断点或跟踪指令
06SIGIOTIOT指令
07SIGEMTEMT指令
08SIGFPE浮点运算溢出
09SIGKILL要求终止进程
10SIGBUS总线错误
11SIGSEGV段违例,即进程试图去访问其地址空间以外的地址
12SIGSYS系统调用错
13SIGPIPE向无读者的管道中写数据
14SIGALARM闹钟
15SIGTERM软件终止
16SIGUSR1用户自定义信号
17SIGUSR2用户自定义信号
18SIGCLD子进程死
19SIGPWR电源故障

kill()

一个进程向同一用户的其他进程pid发送信号。

用法为:

#include <signal.h>

pid_t pid;
int sig;

kill(pid,sig);

pid:可能选择有以下四种

  1. pid大于零时,pid是信号欲送往的进程的标识。
  2. pid等于零时,信号将送往所有与调用kill()的那个进程属同一个组的进程。
  3. pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
  4. pid小于-1时,信号将送往以-pid为组标识的进程。

sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。

返回值说明: 成功执行时,返回0;失败返回-1。

实验思路

要求很明确,很直接。需要注意的就是在代码实现的时候注意写父进程和子进程的运行语句的位置,有的时候很容易把人绕进去。

代码实现

/*  ex2.c
 *  Copyright (c) wpx
 */

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
#include <wait.h>
#include <unistd.h>

int wait_flag = 1;

void stop(){wait_flag = 0;}

int main()
{
    int p1,p2;

    while((p1 = fork()) == -1); //创建子进程1

    if(p1)
    {
        while((p2 = fork()) == -1); //创建子进程2

        if(p2)  //父进程执行语句
        {
            signal(SIGQUIT,stop);
            while(wait_flag);   //等待软中断Quit信号(Ctrl + \)

            kill(p1,16);
            kill(p2,17);
            wait(0);
            wait(0);

            sleep(3);
            printf("\nParent process is killed !!\n");
            exit(0);
        }

        else    //子进程2执行语句
        {
            signal(SIGQUIT,SIG_IGN);    //忽略Quit信号
            wait_flag = 1;

            signal(17,stop);
            while(wait_flag);

            printf("\nChild process 2 is killed by parent !!\n");
            exit(0);
        }
    }

    else    //子进程1执行语句
    {
        signal(SIGQUIT,SIG_IGN);
        wait_flag = 1;

        signal(16,stop);
        while(wait_flag);

        printf("\nChild process 1 is killed by parent !!\n");
        exit(0);
    }

    return 0;
}

运行程序,键盘输入Quit信号(ctrl+\),子进程12退出,父进程等待3秒后退出:

1572248369602

多次执行会发现子进程结束时间不确定:

1572252482068

原因可能是因为子进程之间是彼此并行的,两个子进程同时获得信号,因此结束时间不确定。

避免采坑

while语句

我在创建子进程的时候少加了一个括号,我是这样写的:

while(p1 = fork() == -1);

在运行程序的时候死活没结果,输入啥也不行,ctrl+\没有反应只能ctrl+c强制退出

后来发现其实我在这一步就有问题了,正确的写法是这样的:

while((p1 = fork()) == -1);

加个括号之后,程序就成功运行了。

信号忽略SIG_IGN

我的代码在子进程中有一句signal(SIGQUIT,SIG_IGN),实际上在一开始我并没有加这句代码。在我运行的过程中我发现,不管我试了多少次,子进程都没有输出,只有父进程输出了:

1572248861081

后来我发现如果我不用键盘输入quit,而是在一个新的终端中用kill命令给父进程quit信号,那么就能正常输出。

先通过ps -a获取父进程pid号(pid最小的就是父进程),然后使用kill -QUIT 父进程pid号给进程发送信号:

1572249333433

可以看到通过这种方式成功输出了我们想要的结果。我想了半天,但是还是没懂到底是怎么回事。我猜测可能是因为键盘直接输入的方式同时作用于三个进程,直接杀死了剩下两个子进程导致没有输出。所以在子进程中加上忽略Quit信号,这样quit只作用于父进程,子进程就可以收到父进程的信号打印语句了。

实验2-2——进程的管道通信

实验内容

创建一条管道完成下述工作:

  1. 分配一个隶属于root文件系统的磁盘和内存索引结点inode.
  2. 在系统打开文件表中分别分配一读管道文件表项和一写管道文件表项。
  3. 在创建管道的进程控制块的文件描述表(进程打开文件表u-ofile) 中分配二表项,表项中的偏移量filedes[0]和filedes[1]分别指向系统打开文件表的读和写管道文件表项。

系统调用所涉及的数据结构如下图所示:

图片1

前导知识

管道

所谓“管道”,是指用于连接一个读进程和一个写进程以实现他们之间通信的一个共享文件,又名pipe文件。管道分为匿名管道和命名管道。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。

管道分为匿名管道和命名管道,匿名管道只能用于有亲缘关系的进程(这是由管道利用fork的实现机制所决定的),而命名管道可以用于任意两个进程。由于本实验不涉及命名管道,所以暂且不提,有兴趣的读者可以自行学习。

匿名管道通过int pipe(int fd[2])创建。管道通过read和write函数进行读写。fd[0]是读端,fd[1]是写端。两个进程分别使用读和写端,就可以实现通信。注意在读或写的时候需要用lockf锁定当前端,或者用close关闭另一端

read()

头文件:#include <unistd.h>

函数定义: ssize_t read(int fd, void * buf, size_t count);

函数说明:read从fd中读取count个字节存入buf中。

write()

头文件:#include <unistd.h>

函数定义:ssize_t write(int fd, void * buf, size_t count);

函数说明:write把buf中的count个字节写入fd中。

sprintf()

和printf类似,只不过printf把格式化内容打印到屏幕上,而sprintf把格式化的内容保存到字符串中。例如:sprintf(s,”%d”,123); //把123保存在s中

lockf()

头文件:#include <unistd.h>

函数定义: int lockf(int fd, int function, long size)

函数说明:fd是文件描述符,function表示锁状态,1表示锁定,0表示解锁,size是锁定或解锁的字节数,若为0则表示整个文件。

实现思路

这个实验实际上意思就是父进程创建两个子进程,子进程分别通过管道给父进程发送一条信息“Child process 1 is sending message!”、“Child process 2 is sending message!”,父进程获取并打印消息然后退出。

代码实现

使用lockf,如果要用close的话就把lockf删了,然后把close的注释符删掉

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <wait.h>

int main()
{
    int p1,p2;
    char outpipe[50],inpipe[50];
    int fd[2];

    pipe(fd);

    while((p1 == fork()) == -1);

    if(p1)
    {
        while((p2 = fork()) == -1);

        if(p2)  //父进程执行语句
        {
            //close(fd[1]);
            wait(0);    //等待子进程1结束
            read(fd[0],inpipe,50);
            printf("%s\n",inpipe);

            wait(0);    //等待子进程2结束
            read(fd[0],inpipe,50);
            printf("%s\n",inpipe);

            exit(0);
        }

        else    //子进程2执行语句
        {
            //close(fd[0]);
            lockf(fd[1],1,0);   //锁定管道

            sprintf(outpipe,"\nChild process 2 is sending message!\n");
            write(fd[1],outpipe,50);

            sleep(2);

            lockf(fd[1],0,0);

            exit(0);
        }
    }

    else    //子进程1执行语句
    {
        //close(fd[0]);
        lockf(fd[1],1,0);

        sprintf(outpipe,"\nChild process 1 is sending message!\n");
        write(fd[1],outpipe,50);

        sleep(2);

        lockf(fd[1],0,0);

        exit(0);
    }
    return 0;
}

实验结果如图:

image-20191029155551912

但有趣的是,使用lockf和close的运行结果有些许差异,使用lockf是先输出p1再输出p2,而使用close则几乎同时输出。

使用lockf:

lockf

使用close:

close

具体原因不是很懂,如果有知道的大佬请教教小弟。

避免采坑

这个实验比较简单,我在做的时候没有什么困扰我很久的坑。就是注意几个函数的用法、参数的位置就行了。

实验2-3——内存的分配与回收

实验内容

通过深入理解内存分配管理的三种算法,定义相应的数据结构,编写具体代码。 充分模拟三种算法的实现过程,并通过对比,分析三种算法的优劣。
(1)掌握内存分配FF,BF,WF策略及实现的思路;
(2)掌握内存回收过程及实现思路;
(3)参考给出的代码思路,实现内存的申请、释放的管理程序,调试运行,总结程序设计中出现的问题并找出原因,写出实验报告。

主要功能:

1 – Set memory size (default=1024) 设置内存的大小
2 – Select memory allocation algorithm 设置当前的分配算法
3 – New process 创建新的进程,主要是获取内存的申请数量
4 – Terminate a process 删除进程,归还分配的存储空间,并删除描述该进程内存分配的节点
5 – Display memory usage 显示当前内存的使用情况,包括空闲区的情况和已经分配的情况
0 – Exit

前导知识

FF、BF、WF算法

这三个算法是这次实验的核心。具体的东西课上都讲过,网上也一大堆。简单说一下。FF就是把进程分给第一个匹配的内存块;BF就是把进程分给能容纳进程的最小的内存块,WF和BF相反,就是把进程分给能容纳进程的最大的内存块。

链表

这次实验中内存的数据结构使用的是链表(更准确的说是单链表),因此需要对链表以及链表的操作有一定了解。链表的操作很多,鉴于篇幅问题这里不赘述了,网上很多相关资料,不熟悉的童鞋可以自行搜索。

实现思路

这个实验涉及的模块很多,结果比较复杂,我在编写的过程中经常把自己绕晕,因此我画了一张思维导图,便于理解:

image-20191107220233737

下面我们就按照这个结构来依次实现各个模块。

代码实现

程序在windows平台使用VS 2019编写,经测试windows和linux平台均可正常运行

注意:

  1. 如果使用VS编写scanf会报错,这是VS的安全检查机制导致的,取消报错的方法网上很多,这里就不赘述了。
  2. 函数之间可能存在调用关系,因此注意函数之间的顺序或者提前进行函数声明。

友情提示:有的模块代码较长,如果看着不方便可点击代码块全屏浏览哦

主要数据结构

内存空闲分区的描述

//描述每一个空闲块的数据结构
struct free_block_type
{
    int size;
    int start_addr;
    struct free_block_type* next;
};
//指向内存中空闲块链表的首指针
struct free_block_type* free_block_head = NULL;

已分配内存块的描述

struct allocated_block
{
    int pid;
    int size;
    int start_addr;
    char process_name[PROCESS_NAME_LEN];
    struct allocated_block* next
};
//进程分配内存块链表的首指针
struct allocated_block* allocated_block_head = NULL;

常量定义

#define    PROCESS_NAME_LEN    32  //进程名长度
#define    MIN_SLICE   10  //最小碎片大小
#define    DEFAULT_MEM_SIZE    1024    //默认内存大小
#define DEFAULT_MEM_START  0   //内存起始位置

#define    MA_FF   1
#define MA_BF  2
#define MA_WF  3

int mem_size = DEFAULT_MEM_SIZE;    //可用内存大小,初始化为默认内存大小
int mem_size_total = DEFAULT_MEM_SIZE;  //总共内存大小,初始化为默认大小
int ma_algorithm = MA_FF;   //当前内存分配算法,初始化为FF
static int pid = 0; //进程pid号,初始值为0
int flag = 0;   //设置内存大小标志,防止重新设置

函数模块

主函数及菜单

主函数
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>


int main()
{
    int choice;
    free_block_head = init_free_block(mem_size);

    while (1)
    {
        display_menu(); //显示菜单
        fflush(stdin);  //清空缓冲区,防止误选
        scanf("%d", &choice);   //获取用户输入
        if (choice <= 5 && choice >= 0)
        {
            switch (choice)
            {
            case 1: set_mem_size(); break;  //设置内存大小
            case 2: set_algorithm(); flag = 1; break;   //设置算法
            case 3: new_process(); flag = 1; break; //创建新进程
            case 4: kill_process(); flag = 1; break;    //删除进程
            case 5: display_mem_usage(); flag = 1; break;   //显示内存使用
            case 0: do_exit(); exit(0); //释放链表并退出
            default: break;
            }
        }

        else
        {
            printf("\nInvalid choice, please select again!\n\n");
        }

    }

    return 0;
}
显示菜单
void display_menu()
{

    printf("\n");
    printf("----------------------------------------\n");
    printf("      Memory Management Experiment\n");
    printf("          (c) WPX 2176112425\n");
    printf("               2019.11.3\n");
    printf("----------------------------------------\n\n");
    printf("current algo: %d\tcurrent mem_size: %d\n\n", ma_algorithm, mem_size_total);
    printf("Please enter a number to select the appropriate function:\n\n");
    printf("1 -- Set memory size(default=%d)\n", DEFAULT_MEM_SIZE);
    printf("2 -- Select memory allocation algorithm\n");
    printf("3 -- New process\n");
    printf("4 -- Terminate a process\n");
    printf("5 -- Display memory usage\n");
    printf("0 -- Exit\n\n");
    printf(">> ");
}

初始化内存

初始化空闲块
struct free_block_type* init_free_block(int mem_size)
{
    struct free_block_type* fb;
    fb = (struct free_block_type*)malloc(sizeof(struct free_block_type));

    if (fb == NULL)
    {
        printf("No memory!\n");
        return NULL;
    }

    fb->size = mem_size;
    fb->start_addr = DEFAULT_MEM_START;
    fb->next = NULL;

    return fb;
}
设置内存大小
void set_mem_size()
{
    int size;

    if (flag)   //检查是否重复设置
    {
        printf("Cannot set memory size again!\n");
        return;
    }

    printf("\nTotal memory size = ");
    fflush(stdin);
    scanf("%d", &size);

    if (size >= 0)  //检查内存大小是否合法
    {
        mem_size = size;
        mem_size_total = size;
        free_block_head->size = mem_size;
        flag = 1;
    }

    else printf("Memory size is not valid!\n");
}

设置内存算法

显示设置菜单
void set_algorithm()
{
    int algorithm;
    printf("Please enter a number to select the appropriate algorithm:\n\n");
    printf("1 -- First Fit\n");
    printf("2 -- Best Fit\n");
    printf("3 -- Worst Fit\n");
    printf(">> ");
    scanf("%d", &algorithm);

    if (algorithm >= 1 && algorithm <= 3)
    {
        ma_algorithm = algorithm;
        rearrange(ma_algorithm);
    }
    else
        printf("Invalid choice!\n");
}
按指定算法重新排列空闲区链表
void rearrange(int algorithm)
{
    switch (algorithm)
    {
    case MA_FF: rearrange_FF(); break;
    case MA_BF: rearrange_BF(); break;
    case MA_WF: rearrange_WF(); break;
    }
}

创建进程载入内存

创建新进程,获取内存申请数量
void new_process()
{
    struct allocated_block* ab;
    int size;
    //给进程分配内存的结果
    int allocate_ret;

    //创建一个进程
    ab = (struct allocated_block*)malloc(sizeof(struct allocated_block));
    if (!ab)    exit(1);

    //确定进程属性
    ab->next = NULL;
    pid++;
    sprintf(ab->process_name, "PROCESS-%02d", pid);
    ab->pid = pid;
    printf("\nPlease input the memory for PROCESS-%02d: ", pid);
    scanf("%d", &size);
    if (size > 0)   ab->size = size;
    else
    {
        printf("\nInvalid memory size!\n");
        return;
    }

    //从空闲区分配内存,返回分配结果。1表示分配成功,-1表示分配失败
    allocate_ret = allocate_mem(ab);

    if ((allocate_ret == 1) && (allocated_block_head == NULL))  //如果是第一个节点
    {
        allocated_block_head = ab;
    }

    else if ((allocate_ret == 1) && (allocated_block_head != NULL)) //如果不是第一个节点
    {
        ab->next = allocated_block_head;
        allocated_block_head = ab;
    }

    else if (allocate_ret == -1)    //如果分配失败
    {
        printf("\nAllocation failed!\n");
        free(ab);
        return;
    }

    printf("\nAllocation success!\n");
}
分配内存模块
int allocate_mem(struct allocated_block* ab)
{
    struct free_block_type* fbt, * pre, * ne, * p1, * p2;
    int request_size = ab->size;
    fbt = pre = ne = p1 = p2 = free_block_head;
    int allocate_flag = 0;  //判断是否已经找到匹配空闲块

    //根据当前算法在空闲分区链表中搜索合适空闲分区进行分配,分配时注意以下情况:
    // 1. 找到可满足空闲分区且分配后剩余空间足够大,则分割
    // 2. 找到可满足空闲分区且但分配后剩余空间比较小,则一起分配
    // 3. 找不到可满足需要的空闲分区但空闲分区之和能满足需要,则采用内存紧缩技术,进行空闲分区的合并,然后再分配
    // 4. 在成功分配内存后,应保持空闲分区按照相应算法有序
    // 5. 分配成功则返回1,否则返回-1

    if (mem_size <= 0)  return -1;

    //遍历查找匹配空闲块
    if (ne->next)   //如果空闲块不止一个 
    {
        if (ma_algorithm == 1)  //如果是FF算法,遍历每一个空闲块
        {
            while (ne)
            {
                if (ne != free_block_head)  p2 = p1;
                p1 = ne;
                ne = ne->next;
                if (request_size <= p1->size)
                {
                    fbt = p2;
                    pre = p1;
                    allocate_flag = 1;
                }
            }
        }
        else if (ma_algorithm == 2) //如果是BF算法,则遍历每一个大小满足要求的空闲块
        {
            while ((ne != NULL) && (request_size <= ne->size))  
            {
                if (allocate_flag)  fbt = pre;
                pre = ne;
                ne = ne->next;
                allocate_flag = 1;
            }
        }
        else if (ma_algorithm == 3) //如果是WF算法,则直接查找最后一个空闲块
        {
            while (ne)
            {
                if (ne != free_block_head)  fbt = pre;
                pre = ne;
                ne = ne->next;
            }
            if (pre->size >= request_size)  allocate_flag = 1;
        }
    }
    else
    {
        if (request_size <= pre->size)
            allocate_flag = 1;
    }


    if (allocate_flag)  //找到可用空闲区,判断需不需要一起分配剩余内存空间
    {
        if ((pre->size - request_size) >= MIN_SLICE)    //找到可满足空闲分区且分配后剩余空间比较大,则正常分配
        {
            pre->size = pre->size - request_size;
            ab->start_addr = pre->start_addr;
            pre->start_addr += ab->size;
        }

        else    //找到可满足空闲分区且分配后剩余空间比较小,则一起分配,删除该节点
        {
            if (fbt == pre) //如果头块满足条件
            {
                fbt = pre->next;
                free_block_head = fbt;
            }
            else    //中间空闲块满足条件
                fbt->next = pre->next;

            ab->start_addr = pre->start_addr;
            ab->size = pre->size;
            free(pre);  //释放节点
        }

        mem_size -= ab->size;
        rearrange(ma_algorithm);
        return 1;
    }

    else    //找不到空闲区,则进行内存紧缩
    {
        if (mem_size >= request_size)
        {
            if (mem_size >= request_size + MIN_SLICE)   //分配完内存后还留有空闲内存
                free_memory_rearrange(mem_size - request_size, request_size);
            else    //分配完内存后无空闲内存
                free_memory_rearrange(0, mem_size);
            return 0;
        }

        else
            return -1;
    }

}
紧缩处理
void free_memory_rearrange(int memory_reduce_size, int allocated_size)
{
    struct free_block_type* f1, * f2;
    struct allocated_block* a1, * a2;

    //空闲块处理
    if (memory_reduce_size != 0)    //分配完剩余空间大于最小内存碎片
    {
        f1 = free_block_head;
        f2 = f1->next;

        f1->start_addr = mem_size_total - memory_reduce_size;
        f1->size = memory_reduce_size;
        f1->next = NULL;

        mem_size = memory_reduce_size;

    }
    else
    {
        f2 = free_block_head;
        free_block_head = NULL;
        mem_size = 0;
    }

    while (f2 != NULL)  //逐一释放空闲内存块节点
    {
        f1 = f2;
        f2 = f2->next;
        free(f1);
    }

    //加载块处理
    a1 = (struct allocated_block*)malloc(sizeof(struct allocated_block));
    a1->pid = pid;
    a1->size = allocated_size;
    a1->start_addr = mem_size_total - memory_reduce_size - a1->size;
    sprintf(a1->process_name, "PROCESS-%02d", pid);

    a1->next = allocated_block_head;
    a2 = allocated_block_head;
    allocated_block_head = a1;

    while (a2 != NULL)  //逐一将加载块相邻放置
    {
        a2->start_addr = a1->start_addr - a2->size;
        a1 = a2;
        a2 = a2->next;
    }

}

删除进程移出内存

删除进程
void kill_process()
{
    struct allocated_block* ab;
    int pid;
    printf("\nKill Process, pid = ");
    scanf("%d", &pid);
    ab = find_process(pid); //找到要删除的块
    if (ab != NULL)
    {
        free_mem(ab);   //释放ab所表示的分配区
        dispose(ab);    //释放ab数据结构节点
    }
}
找到进程
struct allocated_block* find_process(int pid)
{
    struct allocated_block* p;
    p = allocated_block_head;
    while (p)   //遍历链表找pid对应进程
    {
        if (p->pid == pid)
            return p;   //找到则返回struct
        p = p->next;
    }

    printf("\nProcess not found!\n");   //没有找到则报错并返回NULL
    return NULL;
}
归还分配区并合并
int free_mem(struct allocated_block* ab)
{
    int algorithm = ma_algorithm;
    struct free_block_type* fbt, * left, * right;   //链表结构认为从左指向右,fbt存储要释放的分区
                                                    //left为插入后左边(靠近表头)的空闲分区、right为插入后右边(远离表头)的空闲分区
    mem_size += ab->size;

    fbt = (struct free_block_type*)malloc(sizeof(struct free_block_type));
    if (!fbt)   return -1;

    //回收内存4种情况:
    // 1. 当前空闲分区和右边空闲分区相邻,合并为同一个分区,且释放右边分区 
    // 2. 当前空闲分区和左边空闲分区相邻,合并为同一个分区,且释放当前分区
    // 3. 当前空闲分区和左右空闲分区都相邻,合并为同一个分区,且释放当前和右边分区
    // 4. 无相邻空闲分区,则插入一个新表项

    fbt->size = ab->size;
    fbt->start_addr = ab->start_addr;
    fbt->next = NULL;
    rearrange(MA_FF);

    left = NULL;
    //从头开始按照起始地址顺序遍历,判断插入链表的位置
    right = free_block_head;
    while ((right != NULL) && (fbt->start_addr < right->start_addr))
    {
        left = right;
        right = right->next;
    }

    if (!left)  //插入位置为链表头
    {
        if (!right) //如果释放内存前已经没有空闲分区
            free_block_head = fbt;
        else
        {
            fbt->next = right;
            free_block_head = fbt;
            if (right->start_addr + right->size == fbt->start_addr) //判断释放的空闲区间和右边空闲分区是否相邻,是则合并
            {
                fbt->next = right->next;
                fbt->start_addr = right->start_addr;
                fbt->size = fbt->size + right->size;
                free(right);
            }
        }
    }

    else    
    {
        if (!right) //如果插入的位置在链表尾
        {
            left->next = fbt;

            if (fbt->start_addr + fbt->size == left->start_addr)    //判断释放的空闲区间和左边空闲分区是否相邻,是则合并
            {
                left->next = right;
                left->size = fbt->size + left->size;
                left->start_addr = fbt->start_addr;
                free(fbt);
            }
        }

        else    //如果插入的位置在链表中间
        {
            fbt->next = right;
            left->next = fbt;

            if ((fbt->start_addr + fbt->size == left->start_addr) && (right->start_addr + right->size == fbt->start_addr))  //和左右都相邻
            {
                left->next = right->next;
                left->size += fbt->size + right->size;
                left->start_addr = right->start_addr;
                free(fbt);
                free(right);
            }
            else if (fbt->start_addr + fbt->size == left->start_addr)   //和左边相邻
            {
                left->next = right;
                left->size = fbt->size + left->size;
                left->start_addr = fbt->start_addr;
                free(fbt);
            }
            else if (right->start_addr + right->size == fbt->start_addr)    //和右边相邻
            {
                fbt->next = right->next;
                fbt->start_addr = right->start_addr;
                fbt->size = fbt->size + right->size;
                free(right);
            }

        }
    }

    rearrange(ma_algorithm);
    return 1;
}
释放ab数据结构节点
int dispose(struct allocated_block* free_ab)
{
    struct allocated_block* pre, * ab;
    if (free_ab == allocated_block_head)    //如果释放头节点
    {
        allocated_block_head = allocated_block_head->next;
        free(free_ab);
        return 1;
    }

    pre = allocated_block_head;
    ab = allocated_block_head->next;
    while (ab != free_ab)   //遍历链表找到要释放的节点
    {
        pre = ab;
        ab = ab->next;
    }
    pre->next = ab->next;
    free(ab);
    return 2;
}

显示内存状态

int display_mem_usage()
{
    struct free_block_type* fbt = free_block_head;
    struct allocated_block* ab = allocated_block_head;

    printf("-------------------------------------------------------------\n");

    //显示空闲区
    printf("Free Memory:\n");
    printf("%20s %20s\n", "start_addr", "size");
    while (fbt != NULL)
    {
        printf("%20d %20d\n", fbt->start_addr, fbt->size);
        fbt = fbt->next;
    }

    //显示已分配区
    printf("\nUsed Memory:\n");
    printf("%10s %20s %10s %10s\n", "PID", "Process Name", "start_addr", "size");
    while (ab != NULL)
    {
        printf("%10d %20s %10d %10d\n", ab->pid, ab->process_name, ab->start_addr, ab->size);
        ab = ab->next;
    }
    printf("-------------------------------------------------------------\n");
    return 0;
}

三种算法

FF算法
void rearrange_FF()
{
    struct free_block_type* p, * p1, * p2;
    struct free_block_type* last_block;
    p1 = (struct free_block_type*)malloc(sizeof(struct free_block_type));
    p1->next = free_block_head;
    free_block_head = p1;
    if (free_block_head != NULL)
    {
        for (last_block = NULL; last_block != free_block_head; last_block = p)  //冒泡排序,按起始地址从大到小排
        {
            for (p = p1 = free_block_head; p1->next != NULL && p1->next->next != NULL && p1->next->next != last_block; p1 = p1->next)
            {
                if (p1->next->start_addr < p1->next->next->start_addr)
                {
                    p2 = p1->next->next;
                    p1->next->next = p2->next;

                    p2->next = p1->next;
                    p1->next = p2;

                    p = p1->next->next;
                }
            }
        }
    }

    p1 = free_block_head;
    free_block_head = free_block_head->next;
    free(p1);
}
BF算法
void rearrange_BF()
{
    struct free_block_type* p, * p1, * p2;
    struct free_block_type* last_block;
    p1 = (struct free_block_type*)malloc(sizeof(struct free_block_type));
    p1->next = free_block_head;
    free_block_head = p1;
    if (free_block_head != NULL)
    {
        for (last_block = NULL; last_block != free_block_head; last_block = p)  //冒泡排序,按块大小从大到小排
        {
            for (p = p1 = free_block_head; p1->next != NULL && p1->next->next != NULL && p1->next->next != last_block; p1 = p1->next)
            {
                if (p1->next->size < p1->next->next->size)
                {
                    p2 = p1->next->next;
                    p1->next->next = p2->next;

                    p2->next = p1->next;
                    p1->next = p2;

                    p = p1->next->next;
                }
            }
        }
    }
    p1 = free_block_head;
    free_block_head = free_block_head->next;
    free(p1);
    p1 = NULL;
}
WF算法
void rearrange_WF()
{
    struct free_block_type* p, * p1, * p2;
    struct free_block_type* last_block;
    p1 = (struct free_block_type*)malloc(sizeof(struct free_block_type));
    p1->next = free_block_head;
    free_block_head = p1;
    if (free_block_head != NULL) {
        for (last_block = NULL; last_block != free_block_head; last_block = p)  //冒泡排序,按块大小从小到大排
        {
            for (p = p1 = free_block_head; p1->next != NULL && p1->next->next != NULL && p1->next->next != last_block; p1 = p1->next)
            {
                if (p1->next->size > p1->next->next->size)
                {
                    p2 = p1->next->next;
                    p1->next->next = p2->next; 

                    p2->next = p1->next;
                    p1->next = p2;

                    p = p1->next->next;
                }
            }
        }


    }

    p1 = free_block_head;
    free_block_head = free_block_head->next;
    free(p1);
    p1 = NULL;
}

退出程序

void do_exit()
{
    struct free_block_type* p1, * p2;
    struct allocated_block* a1, * a2;
    p1 = free_block_head;
    if (p1 != NULL)
    {
        p2 = p1->next;
        for (; p2 != NULL; p1 = p2, p2 = p2->next)
        {
            free(p1);
        }
        free(p1);
    }
    a1 = allocated_block_head;
    if (a1 != NULL)
    {
        a2 = a1->next;
        for (; a2 != NULL; a1 = a2, a2 = a2->next)
        {
            free(a1);
        }
        free(a1);
    }
}

避免采坑

写了好几天,调了无数的bug,很多都由于当时沉迷分析调试改bug忘了记录了,只记几个问题吧。

给了个测试用例发现相邻空闲内存没有合并,如图:

image-20191106233254914

经过调试发现是我排序算法写反了,本来FF排完序之后应该是从头开始起始地址从大到小排列,结果我写成从小到大排列了,而我合并的时候又是把起始地址按照从大到小的顺序插入,这样就导致相邻比较的时候是大的比小的,所以出错,改正之后就好了。

可是改正了之后又发现FF算法实际输出的效果是WF算法,经过分析发现是分配内存的时候出现了问题。我在遍历空闲块的时候直接找的是第一个满足条件的空闲块,而我的链表是从大到小排的,所以出现了问题。修改之后就发现程序一创建进程就崩溃,后来发现我的条件是pre!=NULL,当我所以空闲块都满足条件的时候最终的pre就越界,所以改成pre->next!=NULL就避免了越界。但是这样又发现程序报错。后来发现这样写的话,在一开始只有一个头结点的时候就会出错,因此又加了一个判断是否只有一个节点,这样就解决了问题。

再次调试的时候又发现内存紧缩模块有问题。

本来内存状态如下:

image-20191107001256301

加了一个大小为800的PROCESS-06,结果如下:

image-20191107001446943

调试发现是内存紧缩模块中处理加载块的时候把起始地址填错了

a1->start_addr = mem_size_total – memory_reduce_size改成a1->start_addr = mem_size_total – memory_reduce_size – a1->size就对了

再次调试发现在内存紧缩后,显示内存状态程序就会崩溃,后来发现我在释放节点的时候把f1、f2写反了,这样导致在释放后free_block_head也被释放了,所以显示的时候就会出现问题。修改后就可以了。

后来发现内存紧缩在合并左右两个相邻块时会出问题,调试发现内存大小写错了,left->size = fbt->size + right->size;这样少了左边节点自身的大小,所以出问题。写成left->size += fbt->size + right->size;就没有问题了。

后来又发现内存紧缩的时候出现问题,算法是WF,状态如下:

image-20191107145711780

我给了一个200的进程发现触发了内存紧缩。经过调试发现在分配内存的时候便利空闲块出现了问题,在WF算法下空闲块链表的排序为size从小到大排,而我在遍历的时候由于第一个小于200,所以程序认为没有合适的空闲块。所以为WF算法设置一个专门的分配方式,直接判断最后一个空闲块是否满足要求。再次调试发现FF算法又出问题了,所以干脆给每个算法都写出特定的空闲块查找算法。最终解决问题。

写完了之后我想了一下,好像如果我把内存的结构改成按照内存大小从小到大排会简单很多,在分配算法的时候直接找头结点就行了……{{tuxue}}

C/S架构分布式程序多线程多进程开销测试报告

[toc]

一、题目

编写一个C/S架构的分布式程序,Server接受Client发来的请求,执行一个计算F(X)并给Client返回结果;分别用进程与线程作为服务器Server实现,并比较服务器的开销。可以在一台机器上模拟。

二、实验过程

用python实现Client和Server,实现Client发送一个数,Server返回这个数的4次方。server_process用进程实现,server_thread用线程实现。client.py中计算从发送第一个数据到接收完最后一个数据的时间差

源代码如下:

client.py

import socket
import time


time1 = time.time()
for i in range(1,100):
    s = socket.socket()
    host = socket.gethostname()
    port = 1234
    s.connect((host,port))
    s.send('20')
    print(s.recv(1024).decode('utf-8'))

time2 = time.time()
print(time2 - time1)

server_thread.py

import socket
from threading import Thread


s = socket.socket()
host = socket.gethostname()
s.bind((host,1234))
s.listen(100)
print("Waiting for connection......")

def tcplink(sock,addr):
    print("Accept new connection from %s:%s" % addr)
    data = sock.recv(1024)
    i = int(data)
    sock.send(str(i*i*i*i))
    sock.close()
    print("connection closed!")

while True:
    sock, addr = s.accept()
    t = Thread(target = tcplink, args = (sock,addr))
    t.start()

server_process.py

import socket
from multiprocessing import *


s = socket.socket()
host = socket.gethostname()
s.bind((host,1234))
s.listen(100)
print("Waiting for connection......")

def tcplink(sock,addr):
    print("Accept new connection from %s:%s" % addr)
    data = sock.recv(1024)
    i = int(data)
    sock.send(str(i*i*i*i))
    sock.close()
    print("connection closed!")

while True:
    sock, addr = s.accept()
    t = Process(target = tcplink, args = (sock,addr))
    t.start()

三、实验结果

多进程实现如图所示:

1570463537221

多线程实现如图所示:

1570463576753

从结果图中可以看到,多进程所用时间为0,14178,而多线程所用时间为0.04780,多进程比多线程慢了将近2倍,因此可以得出结论,多进程在服务器中开销较大,使用多线程速度较快。