迁移博客

This commit is contained in:
LazuliKao 2024-05-11 20:53:44 +08:00
parent accf5562b9
commit 7a57412272
274 changed files with 4315 additions and 48 deletions

View file

@ -107,15 +107,6 @@ import BaseLayout from "../layouts/BaseLayout.astro";
}
.body-container {
-webkit-text-size-adjust: 100%;
--r-globalnav-height: 44px;
--r-globalmessage-segment-height: 0px;
--r-globalnav-segmentbar-height: 0px;
--r-globalnav-background-opened: #fafafc;
--r-globalnav-background-opened-dark: #161617;
--sk-focus-color: #0071e3;
--sk-focus-color-alt: rgb(0, 0, 0);
--sk-focus-offset: 1px;
--sk-focus-offset-container: 3px;
color: #333333;
font-synthesis: none;
text-rendering: optimizeLegibility;
@ -167,16 +158,6 @@ import BaseLayout from "../layouts/BaseLayout.astro";
}
.life-at-apple__eyebrow {
-webkit-text-size-adjust: 100%;
--r-globalmessage-segment-height: 0px;
--r-globalnav-segmentbar-height: 0px;
--r-globalnav-height: 48px;
--rem: 16;
--r-globalnav-background-opened: #fafafc;
--r-globalnav-background-opened-dark: #161617;
--sk-focus-color: #0071e3;
--sk-focus-color-alt: rgb(0, 0, 0);
--sk-focus-offset: 1px;
--sk-focus-offset-container: 3px;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
@ -198,16 +179,6 @@ import BaseLayout from "../layouts/BaseLayout.astro";
}
.t-hero-headline-oversized {
-webkit-text-size-adjust: 100%;
--r-globalmessage-segment-height: 0px;
--r-globalnav-segmentbar-height: 0px;
--r-globalnav-height: 48px;
--rem: 16;
--r-globalnav-background-opened: #fafafc;
--r-globalnav-background-opened-dark: #161617;
--sk-focus-color: #0071e3;
--sk-focus-color-alt: rgb(0, 0, 0);
--sk-focus-offset: 1px;
--sk-focus-offset-container: 3px;
color: #333333;
font-synthesis: none;
text-rendering: optimizeLegibility;
@ -228,16 +199,6 @@ import BaseLayout from "../layouts/BaseLayout.astro";
}
.t-hero-headline-oversized-span {
-webkit-text-size-adjust: 100%;
--r-globalmessage-segment-height: 0px;
--r-globalnav-segmentbar-height: 0px;
--r-globalnav-height: 48px;
--rem: 16;
--r-globalnav-background-opened: #fafafc;
--r-globalnav-background-opened-dark: #161617;
--sk-focus-color: #0071e3;
--sk-focus-color-alt: rgb(0, 0, 0);
--sk-focus-offset: 1px;
--sk-focus-offset-container: 3px;
color: #333333;
font-synthesis: none;
text-rendering: optimizeLegibility;
@ -269,15 +230,6 @@ import BaseLayout from "../layouts/BaseLayout.astro";
}
.learn-more-link {
-webkit-text-size-adjust: 100%;
--r-globalmessage-segment-height: 0px;
--r-globalnav-segmentbar-height: 0px;
--r-globalnav-height: 48px;
--r-globalnav-background-opened: #fafafc;
--r-globalnav-background-opened-dark: #161617;
--sk-focus-color: #0071e3;
--sk-focus-color-alt: rgb(0, 0, 0);
--sk-focus-offset: 1px;
--sk-focus-offset-container: 3px;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;

View file

@ -0,0 +1,18 @@
---
pageComponent:
name: Catalogue
data:
path: 01.技术
imgUrl: /img/technology.png
description: 技术文档、教程、技巧、总结等文章
title: 技术
date: 2023-11-27
permalink: /technology/
sidebar: false
article: false
comment: false
editLink: false
author:
name: nbtca
link: https://github.com/nbtca
---

View file

@ -0,0 +1,18 @@
---
pageComponent:
name: Catalogue
data:
path: 02.更多
imgUrl: /img/more_1.png
description: 学习、面试、在线工具等更多文章和页面
title: 更多
date: 2023-11-27
permalink: /more/
sidebar: false
article: false
comment: false
editLink: false
author:
name: nbtca
link: https://github.com/nbtca
---

View file

@ -0,0 +1,341 @@
---
title: 从汇编角度看C语言 - 1
date: 2022-03-27 21:38:45
categories:
- 技术
- C语言
cover: ./assets/c10e40ab02e343e3afa9fb19b5619042/assembly.jpeg
tid: c-from-asm-1
description: 反汇编C语言程序的解读。
permalink: /pages/989a42/
author:
name: N3ptune
link: https://www.cnblogs.com/N3ptune
tags:
-
---
# 从汇编角度看C语言 - 1
## 写在前面
> 参考书目: 《加密与解密》
- 系统平台: Windows 10
- 调试工具: OllyDBG (吾爱破解专版)
- 开发工具: Visual Studio 2008 Debug模式
- 基础要求: 了解C语言和汇编语言
- 大致内容: 简述了一些汇编语言与C语言的关系方便初学者更好的认识程序的底层运行机制。
## 开始
用VS2008编译如下代码
```c
#include <stdio.h>
int main(int argc,char* argv[])
{
printf("Hello World\n");
return 0;
}
```
将生成的**可执行文件**拖入**OllyDBG**。
## 如何寻找C程序入口
明确两点:
1. mainCRTStartup和wmainCRTStartup是控制台环境下多字节编码和Unicode编码的启动函数
2. WinMainCRTStartup和wWinMainCRTStartup是windows环境下多字节编码和Unicode编码的启动函数。
mainCRTStartup做了哪些事
如何通过mainCRTStartup来找到main函数入口
以上述程序为例,寻找其`main`函数入口。
![image-20220327214647589](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327214647589.png)
初步调试文件,可以发现许多`jmp`指令这是编译器生成的启动代码往下按F8跟随跳转。
![image-20220327214714185](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327214714185.png)
连续按`F8`来到`call tmainCRTStartuptringtionFilter`再一次按`F8`后,整个程序就会返回,因此按`F7`单步进入该函数。
接下来连续按下`F8`,并且观察控制台输出:
![image-20220327214738686](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327214738686.png)
发现再`call hello.00241140`后控制台打出Hello World因此在此处设下断点。
按下`Ctrl+F2`后重新启动程序,按下`F9`运行到该断点,按下`F7`单步进入。
![image-20220327214803763](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327214803763.png)
按几次F8后看到如上界面可以看到Hello World字符串程序的开头即初始化栈帧操作显然已经成功来到了main函数中。
![image-20220327214826902](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327214826902.png)
查看如上高亮的指令该指令将“Hello World”字符串的首地址压入栈中而后调用printf将字符串打印在控制台上。
![image-20220327215012755](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327215012755.png)
事实也正是如此!
## 函数
C语言程序是由具有不同功能的函数组成的因此在逆向分析中应将重点放在函数的识别及参数的传递上这样做可以将注意力集中在某一段代码上。
### 函数的识别
下面讨论**函数的识别**
调用函数的代码保存了一个返回地址该地址会与参数一起传递给被调用的函数。绝大多数情况下编译器都使用call和ret指令来调用函数和返回调用位置。
call指令与跳转指令功能类似但call指令保存返回信息这里的返回信息实际上主要就是返回地址。
call指令执行时会将其之后的指令地址压入栈的顶部当遇到ret指令时返回这个地址。
也就是说call指令给出的地址就是被调用函数的起始地址ret指令用于结束函数的执行。
因此可以通过定位call指令或ret指令来识别函数call的操作数就是所调用函数的首地址。
话不多说,看一个例子。
用vs2008编译如下代码使用OllyDBG进行调试
```c
#include <stdio.h>
int add(int x,int y)
{
return x+y;
}
int main(int argc,char* argv[])
{
int a = 5,b = 6;
add(a,b);
return 0;
}
```
进入OD后依然要越过启动代码寻找真正的`main`函数。
![image-20220327220701475](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220701475.png)
连续按F8。
按照以往的经验,调用`main`函数至少在调用`GetCommandLine`函数之后。
![image-20220327220724315](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220724315.png)
于是这里继续往下执行。
![image-20220327220810610](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220810610.png)
看到这里可以push了3个参数发现argv和argc的字样那么下一个call十有八九会到达`main`函数了。
按F7单步进入。
![image-20220327220839595](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220839595.png)
来到这里就已经很明显了,可以明显的看到下图中由`mov ..., 0x5`和`mov ..., 0x6`两个语句这明显是在给变量赋值上5和6那么就可以推测`call test.00DD135C`实际上在调用`add`函数,将光标移动指令处,按回车键。
![image-20220327220903701](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220903701.png)
如下图可以看到`add`字样,因此猜想是对的。
![image-20220327220934315](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220934315.png)
那么回到之前的`main`函数可以看到代码将0x5和0x6放入rax和ecx寄存器后又对其进行了压栈操作实际等价于`push 0x5`和`push 0x6`。
![image-20220327221004603](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327221004603.png)
push操作就是x86架构下典型的压栈方式符合**__cdecl**调用约定(C/C++程序的默认调用约定,在此不作赘述)在该约定下可以看到压栈顺序是逆序的右边的参数先进栈左边的参数后进栈栈顶指针esp指向栈中第1个可用的数据项。
在调用函数时,调用者依次将参数压入栈,然后调用函数。函数被调用以后,在栈中取得数据并进行计算,函数计算结束后,由调用者或者函数本身修改栈,使栈恢复原样(平衡栈数据)。
现在将程序运行到call指令之前查看OD的栈区数据可以看到显示Arg1=5Arg2=6显然这两个参数已经被压栈。
![image-20220327221037201](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327221037201.png)
进入add函数后可以看到程序将arg1存入eax寄存器再和arg2相加就完成了计算。
![image-20220327221100245](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327221100245.png)
另外还有几种调用约定,如**fastcall**和**stdcall**。
### 函数的返回值
再来讨论**函数的返回值**
将`add`函数修改一下,如下所示:
```c
int add(int x,int y)
{
int temp;
temp = x + y;
return temp;
}
```
进入OD再次回到add函数中。
![image-20220327221142426](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327221142426.png)
`mov eax, [local.2]`是将计算的最后结果就保存在eax寄存器中eax就作为存放返回值的寄存器。
![image-20220327221206970](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327221206970.png)
众所周知,带回返回值的方法不只`return`,还可传引用,查看如下代码:
```c
#include <stdio.h>
void max(int* a,int* b)
{
if(*a < *b) {
*a = *b;
}
}
int main(int argc,char* argv[])
{
int a = 5,b = 6;
max(&a, &b);
printf("max: %d",a);
return 0;
}
```
这里定义了一个`max`函数,接收`a`和`b`的地址,将其中较大数放入变量`a`中。使用OD进入`main`函数。
![image-20220327220253195](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220253195.png)
进入`max`函数,`mov eax, [arg.1]`和`mov ecx, [arg.2]`是将参数`a`和`b`的值加载到两个寄存器。
可以看到`cmp`指令这是一个用于比较大小的指令紧跟着的是条件跳转指令表示如果a<b则不跳转继续往下执行这里不多说
直接看到`mov dword ptr ds:[eax], edx`。`dword ptr`是指明数据宽度而这一步操作就是将结果填入变量a所在的内存地址处。
![image-20220327220315921](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220315921.png)
## 数据结构
### 局部变量
现在来谈谈局部变量。
**局部变量**是函数内部定义的一个变量,其作用域和生命周期局限于所在函数内。从汇编角度看,局部变量分配空间时通常会使用栈和寄存器。
编译如下代码:
```c
#include <stdio.h>
int add(int x,int y)
{
int z;
z = x+y;
return z;
}
int main(void)
{
int a=5,b=6;
add(a,b);
return 0;
}
```
进入`add`函数,`sub esp,0xcc`即下降栈顶0xcc个字节实际上是为局部变量开辟空间同时也在预防栈溢出攻击(一种攻击手法,此处不作赘述)。
这里开辟后的空间一部分是用来存放变量z的。
![image-20220327220210074](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220210074.png)
`call`调用完后,会出现`add esp,0x8`这步操作实际上是在平衡栈,可以理解为“回收现场”。
![image-20220327220111908](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220111908.png)
局部变量的起始值是随机的,是其他函数执行后留在栈中的垃圾数据,因此须要对其进行初始化。
### 全局变量
而**全局变量**作用于整个程序,它一直存在,放在全局变量的**内存区**中。
局部变量则存在于函数的**栈区**中,函数**调用结束后便会消失**。
在大多数程序中,**常数**一般放在全局变量中。
全局变量通常位于数据区块(.data)的一个固定地址处,当程序要访问全局变量时,一般会用一个固定的硬编码地址直接对内存进行寻址。
如下是示例程序,`z`是一个全局变量:
```c
#include <stdio.h>
int z;
int add(int x,int y)
{
return x+y+z;
}
int main(void)
{
int a=5,b=6;
z=7;
add(a,b);
return 0;
}
```
这里是对z赋值直接用`mov`将7写入一个固定的内存地址。
![image-20220327220033227](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220033227.png)
在`add`函数中,同样直接从固定的地址中取出`z`的值到eax寄存器中。
![image-20220327220014288](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327220014288.png)
### 数组
最后看看**数组**
```c
#include <stdio.h>
int main(void)
{
static int a[3]={0x11,0x22,0x33};
int i,s=0,b[3];
for(i=0;i<3;i++)
{
s=s+a[i];
b[i]=s;
}
for(i=0;i<3;i++)
{
printf("%d\n",b[i]);
}
return 0;
}
```
一般对数组的访问是通过基址加变址寻址实现的。
![image-20220327215920733](./assets/c10e40ab02e343e3afa9fb19b5619042/image-20220327215920733.png)
在内存中数组可存在于栈、数据段及动态内存中,本例中`a[]`数组就保存在数据段.data中其寻址用“基址+偏移量”实现。
`b[]`数组放在栈中,这些栈在编译时分配。数组在声明时可以直接计算偏移地址,针对数组成员寻址时是采用实际的偏移量完成的。

View file

@ -0,0 +1,271 @@
---
title: 写C语言的种种工具
date: 2022-03-09 22:37:34
tid: tools-to-write-c
categories:
- 技术
- C语言
cover: ./assets/994555a5411c427da8f39428f49b12fb/PYVEjWmMy6tkqdQ.png
description: 写C语言的千奇百怪的编辑器推荐一些在机房使用的编辑器。
permalink: /pages/0066b6/
author:
name: chundot
link: https://chundot.github.io
tags:
-
---
# 写 C 语言的种种工具
## 写在前面
本文记录了千奇百怪的 C/C++编辑器,对各种编辑器做一个简单的评价,避免让大家走弯路踩坑。
文章的本意是想让大家多花些时间在代码上,而不是环境配置上。
希望能够通过本文提升大家在机房打代码的体验。
---
## Dev C++
### 介绍
比较**轻量**的 C/C++的**开源免费**IDE。
[你可以在 SourceForge 上下载安装包(Setup)或者便携程序(Portable)。](https://sourceforge.net/projects/orwelldevcpp/)
**机房的版本是 5.11**也是原团队最后更新的版本2015 年)。
**零配置**就可以编译运行 C/C++代码。
**机考**比较理想的选择,个人开发不算特别推荐,有更好的选择。
### 使用技巧
#### 快速开始
点击**文件** -> **新建** -> **源代码**即可直接开始打代码。
![image-20220309220255982](./assets/994555a5411c427da8f39428f49b12fb/bYGImLQAe6NnvB9.png)
点击**编译并运行**按钮(默认快捷键为`F11`)即可运行你的 C/C++代码。
![image-20220309220436198](./assets/994555a5411c427da8f39428f49b12fb/OyXFgwLKNa3uRWY.png)
#### 中文设置
主界面工具栏 **Tools** -> **Environment Options** (或者键盘依次按下 Alt, t, v)。
![image-20220308155922714](./assets/994555a5411c427da8f39428f49b12fb/iWhrt48meZqUJPF.png)
直接在新窗口**General**一栏**Language**一栏选择**简体中文**即可。
![image-20220308160536079](./assets/994555a5411c427da8f39428f49b12fb/VbXLmk2Ox9dScW3.png)
如果你是**新安装**的,就很简单了,开启界面就会询问语言:
![image-20220308154537126](./assets/994555a5411c427da8f39428f49b12fb/aZTwu1gLAJKOe6S.png)
#### 格式化代码
默认快捷键`Ctrl + Shift + A`,能让你的代码非常**整洁**。
![dev-cpp-format](./assets/994555a5411c427da8f39428f49b12fb/Md9EaDczrFWtxhU.gif)
#### 代码补全
默认快捷键是`Ctrl + Space`,和**切换输入法的快捷键冲突无法使用**,需要在**工具** -> **快捷键选项**中设置其他快捷键后使用。
不像其他 IDE 的**自动提示**Dev C++需要**先按下快捷键**才能出现提示。
![dev-cpp-completion](./assets/994555a5411c427da8f39428f49b12fb/HmnCUGIAvLfJKz1.gif)
除此之外,**C++**也有补全(包括 STL
### 优缺点总结
- 开源免费但已被原团队抛弃2015 年后没有更新);
- 有很多其他团队维护的分支版本,有兴趣的可以在网上搜索;
- **机房**比较合适的选择。
---
## Code::Blocks
### 介绍
[你可以在 Code::Blocks 官网下载安装程序。](http://www.codeblocks.org/downloads/binaries/)
**机房**版本为 16.01。
下载时选择带**mingw**的版本可**零配置**编译运行 C/C++代码。
补全能力非常有限。
另外支持格式化。
**上机考比较理想的选择****个人用也不错**。
适用于**Windows****Linux**及**Mac**。
### 使用方法和技巧
以下演示均为 16.01 版本(即机房的版本)。
#### 快速开始
点击工具栏的中的**File** -> **New** -> **File...**
![image-20220308225001977](./assets/994555a5411c427da8f39428f49b12fb/7Hj84ignasekXy2.png)
弹出的窗口中选择**C/C++**然后点击**Go**
![image-20220309203617508](./assets/994555a5411c427da8f39428f49b12fb/hAaxqHEBoZJrYeg.png)
![image-20220309203903355](./assets/994555a5411c427da8f39428f49b12fb/bToElJW8qcFCBVn.png)
![image-20220309204103773](./assets/994555a5411c427da8f39428f49b12fb/y6IwrJAGpq4uSFj.png)
![image-20220309204314041](./assets/994555a5411c427da8f39428f49b12fb/2ra5ycmElG4OLwo.png)
点击**Build and Run**(默认快捷键为`F9`)即可运行程序。
![image-20220309205239545](./assets/994555a5411c427da8f39428f49b12fb/ns3XE8fithJSgc6.png)
#### 格式化
在编辑器中**右键** -> **Format use AStyle** 即可直接格式化代码。
![cb-format](./assets/994555a5411c427da8f39428f49b12fb/rA2KMziYGCTq367.gif)
#### 代码补全
默认开启代码补全,按`tab`或`Enter`都可以完成代码。
![cb-completion](./assets/994555a5411c427da8f39428f49b12fb/LwG8sKiVemIpqJn.gif)
从测试可以看到代码补全**功能有限**,仅限**头文件****类型****数据结构**和一些**标识符****函数基本无补全**。
虽然测试比较局限,但大部分的机器基本一致。
### 优缺点总结
- 开源免费,更新活跃;
- 代码补全功能聊胜于无;
- **机房**和**小项目开发**比较合适的选择。
---
## C-Free
### 介绍
首先这个软件是**付费**的80 元可以免费升级一年。
其次这个软件**最后一次更新在 2010 年**。
**机房电脑基本都有预装,但不如上面提到的两个 IDE不推荐使用**。
### 优缺点总结
- 付费软件;
- 更新停滞在 2010 年;
- 有其他软件的情况下**不推荐使用**。
---
## Visual Studio Code
### 介绍
微软团队的开源代码编辑器。
[可以在 vs code 官网下载最新版本。](https://code.visualstudio.com/)
想要入门或了解 vs code 的强大功能可以看另一篇文章。
![vscode-w-clang](./assets/994555a5411c427da8f39428f49b12fb/8cWuBzF4XeAjdE2.gif)
配置相对比较复杂,可以检索网上各种 vs code 配置 C/C++的文章。
适合**个人开发**使用,机房可能需要自己安装,**但机考是不会给机会装软件的**。
### 优缺点总结
- 开源免费;
- 支持非常多编程语言的开发(内置支持或扩展支持);
- 扩展丰富;
- 设置和扩展设置十分**复杂**,很多开发需要手动配置;
- 适合**个人项目开发****机房无预装**。
---
## Visual Studio
### 介绍
微软的 IDE。
[可以在 Visual Studio 官网下载各种版本的安装程序](https://visualstudio.microsoft.com/zh-hans/)。
基本可以**零配置**写 C/C++C#等。
这个比较适合写大型的 C/C++项目。
目前最新版已经到了 2022支持 64 位。
安装时只需要**选择自己需要的模块**,之后也可以随时安装卸载其他模块,不需要纠结太多。
![image-20220309201453870](./assets/994555a5411c427da8f39428f49b12fb/WoaDQnP7rtGB2vq.png)
### 优缺点总结
- 社区版免费,功能够用;
- 体积占用过大;
- 开发**有规模的项目**合适;不能运行单文件,必须创建项目;**机房无预装**。
---
## CLion
### 介绍
JetBrains 开发的 IDE。
[可以在官网下载最新版本](https://www.jetbrains.com/clion/)。
基本**零配置**即可编译运行 C/C++代码。
全系软件都是**付费订阅制**,但**在读学生可以免费使用**,用**学校邮箱**注册认证学生身份即可。
### 优缺点总结
- 学生免费;
- 体积占用相对较大;
- 适合开发**有规模的项目****机房无预装**。
## 总结
- 如果上**机房****Dev C++**和**Code::Blocks**这两个预装的软件是比较好的,课程如果有需要机考的强烈建议熟悉一下;
- 其他情况像**大作业和项目开发****vs code**、**Visual Studio**和**CLion**是非常好的选择。
当然,机考能不能过还是要看个人积累的,工具及熟练度只占一小部分。
趁着还在学 C 语言,打好**数据结构**和**算法**的基础,到哪都不慌。
---
## 关于计算机协会
- 计协有自己的博客啦https://nbtca.space/
- 如果你**碰到学习中的种种问题**,想更加**深入学习计算机软硬件知识**;做**网页**,做**游戏**,做**应用**;修**电脑**,玩**硬件**,做**志愿者活动**;不妨来**计算机协会之家**看看,总有热心的程序员~~秃头~~会解决你的问题!
![image-20220309214828491](./assets/994555a5411c427da8f39428f49b12fb/17Yizy52PBMZarL.png)
- 关注我们定期举办的**理工维修日**(不仅仅是维修),成为**志愿者**,或是来这里进行**软件配置**问题和**硬件**咨询,我们非常欢迎!
![image-20220309222044480](./assets/994555a5411c427da8f39428f49b12fb/3TX1Jiphz5MGHKA.png)
<!-- for wechat -->
<!-- ![image-20220309222044480](./assets/994555a5411c427da8f39428f49b12fb/3989f49d21f44a0fa06e1d39c9cc8e5d.png) -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View file

@ -0,0 +1,746 @@
---
title: Go语言极限入门
date: 2022-04-19 10:11:26
categories:
- 技术
- Go语言
cover: ./assets/5973f095d19f42f3b0649a0e2f52f006/image-20220419101405456.png
tid: go-first-tutorial
description: Go语言入门教程。
permalink: /pages/bd144d/
author:
name: N3ptune
link: https://www.cnblogs.com/N3ptune
tags:
-
---
## Go语言极限入门
> 参考书目: 《Go程序设计语言》
>
#### 快速入门
如下是hello world程序
```go
// hello.go
package main
import "fmt"
func main() {
fmt.Println("Hello World")
}
```
终端执行 `go run hello.go`
Go代码是用包来组织的包类似于其他语言中的库和模块。
`package main`指明了这个文件属于哪个包。
后面跟着导入的是其他包的列表fmt用于格式化输出和扫描输入。
main包比较特殊它用来定义一个独立的可执行程序而不是库。import声明必须跟在package声明之后。import导入声明后是组成程序的函数。
一个函数的声明由func关键字、函数名、参数列表(main函数为空)、返回值列表和函数体构成。
###### 命令行参数
命令行参数以os包中Args名字的变量供程序访问在os包外面使用os.Args这个名字这是一个字符串slice。
```go
// echo.go 输出命令行参数
package main
import (
"fmt"
"os"
)
func main() {
var s, sep string
for i := 1; i < len(os.Args); i++ {
s += sep + os.Args[i]
sep = " "
}
fmt.Println(s)
}
```
```shell
$ go build echo.go
./echo hello
hello
```
var 关键字声明了两个string类型的变量s和sep。变量可以声明的时候初始化。如果变量没有明确地初始化它将隐式初始化这个类型的空值。
for 是 go里面唯一的循环语句。
```go
for initlization; condition; post {
//语句
}
```
可选的initialization(初始化)语句在循环开始之前执行。如果存在它必须是一个简单的语句。三部分都是可省的如果三部分都不存在只有一个for那就是无限循环。
另一种形式的for循环是在字符串或slice数据上迭代。
如下是第二种echo程序
```go
// echo.go
package main
import (
"fmt"
"os"
)
func main() {
var s, sep string
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
```
每一次迭代range都产生一对值: 索引和这个索引处元素的值。因为这个例子里用不到索引但是语法上range循环需要处理。应次也必须处理索引。可以将索引赋予一个临时变量然后忽略它但是**go不允许存在无用的变量**。选择使用**空标识符**"__"。空标识符可以用在任何语法需要变量名但逻辑不需要的地方。
如果有大量的数据要处理这样做的代价会比较大。可以使用strings包中的`Join`函数。
```go
package main
import (
"fmt"
"os"
"strings"
)
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
```
###### 找出重复行
如下程序要输出标准输入中出现次数大于1的行前面是次数。
```go
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
counts := make(map[string]int)
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
counts[input.Text()]++
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
```
在上述这个程序中引入了if语句、map类型和bufio包。
像for一样if语句中的条件部分也从不放在圆括号里。
map存储一个键值对集合。在这里map的键是字符串值是数字。内置的函数make可以用来新建map它还可以有其他用途。
`counts := make(map[string]int)`
每次从输入读取一行内容这一行就作为map中的键对应的值递增1。键在map中不存在时也是没有问题的。为了输出结果使用基于range的for循环。
bufio包使用它可以简便和高效地处理输入和输出。其中一个最有用的特性是称为扫描器(Scanner)的类型,可以读取输入,以行或者单词为单位断开。
`input := bufio.NewScanner(os.Stdin)`
Printf函数有超过10个转义字符
| verb | 描述 |
| -------- | ---------------------------- |
| %d | 十进制整数 |
| %x,%o,%b | 十六进制、八进制、二进制整数 |
| %f,%g,%e | 浮点数 |
| %t | 布尔类型 |
| %c | 字符 |
| %s | 字符串 |
| %q | 带引号字符串 |
| %v | 内置格式的任何值 |
| %T | 任何值的类型 |
| %% | 百分号本身 |
如下是从文件中读取字符串:
```go
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
counts := make(map[string]int)
files := os.Args[1:]
if len(files) == 0 {
countLines(os.Stdin, counts)
} else {
for _, arg := range files {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "dup: %v\n", err)
continue
}
countLines(f, counts)
f.Close()
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
func countLines(f *os.File, counts map[string]int) {
input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
}
}
```
读取的文件如下:
```shell
$ cat test.txt
AAAAAAAA
BBBBBBB
AAAAAAAA
CCCCCCC
HHHHHH
```
输入如下:
```shell
$ ./main test.txt
2 AAAAAAAA
```
上述程序是采用"流式"模式读取输入,然后按需拆分为行。
这里引入一个ReadFile函数(从io/ioutil包导入)它读取整个命名文件的内容还引入一个strings.Split函数将一个字符串分割为一个由子串组成的slice
```go
package main
import (
"fmt"
"io/ioutil"
"os"
"strings"
)
func main() {
counts := make(map[string]int)
for _,filename := range os.Args[1:] {
data,err := ioutil.ReadFile(filename)
if err != nil {
fmt.Fprintf(os.Stderr,"dup: %v\n",err)
continue
}
for _,line := range strings.Split(string(data),"\n") {
counts[line]++
}
}
for line,n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n",n,line)
}
}
}
```
ReadFile函数返回一个可以转化成字符串的字节slice这样它可以被strings.Split分割。
###### **获取一个URL**
Go提供了一系列包在net包下面分组管理使用它们可以方便地通过互联网发送和接受信息。
```go
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
for _,url := range os.Args[1:] {
resp,err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr,"fetch: %v\n",err)
os.Exit(1)
}
b,err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr,"fetch: reading %s: %v\n",url,err)
os.Exit(1)
}
fmt.Printf("%s",b)
}
}
```
程序fetch展示从互联网获取信息的最小需求它获取每个指定URL的内容然后不加解析地输出。fetch来自curl工具。
这个程序使用的函数来自两个包: net/http和io/ioutil。http.Get函数产生一个HTTP请求如果没有出错返回结果存在响应结构resp里面其中resp的Body域包含服务器端响应的一个可读取数据流。随后ioutil.ReadAll读取整个响应结果并存入b。
```go
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
fmt.Printf("%s\n", b)
}
}
```
关闭Body数据流来避免资源泄露。
运行结果:
```shell
$ ./fetch https://www.baidu.com
<html>
<head>
<script>
location.replace(location.href.replace("https://","http://"));
</script>
</head>
<body>
<noscript><meta http-equiv="refresh" content="0;url=http://www.baidu.com/"></noscript>
</body>
</html>
```
也可将程序改写:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
_, err = fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
for {
written, err := io.Copy(os.Stdout, resp.Body)
if written == 0 {
break
}
if err != nil {
_, err = fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
}
}
}
```
###### 并发获取多个URL
```go
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"time"
)
func main() {
start := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
go fetch(url, ch) // 启动一个goroutine
}
for range os.Args[1:] {
fmt.Println(<-ch) // 从通道ch接收
}
fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}
func fetch(url string, ch chan<- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprint(err)
return
}
nbytes, err := io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close() // 防止泄露资源
if err != nil {
ch <- fmt.Sprintf("while reading %s: %v", url, err)
return
}
secs := time.Since(start).Seconds()
ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}
```
运行结果:
```shell
$ ./fetchall http://www.baidu.com http://www.qq.com
0.08s 352723 http://www.baidu.com
0.14s 173953 http://www.qq.com
0.14s elapsed
```
这个进程可以并发获取很多URL内容于是这个进程使用的时间不超过耗时最长时间的任务。这个程序不保存响应内容但会报告每个响应的大小和花费的时间。
gorotine是一个并发执行的函数。通道是一种允许某一进程向另一种进程传递制定类型的值的通信机制。main函数在一个goroutine中执行然后go语句创建额外的goroutine。
main函数使用make创建一 个字符串通道。对于每个命令行参数go语句在第一轮循环中启动一个新的goroutine它异步调用fetch来使用http.Get获取URL内容。io.Copy函数读取响应的内容然后通过写入ioutil.Discard输出流进行丢弃。Copy返回字节数和错误信息。每一个结果返回时fetch发送一行汇总信息到通道ch。main中第二轮循环接收并且输出那些汇总行。
###### 一个WEB服务器
如下代码实现一个简单的服务器将返回服务器URL路径部分
```go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
```
运行结果:
```shell
$ ./fetch http://localhost:8000/help
URL.Path = "/help"
```
这里的库函数做了大部分工作。main函数将一个处理函数和以/开头的URL链接在一起代表所有的URL使用这个函数处理然后启动服务器监听8000端口处的请求。一个请求由http.Request类型的结构体表示它包含很多关联的域其中一个是所请求的URL。当一个请求到达时它被转交给处理函数并从请求的URL中提取路径部分使用fmt.Printf格式化然后作为响应发送回去。
为服务器添加功能也很简单,如下程序会返回收到的请求数量:
```go
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
var mu sync.Mutex
var count int
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
// 回显目前为止调用的次数
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}
```
运行结果:
```shell
$ ./fetch http://localhost:8000/
URL.Path = "/"
$ ./fetch http://localhost:8000/
URL.Path = "/"
$ ./fetch http://localhost:8000/count
Count 2
```
这个服务器有两个处理函数通过请求的URL来决定哪一个被调用: 请求/count调用counter其他的调用handler。
以/结尾的处理模式匹配所有含有这个前缀的URL。在后台对于每个传入的请求服务器在不同的goroutine中运行该处理函数这样它可以同时处理多个请求。
然而如果两个并发的请求试图同时更新计数值countcount可能会不一致地增加程序会产生一个严重的竞态BUG。为了避免该问题必须确保最多只有一个goroutine在同一时间访问变量这正是mu.Lock()和mu.Unlock()语句的作用。
修改处理函数,使其可以报告接收到的消息头和表单数据,这样可以方便服务器审查和调试请求。
```go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
fmt.Fprintf(w, "Host = %q\n", r.Host)
fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
}
}
```
运行结果:
```shell
$ ./fetch http://localhost:8000/
GET / HTTP/1.1
Header["User-Agent"] = ["Go-http-client/1.1"]
Header["Accept-Encoding"] = ["gzip"]
Host = "localhost:8000"
RemoteAddr = "127.0.0.1:47766"
```
#### 程序结构
声明是给一个程序实体**命名**并且设定其部分或全部属性。有4个主要声明: 变量(var)、常量(const)、类型(type)函数(func)。
Go程序存储在一个或多个以.go为后缀的文件里。每一个文件以package声明开头表明文件属于哪个包。package 声明后面是import声明然后是*包级别*的类型、变量、常量、函数的声明,不区分顺序。
例如,下面的程序声明一个常量、一个函数和一对变量:
```go
// 输出水的沸点
package main
import "fmt"
const boilingF = 212.0
func main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("boiling point = %g F or %g C\n", f, c)
}
// 输出: boiling point = 212 F or 100 C
```
常量boilingF是一个包级别的声明(main包)f和c是属于main函数的局部变量。包级别的实体名字不仅对于包含其声明的源文件可见而且对于同一个包里面的所有源文件可见。
另一方面,局部声明仅仅是在声明所在的函数内部可见,并且可能对于函数中的一小块区域可见。
**函数的声明**包含一个名字、参数列表(由函数的调用者提供的变量)、一个可选的返回值列表,以及函数体。
下面的函数fToC封装了温度转换的逻辑这样可以只定义一次而在多个地方使用。
```go
package main
import "fmt"
func main() {
const freezingF, boilingF = 32.0, 212.0
fmt.Printf("%g F = %g C\n", freezingF, fToC(freezingF))
fmt.Printf("%g F = %g C\n", boilingF, fToC(boilingF))
}
func fToC(f float64) float64 {
return (f - 32) * 5 / 9
}
/* 输出:
32 F = 0 C
212 F = 100 C
*/
```
#### 变量
通用形式: `var name type = expression`
类型和表达式部分可以省略一个,但不能都省略。
如果类型省略它的类型将由初始化表达式决定。如果表达式省略其初始值对应于类型的零值因此Go中不存在未初始化变量。
###### 短变量声明
在函数中,一种称作**短变量声明**的可选形式可以用来初始化局部变量。
形式: `name := expression`name的类型由expression的类型来决定。
在局部变量的声明和初始化主要使用短声明。
var声明通常是为那些跟初始化表达式类型不一致的局部变量保留的或者用于后面才对变量赋值以及变量初始值不重要的情况。
```go
i := 100
var boiling float64 = 100
i,j := 0,1
```
###### 指针
指针的值是一个变量的地址。
如果一个变量声明为`var x int`,表达式&x获取一个指向整型变量的指针。
```go
x := 1
p := &x // p 是整型指针 只想x
fmt.Println(*p) // "1"
*p = 2 // 等价于x = 2
fmt.Println(x) // 结果"2"
```
每个聚合类型变量的组成都是变量,所以也有一个地址。
指针类型的零值是nil。
函数可以返回局部变量的地址。
```go
var p = f()
func f() *int {
v := 1
return &v
}
```
因为一个指针包含变量的地址,所以传递一个指针参数给函数,能够让函数更新间接传递的变量值。
```go
func incr(p *int) int {
*p++ // 递增p所指向的值 p自身保持不变
return *p
}
v := 1
incr(&v) // v 等于 2
fmt.Println(incr(&v)) // "3"
```
指针对于flag包是很关键的它使用程序的命令行参数来设置整个程序内某些变量的值。
```go
package main
import (
"flag"
"fmt"
"strings"
)
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")
func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
```
flag.Bool函数创建一个新的布尔标识变量它有3个参数。变量sep和n是指向标识变量的指针必须通过sep和n来访问。
当程序运行前在使用标识前必须调用flag.Parse来更新标识变量的默认值。非标识参数也可以从flag.Args()返回的字符串slice来访问。如果flag.Parse遇到错误它输出一条帮助信息然后调用os.Exit(2)来结束程序。
运行示例:
```shell
$ ./echo4 a bc def
a bc def
$ ./echo4 -s / a bc def
a/bc/def
$ ./echo4 -help
Usage of ./echo4:
-n omit trailing newline
-s string
separator (default " ")
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View file

