ZYZ

Unix程序设计头歌知识点整理

有 N 人看过

Linux编程基础

vi/vim工作模式切换

vi/vim编辑器有三种工作模式,每种工作模式都有不同的作用,以下是这三种工作模式的详细介绍:

  1. **命令模式: **查看当前文件内容,此时不能对文件内容进行写入操作,从该模式可以切换为插入模式和底线命令模式。
  2. **插入模式: **可以对文件内容进行编辑操作,从该模式可以切换为命令模式。
  3. **底线命令模式: **不可以对文件内容进行编辑,在此模式下可以执行一些vi/vim的命令,例如: 退出命令、保存内容命令等等。从该模式可以切换为命令模式。

img

注意: 启动vi/vim后,首先进入的是命令模式。

命令模式与插入模式相互切换

首先启动vi/vim编辑器后,首先进入的工作模式是命令模式,在当前模式下,我们只能查看文件内容,不能对文件内容进行写入操作。如果想对文件进行写入操作,那么我们只有进入插入模式下。

  1. 命令模式->插入模式方法 从命令模式到插入模式的切换方法有多种,我们介绍如下3中常用方法:
输入命令 说明
i, I i 为『从目前光标所在处输入』,I 为『在目前所在行的第一个非空格符处开始输入』。
a, A a 为『从目前光标所在的下一个字符处开始输入』, A 为『从光标所在行的最后一个字符处开始输入』。
o, O 这是英文字母 o 的大小写。o 为『在目前光标所在的下一行处输入新的一行』; O 为在目前光标所在处的上一行输入新的一行。
  1. 插入模式->命令模式方法 由插入模式切换到命令模式比较简单,我们只需要点击ESC键即可返回到命令模式。

案例演示1:

使用vi/vim编辑器打开文件testFile,并且将工作模式切换到插入模式,输入Hello vi/vim字符串,最后保存文件并退出,可以使用如下命令:

1
vi testFile` 或 `vim testFile

打卡testFile文件命令; img

首先进入的是命令模式; img

按下字母i后进入插入模式; img

输入Hello vi/vim字符后,按下ESC键后返回命令模式,最后输入:wq保存退出文件; img [请在右侧“命令行”里直接体验]

命令模式与底线命令模式相互切换

vi/vim底线命令模式下如何执行写复杂的命令,例如我们常用的保存退出命令(wq)等。

  1. 命令模式->底线命令模式方法 从命令模式到底线命令模式的切换比较简单,我们只需要输入:字符即可,注意:是英文输入法下的冒号。
  2. 底线命令模式->命令模式方法 由底线命令模式切换到命令模式比较简单,我们只需要点击ESC键即可返回到命令模式。

vi/vim命令模式

vi/vim命令模式下,我们可以对文件进行删除、复制和粘贴操作。

命令模式移动光标操作

vi/vim编辑器与其它编辑器最大的不同之处是不能使用鼠标进行操作(可以在配置文件中设置鼠标属性,默认是禁止使用鼠标),我们可以在命令模式下移动光标位置,常见移动命令如下所示:

命令 说明
h 或 向左箭头键(←) 光标向左移动一个字符
j 或 向下箭头键(↓) 光标向下移动一个字符
k 或 向上箭头键(↑) 光标向上移动一个字符
l 或 向右箭头键(→) 光标向右移动一个字符
[Ctrl] + [f] 屏幕『向下』移动一页,相当于 [Page Down]按键
[Ctrl] + [b] 屏幕『向上』移动一页,相当于 [Page Up] 按键
[Ctrl] + [d] 屏幕『向下』移动半页
[Ctrl] + [u] 屏幕『向上』移动半页

案例演示1:

使用vi/vim编辑器打开文件oldFile,移动当前光标到第一行的第二字符处,可以使用如下步骤:

打卡oldFile文件命令; img

首先进入的是命令模式; img

移动光标到第一行的第5个字符处(按5次→); img

最后输入:q退出文件; img [请在右侧“命令行”里直接体验]

命令模式删除操作

我们不光可以在插入模式下可以对文件内容进行删除操作,我们可以直接在命令模式下对文件进行删除操作,常见删除命令如下所示:

命令 说明
x, X 在一行字当中,x 为向后删除一个字符 (相当于 [del] 按键), X 为向前删除一个字符(相当于 [backspace] 亦即是退格键)
nx n 为数字,连续向后删除 n 个字符。例如,我要连续删除 5 个字符 ,则可以使用5x
dd 删除光标所在的那一整行
ndd n 为数字。删除光标所在的向下 n 行,例如10dd则是删除 10 行
d1G 删除光标所在到第一行的所有数据
dG 删除光标所在到最后一行的所有数据

案例演示1:

使用vi/vim编辑器打开文件oldFile,删除当前文件的第二行所有内容,最后保存文件并退出,可以使用如下步骤:

打卡oldFile文件命令; img

首先进入的是命令模式; img

移动光标到文件第二行; img

输入dd字符后删除当前行内容,最后输入:wq保存退出文件; img [请在右侧“命令行”里直接体验]

命令模式复制粘贴操作

常见复制命令如下所示:

命令 说明
yy 复制光标所在的那一行
nyy n 为数字。复制光标所在的向下 n 行,例如 10yy 则是复制 10 行
y1G 复制光标所在行到第一行的所有数据
yG 复制光标所在行到最后一行的所有数据
y0 复制光标所在的那个字符到该行行首的所有数据
y$ 复制光标所在的那个字符到该行行尾的所有数据

常见粘贴命令为p, Pp 为将已复制的数据在光标下一行贴上,P 则为贴在游标上一行!

案例演示1:

使用vi/vim编辑器打开文件oldFile,将第一行内容复制,然后粘贴到文件的末尾,最后保存文件并退出,可以使用如下步骤:

打卡oldFile文件命令; img

首先进入的是命令模式; img

复制第一行内容(yy),移动光标到最后一行,粘贴(p)内容到当前行的下一行,最后输入:wq保存退出文件; img [请在右侧“命令行”里直接体验]

vi/vim底线命令模式

搜索替换

vi/vim编辑器在底线命令模式下可以对文件内容进行查找和替换操作,常见查找和替换命令如下所示:

命令 说明
/word 向光标之下寻找一个名称为 word 的字符串。例如要在档案内搜寻 vbird 这个字符串,就输入 /vbird 即可。
?word 向光标之上寻找一个字符串名称为 word 的字符串。
n 这个 n 是英文字母。代表重复前一个搜寻的动作。举例来说, 如果刚刚我们执行 /vbird 去向下搜寻 vbird 这个字符串,则按下 n 后,会向下继续搜寻下一个名称为 vbird 的字符串。
N 这个 N 是英文按键。与 n 刚好相反,为『反向』进行前一个搜寻动作。 例如 /vbird 后,按下 N 则表示『向上』搜寻 vbird 。
[:n1,n2s/word1/word2/g n1 与 n2 为数字。在第 n1 与 n2 行之间寻找 word1 这个字符串,并将该字符串取代为 word2 。
:1,$s/word1/word2/g 从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2。
:1,$s/word1/word2/gc 从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2 !且在取代前显示提示字符给用户确认 (confirm) 是否需要取代。

案例演示1:

使用vi/vim编辑器打开文件oldFile,将所有line单词替换为words单词,并保存退出,可以使用如下步骤:

打卡oldFile文件命令; img

首先进入的是命令模式; img

首先输入:切换当前模式为底线命令模式,然后输入1,$s/line/words/g后回车; img

img [请在右侧“命令行”里直接体验]

底线命令模式下执行特殊命令

常见在底线命令模式执行的命令如下所示:

命令 说明
:w 将编辑的数据写入硬盘档案中
:w! 若文件属性为『只读』时,强制写入该档案。不过,到底能不能写入, 还是跟你对该档案的档案权限有关啊!
:q! 若曾修改过档案,又不想储存,使用 ! 为强制离开不储存档案。
:w [filename] 将编辑的数据储存成另一个档案(类似另存新档)
:n1,n2 w [filename] 将 n1 到 n2 的内容储存成 filename 这个档案。
:! command 暂时离开 vi 到指令行模式下执行 command 的显示结果!
:set nu 显示行号,设定之后,会在每一行的前缀显示该行的行号
:set nonu 与 set nu 相反,为取消行号!

案例演示1:

使用vi/vim编辑器打开文件oldFile,显示当前文件行号,将当前文件的第1-3行内容另存为oldFileCpy文件,使用cat命令查看新生成文件内容,可以使用如下步骤:

打卡oldFile文件命令; img

输入:set nu后回车,显示行号; img

img

输入:1,3 w oldFileCpy后回车 img

最后在vi中使用cat命令查看新生成的文件oldFileCpy内容; img

img

按下回车键后返回当前vi编辑器,最后输入q退出文件; img [请在右侧“命令行”里直接体验]

Linux之静态库编写

在实际的软件开发时, 应该遵守一个基本原则:不要重复造轮子。如果某些代码经常要用到,不仅这个项目能使用,而且也能复用到其它项目上,那么就应该把这部分代码从项目中分离出来,将其编译为库文件,供需要的程序调用。

程序库分为两类,分别是静态库动态库。本关将主要讲解如何生成静态库

Windows系统上的静态库是以.lib为后缀,而Linux系统上的静态库是以.a为后缀的特殊的存档。

Linux系统的标准系统库可在目录/usr/lib/lib中找到。比如,在类Unix系统中C语言的数序库一般存储为文件/usr/lib/libm.a。该库中函数的原型声明在头文件/usr/include/math.h中。

生成静态库

Linux下,我们可以使用gccar工具制作和使用自己的静态库,具体过程如下:

1
2
将源码文件编译成.o目标文件;
使用ar工具将多个目标文件打包成.a的静态库文件;

注意Linux系统上默认的静态库名格式为:libxxx.a,其中xxx为生成库的名称。

案例演示1:

编写一个函数printHello,其功能为打印“Hello world”字符串,将其编译生成名为Hello的静态库,可以使用如下命令:

1
2
3
4
vim printHello.h
vim printHello.c
gcc -c printHello.c -o printHello.o
ar rcs libHello.a printHello.o
  • 使用vim编写printHello.h(声明printHello函数,方便以后被其它程序调用)
1
2
3
4
5
#ifndef __PRINTHELLO_H__
#define __PRINTHELLO_H__
#include <stdio.h>
void printHello();
#endif
  • 使用vim编写printHello.c
1
2
3
4
#include <stdio.h>
void printHello(){
printf("Hello world\n");
}

img [请在右侧“命令行”里直接体验]

使用静态库

静态库的使用方法只需要在编译程序的时候指明要链接的库名称即可,gcc中有两个参数是关于链接库时所使用的,**分别是:-L-l**。

1
2
-L:用于告诉gcc要链接的库所在目录;
-l:用于指明链接的库名称(小写l);

案例演示1:

调用以上案例生成的printHello函数,可以使用如下命令:

1
2
3
vim main.c
gcc main.c -o exe -L ./ -lHello(可以换成Hello.o)
./exe
  • 使用vim编写main.c
1
2
3
4
#include "printHello.h"
int main(){
printHello();
return 0;}

img

[请在右侧“命令行”里直接体验]

Linux之动态库编写

  • 静态库动态库的区别:
静态库 动态库
名称 命名方式是”libxxx.a”,库名前加”lib”,后缀用”.a”,”xxx”为静态库名 命名方式是”libxxx.so”, 库名前加”lib”,后缀用”.so”, “xxx”为动态库名
链接时间 静态库的代码是在编译过程中被载入程序中 动态库在编译的时候并没有被编译进目标代码,而是当你的程序执行到相关函数时才调用该函数库里的相应函数
优点 在编译后的执行程序不在需要外部的函数库支持,因为所使用的函数都已经被编进去了。 动态库的改变不影响你的程序,所以动态函数库升级比较方便
缺点 如果所使用的静态库发生更新改变,你的程序必须重新编译 因为函数库并没有整合进程序,所以程序的运行环境必须提供相应的库

Windows系统上的动态库是以.dll为后缀,而Linux系统上的动态库是以.so为后缀的特殊的存档。

生成动态库

Linux下,我们可以使用gcc制作和使用动态库,具体制作过程如下:

1
使用gcc命令加-fPIC参数将源码文件编译成.o目标文件;使用gcc命令加-shared参数将多个目录文件生成一个动态库文件;

注意Linux系统上默认的动态库名格式为:libxxx.so,其中xxx为生成库的名称。

案例演示1:

编写一个函数printHello,其功能为打印”Hello world”字符串,将其编译生成名为Hello的动态库,可以使用如下命令:

1
2
3
4
vim printHello.h
vim printHello.c
gcc -fPIC -c printHello.c -o printHello.o
gcc -shared printHello.o -o libHello.so
  • 使用vim编写printHello.h(申明printHello函数,方便以后被其它程序调用)
1
2
3
4
5
#ifndef __PRINTHELLO_H__
#define __PRINTHELLO_H__
#include <stdio.h>
void printHello();
#endif
  • 使用vim编写printHello.c
1
2
3
4
#include <stdio.h>
void printHello(){
printf("Hello world\n");
}

img [请在右侧“命令行”里直接体验]

使用动态库

动态库的使用方法与静态库使用方式略有不同,除了使用gcc中的-L-l参数外,想要调用动态库还需要更新Linux系统中/etc/ld.so.cache或者修改LD_LIBRARY_PATH环境变量,否则在运行程序的时候会报”No such file or directory”错误。

案例演示1:

调用以上案例生成的printHello函数,可以使用如下命令:

1
2
3
vim main.c
gcc main.c -o exe -L ./ -lHello
./exe

img [使用vim编写程序]

img [请在右侧“命令行”里直接体验]

此时编译正常,当运行的时候会报”No such file or directory”错误。

更新/etc/ld.so.cache来运行动态库
  • 编辑/etc/ld.so.conf配置文件,然后加入需要加载的动态库目录。
  • 运行ldconfig更新/etc/ld.so.cache

案例演示1:

更新/etc/ld.so.cache,然后运行上一个案例生成的exe,可以使用如下命令:

1
2
3
sudo vim /etc/ld.so.conf
sudo ldconfig
./exe

img [使用vim/etc/ld.so.conf文件添加/home/fzm路径]

img [请在右侧“命令行”里直接体验]

修改LD_LIBRARY_PATH环境变量

在运行可执行文件前修改LD_LIBRARY_PATH变量为可执行程序指定需要加载的动态库路径。

案例演示1:

修改LD_LIBRARY_PATH,然后运行上一个案例生成的exe,可以使用如下命令:

1
LD_LIBRARY_PATH=.  ./exe

img [请在右侧“命令行”里直接体验]

注意

1
2
LD_LIBRARY_PATH告诉了exe程序现在当前目录下寻找链接的动态库;
当运行环境中同时存在同名的静态库和动态库时,默认优先链接动态库;

Makefile初体验

什么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些WindowsIDE都为你做了这个工作,但是要作一个专业的程序员,makefile还是要懂的。makefile其实就是描述了整个工程中所有文件的编译顺序,编译规则,并且由make命令来读取makefile文件,然后根据makefile文件中定义的规则对其进行解析,完成对整个项目的编译操作。

makefilelinux操作系统中是比较常见的,例如,我们在使用源码安装一个软件的时候,通常只需执行make命令即可完成对软件的编译,正是因为软件开发者已经编写了makefile文件,所以只需执行make命令就会完成对整个工程的自动编译。

本关将介绍makefile的语法,使用makefile来完成对软件的编译。

Makefile规则

makefile文件中包含了一组用来编译应用程序的规则,一项规则可分成三个部分组成:

1
2
3
工作目标(target)
依赖条件(prerequisite)
所要执行的命令(command)

格式为:

1
2
target : prereq1 prereq2
commands

以上格式就是一个文件的依赖关系,也就是说,target这个目标文件依赖于多个prerequisites文件,其生成规则定义在commands中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,commands所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。

注意

1
2
3
commands前面使用的是TAB键,而不是空格,使用空格会出现错误;
commands可以是任意的shell命令;
在执行make命令时,make会解析第一项规则;

案例演示1:

存在一个源码文件main.c文件,编译一个makefile规则来编译该文件,并生成一个名为HelloWorld的可执行文件,具体操作如下:

1
2
vim makefile
make
  • 使用vim编写如下代码
1
2
3
4
5
#include <stdio.h>
int main(){
printf("Hello world\n");
return 0;
}
  • 使用vim编写makefile
1
2
HelloWorld : main.c    
gcc -o HelloWorld main.c

img [请在右侧“命令行”里直接体验]

通过以上案例可以看到,编写好makefile后,只需要输入make命令即自动只需定义好的规则。

注意:gcc -o HelloWorld main.c命令前是TAB键而不是空格。

案例演示2:

假设一个项目中包含5个源码文件,分别是Add.cSub.cMul.cDiv.cmain.c和一个头文件def.h,编译一个makefile规则来编译该项目,并生成一个名为exe的可执行文件,具体操作如下:

1
2
vim makefile
make
  • vim Add.c
1
2
3
4
#include <stdio.h>
int Add(int a, int b){
return a + b;
}
  • vim Sub.c
1
2
3
4
#include <stdio.h>
int Sub(int a, int b){
return a - b;
}
  • vim Mul.c
1
2
3
4
#include <stdio.h>
int Mul(int a, int b){
return a * b;
}
  • vim Div.c
1
2
3
4
#include <stdio.h>
int Div(int a, int b){
return a / b;
}
  • vim main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include "def.h"
int main(){
int add = Add(10, 5);
int sub = Sub(10, 5);
int mul = Mul(10, 5);
int div = Div(10, 5);
printf("10 + 5 = %d\n", add);
printf("10 - 5 = %d\n", sub);
printf("10 * 5 = %d\n", mul);
printf("10 / 5 = %d\n", div);
return 0;
}
  • vim def.h
1
2
3
4
5
6
7
8
#ifndef __DEF_H__
#define __DEF_H__
#include <stdio.h>
int Add(int a, int b);
int Sub(int a, int b);
int Mul(int a, int b);
int Div(int a, int b);
#endif
  • vim makefile
1
2
3
4
5
6
7
8
9
10
11
12
exe : main.o Add.o Sub.o Mul.o Div.o    
gcc -o exe main.o Add.o Sub.o Mul.o Div.o (若要重命名生成的文件,两个exe都要改名)
main.o : main.c def.h
gcc -c main.c -o main.o
Add.o : Add.c
gcc -c Add.c -o Add.o
Sub.o : Sub.c
gcc -c Sub.c -o Sub.o
Mul.o : Mul.c
gcc -c Mul.c -o Mul.o
Div.o : Div.c
gcc -c Div.c -o Div.o

img [请在右侧“命令行”里直接体验]

以上案例,当只需make命令时,首先解析目标为exe的规则,然后发现exe依赖于main.o、Add.o和Sub.o,然后分别对main.o、Add.o和Sub.o规则进行解析,即分别执行目标为main.o、Add.o和Sub.o的命令。当main.o、Add.o和Sub.o生成后,最后执行exe对应的命令。

Makefile之变量使用

makefile 变量的命令可以包含字符、数字、下划线(可以是数字开头),并且大小写敏感。

makefile变量在声明时需要对其进行赋值,而在使用该变量时需要在变量名前加上**$符号 例如$(VARNAME),如果用户需要在makefile文件中使用真实的$字符,则使用$$**表示。

makefile中对变量的赋值方式有三种,分别是:

1
递归赋值(=):递归赋值,即赋值后并不马上生效,等到使用时才真正的赋值,此时通递归找出当前的值;直接赋值(:=):是将":="右边中包含的变量直接展开给左边的变量赋值;条件赋值(?=):只有此变量在之前没有赋值的情况下才会对这个变量进行赋值,所有一般用在第一次赋值;

makefile除了可以自定义变量外,还存在一些系统默认的特殊变量,这些特殊变量可以方便帮助我们快速的编写makefile文件,例如:$@、$<和$^等等。

本关将介绍makefile的变量的定义和使用方法,以及使用特殊变量来编写makefile文件。

Makefile 自定义变量

自定义变量格式:

  • 递归赋值 变量名 = 变量内容
  • 直接赋值 变量名 := 变量内容
  • 条件赋值 变量名 ?= 变量内容

变量的使用格式为: $变量名或者${变量名}或者$(变量名)

案例演示1:

在上一关中案例2中的项包含了5个源码文件和一个头文件,如果使用变量来编写makefile则会显示出比较简洁的格式,具体操作如下:

1
vim makefilemake
  • vim makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o exe $(object)

main.o : main.c def.h
gcc -c main.c -o main.o

Add.o : Add.c
gcc -c Add.c -o Add.o

Sub.o : Sub.c
gcc -c Sub.c -o Sub.o

Mul.o : Mul.c
gcc -c Mul.c -o Mul.o

Div.o : Div.c
gcc -c Div.c -o Div.o

img [makefile内容]

img [请在右侧“命令行”里直接体验]

可以看到,我们使用object来表示main.o Add.o Sub.o Mul.o Div.o,这样我们就可以使用$(object)来表示以上目标文件,而不是每次输入这5个目标文件。

Makefile 特殊变量

makefile常用的特殊变量有:

1
2
3
$@:表示所有目标;
$^:表示所有依赖目标的集合,以空格分隔;
$<:表示依赖目标中第一个目标的名子;

案例演示1:

接着上一个案例中的项目,如果使用特殊变量来编写makefile则会显示出更加简洁的格式,具体操作如下:

1
2
vim makefile
make
  • vim makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o $@ $(object)main.o :

main.c def.h
gcc -c $< -o $@

Add.o : Add.c
gcc -c $< -o $@

Sub.o : Sub.c
gcc -c $< -o $@

Mul.o : Mul.c
gcc -c $< -o $@

Div.o : Div.c
gcc -c $< -o $@

img [请在右侧“命令行”里直接体验]

Makefile自动推导

make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个.o文件后都写上类似的命令。因为,我们的make会自动识别,并自己推导命令。

只要make看到一个.o文件,它就会自动的把.c文件加在依赖关系中,如果make找到一个main.o,那么main.c就会是main.o的依赖文件。并且 gcc -c main.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。

本关将介绍makefile的自动推导功能。

Makefile 自动推导

自动推导格式: 目标 : 其它依赖

案例演示1:

如果使用自动推导模式来编写上一关卡案例中的makefile,则会有更简洁的格式,具体操作如下:

1
2
vim makefile
make
  • vim makefile
1
2
3
4
5
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o $@ $(object)

main.o : def.h

img

[请在右侧“命令行”里直接体验]

可以看到,我们只需要为main.o创建一个编译规则,其4个目标文件则不需要为其创建编译规则,因为make会自动的为其构造出编译规则。

Makefile伪目标

每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。

通常,我们在使用源码安装软件的时候,都会在编译完软件后,执行make install这个命令来安装软件,或者执行make clean这个命令清空临时生成的目标文件。以上操作就是利用了makefile的伪目标。

本关将介绍makefile的伪目标。

Makefile 伪目标

makefile使用.PHONY`关键字来定义一个伪目标,具体格式为:

1
.PHONY : 伪目标名称

案例演示1:

为上一关卡案例中的makefile添加清空临时目标文件标签clean,具体操作如下:

1
2
vim makefile
make
  • vim makefile
1
2
3
4
5
6
7
8
9
10
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o $@ $(object)

main.o : def.h

.PHONY : clean

clean :
rm $(object)

img [请在右侧“命令行”里直接体验]

可以看到,当我们执行完make命令后会生成多个临时文件,然后我们执行make clean命令后,则会将生成的临时文件删除掉,其实执行make clean命令就是在执行rm main.o Add.o Sub.o Mul.o Div.o

案例演示2:

使用另一个格式来清除临时产生的目录文件和不显示删除命令,具体操作如下:

1
2
vim makefile
make
  • vim makefile
1
2
3
4
5
6
7
8
9
object=main.o Add.o Sub.o Mul.o Div.o
exe : $(object)
gcc -o $@ $(object)

main.o : def.h

clean :
@echo "clean object files"
@rm $(object)

img [请在右侧“命令行”里直接体验]

可以看到,当我们执行make clean命令后,将不会在终端中显示rm main.o Add.o Sub.o Mul.o Div.o命令。

注意:在命令前加了**@**符号,则不会把命令原样输出在终端。

文件编程

文件权限修改

在当前目录中新建文件test.txt

touch test.txt

增加拥有者(u)对该文件的执行权限。

chmod 777 test.txt

增加群组用户(g)对该文件的写权限。

chmod ug+w test.txt

取消其他用户(o)对该文件的读权限。

chmod o-r test.txt

文件I/O

文件的创建

相关知识

文件的创建操作是 I/O 操作的第一步。在Linux系统中creat系统调用可以实现对文件的创建。本关只介绍文件创建函数的使用方法。

在Linux系统中可以使用man命令来查询这些函数的使用方法。具体的查询命令为: man 2 函数名 其中,2表示查找系统调用函数,关于文件的创建、打开和关闭函数都是系统调用函数,因此使用2作为man命令的第一个参数。

案例演示1: 查询creat函数的使用方法可以使用以下命令: man 2 creat

img [查询结果]

通过man命令可以查询常用的系统调用函数的使用方法。

文件的创建

创建文件的系统调用函数是creat,具体的说明如下:

  • 需要的头文件如下:

    1
    2
    3
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
  • 函数格式如下:

    1
    int creat (const char *pathname,mode_t mode);

    参数说明:

    1
    pathname:需要创建文件的绝对路径名或相对路径名;mode:用于指定所创建文件的权限;

    常见的

    1
    mode

    取值及其含义见下表所示:

mode 含义
S_IRUSR 文件所有者的读权限位
S_IWUSR 文件所有者的写权限位
S_IXUSR 文件所有者的执行权限位
S_IRGRP 所有者同组用户的读权限位
S_IWGRP 所有者同组用户的写权限位
S_IXGRP 所有者同组用户的执行权限位
S_IROTH 其他用户的读权限位
S_IWOTH 其他用户的写权限位
S_IXOTH 其他用户的执行权限位
  • 函数返回值说明: 调用成功时,返回值为 文件的描述符(大于0的整数);调用失败时,返回值为-1并设置错误编号errno

案例演示1: 在当前目录下使用creat函数创建一个名为firstFile的文件,并设置文件的权限为644。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int ret = creat("firstFile", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (ret == -1)
{
printf("创建文件失败\n");
}
else
{
printf("创建文件成功\n");
}
return 0;
}

img 将以上代码保存为main.c文件中,编译执行。可以看到当前目录下存在firstFile文件,并且其权限为644

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 在当前目录下创建一个名为testFile的文件,并设置其权限为651
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main()
{
/********** BEGIN **********/
int ret = creat("testFile", S_IRUSR | S_IWUSR | S_IRGRP | S_IXGRP | S_IXOTH);

/********** END **********/

return 0;
}

文件打开与关闭

相关知识

文件的打开与关闭操作是 I/O 操作的第二步。在Linux系统中提供了以下两个系统调用函数用于打开和关闭文件操作,分别是openclose。本关将介绍文件的打开和关闭函数的使用方法。

使用man 2 函数名也可以查询这些函数的使用方法。

文件的打开

打开文件的系统调用函数是open,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>
  • 函数格式如下:

    1
    int open(coust char *pathname, int flags);int open(const char *pathname, int flags, made_t mode);

    参数说明:

    1
    pathname:需要被打开或创建的文件绝对路径名或相对路径名;flags:用于描述文件的打开方式;mode:用于指定所创建文件的权限(与上一关中creat函数中mode取值一致);

第一个open函数用于打开已经存在的文件。而第二个open函数可以创建一个不存在的文件且打开,该函数将flags参数设置为O_CREAT | O_WRONLY | O_TRUNC时等同于上一关中的creat函数。

常见的flags取值及其含义见下表所示:

flags 含义
O_RDONLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWY 以只读写方式打开文件
O_CREAT 若所打开文件不存在则创建此文件
O_TRUNC 若以只写或读写方式打开一个已存在文件时,将该文件截至 0
O_APPEND 向文件添加内容时将指针置于文件的末尾
O_SYNC 只在数据被写外存或其他设备之后操作才返回
  • 函数返回值说明: 调用成功时,返回值为 文件的描述符(大于0的整数);调用失败时,返回值为-1并设置错误编号errno

案例演示1: 在当前目录下使用open函数以只读方式打开一个已存在且名为firstFile的文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int ret = open("firstFile", O_RDONLY);
if (ret == -1)
{
printf("打开文件失败\n");
}
else
{
printf("打开文件成功\n");
}
return 0;
}

img 将以上代码保存为openFile.c文件中,编译执行。

案例演示2: 在当前目录下使用open函数创建一个名为secondFile的文件,并设置文件的权限为644。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int ret = open("secondFile", O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (ret == -1)
{
printf("创建文件失败\n");
}
else
{
printf("创建文件成功\n");
}
return 0;
}

img 将以上代码保存为secondFile.c文件中,编译执行。可以看到当前目录下存在secondFile文件,并且其权限为644

文件的关闭

关闭文件的系统调用函数是close,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下: int close(int fd);

参数说明:

1
fd:需关闭文件的描述符;
  • 函数返回值说明: 调用成功时,返回值为 0;调用失败时,返回值为-1,并设置错误编号errno

案例演示1: 在当前目录下使用close函数关闭一个已经被打开的文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("firstFile", O_RDONLY);
if (fd == -1)
{
printf("打开文件失败\n");
}
else
{
printf("打开文件成功\n");
}
int ret = close(fd);
if(ret == -1)
{
printf("关闭文件失败\n");
}
else
{
printf("关闭文件成功\n");
}
return 0;
}

img 将以上代码保存为closeFile.c文件中,编译执行。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全open_File函数,使其以方式打开一个文件,并返回文件描述符fd
  • 补全close_File函数,使其关闭一个已经被打开的文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

/************************
* fileName: 需要被打开的文件路径
*************************/
int open_File(char *fileName)
{
int fd = 0; //存放文件描述符
/********** BEGIN **********/
fd = open(fileName, O_RDONLY);
/********** END **********/

return fd;
}

/************************
* fd: 需要被关闭的文件描述符
*************************/
void close_File(int fd)
{
/********** BEGIN **********/
fd = close(fd);
/********** END **********/
}

文件读写操作

相关知识

文件的读写是 I/O 操作的核心内容。上一关中已经介绍了如何打开和关闭一个文件,但是要实现文件的 I/O 操作就必须对其进行读写,文件的读写操作所用的系统调用分别是readwrite。本关将介绍文件的读写函数的使用方法。

使用man 2 函数名也可以查询这些函数的使用方法。

文件的写操作

写入文件的系统调用函数是write,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下: ssize_t write(int fd, void *buf, size_t count);

参数说明:

1
fd:表示将对之进行写操作的文件打开时返回的文件描述符;buf:指向存放将写入文件的数据的缓冲区的指针;count:表示本次操作所要写入文件的数据的字节数;
  • 函数返回值说明: 调用成功时,返回值为所写入的字节数;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 在当前目录下往firstFile文件中写入一个字符串。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("firstFile", O_WRONLY);
if (fd == -1)
{
printf("打开文件失败\n");
}
else
{
printf("打开文件成功\n");
}
char *data = "this is firstFile\n";
ssize_t size = write(fd, data, strlen(data));
if(size == -1)
{
printf("写入文件失败\n");
}
else
{
printf("写入文件成功:写入%ld个字符\n", size);
}
if(close(fd) == -1)
{
printf("关闭文件失败\n");
}
else
{
printf("关闭文件成功\n");
}
return 0;
}

img 将以上代码保存为writeFile.c文件中,编译执行。可以看到字符串被写入到firstFile文件中。

文件的读操作

读取文件的系统调用函数是read,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下: ssize_t read(int fd, void *buf, size_t count);

参数说明:

1
fd:表示将对之进行写操作的文件打开时返回的文件描述符;buf:指向存放所读数据的缓冲区的指针;count:读操作希望读取的字节数;
  • 函数返回值说明: 调用成功时,返回值为本次读操作实际读取的字节数;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 读取当前目录下firstFile文件中的前4个字符。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("firstFile", O_RDONLY);
if (fd == -1)
{
printf("打开文件失败\n");
}
else
{
printf("打开文件成功\n");
}
char data[5] = "";
ssize_t size = read(fd, data, sizeof(char)*4);
if(size == -1)
{
printf("读取文件失败\n");
}
else
{
printf("读取文件成功:数据:%s\n", data);
}
if(close(fd) == -1)
{
printf("关闭文件失败\n");
}
else
{
printf("关闭文件成功\n");
}
return 0;
}

img 将以上代码保存为readFile.c文件中,编译执行。可以看到从firstFile文件中读取出了前4个字符。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全write_File函数,完成向文件写入字符串功能。并返回实际写入字符个数。
  • 补全readLine函数,完成从文件中读取一行的功能(不包括换行符),并返回实际读取的字符个数(文件的换行符号为\n)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include<string.h>
#include<stdlib.h>

/************************
* fd: 被打开文件的描述符
* buf: 被写入字符串指针
*************************/
int write_File(int fd, char *buf)
{
int writeSize = 0; //返回实际写入的字符个数

/********** BEGIN **********/
writeSize = write(fd, buf, strlen(buf));
/********** END **********/

return writeSize;
}

/************************
* fd: 被打开文件的描述符
* buf: 存放读取的字符串指针(假设buf足够大)
*************************/
int readLine(int fd, char *buf)
{
int readSize = 0; //返回实际读取的字符个数
char tempC;

//提示:使用while循环每次只读取一个字符,判断该字符是否为换行符或者是否已经读取到文件末尾(读取到文件末尾返回值为0)
/********** BEGIN **********/
int temp=1,length=0;
while(1){
temp = read(fd, &tempC, sizeof(char));
if(temp==0|tempC=='\n'){
break;
}else{
readSize = readSize+1;
buf[length++]=tempC;
}

}

/********** END **********/

return readSize;
}

文件的删除

相关知识

当不需要一个文件时,我们通常直接选中文件按下delete键对其删除,本关将介绍如何在Linux系统中使用C语言删除一个已经存在的文件。

在Linux系统中使用unlinkremove系统调用可以实现对文件的删除操作。

使用man 2 函数名或者man 3 函数名也可以查询这些函数的使用方法。

使用unlink函数删除文件

删除文件的系统调用函数是unlink,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下: int unlink(const char *pathname); 参数说明:

    1
    pathname:需要删除的文件绝对路径名或相对路径名;
  • 函数返回值说明: 调用成功时,返回值为0;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 删除当前目录下的firstFile文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h>
#include <stdio.h>
int main()
{
int ret = unlink("firstFile");
if (ret == -1)
{
printf("删除文件失败\n");
}
else
{
printf("删除文件成功\n");
}
return 0;
}

img 将以上代码保存为deleteFile1.c文件中,编译执行。可以看到当前目录下存在firstFile文件被删除了。

使用unlink函数删除文件

remove是删除文件的另一个函数,该函数是C语言的库函数,其本质是通过调用系统调用unlink来完成文件的删除操作,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <stdio.h>
  • 函数格式如下: int remove(const char *pathname); 参数说明:

    1
    pathname:需要删除的文件绝对路径名或相对路径名;
  • 函数返回值说明: 调用成功时,返回值为0;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 删除当前目录下的secondFile文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{
int ret = remove("secondFile");
if (ret == -1)
{
printf("删除文件失败\n");
}
else
{
printf("删除文件成功\n");
}
return 0;
}

img 将以上代码保存为deleteFile2.c文件中,编译执行。可以看到当前目录下存在secondFile文件被删除了。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 删除当前目录下的testFile文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <unistd.h>

int main()
{
/********** BEGIN **********/

int ret = unlink("testFile");

/********** END **********/

return 0;
}

目录文件I/O

目录文件的创建与删除

相关知识

目录文件是Linux系统中一类比较特殊的文件。它对构成 Linux 系统的整个文件系统结构非常重要。Linux系统提供了两个系统调用函数来实现目录的创建和删除功能,分别是mkdirrmdir函数,这两个函数的名称和创建/删除目录命令的名称一样。其实创建/删除目录命令的背后实现方法就是调用这两个系统函数来实现对目录的创建和删除功能。

在Linux系统中可以使用man命令来查询这些函数的使用方法。具体的查询命令为: man 2 函数名 其中,2表示查找系统调用函数,关于目录的创建、打开、关闭和删除函数都是系统调用函数,因此使用2作为man命令的第一个参数。

案例演示1: 查询mkdir函数的使用方法可以使用以下命令: man 2 mkdir

img [查询结果]

通过man命令可以查询rmdir函数的使用方法。

目录文件的创建

创建目录文件的系统调用函数是mkdir,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/type.h>#include <sys/stat.h>
  • 函数格式如下:

    1
    int mkdir(const char *pathname, mode_t mode);

    参数说明:

    1
    pathname:新创建的目录文件名;mode:用于指定所创建目录文件的权限;

    常见的

    1
    mode

    取值及其含义见下表所示:

mode 含义
S_IRUSR 目录所有者的读权限位
S_IWUSR 目录所有者的写权限位
S_IXUSR 目录所有者的执行权限位
S_IRGRP 所有者同组用户的读权限位
S_IWGRP 所有者同组用户的写权限位
S_IXGRP 所有者同组用户的执行权限位
S_IROTH 其他用户的读权限位
S_IWOTH 其他用户的写权限位
S_IXOTH 其他用户的执行权限位

注意:在Linux系统中,新创建目录的权限位是(mode & ~ umask & 01777),也就是umask为进程创建目录的权限位限制。因此会出现用户在代码中设定的权限与实际创建出来的权限不一致情况。同理,对于文件权限的处理也一样。

  • 函数返回值说明: 调用成功时,返回值0;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 在当前目录下使用mkdir函数创建一个名为firstDir的目录文,并设置目录的权限为644。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <sys/type.h>
#include <sys/stat.h>
int main()
{
int ret = mkdir("firstDir", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (ret == -1)
{
printf("创建目录失败\n");
}
else
{
printf("创建目录成功\n");
}
return 0;
}

img 将以上代码保存为createDir.c文件,编译执行。可以看到当前目录下存在firstDir目录文件,并且其权限为644

目录文件的删除

删除目录文件的系统调用函数是rmdir,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下:

    1
    int rmdir(const char *pathname);

    参数说明:

    1
    pathname:要被删除的目录文件名称;

注意:使用rmdir库函数删除的目录必须为空,如果该目录不为空,则必须删除该目录的所有文件(...文件除外)。

  • 函数返回值说明: 调用成功时,返回值0;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 使用mkdir函数删除当前目录下名为firstDir的目录文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <unistd.h>
int main()
{
int ret = rmdir("firstDir");
if (ret == -1)
{
printf("删除目录失败\n");
}
else
{
printf("删除目录成功\n");
}
return 0;
}

img 将以上代码保存为deleteDir.c文件中,编译执行。可以看到当前目录下的firstDir目录文件被删除。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 在当前目录下创建一个名为testDir的目录,并设置其权限为651
  • 删除当前目录下名为Dir的空目录文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/stat.h>
#include <stdio.h>

int main()
{
/********** BEGIN **********/
int ret = mkdir("testDir", S_IRUSR | S_IWUSR | S_IRGRP | S_IXGRP | S_IXOTH);
int ret1 = rmdir("Dir");

/********** END **********/

return 0;
}

目录文件的打开与关闭

相关知识

在Linux系统中提供了以下两个系统调用函数用于打开和关闭目录操作,分别是opendirclosedir,这些库函数不属于系统调用,它们是C语言提供的库函数。本关将介绍目录的打开和关闭函数的使用方法。

因为这两个函数是C语言提供的库函数,因此可以使用man 3 函数名也可以查询这些函数的使用方法。

目录文件的打开

打开目录文件的库函数是opendir,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <dirent.h>
  • 函数格式如下: DIR *opendir(const char *name);

参数说明:

1
name:需要打开的目录绝对路径名或相对路径名;

注意:打开一个目录后返回一个DIR对象,该对象指向被打开目录的目录流。

  • 函数返回值说明: 调用成功时,返回值为一个不为空的目录流指针;调用失败时,返回值为NULL的空指针,并设置错误编号errno

案例演示1: 使用opendir函数打开当前用户的家目录(本实验环境的用户家目录为/home/fzm)。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
int main()
{
DIR* dirp = opendir("/home/fzm");
if (dirp == NULL)
{
printf("打开目录失败\n");
}
else
{
printf("打开目录成功\n");
}
return 0;
}

img 将以上代码保存为openDir.c文件中,编译执行。

目录文件的关闭

关闭目录文件的库函数是closedir,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <dirent.h>
  • 函数格式如下: int closedir(DIR *dirp);

参数说明:

1
dirp:需要被关闭的目录流指针;
  • 函数返回值说明: 调用成功时,返回值为 0;调用失败时,返回值为-1,并设置错误编号errno

案例演示1: 使用closedir函数关闭一个已经被打开的目录。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
int main()
{
DIR* dirp = opendir("/home/fzm/Downloads");
if (dirp == NULL)
{
printf("打开目录失败\n");
}
else
{
printf("打开目录成功\n");
}
int ret = closedir(dirp);
if(ret == -1)
{
printf("关闭目录失败\n");
}
else
{
printf("关闭目录成功\n");
}
return 0;
}

img 将以上代码保存为closeDir.c文件中,编译执行。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全open_Dir函数,使其打开一个目录并返回目录流指针dirp

  • 补全close_Dir函数,使其关闭一个被打开的目录。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #include <dirent.h>
    #include <sys/types.h>
    #include <stdio.h>

    /************************
    * pathName: 需要被打开的目录路径
    *************************/
    DIR* open_Dir(char *pathName)
    {
    DIR* dirp = NULL; //存放目录流指针
    /********** BEGIN **********/
    dirp = opendir(pathName);

    /********** END **********/

    return dirp;
    }

    /************************
    * dirp: 需要被关闭的目录流指针
    *************************/
    void close_Dir(DIR* dirp)
    {
    /********** BEGIN **********/
    int ret = closedir(dirp);

    /********** END **********/
    }

目录文件的读取操作

相关知识

ls命令的背后实现方法就是通过打开被浏览的目录,然后从目录中读取目录项。Linux系统中使用readdir函数可以读取目录内容。本关将介绍目录的读函数的使用方法。

因为readdir函数是C语言提供的库函数,因此可以使用man 3 函数名来查询该函数的使用方法。

目录文件的读操作

读取目录的库调用函数是readdir,具体的说明如下:

  • 需要的头文件如下:

    1
    #include <dirent.h>
  • 函数格式如下: struct dirent *readdir(DIR *dirp);

参数说明:

1
dirp:表示被打开目录的流指针;

结构dirent指向目录项,其定义在Linux系统中的<dirent.h>头文件中,详细定义如下所示:

1
2
3
4
5
6
7
struct dirent {
ino_t d_ino; /* 索引节点号 */
off_t d_off; /* 在目录文件中的偏移 */
unsigned short d_reclen; /* 文件名长 */
unsigned char d_type; /* 文件类型 */
char d_name[256]; /* 文件名,最长255字符 */
};

其中d_name字段存放着所读取到的目录项名。d_type字段为该目录项的类型,常见类型如下所示:

1
DT_DIR:目录文件;DT_LNK:符号链接文件;DT_REG:常规文件;DT_SOCK:sock文件;DT_UNKNOWN:未知的文件类型;

注意:d_type字段并不是支持所有的文件系统,并且只是由BSD衍生出来的Linux系统中可用。在Linux系统中还提供了另一个系统调用函数用来判断文件类型,其名称为stat,有兴趣的学生可以执行去学习其使用方法。

  • 函数返回值说明: 调用成功时,返回值为所写入的字节数;调用失败时,返回值为-1并设置错误编号errno

案例演示1: 读取当前目录下的所有内容,并打印出其名称。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
int main()
{
//.表示当前目录
DIR* dirp = opendir(".");
if (dirp == NULL)
{
printf("打开目录失败\n");
return -1;
}
else
{
printf("打开目录成功\n");
}
struct dirent *dir = readdir(dirp);
while(dir != NULL)
{
printf("%s ", dir->d_name);
dir = readdir(dirp);
}
printf("\n");
if(closedir(dirp) == -1)
{
printf("关闭目录失败\n");
}
else
{
printf("关闭目录成功\n");
}
return 0;
}

img 将以上代码保存为readdir.c文件中,编译执行。可以看到执行该命令后会将当前目录下所有的内容都打印出来。

案例演示2: 读取当前目录下的所有普通文件,并打印出其名称。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
int main()
{
//.表示当前目录
DIR* dirp = opendir(".");
if (dirp == NULL)
{
printf("打开目录失败\n");
return -1;
}
else
{
printf("打开目录成功\n");
}
struct dirent *dir = readdir(dirp);
while(dir != NULL)
{
if(dir->d_type == DT_REG)
printf("%s ", dir->d_name);
dir = readdir(dirp);
}
printf("\n");
if(closedir(dirp) == -1)
{
printf("关闭目录失败\n");
}
else
{
printf("关闭目录成功\n");
}
return 0;
}

img 将以上代码保存为readRegDir.c文件中,编译执行。可以看到执行该命令后只会将当前目录下常规文件打印出来了。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全scanAll函数,完成读取一个目录下所有的内容,并将每个内容按空格分割打印出来。
  • 补全scanDir函数,完成读取一个目录下直接包含的目录名称(只读取当前目录层的内容,不往下读取),并将每个目录按空格分割打印出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
#include <errno.h>

/************************
* dirp: 被打开的目录流指针
*************************/
void scanAll(DIR *dirp)
{
//提示:不需要关闭dirp指针,输出的内容不能有换行,每个目录项中间用空格(英文空格)分割
/********** BEGIN **********/

struct dirent *dir = readdir(dirp);
while(dir != NULL)
{
printf("%s ", dir->d_name);
dir = readdir(dirp);
}

/********** END **********/
}

/************************
* dirp: 被打开的目录流指针
*************************/
void scanDir(DIR *dirp)
{

//提示:不需要关闭dirp指针,输出的内容不能有换行,每个目录项中间用空格(英文空格)分割
/********** BEGIN **********/
struct dirent *dir = readdir(dirp);
while(dir != NULL)
{
if(dir->d_type == DT_DIR)
printf("%s ", dir->d_name);
dir = readdir(dirp);
}


/********** END **********/

}

进程控制

获取进程常见属性

在 Linux 环境下,进程是一个十分重要的概念。每个进程都由一个唯一的标识符来表示,即进程 ID ,通常称为 pid 。

Linux 系统中存在一个特殊的进程,即空闲进程( idle process ),当没有其他进程在运行时,内核所运行的进程就是空闲进程,它的 pid 为 0 。在启动后,内核运行的第一个进程称为 init 进程,它的 pid 是 1 。通常, Linux 系统中 init 进程就是我们在资源管理器中看到的名为 init 的程序。系统中其它的进程都是由 init 来创建出来的。

创建新进程的那个进程被称为父进程,而新创建的进程被称为子进程。每个进程都是由其他进程创建的(除了 init 进程),因此每个子进程都有一个父进程。

Linux 系统提供了两个系统调用函数来获取一个进程的 pid 和其父进程的 pid ,分别是 getpid 和 getppid 函数。在 Linux 系统中可以使用 man 命令来查询这些函数的使用方法。具体的查询命令为:

1
man 2 函数名 
获取进程自身 pid

获取进程本身的进程 ID 的系统调用函数是 getpid ,具体的说明如下:

  • 需要的头文件如下:
1
2
#include <sys/types.h>
#include <unistd.h>
  • 函数格式如下:

    1
    pid_t getpid(void); 
  • 函数返回值说明: 返回当前进程的 pid 值。

获取父进程 pid

获取父进程的进程 ID 的系统调用函数是 getppid ,具体的说明如下:

  • 需要的头文件如下:
1
2
#include <sys/types.h>
#include <unistd.h>
  • 函数格式如下:

    1
    pid_t getppid(void); 
  • 函数返回值说明: 返回当前进程的父进程的 pid 值。

编程要求

本关的编程任务是补全右侧代码片段中 Begin 至 End 中间的代码,具体要求如下:

  • 补全 getProcInfo 函数,用于获取当前进程 ID 和其父进程 ID (提示:将结果存放在procIDInfo结构体中)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

/**********************
* pid: 当前进程ID
* ppid: 父进程ID
***********************/
struct procIDInfo
{
pid_t pid;
pid_t ppid;
};

/************************
* 返回值: 需要被打开的目录路径
*************************/
struct procIDInfo getProcInfo()
{
struct procIDInfo ret; //存放进程ID信息,并返回
/********** BEGIN **********/
pid_t pid = getpid();
pid_t ppid = getppid();

ret.pid=pid;
ret.ppid=ppid;
/********** END **********/

return ret;
}

进程创建操作-fork

当用户调用 fork 函数时,系统将会创建一个与当前进程相同的新进程。通常将原始进程称为父进程,而把新生成的进程称为子进程。子进程是父进程的一个拷贝,子进程获得同父进程相同的数据,但是同父进程使用不同的数据段和堆栈段。

在早期的系统中,创建进程比较简单。当调用 fork 时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容也复制到子进程的地址空间中。但是从内核角度来说,这种复制方式是非常耗时的。

因此,在现代的系统中采取了更多的优化。现代的 Linux 系统采用了写时复制技术( Copy on Write ),而不是一创建子进程就将所有的数据都复制一份。

Copy on Write ( COW )的主要思路是:如果子进程/父进程只是读取数据,而不是对数据进行修改,那么复制所有的数据是不必要的。因此,子进程/父进程只要保存一个指向该数据的指针就可以了。当子进程/父进程要去修改数据时,那么再复制该部分数据即可。这样也不会影响到子父进程的执行。因此,在执行 fork 时,子进程首先只复制一个页表项,当子进程/父进程有写操作时,才会对所有的数据块进行复制操作。

使用fork函数创建进程

fork 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下:

    1
    pid_t fork(void); 
  • 函数返回值说明: 调用成功, fork 函数两个值,分别是 0 和子进程 ID 号。当调用失败时,返回 -1 ,并设置错误编号 errno 。fork 函数调用将执行两次返回,它将从父进程和子进程中分别返回。从父进程返回时的返回值为子进程的 PID ,,而从子进程返回时的返回值为 0 ,并且返回都将执行 fork 之后的语句

案例演示1: 编写一个程序,使用 fork 函数创建一个新进程,并在子进程中打印出其进程 ID 和父进程 ID ,在父进程中返回进程 ID 。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = fork();
if(pid == -1)
{
//创建进程失败
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
printf("当前进程为子进程:pid(%d),ppid(%d)\n", getpid(), getppid());
}
else
{
//父进程
printf("当前进程为父进程:pid(%d),ppid(%d)\n", getpid(), getppid());
}
//子进程和父进程分别会执行的内容
return 0;
}

img

将以上代码保存为 forkProcess.c 文件,编译执行。可以看到每次执行 forkProcess 时,子进程和父进程都不是固定的执行顺序,因此由 fork 函数创建的子进程执行顺序是由操作系统调度器来选择执行的。因此,子进程和父进行在执行的时候顺序不固定。

编程要求

本关的编程任务是补全右侧代码片段中 Begin 至 End 中间的代码,具体要求如下:

  • 补全 createProcess 函数,使用 fork 函数创建进程,并在子进程中输出 “Children” 字符串,在父进程中输出 “Parent” 字符串。(注意:不要在 createProcess 函数中使用 exit 函数或者 return 来退出程序)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

/************************
* 提示: 不要在子进程或父进程中使用exit函数或者return来退出程序
*************************/
void createProcess()
{
/********** BEGIN **********/
pid_t pid;
pid = fork();
if(pid == 0)
{
//子进程
printf("Children");
}
else
{
//父进程
printf("Parent");
}
//子进程和父进程分别会执行的内容
/********** END **********/
}

进程创建操作-vfork

vfork 函数是一个历史遗留产物。 vfork 创建进程与 fork 创建的进程主要有一下几点区别:

  1. vfork创建的子进程与父进程共享所有的地址空间,而fork创建的子进程是采用COW技术为子进程创建地址空间;
  2. vfork会使得父进程被挂起,直到子进程正确退出后父进程才会被继续执行,而fork创建的子进程与父进程的执行顺序是由操作系统调度来决定

vfork 性能要比 fork 高,主要原因是 vfork 没有进行所有数据的复制,尽管 fork 采用了 COW 技术优化性能,但是也会为子进程的页表项进行复制,因此 vfork 要比 fork 快。

使用 vfork 时要注意,在子进程中对共享变量的修改也会影响到父进程,因此 vfork 在带来高性能的同时,也使得整个程序容易出错,因此,开发人员在使用 vfork 创建进程时,一定要注意对共享数据的修改。

由于 vfork 创建的子进程和父进程共享所有的数据(栈、堆等等),因此,采用 vfork 创建的子进程必须使用 exit 或者 exec 函数族(下一关将介绍这些函数的功能)来正常退出,不能使用 return 来退出

exit 函数是用来结束正在运行的整个程序, exit 是系统调用级别,它表示一个进程的结束;而 return 是语言级别的,它表示调用堆栈的返回

使用 vfork 函数创建进程

vfork 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    2
    #include <sys/types.h>
    #include <unistd.h>
  • 函数格式如下:

    1
    pid_t vfork(void); 
  • 函数返回值说明: 调用成功, vfork 函数两个值,分别是 0 和子进程 ID 号。当调用失败时,返回 -1 ,并设置错误编号 errno 。

注意: vfork 函数调用将执行两次返回,它将从父进程和子进程中分别返回从父进程返回时的返回值为子进程的 PID ,而从子进程返回时的返回值为 0 ,并且返回都将执行 vfork 之后的语句。 vfork 创建的子进程必须调用 exit 函数来退出子进程

案例演示 1 : 编写一个程序,使用 vfork 函数创建一个新进程,并在子进程中打印出其进程 ID 和父进程 ID ,在父进程中返回进程 ID 。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = vfork();
if(pid == -1)
{
//创建进程失败
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
sleep(2); //睡眠2秒
printf("当前进程为子进程:pid(%d),ppid(%d)\n", getpid(), getppid());
}
else
{
//父进程
printf("当前进程为父进程:pid(%d),ppid(%d)\n", getpid(), getppid());
}
//子进程和父进程分别会执行的内容
exit(0);
}

img

将以上代码保存为 vforkProcess.c 文件,编译执行。可以看到 vforkProcess 创建的子进程尽管使用 sleep 函数睡眠了 2 秒,但是函数父进程的执行顺序在子进程后,这就是 vfork 的特性。

当我们将以上代码中的 exit(0) 换成 return 0 时,则会出现如下错误。

img

出现以上错误的原因是当子进程使用 return 退出时,操作系统也会把栈清空,那么当父进程继续使用 return 退出时,则会发现栈已经被清空了,这就相当于 free 两次同一块内存,因此会出现错误。

编程要求

本关的编程任务是补全右侧代码片段中 Begin 至 End 中间的代码,具体要求如下:

  • 补全 createProcess 函数,使用 vfork 函数创建进程,并在子进程中输出”Children”字符串(提示:需要换行),在父进程中输出”Parent”字符串(提示:需要换行)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

/************************
* 提示: 不要在子进程中使用return来退出程序
*************************/
void createProcess()
{
/********** BEGIN **********/
pid_t pid;
pid = vfork();
if(pid == 0)
{
//子进程
printf("Children\n");
}
else
{
//父进程
printf("Parent\n");
}
//子进程和父进程分别会执行的内容

/********** END **********/

exit(0);
}

进程终止

常见与退出进程相关的函数有: exit 、 _exit 、 atexit 、 on_exit 、 abort 和 assert 。

  1. exit 函数是标准 C 库中提供的函数,它用来终止正在运行的程序,并且关闭所有 I/O 标准流
  2. _exit 函数也可用于结束一个进程,与 exit 函数不同的是, _exit 不会关闭所有 I/O 标准流
  3. atexit 函数用于注册一个不带参数也没有返回值的函数以供程序正常退出时被调用
  4. on_exit 函数的作用与 atxeit 函数十分类似,不同的是它注册的函数具有参数,退出状态和参数 arg 都是传递给该程序使用的
  5. abort 函数其实是用来发送一个 SIGABRT 信号这个信号将使当前进程终止
  6. assert 是一个宏。调用 assert 时,它将先计算参数表达式 expression 的值,如果为 0 ,则调用 abort 函数结束进程

img

[ exit 和 _exit 区别]

exit 和 _exit 使用方法

exit 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <stdlib.h>
  • 函数族格式如下:

    1
    void exit(int status);

    参数说明: status:设置程序退出码;

    _exit 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数族格式如下:

    1
    void _exit(int status);

    参数说明: status :设置程序退出码;

  • 函数返回值说明: exit 和 _exit 均无返回值。

atexit 和 on_exit 使用方法

atexit 和 on_exit 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <stdlib.h>
  • 函数族格式如下:

    1
    2
    int atexit(void (*function)(void));
    int on_exit(void (*function)(int , void *), void *arg);

    参数说明: atexit 函数的 function 参数是一个函数指针,指向无返回值和无参数的函数; on_exit 函数的 function 参数是一个函数指针,指向无返回值和有两个参数的函数,其中第一个参数是调用 exit() 或从 main 中返回时的值,参数 arg 指针会传给参数 function 函数;

  • 函数返回值说明: atexit 和 on_exit 调用成功返回 0 ;调用失败返回一个非零值。

注意: atexit 和 on_exit 只有在程序使用 exit 或者 main 中正常退出时才会有效。如果程序使用 _exit 、 abort 或 assert 退出程序时,则不会执行被注册的函数

abort 和 assert 使用方法

abort 函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <stdlib.h>
  • 函数族格式如下:

    1
    void abort(void);

    assert 宏的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <assert.h>
  • 函数族格式如下:

    1
    void assert(scalar expression);

    参数说明: expression :需要被判断的表达式;

注意: assert 宏通常用于调试程序。

  • 函数返回值说明: abort 和 assert 无返回值。

案例演示 1 : 使用 atexit 注册一个退出函数,使其在调用退出函数前被执行,详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <stdio.h>
void out()
{
printf("程序正在被退出\n");
}
int main()
{
if(atexit(out) != 0)
{
printf("调用atexit函数错误\n");
}
return 0;```//或者exit(0)
}