@ -0,0 +1,498 @@
---
title: Linux笔记 - 进程
date: 2022-04-05 13:13:35
categories:
- 技术
- Linux
cover: ./assets/f835945d5de246bcabdff51dd984aaf2/20220320105749.png
tid: linux-note-process
description: 有关linux进程的一系列笔记。
permalink: /pages/324bec/
author:
name: N3ptune
link: https://www.cnblogs.com/N3ptune
tags:
-
---
## Linux进程
本文会简单讲述进程创建和进程间通信。
简单说一下,进程是操作系统调度资源的基本单位,比如将可执行程序跑起来就变成了进程。
进程是由数据(全局变量、静态变量、只读变量...)、代码和堆栈(局部变量、动态分配的内存...)组成的。
进程的模式有用户模式、内核模式。
可以输入`ps`命令来查看正在运行的进程:
![process-1](./assets/f835945d5de246bcabdff51dd984aaf2/process-1.png)
![process-2](./assets/f835945d5de246bcabdff51dd984aaf2/process-2.png)
`ps -aue`命令可以查看更为详细的信息:
![process-3](./assets/f835945d5de246bcabdff51dd984aaf2/process-3.png)
`top`命令可以实时查看正在运行的程序:
![process-4](./assets/f835945d5de246bcabdff51dd984aaf2/process-4.png)
在终端输入`top`输入q可以退出
![process-5](./assets/f835945d5de246bcabdff51dd984aaf2/process-5.png)
Linux系统上可以管理多个进程进程被分时分片处理。
下面演示在程序中如何创建进程:
1. `system`函数可以在命令行中执行命令,可以借此来创建一个进程,不作赘述
![process-6](./assets/f835945d5de246bcabdff51dd984aaf2/process-6.png)
2. `fork`函数、`vfork`函数 创建子进程: 当前进程是父进程,被创建进程是子进程,创建完后父子进程通知执行
![process-7](./assets/f835945d5de246bcabdff51dd984aaf2/process-7.png)
3. `exec`家族
![process-8](./assets/f835945d5de246bcabdff51dd984aaf2/process-8.png)
这些函数都可以创建一个进程,暂且不展开描述。
进程有很多种状态,例如运行时、睡眠、挂起、等待、死亡、僵尸...
下面代码演示:
```c
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("This is parent process,pid: %d\n",getpid());
sleep(2);
printf("Create child process\n");
int ret = fork();
if (ret) {
printf("This is parent process: %d\n",getpid());
while(1) sleep(1);
}
else {
printf("This is child process: %d\n",getpid());
while(1) sleep(1);
}
return 0;
}
```
运行这个程序,程序已经打印出了进程号:
![process-9](./assets/f835945d5de246bcabdff51dd984aaf2/process-9.png)
这里有必要明确一下`fork`函数的返回值:
![process-10](./assets/f835945d5de246bcabdff51dd984aaf2/process-10.png)
根据fork函数的返回值来看是在子进程中还是父进程中调用成功的话父进程中返回值是子进程的ID子进程中是0。实际上这里fork返回了两次。
fork做了两件事第一件是复制结构将父进程的数据结构都复制了一份。
第二件事就是唤醒新进程,让子进程运行起来。
运行如下代码:
```c
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("This is parent process,pid: %d\n",getpid());
sleep(2);
printf("Create child process\n");
int ret = fork();
int n = 10;
if (ret) {
printf("This is parent process: %d\n",getpid());
while(1) {
printf ("n = %d\n",n++);
sleep(1);
}
}
else {
printf("This is child process: %d\n",getpid());
while(1) {
printf("n = %d\n", n += 2);
sleep(1);
}
}
return 0;
}
```
在代码中定义了一个变量n初始值为10子进程的n同样初始为10
![process-11](./assets/f835945d5de246bcabdff51dd984aaf2/process-11.png)
下面谈谈僵尸进程。
有一种情况,父进程创建了子进程,父进程先于子进程结束,子进程资源没有被释放,就会变成僵尸进程,持续占用系统资源(内核中进程树会保存进程的数据,树中节点会保存进程的一些数据)。
子进程结束前会向父进程发送SIGCHILD信号父进程收到信号后回收子进程资源然后父进程再结束。父进程可以写一个wait函数等待子进程发送SIGCHILD信号。
![process-12](./assets/f835945d5de246bcabdff51dd984aaf2/process-12.png)
如下是代码演示:
```c
#include <stdio.h>
#include <unistd.h>
int main(void) {
if (fork()) {
printf("create child process\n");
wait(0);
}
else {
for(int i=0;i<5;i++) {
printf("child process: %d\n",i);
sleep(1);
}
}
printf("end parent process\n");
return 0;
}
```
使用wait函数就是要等待子进程打印完所有数字父进程才结束。
最后看看守护进程。
守护进程是一个独立的进程,最常见的用途就是记录其他进程的情况,保存系统日志。
终端输入`ps axj`可以查看系统中的守护进程:
![process-13](./assets/f835945d5de246bcabdff51dd984aaf2/process-13.png)
TPGID为-1的话就说明是守护进程。
如果要把一个进程变成守护进程,要先`kill`其父进程,同时摆脱终端的控制。
要摆脱终端的控制就要关闭三个文件描述符号标准输入设备标准输出设备标准错误输出设备然后重定向当前进程IO操作到/dev/null (黑洞设备)。然后要创建新的会话,摆脱原有会话进程组的控制。
这里要提到进程的组织形式:多个进程组成一个进程组,多个进程组组成一个会话。这里不详细解释会话是什么。
守护进程创建编程模型:
第一种:
1. 创建新会话 `setsid`;
2. 改变当前工作目录 `chdir`;
3. 重设当前文件权限 `umask`;
4. 关闭文件 `fclose`.
第二种:
    1. 重设文件权限 `umask;`
    2. 创建子进程 `fork`;
    3. 结束父进程;
    4. 创建新会话 `setsid;`
    5. 防止子进程成为僵尸进程 忽略SIGCHILD SIGUP信号;
    6. 改变当前工作目录 `chdir`;
    7. 重定向文件描述符号 `open dup(fd,0) dup(fd,1)` .