img

将以上代码保存为 atexit.c 文件,编译执行。可以看到执行 atexit 程序后, out 函数被调用。

案例演示 2 : 使用 on_exit 注册一个退出函数,使其在调用退出函数前被执行,详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <stdio.h>
void out(int status, void *arg)
{
printf("%s(%d)\n", (char *s)arg, status);
}
int main()
{
if(on_exit(out, "程序正在被退出") != 0)
{
printf("调用on_exit函数错误\n");
}
exit(1);```//或者return 1
}

img

将以上代码保存为 on_exit.c 文件,编译执行。可以看到执行 on_exit 程序后, out 函数被调用,并且 status 变量的值就是 exit 函数退出的值。

案例演示1: 使用 abort 终止一个程序,详细代码如下所示:

1
2
3
4
5
6
7
#include <stdlib.h>
#include <stdio.h>
int main()
{
printf("Hello world\n");
abort();
}

img

将以上代码保存为 abort.c 文件,编译执行。可以看到执行 abort 程序后,程序被强行终止。

编程要求

本关的编程任务是补全右侧代码片段中 Begin 至 End 中间的代码,具体要求如下:

  • 补全 exitProcess 函数,使用 atexit 函数注册一个函数,在注册函数中打印出当前进程的 ID 号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>

/************************
* 提示: 用户需要在exitProcess函数中使用atexit函数注册一个自定义函数,并在自定义函数中打印出当前进程ID号
*************************/
void out()
{
printf("%d",getpid());
}

void exitProcess()
{
/********** BEGIN **********/
if(atexit(out) != 0)
{
printf("调用atexit函数错误\n");
}
/********** END **********/
}

进程等待

**如果,当子进程在父进程前结束,则内核会把子进程设置为一个特殊的状态。这种状态的进程叫做僵死进程(zombie)**。尽管子进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存。但是仍然保留了一些信息(如进程号pid退出状态 运行时间等)。只有父进程获取了子进程的这些信息后,子进程才会彻底的被销毁,否则一直保持僵死状态。如果系统中产生大量的僵尸进程,将导致系统没有可用的进程号,从而导致系统不能创建新的进程。

Linux处理僵死进程的方法之一是使用进程等待的系统调用waitwaitpid来使得父进程获取子进程的终止信息。

wait函数使用方法

wait函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/wait.h>
  • ```
    wait

    1
    2
    3

    函数格式如下:

    pid_t wait(int *status);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    参数说明: 参数`status`是一个整数指针,当子进程结束时,将子进程的结束状态字存放在该指针指向的缓存区。利用这个状态字,需要时可以使用一些由 Linux 系统定义的宏来了解子程序结束的原因。这些宏的定义与作用如下:

    | 宏定义 | 含义 |
    | ------------------- | ------------------------------------------------------------ |
    | WIFEXITED(status) | 子进程正常结束时,返回值为真(非零值) |
    | WEXITSTATUS(status) | 当WIFEXITED为真时,此宏才可以使用。返回进程退出的代码 |
    | WIFSIGNALED(status) | 子进程接收到信号结束时,返回值为真。但如果进程接收到信号时调用exit函数结束,则返回值为假 |
    | WTERMSIG(status) | 当 WIFSIGNALED 为真时,将获得终止该进程的信号 |

    - 函数返回值说明: 调用成功时,返回值为被置于等待状态的进程的 `pid`;执行失败返回`-1`并设置错误代码`errno`。

    ##### waitpid函数使用方法

    `waitpid`函数的具体的说明如下:

    - 需要的头文件如下:

    #include <sys/types.h>#include <sys/wait.h>

    1
    2
    3

    - ```
    waitpid

    函数格式如下:

    1
    pid_t waitpid(pid_t pid, int *status, int options);

    参数说明:

    1
    pid

    :用于指定所等待的进程。其取值和相应的含义如下所示:

pid 含义
pid > 0 等待进程IDpid所指定值的子进程
pid = 0 等待进程组ID与该进程相同的子进程
pid = -1 等待所有子进程,等价于wait调用
pid < -1 等待进程组IDpid绝对值的子进程

参数option则用于指定进程所做操作。其取值和相应的含义如下所示:

option 含义
0 将进程挂起等待其结束
WNOHANG 不使进程挂起而立刻返回
WUNTRACED 如果进程已结束则返回

参数status是一个整数指针,当子进程结束时,将子进程的结束状态字存放在该指针指向的缓存区。

宏定义 含义
WIFEXITED(status) 子进程正常结束时,返回值为真(非零值)
WEXITSTATUS(status) 当WIFEXITED为真时,此宏才可以使用。返回进程退出的代码
WIFSIGNALED(status) 子进程接收到信号结束时,返回值为真。但如果进程接收到信号时调用exit函数结束,则返回值为假
WTERMSIG(status) 当WIFSIGNALED为真时,将获得终止该进程的信号
WIFSTOPPED(status) 在调用函数waitpid时制定了WUNTRACED选项,且该子进程使waitpid返回时,这个宏的返回值为真
WSTOPSIG(status) 当WIFSTOPPED为真时,将获得停止该进程的信号
  • 函数返回值说明: 调用成功时,返回收集到的子进程的进程pid;当设置选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;执行失败返回-1并设置错误代码errno

案例演示1: 编写一个程序,使用fork函数与wait函数结合创建一个新进程,使得新创建的子进程在父进程前执行。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = fork();
if(pid == -1)
{
//创建进程失败
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
sleep(2);
printf("This is child process\n");
exit(1);
}
else
{
//父进程
int status;
if(wait(&status) != -1)
{
if(WIFEXITED(status))
printf("子进程正常退出,退出代码:%d\n", WEXITSTATUS(status));
}
printf("This is parent process\n");
exit(0);
}
}

img

案例演示1: 编写一个程序,使用fork函数与waitpid函数结合创建一个新进程,使得新创建的子进程在父进程前执行。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = fork();
if(pid == -1)
{
//创建进程失败
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
sleep(2);
printf("This is child process\n");
exit(1);
}
else
{
//父进程
int status;
if(waitpid(-1, &status, 0) != -1)
{
if(WIFEXITED(status))
printf("子进程正常退出,退出代码:%d\n", WEXITSTATUS(status));
}
printf("This is parent process\n");
exit(0);
}
}

img 将以上代码保存为waitpidProcess.c文件,编译执行。可以看到执行waitpidProcess程序后,尽管子进程使用sleep睡眠了2秒,还是子进程先执行,然后父进程才执行。waitpid函数可以实现与wait函数相同的功能。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全waitProcess函数,等待子进程结束,并且返回子进程的退出的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>


/************************
* 返回值: 调用成功且子进程正常退出返回退出代码,否则返回-1
*************************/
int waitProcess()
{
int status = -1;
/********** BEGIN **********/
pid_t pid;
pid = fork();
if(pid == 0)
{
//子进程
sleep(2);
printf("This is child process\n");
exit(1);
}
else
{
//父进程
int status;
if(waitpid(-1, &status, 0) != -1)
{
if(WIFEXITED(status))
return WEXITSTATUS(status);
}
exit(0);
}

/********** END **********/

return status;
}

进程创建操作-exec函数族

在上一个实训中提到,**vfork函数创建的子进程可以通过调用exec函数族来正确退出。其原理是,使用exec函数族可以执行一个新的程序,并且以新的子进程来完全替换原有的进程地址空间**。

exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

通常exec函数族用来与vfork函数结合一起使用。使用vfork函数创建一个子进程,然后在子进程中使用exec函数族来执行一个新的程序。当在由vfork创建的子进程中使用exec函数族来执行新程序时,子进程的地址空间会被新执行的程序完全覆盖,并且此时vfork的父进程与子进程地址空间被分离开,也就是使用exec函数族创建的新程序不会对vfork的父进程造成任何影响。

exec函数族是库函数,因此使用man 3 exec来查看其使用方法。

使用exec函数族创建进程

exec函数族的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数族格式如下:

    1
    2
    3
    4
    5
    6
    int execl(const char *path, const char *arg, ... /* (char  *) NULL */);
    int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
    int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
    int execvpe(const char *file, char *const argv[], char *const envp[]);

    参数说明:

    1. 函数名中含有字母l的函数,其参数个数不定。其参数由所调用程序的命令行参数列表组成,最后一个NULL表示结束。函数名中含所有字母v的函数,则是使用一个字符串数组指针argv指向参数列表,这一字符串数组和含有l的函数中的参数列表完全相同,也同样以NULL结束。

    2. 函数名中含有字母p的函数可以自动在环境变量PATH指定的路径中搜索要执行的程序。因此它的第一个参数为file表示可执行函数的文件名。而其他函数则需要用户在参数列表中指定该程序路径,其第一个参数path 是路径名。路径的指定可以是绝对路径,也可一个是相对路径。但出于对系统安全的考虑,建议使用绝对路径而尽量避免使用相对路径。

    3. 函数名中含有字母e的函数,比其他函数多含有一个参数envp。该参数是字符串数组指针,用于制定环境变量。调用这两个函数时,可以由用户自行设定子进程的环境变量,存放在参数envp所指向的字符串数组中。这个字符串数组也必须由NULL结束。其他函数则是接收当前环境。

      函数返回值说明: 只有当函数执行失败时,exec函数族才会返回-1并设置错误代码errno。当执行成功时,exec函数族是不会返回任何值。

案例演示1: 编写一个程序,使用vfork函数与exec函数族结合创建一个新进程,并在子进程中执行touch testFile命令创建一个testFile文件,在父进程中返回进程ID。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = vfork();
if(pid == -1)
{
//创建进程失败
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
if(execlp("touch", "touch", "testFile", NULL) < 0)
{
//执行execlp函数失败
exit(-1);
}
}
else
{
//父进程
printf("当前进程为父进程:pid(%d),ppid(%d)\n", getpid(), getppid());
}
//如果执行execlp成功,则以下代码只会被父进程执行
exit(0);
}

img 将以上代码保存为execlProcess.c文件,编译执行。可以看到执行execlProcess程序后,在当前目录下创建了一个名为testFile的文件。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全execlProcess函数,使用vfork函数创建进程,并在子进程中调用创建一个名为testDir的目录,在父进程中输出”Parent Process”字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/************************
* 提示: 在子进程中如果执行exec函数失败要使用exit函数正确退出子进程
*************************/
void execlProcess()
{
pid_t pid = vfork();
if(pid == -1)
{
printf("创建子进程失败(%s)\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
//子进程
/********** BEGIN **********/
if(execlp("mkdir", "mkdir", "testDir", NULL) < 0)
{
//执行execlp函数失败
exit(-1);
}

/********** END **********/
}
else
{
//父进程
/********** BEGIN **********/
printf("Parent Process");

/********** END **********/
}
}

进程创建操作-system

system函数是一个和操作系统相关紧密的函数。用户可以使用它来在用户自己的程序中调用系统提供的各种命令。

执行系统的命令行,其实也是调用程序创建一个进程来实现的。实际上,system函数的实现正是通过调用forkexecwaitpid函数来完成的。详细的实现思路是:首先使用fork创建一个新的进程,并且在子进程中通过调用exec函数族来执行一个新程序,在父进程中通过waitpid函数等待子进程的结束,同时也获取子进程退出代码。

system函数是库函数,因此使用man 3 system来查看其使用方法。

使用system函数执行程序一个新程序

system函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <stdlib.h>
  • 函数格式如下:

    1
    int system(const char *command);

    参数说明: command:需要被执行的命令;

  • 函数返回值说明: 执行成功,返回值是执行命令得到的返回状态,如同wait的返回值一样。执行失败时返回的值分为以下几种情况:执行system函数时,它将调用forkexecwaitpid函数。其中任意一个调用失败都可以使得system函数的调用失败。如果调用fork函数出错,则返回值为-1errno被设置为相应错误;如果调用exec时失败,则表示shell无法执行所设命令,返回值为shell操作的返回值;如果调用waitpid函数失败,则返回值也为-1errno被置为相应值。

案例演示1: 编写一个程序,使用system函数来执行touch testFile命令创建一个testFile文件。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
int ret = system("touch testFile");
if(ret == -1)
{
printf("执行 touch testFile 命令失败(%s)\n", strerror(errno));
return -1;
}
return 0;
}

img 将以上代码保存为system.c文件,编译执行。可以看到执行system程序后,在当前目录下创建了一个名为testFile的文件。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全createProcess函数,使用system函数创建一个名为testDir的目录(** 调用成功返回命令的状态码,失败返回-1**)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

/************************
* 返回值: 调用成功返回命令的状态码,失败返回-1
*************************/
int createProcess()
{
int ret = -1;
/********** BEGIN **********/
ret = system("mkdir testDir");
if(ret == -1)
{
return -1;
}

/********** END **********/

return ret;
}

实现一个简单的命令解析器

Linux系统中Shell是非常重要的一个工具,Shell是一个用C语言编写的程序,它是用户使用 Linux 的桥梁。当打开一个Shell(终端),我们直接可以在命令行中输入要执行的命令,然后Shell会自动的读取我们输入的命令,最后执行这些命令。

RShell的主要思路是:(1)读取用户输入的命令;(2)然后创建一个子进程;(3)使用exec函数族来执行输入的命令,同时挂起父进程;当子进程执行完成后,重复执行步骤1-3即可实现一个简单的命令解析器工具。

使用system函数实现一个简单的命令解析器

system函数可以执行一个命令,那么本案例将介绍如何使用system函数来实现一个简单的命令解析器RShell。详细的步骤可分为以下几步:

1
2
3
读取用户输入;
调用system函数执行命令;
重复第一步;

详细的代码设计为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, char *argv[])
{
char command[128];
while(1)
{
printf("RShell>>");
gets(command);
if(strcasecmp(command, "exit") == 0)
break; //当用户输入exit命令后,退出RShell工具
system(command);
}
return 0;
}

img 将以上代码保存为systemRShell.c文件中,编译执行。可以看到执行systemRShell命名后,我们就可以输入要执行的命令,然后按下回车,该命令就会被执行。当想退出systemRShell时,只需要输入exit回车即可。

使用forkexec函数族和wait实现一个简单的命令解析器

forkexec也可以完成执行一个新程序。详细的步骤可分为以下几步:

1
2
3
4
5
读取用户输入;
调用fork函数创建一个子进程;
在子进程中调用exec来执行用户输入的命令;
在父进程中使用wait来等待子进程执行结束;
重复第一步;

详细的代码设计为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
int main(int argc, char *argv[])
{
char command[128];
while(1)
{
printf("RShell>>");
gets(command);
if(strcasecmp(command, "exit") == 0)
break; //当用户输入exit命令后,退出RShell工具
pid_t pid = fork();
if(pid < 0)
{
printf("创建进程失败(%s)!\n", strerror(errno));
return -1;
}
else if(pid == 0)
{
char *file;
char *point = NULL;
char *tmpArg[10];
point = strtok(command, " ");
file = point;
int index = 0;
while(point != NULL)
{
if(index == 9)
break;
tmpArg[index++] = point;
point = strtok(NULL, " ");
}
char **arg = malloc(sizeof(char *)*(index+1));
int i;
for(i = 0; i < index; i++)
arg[i] = tmpArg[i];
arg[index] = NULL;
if(execvp(file, arg) < 0)
{
//执行execvp函数失败
exit(-1);
}
}
else
{
int status;
wait(&status);
}
}
return 0;
}

img 将以上代码保存为execRShell.c文件中,编译执行。可以看到使用forkexecwait也可以实现一个简单的命令解析器工具。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全RShell函数,实现一个简单的命令解析器工具。(提示:可以使用system函数或者fork+exec+wait。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

/************************
* 参数cmd: 存放要被执行的命令
* 参数commandNum: 命令的个数
* 案例: cmd = {"ls", "touch testFile"} commandNum = 2
*************************/
void RShell(char *cmd[], int commandNum)
{
/********** BEGIN **********/
int temp=0;
while(temp<=commandNum){
pid_t pid = fork();
if(pid == 0)
{
system(cmd[temp]);
exit(0);
}
else
{
sleep(2);
temp++;
}
}
/********** END **********/
}


进程通讯

信号处理函数

相关知识

在 Linux 中,每一个信号都有一个名字,这些名字以 SIG 开头。例如, SIGABRT 是夭折信号,当进程调用 abort 函数时会产生这种信号。SIGALRM 是闹钟信号,由 alarm 函数设置的定时器超时后将产生此信号。

信号产生

信号产生是指触发信号的事件的发生。

例如,通过键盘输入组合键CTRL+C系统会收到 SIGINT。 通过killall -sigid processname以给指定进程发送信号。

比如killall -SIGKILL testsignal给 testsignal 发送 SIGKILL 信号,即杀死进程的信号。

SIGUSR1 和 SIGUSR2 是用户自定义信号,通过上述的方式也可以将信号 SIGUSR1 和 SIGUSR2 传递给进程。

信号的处理动作

信号是异步事件的经典实例,产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量来判断是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行以下操作”。

在某个信号出现时,可以告诉内核按照以下三种方式之一进行处理,我们称之为信号的处理或与信号相关的动作:

  • 忽略此信号。大多数信号可以使用这种方式进行处理,但是 SIGKILL 和 SIGSTOP 除外。
  • 捕获信号。为了做到这一点,要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。
  • 执行系统默认动作。对于大多数信号来说,系统默认动作是终止该进程。
信号处理过程
注册信号处理函数

信号的处理是由内核来代理的,首先程序通过 signal 为每个信号注册处理函数,而内核中有一张信号向量表,对应信号处理机制。这样,信号在进程中注销完毕之后,会调用相应的处理函数进行处理。

信号的检测与响应时机

在系统调用或中断返回用户态的前夕,内核会检查未决信号集,进行相应的信号处理。

处理过程
  • 程序运行在用户态时;
  • 进程由于系统调用或中断进入内核;
  • 转向用户态执行信号处理函数;
  • 信号处理函数完毕后进入内核;
  • 返回用户态继续执行程序。
signal处理接口

signal 函数是最简单的信号处理接口,也是使用比较广泛的一个接口。

1
2
3
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

预览大图

参数的含义:

  • signum:信号名,一般不允许是 SIGKILL 或 SIGSTOP ;
  • handler:常量 SIG_IGN、常量 SIG_DFL或者当收到此信号后要调用的函数的地址。如果是 SIG_IGN,则忽略此信号。如果是 SIG_DFL,则使用系统默认动作。

返回值:返回 sighandler_t句柄或者 SIG_ERR。

应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int catch(int sig);
int main()
{
signal(SIGINT,catch);
printf("hello!
");
sleep(10);
printf("hello!
");
}
int catch(int sig)
{
printf("catch signal!
");
return 1;
}

运行步骤如下: 运行程序: 在 10s 内按键CTRL+C

运行结果如下: hello! ^Ccatch signal! hello!

编程要求

在主函数的最开始会初始化一个全部变量 g_i4event 为 0。

本关的编程任务是补全右侧代码片段中两段BeginEnd中间的代码,具体要求如下:

  • 在 do _signal中分别为信号 SIGUSR1 、 SIGUSR2 注册信号处理函数 funcA 和 funcB ,而后将 g_i4event 置为 1;
  • 完成两个信号处理函数,其中 funcA 中将 g_i4event 置为 2, funcB 中将 g_i4event 为 3。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int g_i4event;
typedef void (*sighandler_t)(int);
/********Begin********/
/*实现funcA和funcB*/
void funcA(int sig)
{
if(g_i4event==1){
printf("The courier has received the task of dispatching milk\n");
printf("The courier has gotten the milk from the store\n");
}
g_i4event=2;
}
void funcB(int sig)
{
if(g_i4event==1){
printf("The courier has received the task of dispatching milk\n");
printf("The courier has not taken the milk from store\n");
}else if(g_i4event==2){
printf("The courier has put the milk in your box\n");
}
g_i4event=3;
}
/*********End*********/

int do_signal(void)
{
/********Begin********/

signal(SIGUSR1,funcA);
signal(SIGUSR2,funcB);

g_i4event=1;

/*********End*********/
}

signal高级处理之sigaction

在 Linux 信号处理函数中,signal函数是最基本的,由于系统版本的不同,signal 由ISO C定义。因为 ISO C 不涉及到多进程、进程组以及终端 I /O等,所以它对信号的定义比较模糊

Unix system V派生的实现支持 signal 函数,但该函数提供旧的不可靠信号语义。4.4BSD 也提供了 signal 函数,并且提供了的信号语义。

因此,signal 的语义与实现有关,为了保险起见,最好使用别的函数来代替 signal 函数。这个函数是 sigaction,也是本实训讲解的重点。

sigaction函数

sigaction 函数取代了 UNIX 早期版本使用的 signal 函数。

1
2
#include <signal.h>
int sigaction(int signo, const struct sigaction *act,struct sigaction *oldact));

img

参数的含义:

  • signo :信号的值,可以为除 SIGKILL 及 SIGSTOP 外的任何一个特定有效的信号;
  • act :指向结构 sigaction 的一个实例的指针,在结构 sigaction 的实例中,指定了对特定信号的处理,但可以为空,进程会以缺省方式对信号处理;
  • oldact :对象指针,指向的对象用来保存返回的原来对相应信号的处理,可指定 oldact 为 NULL 。

注:如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

返回值: 0 表示成功,-1 表示有错误发生。

功能: sigaction 函数用于改变进程接收到特定信号后的行为。

sigaction结构体详解

sigaction 函数最重要的部分就是sigaction结构体,这个被应用于参数 act 和 oldact 中,其定义如下:

1
2
3
4
5
6
7
8
9
10
struct sigaction 
{
union
{
__sighandler_t _sa_handler;
void (*_sa_sigaction)(int,struct siginfo *, void *);
}_u
sigset_t sa_mask;
unsigned long sa_flags;
}
  • 联合数据结构中的两个元素_sa_handler以及 _sa_sigaction 指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为 SIG_IGN (忽略信号);
  • 由 _sa_sigaction 指定的信号处理函数带有三个参数,是为实时信号而设的,它指定一个三参数信号处理函数。第一个参数为信号值,第三个参数没有使用,第二个参数是指向 siginfo_t 结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
siginfo_t 
{
int si_signo; /* 信号值,对所有信号有意义*/
int si_errno; /* errno值,对所有信号有意义*/
int si_code; /* 信号产生的原因,对所有信号有意义*/
union
{
/* 联合数据结构,不同成员适应不同信号 */
//确保分配足够大的存储空间
int _pad[SI_PAD_SIZE];
//对SIGKILL有意义的结构
struct
{
...
}...
... ...
//对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构
struct
{
...
}...
... ...
}
}
  • sa_mask : 信号集,指定在信号处理程序执行过程中,哪些信号应当被阻塞缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定 SA_NODEFER 或者 SA_NOMASK 标志位。

注:请注意 sa_mask 指定的信号阻塞的前提条件:在sigaction()安装信号的处理函数执行过程中,由 sa_mask 指定的信号才会被阻塞。在使用 sigaction 之前,请务必清空或者设置自己所需要的屏蔽字段

  • sa_flags 中包含了许多标志位,包括 SA_NODEFER 及 SA_NOMASK 标志位。另一个比较重要的标志位是 SA_SIGINFO ,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为 sigaction 结构中的 sa_sigaction 指定处理函数,而不应该为 sa_handler 指定信号处理函数,否则设置该标志变得毫无意义。即使为 sa_sigaction 指定了信号处理函数,如果不设置 SA_SIGINFO ,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致错误。 一般的做法是,如果采用 _sa_handler 作为处理函数,则将 sa_flags 设定为0;如果采用 _sa_sigaction 作为处理函数,则将 sa_flags 设定为 SA_SIGINFO。

应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <signal.h>
int catch(int sig);
int main()
{
struct sigaction act;
struct sigaction oldact;
/*注册信号处理函数*/
act.sa_handler = catch;
sigemptyset(&act.sa_mask);//清空sa_mask,这点尤为重要
act.sa_flags = 0;
sigaction(SIGINT, act ,oldact);
printf("hello!
");
sleep(10);
printf("hello!
");
}
int catch(int sig)
{
printf("catch signal!
");
return 1;
}

运行步骤如下:

  1. 运行程序;
  2. 在 10s 内按键CTRL +C

运行结果如下: hello! ^Ccatch signal! hello!

编程要求

在主函数的最开始会初始化一个全部变量 g_i4event 为 0。

本关的编程任务是补全右侧代码片段中两段BeginEnd中间的代码,具体要求如下:

  • 在 do _sigaction中分别为信号 SIGUSR1 、 SIGUSR2 注册信号处理函数 funcA 和 funcB ,而后将 g_i4event 置为 1;
  • 完成两个信号处理函数,其中 funcA 中将 g_i4event 置为 2, funcB 中将 g_i4event 置为 3。

注:采用_sa_sigactionSA_SIGINFO来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int g_i4event;
/********Begin********/
/*实现funcA和funcB*/
void funcA(int sig)
{
if(g_i4event==1){
printf("The JD has notified the storehouse and installer\n");
printf("The TV has delivered to your house from the storehouse\n");
}
g_i4event=2;


}
void funcB(int sig)
{
if(g_i4event==1){
printf("The JD has notified the storehouse and installer\n");
printf("The installer has arrived your house, but the TV is not delivered\n");
}else if(g_i4event==2){
printf("The installer has installed your TV\n");
}
g_i4event=3;

}

/*********End*********/

int do_sigaction(void)
{
/********Begin********/
signal(SIGUSR1,funcA);
signal(SIGUSR2,funcB);
g_i4event=1;
/*********End*********/
}

Linux定时器

alarm函数

使用 alarm 函数可以设置一个定时器,在将来的某个时刻,这个定时器就会超时。当超时时,会产生 SIGALRM 信号。如果忽略或者不捕捉此信号,则其默认动作时终止调用该 alarm 函数的进程。

1
2
#include <unistd.h>
unsigned int alarm(unsigned int seconds);

参数: seconds ,是产生信号需要经过的时钟秒数,也就是定时器的时间。

alarm 安排内核调用进程——在指定的 seconds 秒后发出一个 SIGALRM 的信号。如果指定的参数 seconds 为 0 ,则不再发送 SIGALRM 信号。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回 0 。

注意,在使用时,alarm 只设定为发送一次信号,如果要多次发送,需要多次使用 alarm 调用。

应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
typedef void (*sighandler_t)(int);
int catch(int sig)
{
printf("catch alarm signal!
");
return 0;
}
int main()
{
alarm(3);
sighandler_t res = signal(SIGALRM, catch);
printf("wait for alarm signal!
");
sleep (5);
}

运行结果如下:

1
2
wait for alarm signal!
catch alarm signal!

在主函数的最开始会初始化一个全部变量 g_i4event 为 0 。

本关的编程任务是补全右侧代码片段中两段BeginEnd中间的代码,具体要求如下:

  • 在 do _alarm中首先启动 5s 定时器,将 g_i4event 置为 1;
  • 睡眠一秒,然后为信号 SIGALRM 注册信号处理函数 funcalarm ,将 g_i4event 置为 2;
  • 在信号处理函数,将 g_i4event 置为 3。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int g_i4event;
typedef void (*sighandler_t)(int);
/********Begin********/
/*实现funcA和funcB*/
void funcA(int sig)

{
if(g_i4event==1){printf("You set the alarm clock\n");
printf("You start to work and wait for the alarm ring\n");
}
g_i4event=3;
//return 1;
}

void funcB(int sig)
{
if(g_i4event==2){
printf("Now is 15:30, please leave home and go to apointment\n");
}
g_i4event=3;

}

/*********End*********/

int do_alarm(void)
{
/********Begin********/
alarm(5);
g_i4event=1;
sighandler_t res = signal(SIGALRM, funcA);
sleep (1);
g_i4event=2;

/*********End*********/
}

FIFO管道使用

本关将介绍命名管道的使用方法。对于命名管道和普通文件的操作一样,使用open函数打开,然后使用read和close进行读写操作,最后使用close函数对其进行关闭。与普通文件操作的不同点是:使用read读取普通文件,read不会阻塞。而read读取管道文件,read会阻塞运行,直到管道中有数据或者所有的写端关闭。

命名管道相对比无名管道的好处是可以在两个不同的程序进行数据的传输。并且命名管道当写端彻底关闭后,读端read才会返回0。 注意:

如果管道的读端提前关闭,写端继续写入数据就会发生异常。 读端读取管道中的数据,只要读过的数据就会被清空 。 命名管道使用方法 创建命名管道的库函数是mkfifo,具体的说明如下:

需要的头文件如下: #include <sys/types.h> #include <sys/stat.h> 函数格式如下: int mkfifo(const char *pathname, mode_t mode); 参数说明: pathname:存放命名管道的文件名; mode:创建命名管道的权限,与创建文件的权限参数一致; mode设置说明:

S_IRUSR: 文件所有者的读权限位 S_IWUSR: 文件所有者的写权限位 S_IXUSR : 文件所有者的执行权限位 S_IRGRP: 所有者同组用户的读权限位 S_IWGRP: 所有者同组用户的写权限位 S_IXGRP: 所有者同组用户的执行权限位 S_IROTH: 其他用户的读权限位 S_IWOTH: 其他用户的写权限位 S_IXOTH: 其他用户的执行权限位 函数返回值说明: 调用成功时,返回值0;调用失败时,返回值为-1并设置错误编号errno。 案例演示1: 使用mkfifo函数在当前目录下创建一个命名管道testFIFO,并设置权限为644,并用来传送数据。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int ret = mkfifo("testFIFO", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if(ret == -1)
{
printf("创建命名管道失败(%s)!\n", strerror(errno));
return -1;
}
else
{
printf("创建命名管道成功!\n");
}
return 0;
}

将以上代码保存为createFIFO.c文件,编译执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
char *buf = "This is test FIFO";
int fd = open("testFIFO", O_WRONLY);
if(fd == -1)
{
printf("打开命名管道失败(%s)\n", strerror(errno));
return -1;
}
else
{
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}

将以上代码保存为writeFIFO.c文件,编译执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
char buf[128] = "";
int fd = open("testFIFO", O_RDONLY);
if(fd == -1)
{
printf("打开命名管道失败(%s)\n", strerror(errno));
return -1;
}
else
{
int size = read(fd, buf, 128);
printf("管道中的数据为:%s(%d个字符)\n", buf, size);
}
close(fd);
return 0;
}

将以上代码保存为readFIFO.c文件,编译执行。 首先执行createFIFO程序来创建一个命名管道,然后执行writeFIFO程序用来向管道中写入数据,最后在另一个终端中执行readFIFO程序从管道中读取数据。

编程要求

本关的编程任务是补全右侧代码片段中Begin至End中间的代码,具体要求如下:

创建一个名为FIFO的命名管道文件,并设置权限为650。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
/********** BEGIN **********/
int ret = mkfifo("FIFO", S_IRUSR | S_IWUSR | S_IRGRP | S_IXGRP);
if(ret == -1)
{
printf("创建命名管道失败(%s)!\n", strerror(errno));
return -1;
}
else
{
printf("创建命名管道成功!\n");
}

/********** END **********/

return 0;
}

网络编程

TCP套接字创建与端口绑定

相关知识

在Linux系统中,每种协议都有自己的网络地址数据结构,这些数据结构都以sockaddr_开头,不同的是后缀表示不同的协议。最为常见的是IPv4协议,它的网络地址数据结构为sockaddr_in

由于历史的缘故,在有些库函数中,特定协议的套接字地址结构都要强制转为通用的套接字地址结构,该数据结构被定义在<sys.socket.h>头文件中,详细定义如下所示:

1
2
3
4
5
struct sockaddr
{
unsigned short int sa_family; //套接字地址族
unsigned char sa_data[14]; //14个字节的协议地址
};

其中,sa_family表示套接字的协议族类型,对应于TCP/IP协议该值为AF_INET;成员sa_data存储具体的协议地址。一般在编程中并不对该结构体进行操作,而是使用另一个与它等价的数据结构,例如,IPv4协议的网络地址数据结构为sockaddr_in,格式如下所示:

1
2
3
4
5
6
struct sockaddr_in {
unsigned short sin_family; /*地址类型*/
unsigned short int sin_port; /* 端口号*/
struct in_addr sin_addr; /*IP地址*/
unsigned char sin_zero[8]; /* 填充字节,一般赋值为0 */
};

其中,sa_family表示套接字的协议族类型,对应于TCP/IP协议该值为AF_INETsin_port表示端口号;sin_addr用来存储32位的IP地址,数组sin_zero为填充字段,一般赋值为0

struct in_addr的定义如下所示:

1
2
3
struct in_addr {    
unsigned long s_addr;
}

结构体sockaddr的长度为16字节,结构体sockaddr_in的长度也为16字节,通常在编写基于TCP/IP协议的网络程序时,使用结构体sockaddr_in来设置地址,然后通过强制类型转换成sockaddr类型。结构sockaddr_insockaddr的转换关系如下图所示:

img

TCP网络编程是目前比较通用的方式,例如:HTTP协议、FTP协议等很多广泛应用的协议都是基于TCP协议实现的。TCP编程有两种模式,分别是服务器模式和客户端模式。无论是服务器模式还是客户端模式首先需要创建一个TCP套接字,对于服务器模式,我们还需要绑定一个本地端口。

Linux系统中提供了socketbind两个系统调用函数用来创建套接字与绑定端口操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

创建TCP套接字

Linux系统提供一个socket系统调用来创建一个套接字。 socket函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下:

    1
    int socket(int domain, int type, int protocol);

    参数说明:

    1
    domain:创建套接字所使用的协议族;type:套接字类型;protocol:用于指定某个协议的特定类型,通常某个协议中只有一种特定类型,这样该参数的值仅能设置为0;

domain参数的常用的协议族如下表所示:

取值 含义
AF_UNIX 创建只在本机内进行通信的套接字
AF_INET 使用IPv4 TCP/IP协议
AF_INET6 使用IPv6 TCP/IP协议

type参数的常用的套接字类型如下表所示:

取值 含义
SOCK_STREAM 创建TCP流套接字
SOCK_DGRAM 创建UDP流套接字
SOCK_RAW 创建原始套接字
  • 函数返回值说明: 执行成功返回值为一个新创建的套接字,否则返回-1,并设置错误代码errno

案例演示1: 创建一个使用IPv4协议族的TCP类型的套接字。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
return 0;
}

img 将以上代码保存为createTCP.c文件,编译执行。

绑定端口

TCP服务器模式编程中,我们需要讲一个端口绑定到一个已经建立的套接字上,这样方便客户端程序根据绑定的端口来连接服务器程序。Linux提供了一个bind函数来将一个套接字和某个端口绑定在一起。 socket函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明:

    1
    sockfd:已经创建的套接字;addr:是一个指向sockaddr参数的指针,其中包含了IP地址、端口;addrlen:addr结构的长度;
  • 函数返回值说明: 调用成功,返回值为0,否则返回-1,并设置错误代码errno

注意:

1
2
如果创建套接字时使用的是AF_INET协议族,则addr参数所使用的结构体为struct sockaddr_in指针(详细定义见相关知识)。当设置addr参数的sin_addr为INADDR_ANY而不是某个确定的IP地址时,就可以绑定到任何网络接口。对于只有一个IP地址的计算机,INADDR_ANY对应的就是它的IP地址;对于有多个网卡的主机,INADDR_ANY表示本服务器程序将处理来自任何网卡上相应端口的连接请求。由于计算机中的字符与网络中的字符存储顺序不同。计算机中的整型数与网络中的整型数进行交换时,需要借用相关的函数进行转换。这些函数如下所示:
uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);

使用man 2 函数名可以查看其详细的介绍。

案例演示1: 创建一个使用IPv4协议族的TCP类型的套接字,并与6666这个端口进行绑定。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("绑定端口%d成功\n", PORT);
}
return 0;
}

img 将以上代码保存为bindPort.c文件,编译执行。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全bindSocket函数中代码,绑定一个指定的本地端口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

/************************
* sockfd: 已经创建的套接字
* port: 需要绑定的端口号
* 返回值: 调用成功返回0,否则返回-1
*************************/
int bindSocket(int sockfd, unsigned short port)
{
int ret = -1;
/********** BEGIN **********/
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
ret=bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));

/********** END **********/

return ret;
}

TCP监听与接收连接

相关知识

所谓TCP套接字的监听,指的是TCP套接字的端口处于等待状态,如果有客户端发出连接请求时,这个端口将会接受这个连接请求。连接指的是客户端向服务器发出一个通信请求,服务器会响应这个请求。

当服务器处于监听状态时,如果获得了一个客户端请求,则服务器将会将这个请求存放等待队列中。当系统处于空闲状态时,服务器将会从等待队列中取出请求,接受这个请求并处理这个请求。

Linux系统中提供了listenaccept两个系统调用函数用来监听和接受连接请求操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

TCP监听

Linux系统提供一个listen系统调用来实现监听等待功能。 listen函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下: int listen(int sockfd, int backlog); 参数说明:

    1
    sockfd:已经创建的套接字;backlog:能同时监听的最大连接请求;
  • 函数返回值说明: 调用成功,返回值为0,否则返回-1,并设置错误代码errno

注意:listen并未真正接受客户端的连接请求,只是设置socket的状态为listen模式。

案例演示1: 使用listen函数来监听指定端口的连接信息。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定本地6666端口
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("绑定端口%d成功\n", PORT);
}
//设置最大监听客户端数为5
if(listen(sockfd, 5) == -1)
{
printf("监听端口%d失败: %s\n", PORT, strerror(errno));
return -1;
}
else
{
printf("监听%d端口中...\n", PORT);
}
return 0;
}

img 将以上代码保存为listenPort.c文件,编译执行。

接受连接

当服务器处理监听状态时,如果客户端发出一个连接请求,则服务器需要接受这个请求,并处理该请求。Linux提供了一个accept函数来接受客户端的连接请求。 accept函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下: int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 参数说明:

    1
    sockfd:已经创建的套接字;addr:保存发起连接请求的主机IP地址和端口信息;addrlen:addr结构的长度;
  • 函数返回值说明: 调用成功返回值为被接受请求的套接字编号,否则返回-1,并设置错误代码errno

注意:accept函数接受了一个连接时,会返回一个新的socket编号。接下来的数据传送与读取都是通过这个新的socket编号进行处理的。

案例演示1: 使用accept函数来接受客户端的请求,并打印出客户端主机的基本信息。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#define PORT 6666
int main()
{
//创建一个TCP套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//与6666端口进行绑定
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("绑定端口%d成功\n", PORT);
}
//监听6666端口,并设置最大监听个数为5
if(listen(sockfd, 5) == -1)
{
printf("监听端口%d失败: %s\n", PORT, strerror(errno));
return -1;
}
else
{
printf("监听%d端口中...\n", PORT);
}
int clientSockfd;
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(struct sockaddr_in);
//接受连接请求
if((clientSockfd = accept(sockfd, (struct sockaddr *)&clientAddr, &clientAddrSize)) == -1)
{
printf("接受客户端请求失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("接受客户端请求成功\n");
//inet_ntoa函数将网络地址转换成.点隔的字符串格式
printf("客户端的IP地址:%s \t 端口:%d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
}
return 0;
}

img 将以上代码保存为acceptClient.c文件,编译执行。在浏览器中,输入localhost:6666进行请求,由于HTTP请求也是采用TCP协议进行传送,所以在对6666端口进行访问时,服务器接受了该请求,并打印出了发起连接请求客户端IP地址和端口。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全main函数中代码,监听8888号端口(设置监听的个数大于1),并接受来自客户端的第一个连接请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main()
{
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//与8888端口进行绑定
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}

//监听8888端口,并设置最大监听个数大于1
/********** BEGIN **********/
listen(sockfd, 5);
/********** END **********/

//接受来自客户端的第一个连接请求
/********** BEGIN **********/
int clientSockfd;
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(struct sockaddr_in);
if((clientSockfd = accept(sockfd, (struct sockaddr *)&clientAddr, &clientAddrSize)) != -1)
{
printf("接受客户端请求失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("监听8888端口成功,并且成功接受来自客户端的第一个请求\n");
}

/********** END **********/

close(sockfd);

return 0;
}


TCP连接的建立与终止相关知识

所谓请求连接,指的就是在客户机上向服务器发送信息时,首先需要发送一个连接请求。当服务器接受了客户端的连接请求后,客户端就可以与服务器进行数据的交换。

所谓终止连接,指的就是客户端与服务器之间交换完数据后,需要断开当前连接并释放相关的资源,这样方便下一个客户端的请求。由于服务器同时处理的连接请求数是有限的,所以当客户端交换完数据后不终止连接就会导致后续请求的连接失败。

建立TCP连接时需要经历3次握手操作,主要步骤如下:

1
连接开始时,客户端发送SYN包,并包含了自己的初始序号a;服务器收到SYN包以后会回复一个SYN包,其中包含了对上一个a包的回应信息ACK和自己的初始序号b;客户端收到回应的SYN包以后,回复一个ACK包做响应;

img [TCP建立连接三次握手过程]

终止TCP连接时需要经历4次握手操作,主要步骤如下:

1
首先进行关闭的一方(即发送第一个FIN)将执行主动关闭,而另一方(收到这个FIN)执行被动关闭;当被动关闭方收到这个FIN,它发回一个ACK;被动关闭方发送一个FIN请求;主动关闭方收到这个FIN请求后,回复一个ACK请求;

img [TCP终止连接四次握手过程]

Linux系统中提供了connectclose两个系统调用函数用来建立和终止连接请求操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

TCP建立连接请求

Linux系统提供一个connect系统调用来实现连接目标网络服务功能。 connect函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明:

    1
    sockfd:已经创建的套接字;addr:指针要连接的服务器IP地址和端口信息;addrlen:addr结构的长度;
  • 函数返回值说明: 调用成功,返回值为0,否则返回-1,并设置错误代码errno

案例演示1: 在上一关卡的案例中,我们实现了如何监听和接受连接,本案例将使用connect实现与上一关卡案例中的服务器建立连接关系。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define SERVER_IP "127.0.0.1"
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建TCP套接字成功\n");
}
struct sockaddr_in servAddr;
bzero(&servAddr, sizeof(servAddr)); //清空
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
//使用inet_addr函数将点分十进制的IP转换成一个长整数型数
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
//连接服务器
if(connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)
{
printf("请求连接服务器失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("请求连接%s:%d成功\n", SERVER_IP, PORT);
}
return 0;
}

img 将以上代码保存为connectServer.c文件,编译执行。可以看到建立连接成功,服务器将客户端的IP地址和端口打印出来。注意:客户端的端口是随机分配的。

终止连接

当客户端与服务器进行数据交换完成后,我们需要断开已经建立的连接。Linux提供了两个函数来终止连接,分别是close函数和shutdown函数。

close函数的使用方法与文件的关闭一样,只需将要关闭的套接字传给函数即可,此处就不做详细的介绍。

shutdown函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/socket.h>
  • 函数格式如下:

    1
    int shutdown(int sockfd, int how);

    参数说明:

    1
    sockfd:已经建立连接的套接字编号;how:具体的关闭行为;

how参数的常用取值及其含义如下表所示:

取值 含义
SHUT_RD 表示切断读,之后不能使用该套接字进行读操作
SHUT_WR 表示切断写,之后不能使用该套接字进行写操作
SHUT_RDWR 表示切断读写,与close函数功能一样
  • 函数返回值说明: 调用成功,返回值为0,否则返回-1,并设置错误代码errno

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全connectSocket函数中代码,向指定的服务器发出连接请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

/************************
* ipAddr: 远程服务器的IP地址
* port: 远程服务器的端口
*************************/
void connectSocket(char *ipAddr, unsigned short port)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return ;
}

//连接到指定的服务器
/********** BEGIN **********/
struct sockaddr_in servAddr;
bzero(&servAddr, sizeof(servAddr)); //清空
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(port);
//使用inet_addr函数将点分十进制的IP转换成一个长整数型数
servAddr.sin_addr.s_addr = inet_addr(ipAddr);
//连接服务器
connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr));

/********** END **********/

close(sockfd);
}


TCP数据传送

相关知识

当创建好TCP套接字并且完成了客户端与服务器间的连接,接下来,我们就可以实现在客户端与服务器间的数据传送功能。

在客户端与服务器间进行数据传送时,一端(客户端/服务器)用于向建立好的套接字写入数据,另一端(服务器/客户端)用于从建立好的套接字中读取数据,这样一来一回的就实现了客户端与服务器间的数据交换功能。

img [TCP服务器与客户端间的数据传送框架]

Linux系统中提供了recvsendreadwrite四个系统调用函数用来完成客户端与服务器间的数据发送和接收操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

数据发送与接收方式一

Linux系统提供recvsend系统调用来实现数据的发送与接收功能。 recvsend函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下:

    1
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t send(int sockfd, const void *buf, size_t len, int flags);

    参数说明:

    1
    sockfd:已经创建的套接字;buf:用于存放接收和发送的数据;len:buf的长度;flags:设置接收与发送的控制选项,一般设置为0;
  • 函数返回值说明: 调用成功,返回值为实际接收或发送的数据字节个数,否则返回-1,并设置错误代码errno

案例演示1: 使用recvsend函数实现客户端与服务器间的数据传送功能,客户端读取用户的数据并发送给服务器,服务器接收到数据后打印出来,当客户端读取到exit字符串时,关闭当前连接。详细代码如下所示:

客户端主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define SERVER_IP "127.0.0.1"
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in servAddr;
bzero(&servAddr, sizeof(servAddr)); //清空
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
//使用inet_addr函数将点分十进制的IP转换成一个长整数型数
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
//连接服务器
if(connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)
{
printf("请求连接服务器失败: %s\n", strerror(errno));
return -1;
}
else
{
char userInput[100];
while(gets(userInput) != NULL)
{
if(strcasecmp(userInput, "exit") == 0)
break;
send(sockfd, userInput, strlen(userInput), 0); //发送数据
}
close(sockfd); //关闭连接
}
return 0;
}

将以上代码保存为sendData.c文件。

服务器主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
printf("创建TCP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定本地6666端口
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
//监听6666端口,并设置最大监听个数为5
if(listen(sockfd, 5) == -1)
{
printf("监听端口%d失败: %s\n", PORT, strerror(errno));
return -1;
}
int clientSockfd;
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(struct sockaddr_in);
//接受连接请求
if((clientSockfd = accept(sockfd, (struct sockaddr *)&clientAddr, &clientAddrSize)) == -1)
{
printf("接受客户端请求失败: %s\n", strerror(errno));
return -1;
}
else
{
char userInput[100];
memset(userInput, 0, sizeof(userInput));
while(recv(clientSockfd, userInput, sizeof(userInput), 0) > 0)
{
printf("客户端:%s\n", userInput);
memset(userInput, 0, sizeof(userInput)); //清空上次接收的缓存
}
close(clientSockfd); //关闭客户端连接
close(sockfd); //关闭服务器套接字
}
return 0;
}

将以上代码保存为recvData.c文件。

img 编译执行以上两个程序,可以看到服务器接收到了客户端发过来的数据,并将其打印出来。注意:先执行recvData,再执行sendData程序。

数据发送与接收方式二

Linux系统可以使用writeread系统调用来实现数据的发送与接收功能。 writeread函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <unistd.h>
  • 函数格式如下:

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

    参数说明:

    1
    fd:已经创建的套接字;buf:用于存放接收和发送的数据;count:buf的长度;
  • 函数返回值说明: 调用成功,返回值为实际接收或发送的数据字节个数,否则返回-1,并设置错误代码errno

注意:writeread函数的使用方法与文件的读写相同,此处就不做详细的案例介绍。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全main函数中代码,实现服务器与客户端间的数据传送功能。
  • 将客户端发来的数据完全打印出来(提示:换行打印),并且将接收到的数据原样发送给客户端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

#define PORT 8888

int main()
{
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//与PORT端口进行绑定
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
return -1;
}

//监听PORT端口,并设置最大监听个数为5
if(listen(sockfd, 5) == -1)
{
return -1;
}

int clientSockfd;
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(struct sockaddr_in);
//接受连接请求
if((clientSockfd = accept(sockfd, (struct sockaddr *)&clientAddr, &clientAddrSize)) == -1)
{
return -1;
}
else
{
char data[100];
//接收客户端传来的数据,并打印出来(提示:换行打印)
//同时将接收到的数据原样发送给客户端
/********** BEGIN **********/
memset(data, 0, sizeof(data));
while(recv(clientSockfd, data, sizeof(data), 0) > 0)
{
printf("%s\n", data);
send(clientSockfd, data, strlen(data), 0); //发送数据
memset(data, 0, sizeof(data)); //清空上次接收的缓存
}

/********** END **********/
}

close(clientSockfd);
close(sockfd);

return 0;
}


UDP套接字创建与端口绑定

相关知识

所谓无连接的套接字通信,指的就是使用UDP协议进行数据的传输。使用这种协议进行通信时,两台计算机间是不需要建立连接的。

在实训Linux之网络编程(TCP)中,我们介绍了使用TCP协议进行数据的传输。TCP协议是需要在服务器与客户端间进行建立连接的,并且能够保证传输的数据可靠的到达对方。那么TCP协议与UDP协议的区别主要包括以下几点:

1
TCP是面向连接的,也就是在数据传输前需要建立连接;而UDP时无连接的,即数据传输前不需要建立连接;TCP提供可靠的服务,也就是使用TCP协议传输的数据不会丢失、不会重复并且按序到达;而UDP协议尽最大努力将数据传输到对方,即不保证可靠交付;由于TCP需要对数据进行校验保证可靠交付,所以其实时性不如UDP好;TCP对系统资源需求较大;而UDP相对需求较少;

由于UDP以简单、传输快的优势,在很多应用场景下取代了TCP。例如:在视频会议、语音通话场景下,UDP协议就比TCP协议更适合。因为,视频和语音丢部分内容不会影响到结果,而这些对实时性要求很高,所以采用UDP协议更适合。

在编程方面,UDP使用到的数据结构与TCP一致。例如在IPv4协议下,都使用struct sockaddr_in结构体来表示网络地址信息。

Linux系统中提供了socketbind两个系统调用函数用来创建套接字与绑定端口操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

创建UDP套接字

Linux系统提供一个socket系统调用来创建一个套接字。 socket函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下:

    1
    int socket(int domain, int type, int protocol);

    参数说明:

    1
    domain:创建套接字所使用的协议族;type:套接字类型;protocol:用于指定某个协议的特定类型,通常某个协议中只有一种特定类型,这样该参数的值仅能设置为0;

domain参数的常用的协议族如下表所示:

取值 含义
AF_UNIX 创建只在本机内进行通信的套接字
AF_INET 使用IPv4 TCP/IP协议
AF_INET6 使用IPv6 TCP/IP协议

type参数的常用的套接字类型如下表所示:

取值 含义
SOCK_STREAM 创建TCP流套接字
SOCK_DGRAM 创建UDP流套接字
SOCK_RAW 创建原始套接字
  • 函数返回值说明: 执行成功返回值为一个新创建的套接字,否则返回-1,并设置错误代码errno

案例演示1: 创建一个使用IPv4协议族的UDP类型的套接字。详细代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("创建UDP套接字失败: %s\n", strerror(errno));
return -1;
}
else
{
printf("创建UDP套接字成功\n");
}
return 0;
}

img 将以上代码保存为createUDP.c文件,编译执行。

绑定端口

UDP服务器模式编程中,我们需要将一个端口绑定到一个已经创建好的套接字上,这样方便客户端程序根据绑定的端口来向服务器传输数据。Linux提供了一个bind函数来将一个套接字和某个端口绑定在一起。 socket函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明:

    1
    sockfd:已经创建的套接字;addr:是一个指向sockaddr参数的指针,其中包含了IP地址、端口;addrlen:addr结构的长度;
  • 函数返回值说明: 调用成功,返回值为0,否则返回-1,并设置错误代码errno

提示:使用bind函数为UDP协议绑定一个端口的使用方法与为TCP协议绑定端口一致,详细介绍请参看实训 Linux之网络编程(TCP) 中的第一关卡内容。

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全UDPSocket函数中代码,创建一个UDP套接字,并绑定一个指定的本地端口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

/************************
* port: 需要绑定的端口号
* 返回值: 调用成功返回0,否则返回-1
*************************/
int UDPSocket(unsigned short port)
{
int ret = -1;
/********** BEGIN **********/
struct sockaddr_in adr_inet;
adr_inet.sin_family=AF_INET;
adr_inet.sin_port=htons(port);
adr_inet.sin_addr.s_addr =htonl(INADDR_ANY);
bzero(&(adr_inet.sin_zero),8);

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(bind(sockfd,(struct sockaddr *)&adr_inet,sizeof(adr_inet))!=-1){
ret=0;
}
/********** END **********/

return ret;
}

UDP数据传送

相关知识

由于UDP协议不需要建立连接即可进行数据的传输,在服务器端创建好UDP套接字并绑定一个本地端口后,接下来,我们就可以实现在客户端与服务器间的数据传送功能。

在客户端与服务器间进行数据传送时,一端(客户端/服务器)用于向建立好的套接字写入数据,另一端(服务器/客户端)用于从建立好的套接字中读取数据,这样一来一回的就实现了客户端与服务器间的数据交换功能。

在实训Linux之网络编程(TCP)中介绍的TCP数据传输使用到的函数是recvsendreadwrite四个系统调用。而UDP协议数据传输使用的函数是sendtorecvfrom两个系统调用函数。

Linux系统中提供了sendtorecvfrom两个系统调用函数用来完成UDP协议的客户端与服务器间的数据发送和接收操作。

以上函数我们可以使用man命令来查询该函数的使用方法。具体的查询命令为:man 2 函数名

UDP协议的数据发送

Linux系统提供sendto系统调用来实现数据的发送功能。 sendto函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下:

    1
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

    参数说明:

    1
    sockfd:已经创建的套接字;buf:用于存放接收和发送的数据;len:buf的长度;flags:设置接收与发送的控制选项,一般设置为0;dest_addr:要发往主机的网络地址信息;addrlen:dest_addr的长度;
  • 函数返回值说明: 调用成功,返回值为实际发送的数据字节个数,否则返回-1,并设置错误代码errno

UDP协议的数据接收

Linux系统可以使用recvfrom系统调用来实现数据的接收功能。 recvfrom函数的具体的说明如下:

  • 需要的头文件如下:

    1
    #include <sys/types.h>#include <sys/socket.h>
  • 函数格式如下:

    1
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

    参数说明:

    1
    sockfd:已经创建的套接字;buf:用于存放接收和发送的数据;len:buf的长度;flags:设置接收与发送的控制选项,一般设置为0;src_addr:要接收主机的网络地址信息;addrlen:src_addr的长度;
  • 函数返回值说明: 调用成功,返回值为实际接收的数据字节个数,否则返回-1,并设置错误代码errno

案例演示1: 利用UDP协议实现如下功能:使用recvfromsendto函数实现客户端与服务器间的数据传送功能,客户端读取用户的数据并发送给服务器,服务器接收到数据后打印出来,当客户端读取到exit字符串时,关闭当前客户端套接字,当服务器收到exit字符串时,关闭当前服务器套接字。详细代码如下所示:

客户端主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define SERVER_IP "127.0.0.1"
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("创建UDP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in servAddr;
bzero(&servAddr, sizeof(servAddr)); //清空
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
//使用inet_addr函数将点分十进制的IP转换成一个长整数型数
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
char userInput[100];
while(gets(userInput) != NULL)
{
sendto(sockfd, userInput, sizeof(userInput), 0, (struct sockaddr *)&servAddr, sizeof(servAddr)); //发送数据
if(strcasecmp(userInput, "exit") == 0)
break;
}
close(sockfd); //关闭连接
return 0;
}

将以上代码保存为sendData.c文件。

服务器主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#define PORT 6666
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("创建UDP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定本地6666端口
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
char userInput[100];
while(recvfrom(sockfd, userInput, sizeof(userInput), 0, (struct sockaddr *)&clientAddr, &clientAddrLen) > 0)
{
if(strcasecmp(userInput, "exit") == 0)
break;
printf("客户端:%s\n", userInput);
}
close(sockfd); //关闭服务器套接字
return 0;
}

将以上代码保存为recvData.c文件。

img 编译执行以上两个程序,可以看到服务器接收到了客户端发过来的数据,并将其打印出来。 注意:

1
先执行recvData,再执行sendData程序;由于UDP协议可以实现多个客户端对于一个服务器,所以当客户端退出后,服务器是不会退出的,因此,我们通过接收客户端传来的数据来判断是否服务器也需要退出;

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全main函数中代码,使用UDP协议实现服务器与客户端间的数据传送功能。
  • 将客户端发来的数据完全打印出来(提示:换行打印),并且将接收到的数据原样发送给客户端。
  • 当服务器收到exit字符串时,退出当前服务器程序(提示:不打印退出字符串exit)。
  • 提示:在每次接收字符串前要将存放字符串的变量清空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

#define PORT 8888

int main()
{
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//与PORT端口进行绑定
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
return -1;
}

char data[100];
//接收客户端传来的数据,并打印出来(提示:换行打印)
//同时将接收到的数据原样发送给客户端
//当接收到"exit"字符串时,退出当前程序,不打印出"exit"字符串
//提示:在每次接收字符串前要将存放字符串的变量清空
/********** BEGIN **********/
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
memset(data, 0, sizeof(data));
while(recvfrom(sockfd, data, sizeof(data), 0 ,(struct sockaddr *)&clientAddr, &clientAddrLen) > 0)
{
if(strcasecmp(data, "exit") == 0)
break;
printf("%s\n", data);
sendto(sockfd, data, strlen(data), 0 ,(struct sockaddr *)&clientAddr, sizeof(clientAddr)); //发送数据
memset(data, 0, sizeof(data)); //清空上次接收的缓存
}

/********** END **********/

close(sockfd);

return 0;
}

UDP项目实战

相关知识

通过前2关的学习,我们学会如何使用UDP协议来在不同的计算机间传输数据。使用UDP协议通信时,客户端与服务器的交互过程如下图所示:

img [UDP协议的套接字通信]

利用以上2关的知识就可以简单的文件上传工具。该工具包含两部分内容,分别是接收文件的服务器和上传文件的客户端。

实现文件上传工具-服务器端

实现文件上传服务器主要分为以下几个步骤:

1
首先是接收要上传的文件名称;当接收到客户端发来的文件名称后,在本地创建该文件;从客户端中接收文件的内容,并保存在创建好的文件中;当客户端上传文件内容接收后,服务器关闭创建的文件;

通过以上几步就可以实现文件上传服务器的功能。为了识别客户端发送来的数据类型,我们定义了以下协议:

1
我们定义客户端与服务器间的数据块为1024KB(也就是每次传输的数据大小);我们将每个数据块的前1个字节定义为数据块的类型,也就是用于标示该数据块中的内容是文件名称或文件内容或上传结束,我们使用f表示该数据块为文件名称,c标示该数据块为文件内容,e表示上传结束;

详细的代码设计为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <arpa/inet.h>
//定义数据块大小
char data[1024];
//定义服务器端口
#define PORT 6667
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("创建UDP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr)); //清空
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定本地6666端口
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
//存放客户端主机信息
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
int fd;
int recvLen;
//接收来自客户端发来的数据
while((recvLen = recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&clientAddr, &clientAddrLen)) > 0)
{
//根据自定义的协议来判断客户端发送来的数据块类型
if(data[0] == 'e')
{
//上传文件完成,关闭当前打开的文件
close(fd);
break;
}
else if(data[0] == 'f')
{
//数据块是上传文件的名称,在本地创建该文件
fd = open(&(data[1]), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
printf("接收客户端上传的%s文件...\n", &(data[1]));
}
else
{
//数据块为上传文件的内容,将内容写入到新创建的文件中
//因为data的第一个字符为文件块类型,所以只需从第二个字符开始写文件
write(fd, &(data[1]), recvLen - 1);
}
//给客户端回复一个接收确认的标识OK
sendto(sockfd, "OK", strlen("OK"), 0, (struct sockaddr *)&clientAddr, clientAddrLen);
memset(data, 0, sizeof(data));
}
close(sockfd); //关闭服务器套接字
return 0;
}

将以上代码保存为uploadFileServer.c文件中,编译执行。

实现文件上传工具-客户端

实现文件上传客户端主要分为以下几个步骤:

1
首先是获取要上传文件的名称,并将该名称发送给服务器;当收到服务器的回复后,打开文件并读取内容,将读取的内容发送给服务器;当文件所有内容发送完成后,给服务器发送一个上传完成的标识,并退出客户端;

通过以上几步就可以实现文件上传客户端的功能。我们遵循在实现服务器时定义的协议,同时为了有序的将文件内容发送到服务器,因此,我们需要服务器每次接收成功一块数据后,给客户端返回一个标识,当客户端收到该标识后再继续发送下一块的内容。

详细的代码设计为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
//定义数据块大小
char data[1024];
//定义服务器端口和服务器地址
#define PORT 6667
#define SERVER_IP "127.0.0.1"
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("创建UDP套接字失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in servAddr;
int servAddrLen = sizeof(servAddr);
bzero(&servAddr, sizeof(servAddr)); //清空
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
//使用inet_addr函数将点分十进制的IP转换成一个长整数型数
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
char filePath[100];
//读取要上传文件的路径
printf("请输入上传的文件路径+名称: ");
scanf("%s", filePath);
memset(data, 0, 1024);
data[0] = 'f'; //根据上传协议,设置数据块类型为文件名称
strcpy(&(data[1]), filePath);
//向服务器发送要上传文件的名称
sendto(sockfd, data, strlen(data), 0, (struct sockaddr *)&servAddr, servAddrLen);
memset(data, 0, 1024);
//等待服务器接收确认,然后再继续上传文件内容
recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&servAddr, &servAddrLen);
int fd = open(filePath, O_RDONLY); //打开要上传的文件
int readSize = 0;
data[0] = 'c'; //设置数据块类型为文件内容
//循环读取文件内容(注意:一次实际读取的文件内容最大字符个数为1023)
while((readSize = read(fd, &(data[1]), 1023*sizeof(char))) != 0)
{
//将读取到的文件内容连同数据块类型标识一起发送给服务器
sendto(sockfd, data, readSize+1, 0, (struct sockaddr *)&servAddr, servAddrLen);
//等待服务器接收确认,然后再上传文件下一块的内容
recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&servAddr, &servAddrLen);
memset(data, 0, sizeof(data));
}
//当文件内容上传完成后,发送上传结束标识
data[0] = 'e';
sendto(sockfd, data, 1, 0, (struct sockaddr *)&servAddr, servAddrLen);
close(fd);
close(sockfd); //关闭客户端套接字
return 0;
}

将以上代码保存为uploadFileClient.c文件中,编译执行。

img 编译执行,我们可以看到客户端将要上传的文件成功上传到了服务器。 注意:

1
因为同一目录下不能存在两个名称相同的文件,因此不要将服务器程序放置到与上传文件所在的目录下;首先要先执行服务器程序uploadFileServer,然后再执行客户端程序uploadFileClient;

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 补全downloadFileClient函数,该函数是用来从服务器上下载一个指定的文件,实现下载文件的客户端部分。

  • 下载文件要遵循以下步骤:

    1
    首先向服务器发送当前要下载的文件名称;从服务器接收数据块;向服务器发送一个接收确认请求(例如发送OK字符串);重复第2步骤,直到收到的数据块类型为下载文件结束时,关闭当前打开的文件,然后退出程序;
  • 客户端与服务器间所遵循以下下载协议:

    1
    我们定义客户端与服务器间的数据块为16KB(也就是每次传输的数据大小);我们将每个数据块的前1个字节定义为数据块的类型,也就是用于标示该数据块中的内容是文件内容还是下载结束标识,我们使用c标示该数据块为文件内容,e表示下载结束;
  • 提示:首先客户端向服务器先发送要下载文件的名称,告诉服务器要下载哪个文件;然后服务器读取文件,并发送给客户端。下载文件的客户端实现部分与以上案例中介绍的上传文件服务器端的实现大致相同,请仔细参看以上案例的实现。

文件下载工具的服务器端的核心伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//接收要下载的文件名称
recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&clientAddr, &clientAddrLen);
//数据块是下载文件的名称,在本地打开该文件
fd = open(filePath, O_RDONLY);
//设置数据块类型为文件内容
data[0] = 'c';
int readSize = 0;
//读取要下载文件的数据
while((readSize = read(fd, &(data[1]), 15*sizeof(char))) != 0)
{
//向客户端发送数据
sendto(sockfd, data, readSize+1, 0, (struct sockaddr *)&clientAddr, clientAddrLen);
//等待客户端的接收确认
recvfrom(sockfd, ack, sizeof(ack), 0, (struct sockaddr *)&clientAddr, &clientAddrLen);
//清空data文件数据部分
memset(&(data[1]), 0, sizeof(data)-1);
}
//当文件内容读取完成后,发送下载结束标识
data[0] = 'e';
sendto(sockfd, data, 1, 0, (struct sockaddr *)&clientAddr, clientAddrLen);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

//定义数据块大小
char data[16];
//定义服务器端口和服务器地址
#define PORT 8889
#define SERVER_IP "127.0.0.1"

int main(int argc, char *argv[])
{
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in servAddr;
int servAddrLen = sizeof(servAddr);
bzero(&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);

//由用户传入的要下载文件的名称
char *downLoadFileName = argv[1];
printf("%s\n", argv[1]);
//先在本地创建要下载的文件
int fd = fd = open(downLoadFileName, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
//向服务器发送要上传文件的名称
sendto(sockfd, downLoadFileName, strlen(downLoadFileName), 0, (struct sockaddr *)&servAddr, servAddrLen);

/********** BEGIN **********/
//等待服务器接收确认,然后再继续上传文件内容
int recvLen = 0;
recvLen =recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&servAddr, &servAddrLen);
write(fd, &(data[1]), recvLen - 1);
sendto(sockfd, "OK", strlen("OK"), 0, (struct sockaddr *)&servAddr, servAddrLen);

//data[0] = 'c'; //设置数据块类型为文件内容
//循环读取文件内容(注意:一次实际读取的文件内容最大字符个数为1023)
while((recvLen=recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&servAddr, &servAddrLen)) > 0)
{
//根据自定义的协议来判断客户端发送来的数据块类型
if(data[0] == 'e')
{
//下载文件完成,关闭当前打开的文件
close(fd);
break;
}
else
{
//数据块为下载文件的内容,将内容写入到新创建的文件中
//因为data的第一个字符为文件块类型,所以只需从第二个字符开始写文件
write(fd, &(data[1]), recvLen - 1);
sendto(sockfd, "OK", strlen("OK"), 0, (struct sockaddr *)&servAddr, servAddrLen);
}
memset(data, 0, sizeof(data));
}

/********** END **********/

close(sockfd);

return 0;
}

select机制

相关知识

select机制是一种很常见的多路复用方法,准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。 当调用select()时,由内核根据 IO 状态修改 fd_set 的内容,由此来通知执行了select()的进程的那个 Socket 或文件可读或可写。主要用于 Socket 通信当中。

select函数

使用select()可以完成非阻塞(所谓非阻塞方式 non-block ,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生,则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。

1
2
3
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

img

######函数参数 通过传递函数参数告知内核以下三个信息: - 我们所关心的描述符; - 对于每个描述符我们关心的条件(读,写,异常); - 希望等待多长时间(永久等待,等一段时间,不等待)。

函数参数详解如下:

  • 第一个参数 maxfdp1 指定待测试的描述字个数,它的值是待测试的最大描述字加 1 (因此把该参数命名为 maxfdp1),描述字0,1,2...,maxfdp1-1均将被测试。文件描述符是从 0 开始的。
  • 中间的三个参数 readset 、writeset 和 exceptset 指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
1
2
3
4
void FD_ZERO(fd_set *fdset);//清空集合
void FD_SET(int fd, fd_set *fdset);//将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset);//将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset);// 检查集合中指定的文件描述符是否可以读写
  • timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
1
2
3
4
5
struct timeval
{
long tv_sec; //seconds
long tv_usec; //microseconds
};

这个参数有三种可能:

  • 永远等待下去:仅在有一个描述字准备好 I/O 时才返回。为此,把该参数设置为空指针 NULL 。

  • 等待一段固定时间:在有一个描述字准备好 I/O 时返回,但是不超过由该参数所指向的 timeval 结构中指定的秒数和微秒数。

  • 根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个 timeval 结构,而且其中的定时器值必须为 0。

    函数返回值

    select 函数返回,内核告诉我们:

  • 已准备好的描述符数量。

  • 哪个描述符已准备好读、写、或异常。 使用该返回值,就可以调用相应的 I/O 函数(read,write),并且明确知道该函数不会阻塞。

返回值如下:

  • 返回 -1,表示出错,例如指定的描述符集都没准备好时捕捉到一个信号。
  • 返回 0,表示没有描述符准备好,指定的时间已经超过。
  • 返回正值,表示已经准备好的描述符数,三个描述符集中仍旧打开的位是对应已准备好的描述符位。

整个 select 流程如下:

img

应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
......
fd_set readfd;
struct timeval timeout;
int fd;
while(1)
{
ret=select(fd+1,&readfd,NULL,NULL,&timeout);
if(ret)//返回正值
{
......
}
continue;
......

编程要求

本关的编程任务是补全右侧代码片段中BeginEnd中间的代码,具体要求如下:

  • 将文件描述符加入到读集合中去。
  • 设置 3s 超时机制。
  • 检测 I/O 有变化,读取文件中的数据。
  • 注意:数据读取完后,立刻退出函数体,以免造成死循环.
  • 具体请参见后续测试样例。

本关涉及的代码文件SelectDamo.c的代码框架如下:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#define BUFFER_SIZE 1024
/*buffer为数据缓冲区,将读取的到数据填充进来*/
int do_select(int fd, char *buffer)
{
/*********Begin*******/
/**********End********/
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

int do_select(int fd, char *buffer)
{
/*********Begin*******/
fd_set readfd;
FD_ZERO(&readfd);
FD_SET(fd,&readfd);
struct timeval timeout;
timeout.tv_sec=3;
int ret=-1;
while(1)
{
ret=select(fd+1,&readfd,NULL,NULL,&timeout);
if(ret)//返回正值
{
//int readSize = 0;
//char tempC[BUFFER_SIZE];
int temp=1,length=0;
//while(1){
temp = read(fd, buffer, BUFFER_SIZE);
/*if(buffer[length-1]==0|buffer[length-1]=='\n'){
break;
}else{
readSize = readSize+1;
buffer[length++]=tempC;
}
}*/
break;
}

}
/**********End********/
return 0;
}