下面演示创建守护进程:
```c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <stdbool.h>
#include <fcntl.h>
#include <stdlib.h>
int main(void)
{
// 重设文件权限
umask(0);
// 创建子进程 结束父进程
int ret = fork();
if (ret < 0) {
printf("create process failed: %m\n");
exit(-1);
}
if (ret > 0) {
printf("parent process end\n");
exit(0);
}
if (0 == ret) {
printf("pid: %d\n",getpid());
// 创建新会话
setsid();
// 防止子进程成为僵尸进程 忽略SIGCHILD、SIGUP信号
signal(SIGCHLD,SIG_IGN);
signal(SIGHUP,SIG_IGN);
// 改变当前工作目录
chdir("/");
// 重定向文件描述符号 open、dup函数
int fd = open("/dev/null",O_RDWR);
dup2(fd,0);
dup2(fd,1);
}
//模拟守护进程工作
while (true) {
sleep(1);
}
return 0;
}
```
在终端中编译运行:
![process-14](./assets/f835945d5de246bcabdff51dd984aaf2/process-14.png)
现在关闭终端,在命令行输入`ps -axj`,可以看到运行起来的守护进程还在运行:
![process-15](./assets/f835945d5de246bcabdff51dd984aaf2/process-15.png)
这里要用`kill`杀掉进程。
接下来谈进程间通信。
顾名思义,要实现不同进程间进行传递信息。
这里可以分为两大类,一个是不同主机上的进程间进行通信(网络通信),另一个是同一主机上的进程进行通信。
先谈同一主机上进程通信,此时又可以分为两类:父子进程间通信和非父子进程间通信。
上文提到,`fork`子进程会拷贝父进程的数据,因此父子进程间通信还是比较简单的。
第一种通信方式较为朴素使用普通文件进程A将要传递的信息放入这个文件进程B再去读这个文件即可。父子进程间可通过文件描述符号非父子进程之间就只能通过具体文件来通信。
第二种方式,文件映射虚拟内存 `mmap`
第三种方式,匿名管道(父子间进程使用)和有名管道(非父子进程间使用)。
除此之外,还有信号、共享内存、消息队列、信号量和网络可用于通信。
本文主要讲前3种。
下面代码简单演示了第一种:
```c
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
// 通过文件进行通信
// 父进程往文件里写入数据
// 子进程读出来并打印
int main(void)
{
int fd = open("test.dat",O_CREAT,0666);
if (-1 == fd) {
printf("创建文件失败\n");
exit(-1);
}
printf("创建文件成功\n");
if (fork())
{
int n = 0;
while(true) {
int fd = open("test.dat",O_WRONLY);
write(fd,&n,4);
close(fd);
sleep(1);
n++;
}
}
else
{
int m;
while(true) {
sleep(1);
int fd = open("test.dat",O_RDONLY);
read(fd,&m,4);
close(fd);
printf(">> %d\n",m);
}
}
}
```
运行程序,可以看见子进程将父进程写入文件的数据都打印了出来:
![process-16](./assets/f835945d5de246bcabdff51dd984aaf2/process-16.png)
下面讨论管道。
管道也是FIFO结构分为两种匿名管道和有名管道。
父子进程使用匿名管道。
1. 创建文件描述符号;
2. 将文件描述符号变成管道。
![process-17](./assets/f835945d5de246bcabdff51dd984aaf2/process-17.png)
```c
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
// 使用匿名管道
int main(void)
{
// 创建管道描述符号
int fd[2];
int ret = pipe(fd);
if (ret == -1) {
printf("创建管道失败\n");
exit(-1);
}
printf("创建管道成功\n");
// 父子进程之间使用管道
if (fork()) {
char buff[1024] = {0};
while(true) {
scanf("%s",buff);
write(fd[1],buff,strlen(buff));
}
}
else {
char temp[1024] = {0};
while(true) {
ret = read(fd[0],temp,1023);
if (ret > 0) {
temp[ret] = 0;
printf(">> %s\n",temp);
}
}
}
close(fd[0]);
close(fd[1]);
return 0;
}
```
上述代码的功能是父进程接收用户输入后传入管道,子进程从管道读出并显示。
![process-18](./assets/f835945d5de246bcabdff51dd984aaf2/process-18.png)
接着演示有名管道,流程如下:
| 进程A | 进程B |
| ------------------- | ------------------ |
| 创建管道文件 mkfifo | |
| 打开管道文件 | 打开管道文件 |
| 往管道文件写入数据 | 从管道文件读取数据 |
| 关闭管道 | 关闭管道 |
| 删除管道文件 | |
| | |
![process-19](./assets/f835945d5de246bcabdff51dd984aaf2/process-19.png)
进程A的代码
```c
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(void)
{
// 创建管道文件
int ret = mkfifo("test.pipe",0);
if (-1 == ret) {
printf("创建管道文件失败:%m\n");
exit(-1);
}
printf("创建管道文件成功\n");
// 打开管道文件
int fd = open("test.pipe",O_WRONLY);
if (-1 == fd) {
printf("打开管道文件失败:%m\n");
unlink("test.pipe");
exit(-1);
}
printf("打开管道文件成功\n");
// 循环写入
int n = 0;
char buff[1024] = {0};
while(true) {
sprintf(buff,"Hello Linux %d",n++);
write(fd,buff,strlen(buff));
sleep(1);
}
close(fd);
unlink("test.pipe");
return 0;
}
```
进程B
```c
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(void)
{
// 打开管道文件
int fd = open("test.pipe",O_RDONLY);
if (-1 == fd) {
printf("打开管道文件失败:%m\n");
unlink("test.pipe");
exit(-1);
}
printf("打开管道文件成功\n");
// 循环读取
while (true) {
char buff[1024] = {0};
int ret = read(fd,buff,1023);
if (ret > 0) {
buff[ret] = 0;
printf(">>> %s\n",buff);
}
}
close(fd);
unlink("test.pipe");
return 0;
}
```
运行结果:
![process-20](./assets/f835945d5de246bcabdff51dd984aaf2/process-20.png)
![process-21](./assets/f835945d5de246bcabdff51dd984aaf2/process-21.png)
![process-22](./assets/f835945d5de246bcabdff51dd984aaf2/process-22.png)
这里要注意的是,这个程序不能在共享的文件夹下运行,因为共享文件夹下不能创建管道。同时必须两个进程都打开,这个程序才能返回,否则会阻塞。先关闭读取端,会导致写入端结束而先关写入端,不会对读取端造成影响。

View file

@ -0,0 +1,313 @@
---
title: Linux笔记 - 文件I/O
date: 2022-03-19 22:38:56
categories:
- 技术
- Linux
cover: ./assets/cbea6761a0164906b8396942cf61c664/20220320105749.png
tid: linux-note-file-io
description: 有关linux文件io的一系列笔记。
permalink: /pages/82d273/
author:
name: N3ptune
link: https://www.cnblogs.com/N3ptune
tags:
-
---
## Linux 文件系统结构
文件结构是文件存放在磁盘等存储设备中的组织方法,主要体现在对文件和目录的组织上,目录提供了管理文件的一个方便而有效的途径,用户能够从一个目录切换到另一个目录,而且可以设置目录和文件的权限,设置文件的共享程度。
Linux 文件系统是目录和文件的一种层次安排,目录的起点称为**根**(root),其名字是一个字符`/`目录(directory)是一个包含目录项的文件,在逻辑上可以认为每个目录都包含一个文件名,同时包含说明该文件的信息 ,文件属性是文件类型、文件长度、文件所有者、文件许可权(其他用户是否能访问该文件)、文件最后修改的时间等。
使用 Linux 的用户可以设置目录和文件的权限以便允许或拒绝其他人对其进行访问Linux 目录采用多级树形等级结构,用户可以浏览整个系统,可以进入如何一个已授权进入的目录,访问那里的文件。
文件结构的相互关联性使共享数据变得很容易,几个用户可以访问同一个文件。
Linux 是一个**多用户系统**,操作系统本身的驻留程序存放在以根目录开始的专用目录中,有时被指定为系统目录。 **内核**、**Shell**和**文件结构**一起形成了 Linux 的基本操作系统结构。它们使得用户可以允许程序, 管理文件及使用系统。
![image-20220320093722657](./assets/cbea6761a0164906b8396942cf61c664/image-20220320093722657.png)
### 文件类型
在终端输入`ls -l`命令 系统会打印处当前目录下所有文件的信息。
![image-20220320100405899](./assets/cbea6761a0164906b8396942cf61c664/image-20220320100405899.png)
这些信息包括:**文件类型**、**文件属性**、**用户名**、**用户所在组**、**文件大小**、**修改时间**、**文件名**等。
而其中的第一栏信息是文件类型和文件属性 包含了 10 个字符 分为 4 组:
- 第**1**位表示文件的类型;
- 第 2 组为**2-4**位 代表文件所有者(User)权限,分别为**读**、**写**、**执行**
- 第 3 组为**5-7**位代表文件所有者的同组用户(Group)的权限,分别为**读**、**写**、**执行**
- 第 4 组为**8-10**位 代表其他组用户(Other)权限,分别为**读**、**写**、**执行**。
1. **普通文件**:符号是 - 例如保存在磁盘上的 C 文件、可执行的文件以及目录等,这种文件的特性是数据在存储 设备上存放,内核提供了对数据的抽象访问,此种文件为一种**字节流**,访问接口完全独 立于磁盘上的存储数据
2. **目录文件**:符号是 d
3. **字符设备文件**:符号是 c ,是一种能够像文件一样被访问的设备,例如控制台、串口等
4. **块设备文件**:符号是 b 磁盘是此类设备文件的典型代表,与普通文件的区别是操作系统对数据的访问进行的**重新的格式设计**
5. **符号链接文件**:符号是 l
6. **socket 文件**:是 UNIX 中通过网络进行通信的方式,对网络的访问可以通过文件描述符实现,访问网络和访问普通文件相似。
在 Linux 中用户空间对各种文件的操作是类似的,因为虚拟文件系统 VFS 同一套 API。
#### 简单实验
文件系统的创建:
![image-20220320100449488](./assets/cbea6761a0164906b8396942cf61c664/image-20220320100449488.png)
### 基于文件描述符的 IO 操作
#### 文件描述符
操作系统内核(kernel)利用文件描述符(file descriptor)来访问文件。
文件描述符是一个非负整数,是一个用于描述被打开文件的索引值,它指向该文件的相关信息的记录表。
当内核打开一个现存文或创建一个新文件时,就会返回一个文件描述符,当读、写文件时, 也需要使用文件描述符来指定待读写的文件。
##### POSIX 可移植操作系统接口
文件描述符的有效范围是 0 到 OPEN_MAX 一般来说,每个进程最多打开 1024 个文件(0~ 1023),这个值可以使用 ulimit -n 命令查看。
文件描述符是由无符号整数表示的句柄,进程使用它来标识打开的文件。
文件描述符与包括相关信息(如文件的打开模式、文件的位置类型、文件的初始类型等)的文件对象相关联,这些信息被称作文件的**上下文**。
##### 标准输入、标准输出和标准出错
在 UNIX\Linux 系统中,每当运行一个新程序时,所有的 shell 都为其打开 3 个文件描述符: 标准输入、标准输出及标准出错,分别对应 0 1 2。
#### 文件的创建、打开和关闭
要对一个文件进行操作,首先要求这个文件**存在**,其次是要在操作之前将这个文件**打开**,这样才能实现对该文件的操作,当完成操作后,则必须将文件关闭,文件的创建、打开与关闭是文件 I/O 操作的第一步。
##### open 函数
调用`open`函数可以打开或创建一个文件。
函数原型:`int open(const char* pathname,int flags);`
增加一个参数:`int open(const char* pathname,int flags,mode_t mode);`
返回值:若成功则返回文件描述符,若出错则返回-1。其中参数`pathname`是一个字符串指针,指向需要打开(或创建)文件的**绝对路径名**或**相对路径名**。
![image-20220320101157474](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101157474.png)
![image-20220320102217610](./assets/cbea6761a0164906b8396942cf61c664/image-20220320102217610.png)
`mode`取值:
![image-20220320102222415](./assets/cbea6761a0164906b8396942cf61c664/image-20220320102222415.png)
###### 案例程序
![image-20220320102302686](./assets/cbea6761a0164906b8396942cf61c664/image-20220320102302686.png)
将`FLAGS`定义为:只写,不存在则创建,文件长度截短为 0。
定义`mode`:创建文件的权限为用户读写执行,组读、执行,其他用户读、执行。
![image-20220320102333633](./assets/cbea6761a0164906b8396942cf61c664/image-20220320102333633.png)
运行程序后创建一个新文件:`test.txt`。
查看其权限属性,可见恰好与创建时定义的 mode 吻合。
![image-20220320102355312](./assets/cbea6761a0164906b8396942cf61c664/image-20220320102355312.png)
##### creat 函数
用于创建文件。
![image-20220320101227035](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101227035.png)
函数原型:`creat(const char *pathname,int flags);`
若成功则返回以只写方式打开的文件描述符,若出错为-1 参数 pathname 和 mode 的含义与 open 函数的含义相同。
`creat`函数等效于`open(pathname,O_WRONLY|O_CREAT|O_TRUNC,mode);`其有一个不足之处是它以只写方式打开所创建的文件。
在提供 open 的新版本之前,如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用`create`、`close`,然后再调用`open`。
##### close 函数
close 函数用于关闭一个文件。
![image-20220320101434997](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101434997.png)
若成功则返回 0若出错则返回-1。
参数`fd`是需关闭文件的文件描述符 系统调用`close`不经将释放该文件的描述符,而且也将释放该文件所占的描述表项。
关闭一个文件时也释放该进程加载该文件上所有的记录锁。当一个进程终止时,它所有的打开文件都由内核自动关闭。
文件的定位 每个已打开的文件都有一个与其相关联的**当前文件位移量**,它是一个非负整数,用以度量从文件开始处计算的字节数。
通常,读、写操作都从当前文件位移量处开始,并且使位移量增加所读或写的字节数。
系统默认设置位移量为 0。
![image-20220320101454081](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101454081.png)
可以调用`lseek`函数显式地定位一个打开文件 若成功则返回新的文件位移量,出错则返回-1。
l 表示长整型 参数`fd`表示已打开文件的描述符,参数`offset`表示位移量大小,单位字节,对参数`offset`的解释于参数`whence`的取值有关。
![image-20220320101504852](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101504852.png)
可见当`whence`取`SEEK_CUR`和`SEEK_END`时,可以取负值。
###### 测试程序
![image-20220320102440403](./assets/cbea6761a0164906b8396942cf61c664/image-20220320102440403.png)
该程序可测试标准输入能否被设置成偏移量:
![image-20220320102453240](./assets/cbea6761a0164906b8396942cf61c664/image-20220320102453240.png)
默认标准输入是键盘 从程序的运行结果看出 对于标准输入一般不能设置位移量。
但对系统中文件以及用户创建的一般文件是可以设置位移量的。
通常情况下 文件的当前位移量应当是一个非负整数,但是某些设备也可能允许负的位移量。但对于普通文件,则其位移量必须是非负值。
`lseek`仅将当前的文件位移量记录在内核内,并不能引起如何 I/O 操作,然后该位移量用于下一个读或写操作。
文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次操作将延长该文件,并在文件中构成一个空洞,这一点是允许的。
#### 文件的读写
文件的读写操作的系统调用分别是`read`和`write`。
##### read 函数
从打开文件中读取数据返回:读到的字节数,若已到文件尾返回 0若出错为-1。
![image-20220320095118775](./assets/cbea6761a0164906b8396942cf61c664/image-20220320095118775.png)
- 其中参数`fd`表示要进行读操作的文件的描述符;
- `buf`是一个指向缓冲区的指针,该缓冲区存放将要读取到中断的数据;
- `count`表示本次操作将要读取的数据的字节数。
读操作从文件的当前位移量处开始,在成功返回之前,该位移量增加实际读得的字节数
有几种情况可使实际读到的字节数少于要求读的字节数:
1. 读普通文件时,在读到要求字节数之前已到达了文件尾端;
2. 从终端设备读时,通常一次最多读一行;
3. 从网络中读;
4. 面向记录的设备。
##### write 函数
![image-20220320095155376](./assets/cbea6761a0164906b8396942cf61c664/image-20220320095155376.png)
打开文件写入数据,若成功则返回写的字节数,若出错为-1。
![image-20220320095200261](./assets/cbea6761a0164906b8396942cf61c664/image-20220320095200261.png)
对于普通文件 ,写操作从文件的当前位移量处开始,如果在打开该文件时,指定了 O_APPEND 选择项,则在每次写操作之前,将文件位移量设置在文件的当前结尾处。在一次成功操作写之后,该文件位移量增加实际写的字节数。
###### 案例程序
![image-20220320103210374](./assets/cbea6761a0164906b8396942cf61c664/image-20220320103210374.png)
![image-20220320103621058](./assets/cbea6761a0164906b8396942cf61c664/image-20220320103621058.png)
###### 另外的程序
![image-20220320105413049](./assets/cbea6761a0164906b8396942cf61c664/image-20220320105413049.png)
该程序首先打开(或创建)文件`/home/hwx/test.txt`接着调用 write 函数将缓冲区 1 的数据写入文件,此时写入的数据长度为 10然后调用 lseek 定位文件,文件偏移量为 50并从文件开头计算偏移值最后调用 write 将缓冲区 2 的数据也写入文件,写入的数据长度也为 10。
在第二次写入数据时 之间 40 字节的内容为空 文件的总长度为 60 成功运行程序后 利用 ls 和 od 命令可以检验出:
![image-20220320105426083](./assets/cbea6761a0164906b8396942cf61c664/image-20220320105426083.png)
使用 od 命令观察该文件的实际内容。(命令行中的-c 标志表示以字符方式打印文件内容)
中间 40 字节全为 0。
### 文件的属性操作
Linux 的文件系统具有比较复杂的属性,包括**文件访问权限**、**文件所有者**、**文件名本身**、**文件长度**等。
![image-20220320101732493](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101732493.png)
改变文件访问权限:`chmod`、`fchmod`这两个函数使用户可以更改现存文件的存取许可权
两个函数的返回:若成功则为 0出错则为-1。
`chmod`函数在指定的文件上进行操作pathname 指定了这个文件的绝对路径名或相对路径名,而`fchmod`函数则对已打开的文件进行操作,`fd`是这个打开文件的描述符。
为了改变一个文件的访问许可权位 进位的有效用户 ID 必须等于文件的所有者(User)或者该进程必须具有 root 权限。
![image-20220320101745292](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101745292.png)
当前`test.txt`文件权限:
![image-20220320101820792](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101820792.png)
![image-20220320101824816](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101824816.png)
可见运行程序后文件权限被改变。
### 文件的其他操作
Linux 系统中所有文件都有一个与之对应的索引节点,该节点包含了文件的相关信息。
这些信息被保存在 stat 结构体中,可以通过调用下面 3 个 stat 函数来返回文件的信息:
![image-20220320101844194](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101844194.png)
![image-20220320101854044](./assets/cbea6761a0164906b8396942cf61c664/image-20220320101854044.png)
**stat**函数、**fstat**函数和**lstat**函数。
若成功则返回 0出错则返回-1。
- **stat**函数返回指定文件的信息结构,参数 pathname 指定了该文件的绝对路径名或相对路径名;
- **fstat**函数获得已在描述符 fd 上打开的文件的有关信息;
- **lstat**函数类似于 stat但是当命名的文件是一个符号链接时lstat 返回该符号链接的有关信息,而非由该符号链接引用的文件的信息 0。
## 基于流的 I/O
### 流和 FILE 对象
对于标准 I/O 库,它们的操作则是围绕流进行的,当用标准 I/O 库打开或创建一个文件时已使一个流与一个文件相结合。I/O 函数 fopen 返回一个指向 FILE 对象的指针。该对象通常是一个结构体,包含了 I/O 库为管理该流所需要的所有信息,包括实际用于 I/O 的文件描述符,指向流缓存的指针,缓存的长度,当前在缓存中的字符数,出错标志等。
### 大致过程
对流进行操作的第一步是通过调用`fopen`函数将其打开,并返回一个 FILE 结构指针。当流成功打开以后就可以调用相应的库函数对其进行 I/O 操作。当完成操作后要执行清空缓冲区、保存数据等操作,然后将流关闭,这些工作可通过`fclose`函数来完成。
当使用流 I/O 时,有 3 个流会自动地打开:标准输入、标准输出和标准错误,用文件描述符`STDIN_FILENO`、`STDOUT_FILENO`、`STDERR_FILENO`表示它们,这 3 个符号定义在头文件`unistd.h`。
而在基于流地 I/O 操作中,通过预定义文件指针`stdin`、`stdout`和`stderr`来引用标准输入、标准输出和标准出错。
### 缓存
基于流地操作最终会调用`read`或者`write`函数进行 I/O 操作。为了提高程序的运行效率,尽可能减少使用 read 和 write 调用的数量,流对象通常会提供缓冲区,以减少调用系统 I/O 库函数的次数,缓存用 3 种类型:
1. 全缓存(`_IO_FULL_BUF`):直到缓冲区被填满,才调用系统 IO 函数,直到读入的内容的字节数等于缓冲区大小或者文件已经到达结尾,才进行实际的 I/O 操作,将外村文件内容读入缓冲区,对于写操作来说,直到缓冲区被填满,才进行实际的 I/O 操作;
2. 行缓存(`_IO_LINE_BUF`):直到遇到换行符`\n` 才调用系统 I/O 库函数,对于读操作来说,遇到换行符`\n`才进行 I/O 操作,将缓冲区内容写到外存中。由于缓冲区大小是有限的,所以当缓冲区被填满时,即使没有遇到换行符`\n`,也同样会进行实际的 I/O 操作;
3. 无缓存(`_IO_UNBUFFERED`):没有缓冲区,数据会立即读入或者输出到外存文件和设备上。标准出错 stderr 是无缓冲的,这样保证错误提示和输出能够即使反馈给用户,供用户排除错误。
## 深入探究 I/O
### 原子操作和竞争条件
所有系统调用都是以原子操作方式执行的,内核保证了某系统调用中的所有步骤会作为独立操作而一次性加以执行,其间不会为其他进程或线程所中断。
原子性规避了竞争状态,竞争状态:操作共享资源的两个进程(或线程),其结果取决于一个无法预期的顺序,即这些进程获得 CPU 使用权的先后相对顺序。
### 文件 I/O 的两种竞争状态
以独占方式创建一个文件, 当同时指定`O_EXCL`与`O_CREAT`作为`open`的标志位时,如果要打开的文件依然存在,则`open`将返回一个错误。
这提供了一种机制,保证进程是打开文件的创建者。对文件是否存在的检查和创建文件属于同一原子操作。
![image-20220320100348242](./assets/cbea6761a0164906b8396942cf61c664/image-20220320100348242.png)

View file

@ -0,0 +1,338 @@
---
title: Linux笔记 - 信号
date: 2022-04-05 13:13:28
categories:
- 技术
- Linux
cover: ./assets/dfc39c2948664aa78df4a4b4aa11bb2c/20220320105749.png
tid: linux-note-signal
description: 有关linux信号的一系列笔记。
permalink: /pages/aff776/
author:
name: N3ptune
link: https://www.cnblogs.com/N3ptune
tags:
-
---
## **Linux 信号**
信号本质上是整数,用户模式下用来模拟硬件中断。
- 什么是硬件中断?先理解为硬件引发的中断。
- 什么是中断简言之就是让CPU停下当前干的事转而去处理新的情况。
- 信号是谁产生的?可以由硬件、内核和进程产生。
例如在终端上用`Ctrl+C`可以结束掉当前终端进程本质上是发送SIGINT信号。
如下是一个实例程序,该程序循环打印数字。运行起来,看看`Ctrl+C`能不能中止它:
```c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
void hand(int val)
{
printf("val: %d\n",val);
printf("Do you want to kill me? No way!\n");
}
int main()
{
signal(SIGINT,hand);
int n = 0;
while (true) {
printf("n:%d\n",n++);
sleep(1);
}
return 0;
}
```
如下所示现在使用Ctrl+C是没办法中断这个程序的
![signal-1](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-1.png)
这里可以用man命令查看一下signal函数的帮助文档
![signal-2](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-2.png)
signal关联上了一个信号处理函数来处理信号。
现在修改这个代码,添加一行打印当前进程号,然后打开两个终端,一个终端运行上面的程序,一个终端尝试`kill`命令来发送信号:
![signal-3](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-3.png)
kill 要指定参数:
![signal-4](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-4.png)
这是可行的,结果如上。
**信号处理的过程**
进程A在运行内核、硬件或者其他进程发送信号给进程A。进程A接收到信号后直接做信号本身规定的对应处理或者做事先注册好的信号处理。如上面signal函数就是注册信号处理hand函数替换了本身默认的信号处理。当然信号是可以屏蔽的不作任何处理。
Linux有哪些信号类型
![signal-5](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-5.png)
Linux 信号有64个分为不可靠信号(非实时1-31Unix提供)和可靠信号(32-64后来扩充的)。又有系统自带的标准信号和用户自定义的信号。
介绍几个命令或函数:
- 信号注册:`signal`、`sigaction;`
- 信号发送:`kill`命令、`kill`函数、`sigqueue;`
- 信号屏蔽:`sigprocmask`;
- 信号集:`sigset_t`.
下面写一个发送信号的程序:
```c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc,char* argv[])
{
int pid = atoi(argv[1]);
int sig = atoi(argv[2]);
printf("pid:%d sig:%d\n",pid,sig);
kill(pid,sig);
return 0;
}
```
![signal-6](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-6.png)
![signal-7](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-7.png)
成功发送了信号。
`sigaction`函数有所不同,功能更多:
![signal-8](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-8.png)
这里用到了一个结构体:
![signal-9](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-9.png)
结构体的第一个成员依然是原来的信号处理函数,第二个成员被称作高级信号处理函数,第三个成员用于信号屏蔽,剩下两个暂时还用不到。
这个函数不仅可以发信号、做信号处理,还可以接收信号的同时接收数据
下列代码使用了高级信号处理:
```c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
// 基本信号处理函数
void hand(int n)
{
printf("基本信号处理函数\n");
}
// 高级信号处理函数
void handler(int n,siginfo_t* siginfo,void* arg)
{
printf("高级信号处理函数\n");
printf("n:%d msg:%d\n",n,siginfo->si_int);
}
int main(void)
{
struct sigaction act = {0};
struct sigaction old_act = {0};
act.sa_handler = hand;
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
// 高级信号处理
sigaction(2,&act,&old_act);
printf("pid:%d\n",getpid());
int n = 0;
while (true) {
printf("n:%d\n",n++);
sleep(1);
}
return 0;
}
```
同时还要实现高级的信号发送,采用`sigqueue`
![signal-10](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-10.png)
这里有个重要的联合体,用来存储数据和信号一起发送:
![signal-11](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-11.png)
```c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc,char* argv[])
{
int pid = atoi(argv[1]);
int sig = atoi(argv[2]);
printf("pid:%d sig:%d\n",argv[1],argv[2]);
union sigval u;
u.sival_int = 12345678;
sigqueue(pid,sig,u);
return 0;
}
```
运行结果如下:
![](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-12.png)
![signal-13](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-13.png)
可以看到程序接收到了信号和数据。
也试试发送其他数据,联合体中还有一个指针类型的成员。
## Linux信号(续)
> 参考网站: Github、《极客时间》
现在从底层角度来观赏Linux信号机制。
首先看信号的具体作用:
![signal-14](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-14.png)
![signal-15](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/signal-15.png)
如上文所提处理信号有3种方式执行默认操作捕获信号和忽略信号
设置信号处理有两种方式: signal和sigaction
signal将一个动作和信号关联sigaction也是将一个动作和信号关联但这个动作用一个结构体表示了处理信号将更加细致
siganal不是系统调用而是glibc封装的一个函数实现如下
```c
# define signal __sysv_signal
__sighandler_t
__sysv_signal (int sig, __sighandler_t handler)
{
struct sigaction act, oact;
......
act.sa_handler = handler;
__sigemptyset (&act.sa_mask);
act.sa_flags = SA_ONESHOT | SA_NOMASK | SA_INTERRUPT;
act.sa_flags &= ~SA_RESTART;
if (__sigaction (sig, &act, &oact) < 0)
return SIG_ERR;
return oact.sa_handler;
}
weak_alias (__sysv_signal, sysv_signal)
```
这里可以看到 sa_flags 设置了一个SA_ONESHOT这意味着信号处理函数只作用一次用完一次后就返回默认行为
同时设置了SA_NOMASK通过 __sigemptyset将 sa_mask 设置为空。这样的设置表示在这个信号处理函数执行过程中,如果再有其他信号,哪怕相同的信号到来的时候,这个信号处理函数会被中断。如果一个信号处理函数真的被其他信号中断,问题也不大,因为当处理完了其他的信号处理函数后,还会回来接着处理这个信号处理函数的,但是对于相同的信号就有点尴尬了。一般的思路应该是,当某一个信号的信号处理函数运行的时候,暂时屏蔽这个信号,屏蔽并不意味着信号一定丢失,而是暂存,这样能够做到信号处理函数对于相同的信号,处理完一个再处理下一个,这样信号处理函数的逻辑要简单得多。
还有一个设置就是设置了 SA_INTERRUPT清除了 SA_RESTART。信号的到来时间是不可预期的有可能程序正在调用某个漫长的系统调用的时候突然到来一个信号中断了这个系统调用去执行信号处理函数。那么执行完后信号处理函数系统调用怎么办
时候有两种处理方法,一种就是 SA_INTERRUPT也即系统调用被中断了就不再重试这个系统调用了而是直接返回一个 -EINTR 常量,告诉调用方,这个系统调用被信号中断了,调用方可以根据自己的逻辑,重新调用或者直接返回,这会使得代码非常复杂,在所有系统调用的返回值判断里面,都要特殊判断一下这个值。
另外一种处理方法是 SA_RESTART。这个时候系统调用会被自动重新启动不需要调用方自己写代码。当然也可能存在问题例如从终端读入一个字符这个时候用户在终端输入一个'a'字符,在处理'a'字符的时候被信号中断了,等信号处理完毕,再次读入一个字符的时候,如果用户不再输入,就停在那里了,需要用户再次输入同一个字符。
可知signal函数是受到限制的因此建议使用 sigaction 函数,根据自己的需要定制参数。
下面是glibc中的实现
```c
int
__sigaction (int sig, const struct sigaction *act, struct sigaction *oact)
{
......
return __libc_sigaction (sig, act, oact);
}
int
__libc_sigaction (int sig, const struct sigaction *act, struct sigaction *oact)
{
int result;
struct kernel_sigaction kact, koact;
if (act)
{
kact.k_sa_handler = act->sa_handler;
memcpy (&kact.sa_mask, &act->sa_mask, sizeof (sigset_t));
kact.sa_flags = act->sa_flags | SA_RESTORER;
kact.sa_restorer = &restore_rt;
}
result = INLINE_SYSCALL (rt_sigaction, 4,
sig, act ? &kact : NULL,
oact ? &koact : NULL, _NSIG / 8);
if (oact && result >= 0)
{
oact->sa_handler = koact.k_sa_handler;
memcpy (&oact->sa_mask, &koact.sa_mask, sizeof (sigset_t));
oact->sa_flags = koact.sa_flags;
oact->sa_restorer = koact.sa_restorer;
}
return result;
}
```
内核代码注释表明,系统调用 signal 是为了兼容过去,系统调用 sigaction 也是为了兼容过去,连参数都变成了 struct compat_old_sigaction所以说我们的库函数虽然调用的是 sigaction到了系统调用层调用的可不是系统调用 sigaction而是系统调用 rt_sigaction。
`rt_sigaction` 里面,将用户态的 struct sigaction 结构,拷贝为内核态的 `k_sigaction`,然后调用 `do_sigaction`
`do_sigaction` 也很简单进程内核的数据结构里struct task_struct 里面有一个成员 sighand里面有一个 action。
这是一个数组,下标是信号,内容就是信号处理函数,`do_sigaction` 就是设置 sighand 里的信号处理函数。
```c
int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
{
struct task_struct *p = current, *t;
struct k_sigaction *k;
sigset_t mask;
......
k = &p->sighand->action[sig-1];
spin_lock_irq(&p->sighand->siglock);
if (oact)
*oact = *k;
if (act) {
sigdelsetmask(&act->sa.sa_mask,
sigmask(SIGKILL) | sigmask(SIGSTOP));
*k = *act;
......
}
spin_unlock_irq(&p->sighand->siglock);
return 0;
}
```
总结:
![](./assets/dfc39c2948664aa78df4a4b4aa11bb2c/7cb86c73b9e73893e6b0e0433d476928.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Some files were not shown because too many files have changed in this diff Show more