ZYZ

Unix程序设计课堂知识点整理

有 N 人看过

Linux编程基础

[TOC]

1.1 从Unix到Linux

为了提升UNICS系统的性能与兼容性,采用高级语言对其进行重构,并确定该操作系统名称为UNIX,这就是最早的 UNIX 操作系统(相对于 Multics ,UNIX 具有单一的意思)

GNU通用公共许可协议(GNU GPL)是一个广泛被使用的自由软件许可协议条款,最初由Stallman为GNU计划而撰写,GPL授予程序接受人以下权利,或称“自由”:

⚫ 以任何目的运行此程序的自由;

⚫ 再发行复制件的自由;

⚫ 改进此程序,并公开发布改进的自由

1.2 Linux概述

Linux是一个类Unix(Unix-like)的操作系统,在1991年发行了它的第一个版本

1991年11月,芬兰赫尔辛基大学的学生 Linus Torvalds写了个小程序,取名为Linux,放在互联网上。

1993,在一批高水平黑客的参与下,诞生了Linux 1.0 版

1994年,Linux 的第一个商业发行版 Slackware 问世

1996年,美国国家标准技术局的计算机系统实验室确认 Linux 版本 1.2.13(由 Open Linux 公司打包)符合 POSIX 标准

1.3 GNU & Linux

image-20210611103621201

1.4 Linux 内核

Linux内核采用的是双树系统

一棵是稳定树,主要用于发行

另一棵是非稳定树或称为开发树,用于产品开发和改进

Linux内核版本号由3位数字组成

image-20210611103506715

2.2 Vi编辑器使用

1.vi的工作模式

输入模式:输入字符为命令,可进行删除、修改、存盘等操作 。

命令模式:输入字符作为文本内容。

末行模式:命令模式下输入“:/?”三个中任意一个,可移到屏幕最底一行。

image-20210611103853138

(1)命令模式

输入模式下,按ESC可切换到命令模式,常用命令:

:q! 离开vi,并放弃刚在缓冲区内编辑的内容
:wq 将缓冲区内的资料写入磁盘中,并离开vi
:ZZ 同wq
:x 同wq
:w 将缓冲区内的资料写入磁盘中,但并不离开vi
:q 离开vi,若文件被修改过,则要被要求确认是否放弃修改的内容,此指令可与:w配合使用

(2)输入模式

输入以下命令即可进入vi输入模式

a(append) 在光标之后加入资料
A 在该行之末加入资料
i(insert) 在光标之前加入资料
I 在该行之首加入资料
o(open) 新增一行于该行之下,供输入资料用
O 新增一行于该行之上,供输入资料用
dd 删除当前光标所在行
x 删除当前光标字符
X 删除当前光标之前字符
U 撤消
F 查找
ESC 离开输入模式

2.Vi其他功能命令

(1)复制粘贴

yw 将光标所在之处到字尾的字符复制到缓冲区
yy 复制光标所在行到缓冲区
#yy 如:6yy表示拷贝从光标所在行往下数6行文字
p 将缓冲区内的字符贴到光标所在位置

(2)查找/替换

?字符串 从当前光标位置开始向后查找字符串
/字符串 从当前光标位置开始向前查找字符串
n 继续上一次查找
Shift+n 以相反的方向继续上一次查找

(3)环境设置

:set ai 自动缩进,每一行开头都与上一行的开头对齐
:set nu 编辑时显示行号
:set dir=./ 将交换文件.swp保存在当前目录
:set sw=4 设置缩进的字符数为4
:syntax on 或者 :syntax=on 开启语法着色

3.1 GCC编译器介绍

GCC是一个强大的工具集合,它包含了预处理器、编译器、汇编器、链接器等组件。它会在需要的时候调用其他组件。
输入文件的类型和传递给gcc的参数决定了gcc调用具体的哪些组件。

GCC 参数选项

Usage:

gcc [options] [filename]

Basic options:

-E: 只对源程序进行预处理(调用cpp预处理器)
-S: 只对源程序进行预处理、编译
-c: 执行预处理、编译、汇编而不链接
-o: output_file: 指定输出文件名
-g: 产生调试工具必需的符号信息
-O/On: 在程序编译、链接过程中进行优化处理
-Wall: 显示所有的警告信息
-I dir: 在头文件的搜索路径中添加dir目录
-L dir : 在库文件的搜索路径列表中添加dir目录

3.2 GCC编译过程

1、预处理

2、编译成汇编代码

3、汇编成目标代码

4、链接

1.预处理

预处理:使用-E参数

gcc –E –o gcctest.i gcctest.c

使用wc命令比较预处理后的文件与源文件,可以看到两个文件的差异

image-20210611105310558

2.编译成汇编代码

预处理文件—->汇编代码
使用-S说明生成汇编代码后停止工作

gcc –S –o gcctest.s gcctest.i

直接编译到汇编代码

gcc –S gcctest.c

image-20210611105654986

image-20210611105702671

3.编译成目标代码

汇编代码à目标代码

gcc –x assembler –c gcctest.s

直接编译成目标代码

gcc –c gcctest.c

使用汇编器生成目标代码

as –o gcctest.o gcctest.s

image-20210611105827814

4.编译成执行代码

目标代码à执行代码

gcc –o gcctest gcctest.o

直接生成执行代码

gcc –o gcctest gcctest.c

image-20210611105949690

3.3 GCC编译优化

优化编译选项有:

-O0
缺省情况,不优化

-O1

-O2

-O3

等等

image-20210611110447409

image-20210611110459630

3.4 头文件和库函数目录

1. GCC –I dir 参数使用

头文件和gcc不在同一目录下,用 –I dir指明头文件所在的目录。

#include <>:在默认路径“/usr/include”中搜索头文件

#include “”:在本目录中搜索

image-20210611111109662

image-20210611111326888

解决办法:

  1. gcc opt.c –o opt –I ./

  2. 修改main

    1
    2
    3
    #include <my.h> 

    #include “my.h”

2. GCC创建函数库

函数库:公用函数定义为函数库,供其他程序使用。函数库分为静态库和动态库。

静态库:程序编译时会链接到目标代码中,程序运行时不再需要静态库。程序生成的可执行程序比较大。后缀名为“.a”

动态库:程序编译时不会链接到目标代码,在程序运行时载入,运行时需要动态库存在。动态库可方便多个程序共享一个函数库。后缀名为”.so”

函数库的生成:由编译过的.o文件生成。

创建静态库:

1.将需要生成函数库的函数执行gcc –c,生成.o文件

​ gcc –c hello.c

2.由.o文件创建静态库,静态库命名格式为:lib静态库名.a

​ ar -rv libmyhello.a hello.o

  1. 使用静态库:在调用静态库的程序编译时指定静态库名

    $gcc –o hello main.c –L. –lmyhello

    $./hello

创建动态库:

1.由.o文件生成动态库,动态库的命名:lib动态库名.so

1
2
3
gcc –shared –fPIC –o libmyhello.so hello.o

gcc –shared –fPIC –o libmyhello.so hello.c (centos版本)

2.使用动态库:用gcc命令指定动态库名进行编译,编译之前需将动态库文件复制到系统默认库函数目录/usr/lib中或者设置搜索路径。 sudo ldconfig

1
2
3
gcc –o hello main.c –L. –lmyhello

gcc –o hello main.c –L. –lmyhello -Wl,-rpath=./

gdb commands

image-20210611111727753

Gdb using

$gdb filename

gdb将装入名为filename的可执行文件

在编译时需要使用-g选项

image-20210611111834934

image-20210611111847709

image-20210611111917435

list

image-20210611111947211

image-20210611111954948

help

image-20210611112004401

设置断点 break

image-20210611112101376

显示断点信息 info breakpoints

image-20210611112126359

清除已经定义的断点 clear

image-20210611112155046

delete [bkpoints-num]删除指定/全部断点

image-20210611112342864

image-20210611112424180

quit:退出gdb

image-20210611112436348

实例

image-20210611112521483

image-20210611112526161

image-20210611112545028

image-20210611112553240

image-20210611112603726

调试子命令总结

file :装入想要调试的可执行文件

kill:终止正在调试的程序

list:列出正在执行的程序清单

next:执行一行代码但不进入函数内部

step:执行一行代码并进入函数内部

run: 执行当前正在调试的程序

quit:终止gdb调试

break:设置断点 (break 行号)

watch:设置观察点,观察表达式的值是否发

​ 生变化

info: 查看断点信息

info breakpoints、info watchpoints

info break 显示当前断点清单,包括到达

断点处的次数等。

info files 显示被调试文件的详细信息。

info func 显示所有的函数名称。

info local 显示当函数中的局部变量信息。

info prog 显示被调试程序的执行状态。

info var 显示所有的全局和静态变量名称

delete: 删除某个或所有的断点

delete 断点号 或 delete

disable: 使断点失效(但仍存在)

enable: 使断点有效

clear: 清除断点信息

clear 断点所在行号

clear 函数入口

continue: 继续执行程序直到程序结束

5.1 Make 引入

Make的引入:

Ø文件数量太大,手工gcc编译不方便

Ø仅需要编译已经做了修改的源代码文件;其他文件只需要重新连接

Ø记录哪些文件已改变且需要编译,哪些文件仅仅需要连接很难

make & makefile

Multi-file project

IDE—Eclipse

make

make & makefile

makefile描述模块间的依赖关系;

make命令根据makefile对程序进行管理和维护;make判断被维护文件的时序关系

make

make [-f filename] [targetname]

使用方法:

v make 自动找当前目录下名为Makefile/makefile的文件

v make –f 文件名 找当前目录下指定文件名的文件

makefile的组成

  • 显式规则:明确指出目标文件的生成规则
  • 隐式规则:需要make自动推导的规则
  • 变量定义:声明时赋值,引用时加”$”
  • 文件指示:引用外部的文件
  • 注释:#

Makefile的显式规则

规则
一条规则包含3个方面的内容,

1)要创建的目标(文件),

2)创建目标(文件)所依赖的文件列表;

3)通过依赖文件创建目标文件的命令组

规则一般形式

1
2
3
4
5
6
7
*target* ... : *prerequisites* ... 

*<tab>command*

<tab>...

<tab>...

每条规则由一个带冒号的“依赖行”和一条或多条以tab开头的“命令行”组成

目标1 [目标2…]:[依赖文件列表]

[\t 命令]

ex:

1
2
3
*make_test:make_main.o* *wrtlog.o*

**<TAB>***gcc* *-o* *make_test* *make_main.o* *wrtlog.o*

冒号左边是目标,冒号右边是依赖文件

目标和依赖文件均是由字母、数字、句点和斜杠组成的字符串

目标或依赖文件的数目多于一个时,以空格分隔

image-20210611160145895

一个简单的makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o
gcc -o edit main.o kbd.o command.o display.o insert.o \
search.o files.o utils.o
main.o : main.c defs.h
gcc -c main.c
kbd.o : kbd.c defs.h command.h
gcc -c kbd.c
command.o : command.c defs.h command.h
gcc -c command.c
display.o : display.c defs.h buffer.h
gcc -c display.c
insert.o : insert.c defs.h buffer.h
gcc -c insert.c
search.o : search.c defs.h buffer.h
gcc -c search.c
files.o : files.c defs.h buffer.h command.h
gcc -c files.c
utils.o : utils.c defs.h
gcc -c utils.c

clean :
rm edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o

Make的工作过程

default goal
在缺省的情况下,make从makefile中的第一个目标开始执行

Make的工作过程类似一次深度优先遍历过程

Makefile的变量

自定义变量
例:

object=main.o Add.o Sub.o Mul.o Div.o

exe : $(object)

gcc -o exe $(object)

特殊变量

$@:表示目标文件;

$^:表示所有依赖目标的集合,以空格分隔;

$<:表示第一个依赖文件;

Makefile的隐式规则(自动推导)

make能够自动推导文件以及文件依赖关系后面的命令

例:

object=main.o Add.o Sub.o Mul.o Div.o

exe : $(object)

gcc -o $@ $(object)

main.o : def.h

Makefile的伪目标

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

.PHONY : 伪目标名称

例:

.PHONY : clean

clean :

​ rm $(object)

总结: C语言编程步骤

  1. 编辑:vi ,emacs,gedit,Eclipse…
  2. 编译: gcc
  3. 调试:gdb
  4. 运行: ./executable file
  5. 项目管理:make

扩展

gdb命令

命令 解释 示例
file <文件名> 加载被调试的可执行程序文件。 因为一般都在被调试程序所在目录下执行GDB,因而文本名不需要带路径。 (gdb) file gdb-sample
r Run的简写,运行被调试的程序。 如果此前没有下过断点,则执行完整个程序;如果有断点,则程序暂停在第一个可用断点处。 (gdb) r
c Continue的简写,继续执行被调试程序,直至下一个断点或程序结束。 (gdb) c
b <行号> b <函数名称> b *<函数名称> b *<代码地址>d [编号] b: Breakpoint的简写,设置断点。两可以使用“行号”“函数名称”“执行地址”等方式指定断点位置。 其中在函数名称前面加“*”符号表示将断点设置在“由编译器生成的prolog代码处”。如果不了解汇编,可以不予理会此用法。d: Delete breakpoint的简写,删除指定编号的某个断点,或删除所有断点。断点编号从1开始递增。 (gdb) b 8 (gdb) b main (gdb) b *main (gdb) b *0x804835c(gdb) d
s, n s: 执行一行源程序代码,如果此行代码中有函数调用,则进入该函数; n: 执行一行源程序代码,此行代码中的函数调用也一并执行。s 相当于其它调试器中的“Step Into (单步跟踪进入)”; n 相当于其它调试器中的“Step Over (单步跟踪)”。这两个命令必须在有源代码调试信息的情况下才可以使用(GCC编译时使用“-g”参数)。 (gdb) s (gdb) n
si, ni si命令类似于s命令,ni命令类似于n命令。所不同的是,这两个命令(si/ni)所针对的是汇编指令,而s/n针对的是源代码。 (gdb) si (gdb) ni
p <变量名称> Print的简写,显示指定变量(临时变量或全局变量)的值。 (gdb) p i (gdb) p nGlobalVar
display …undisplay <编号> display,设置程序中断后欲显示的数据及其格式。 例如,如果希望每次程序中断后可以看到即将被执行的下一条汇编指令,可以使用命令 “display /i pc”其中pc”其中pc 代表当前汇编指令,/i 表示以十六进行显示。当需要关心汇编代码时,此命令相当有用。undispaly,取消先前的display设置,编号从1开始递增。 (gdb) display /i $pc(gdb) undisplay 1
i Info的简写,用于显示各类信息,详情请查阅“help i”。 (gdb) i r
q Quit的简写,退出GDB调试环境。 (gdb) q
help [命令名称] GDB帮助命令,提供对GDB名种命令的解释说明。 如果指定了“命令名称”参数,则显示该命令的详细说明;如果没有指定参数,则分类显示所有GDB命令,供用户进一步浏览和查询。 (gdb) help display

.o文件是二进制文件

雨课堂题目整理

image-20210618143002794

image-20210618143011579

image-20210618143019456

image-20210618143027894

image-20210618143042533

image-20210618143052676

image-20210618143100984

image-20210618143111912

image-20210618143125572

image-20210618143147555

image-20210618143155638

image-20210618143205552

image-20210618143219860

image-20210618143233469

image-20210618143245888

image-20210618143254740

image-20210618143302040

image-20210618143310814

image-20210618143340720

image-20210618143348878

image-20210618143404564

image-20210618143416361

image-20210618143424721

文件I/O

1.1 文件属性

  1. 文件属性数据结构struct stat ——文件控制块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct stat {
mode_t st_mode; /*file type & mode*/
ino_t st_ino; /*inode number (serial number)*/
dev_t st_rdev; /*device number (file system)*/
nlink_t st_nlink; /*link count*/
uid_t st_uid; /*user ID of owner*/
gid_t st_gid; /*group ID of owner*/
off_t st_size; /*size of file, in bytes*/
time_t st_atime; /*time of last access*/
time_t st_mtime; /*time of last modification*/
time_t st_ctime; /*time of lat file status change*/
long st_blksize; /*Optimal block size for I/O*/
long st_blocks; /*number 512-byte blocks allocated*/
};

命令查看:ls -l file

  1. stat/fstat/lstat函数

获取文件属性

1
2
3
4
5
6
7
8
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *file_name, struct stat *buf);
int fstat(int filedes, struct stat *buf);
int lstat(const char *file_name, struct stat *buf);
(Return: 0 if success; -1 if failure)

1.2 文件类型

**1.**文件类型

Unix/Linux系统支持的文件类型:

  • Directory(d):目录文件
  • Link(l):链接文件
  • Pipe(p):管道文件
  • Block Device(b):块设备文件
  • Character Device(c):字符设备文件
  • Regular(-):普通文件
  • Socket(s):套接字文件

查看文件类型

使用命令:ls –l /dev/sda1

2.1设计一个程序,要求列出当前目录下的文件信息,以及系统“/dev/sda1”和“/dev/lp0”的文件信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>	                
#include<stdlib.h> /*文件预处理,包含system函数库*/
int main ()
{
int newret;
printf("列出当前目录下的文件信息:\n");
newret=system("ls -l");
printf("列出\"dev/sda1\"的文件信息:\n");
newret=system("ls /dev/sda1 -l"); /*列出“/dev/sda1”的文件信息*/
printf("列出\"dev/ lp0\"的文件信息:\n");
newret=system("ls /dev/lp0 -l"); /*列出“/dev/ lp0”的文件信息*/
return 0;
}

**2.**获取文件类型

st_mode存储文件类型和许可权限,形式如下:
type3 type2 type1 type0 suid sgid sticky rwx rwx rwx

用来确定文件类型的宏
S_ISBLK – 测试块文件
S_ISCHR – 测试字符文件
S_ISDIR – 测试目录
S_ISFIFO – 测试FIFO
S_ISREG – 测试普通文件
S_ISLNK – 测试符号链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
int main(int argc,char *argv[])
{ int i; struct stat statbuf;
for(i=1;i<argc;i++)
{ printf("%s:",argv[i]);
if(lstat(argv[i],&statbuf)==-1) { printf("error\n"); continue; }
if(S_ISDIR(statbuf.st_mode)) printf("%s is a directory\n",argv[i]);
else if(S_ISREG(statbuf.st_mode)) printf("%s is a regular file\n",argv[i]);
else if(S_ISBLK(statbuf.st_mode)) printf("%s is a block device\n",argv[i]);
else if(S_ISCHR(statbuf.st_mode)) printf("%s is a charcter device\n",argv[i]);
#ifdef S_ISLNK
else if (S_ISLNK(statbuf.st_mode)) printf("%s is a link file\n",argv[i]);
#endif
#ifdef S_ISSOCK
else if (S_ISSOCK(statbuf.st_mode)) printf("%s is a socket \n",argv[i]);
#endif
else printf("%s is an unknown type file \n",argv[i]);
}
}

image-20210611162747182

1.3 文件存取权限

**1.**文件存取权限

image-20210611162858324

读的权限:显示目录文件,进入目录

写的权限:目录下创建文件

执行的权限:显示目录文件,进入目录,创建文件

**2.**改变文件存取权限——命令

image-20210611163120067

image-20210611163125022

2.3 设计一个程序,要求把系统中“/home/mylinux目录下的myfile文件权限,设置成文件所有者可读可写,其他用户只读权限。

1
2
3
4
5
6
7
#include<sys/types.h>		
#include<sys/stat.h>
int main ()
{
chmod("/home/mylinux/myfile",S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
return 0;
}
1
2
3
4
5
6
7
chmod 函数
Change permissions of a file
#include <sys/types.h>
#include <sys/stat.h>
int chmod(const char *path, mode_t mode);
int fchmod(int fildes, mode_t mode);// 打开的文件
(Return: 0 if success; -1 if failure)

3. 改变文件存取权限

image-20210611163410933

4. 默认文件存取权限——umask

新创建的文件和目录的默认权限是(root)

File: -rw-r–r– 644

Directory: drwxr-xr-x 755

Why?

umask: 包含未被设置为权限位的==八进制数字(即无x位(可执行位))==。默认002为普通用户,022为root用户。666-644=022

2.4 设计一程序,要求设置系统文件和目录的权限掩码。

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<sys/stat.h>
#include<sys/types.h>
int main()
{ mode_t new_umask,old_umask;
new_umask=0666;
old_umask=umask(new_umask);
printf("系统原来的权限掩码是:%o\n",old_umask);
printf("系统新的权限掩码是:%o\n",new_umask);
system("touch liu1");
printf("创建了文件liu1\n");
new_umask=0444;
old_umask=umask(new_umask);
printf("系统原来的权限掩码是:%o\n",old_umask);
printf("系统新的权限掩码是:%o\n",new_umask);
system("touch liu2");
printf("创建了文件liu2\n");
system("ls liu1 liu2 -l");
return 0;
}
1
2
3
4
5
6
7
umask 函数
为进程设置文件存取权限屏蔽字,并返回以前的值
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
建立文件,文件的权限为0666-mask;
建立目录,目录权限为0777-mask。

1.4 文件其他属性

**1.chown/fchown/**lchown 函数

1
2
3
4
5
6
7
8
改变文件所有者
#include <sys/types.h>
#include <unistd.h>

int chown(const char *path, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char *path, uid_t owner, gid_t group);
(Return: 0 if success; -1 if failure)

2.获取文件存取时间——stat

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#include<sys/stat.h>
#include<time.h>
main()
{
char *path="./2-5.c";
struct stat statbuf;
if (stat(path,&statbuf)==-1)
perror("Failed to get file status");
else printf("%s last accessed at %s",path,ctime(&statbuf.st_atime));
}

**3.**获取文件大小——stat

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h> 
#include<unistd.h>
#include<sys/stat.h>
int main ()
{
struct stat buf;
stat("/etc/passwd",&buf);
printf("\"/etc/passwd\"文件的大小是:%d\n",buf.st_size);
return 0;
}

2.1 两种I/O方式

1. 无缓冲和缓冲I/O

无缓冲I/O

  • read/write ->系统函数
  • 文件描述符
  • POSIX.1 and XPG3标准

缓冲 I/O

  1. 标准I/O库实现
  2. 处理很多细节, 如缓存分配, 以优化长度执行I/O等.
  3. 流 -> FILE类型指针

**2.**无缓冲 I/O 系统调用

基本 I/O

  • open/creat, close, read, write, lseek
  • dup/dup2
  • fcntl

2.2 文件描述符

1.文件描述符概述

非负整数

​ int fd;

​ (in <unistd.h>)

​ STDIN_FILENO (0), STDOUT_FILENO (1), STDERR_FILENO (2)

文件操作一般步骤:

open-read/write-[lseek]-close

2. 进程打开文件的内核数据结构

image-20210611170650485

image-20210611170732113

image-20210611170743253

image-20210611170755032

image-20210611170805503

image-20210611170815361

image-20210611170829150

image-20210611170853877

image-20210611170909100

image-20210611170919482

image-20210611170928977

2.3 无缓冲 I/O函数

**0.**错误处理

1
2
3
4
5
6
7
8
9
10
UNIX方式
Return value
“errno”变量( defined in /usr/include/errno.h)
extern int errno;

strerror & perror
#include <string.h>
char *strerror(int errnum);
#include <stdio.h>
void perror(const char *msg);

1. creat 函数

1
2
3
4
5
6
create a file or device 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
(Return: a new file descriptor if success; -1 if failure)

2.7 **设计一程序,要求在“/**home”目录下创建一个名称为“2-7file”的文件,并且把此文件的权限设置为所有者具有只读权限,最后显示此文件的信息。

1
2
3
4
5
6
7
8
9
10
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd;
fd=creat("./2-7file",S_IRUSR);
system("ls ./2-7file -l");
return 0;
}

参数 mode”

“mode”: 指定创建的新文件的存取权限

image-20210611171422005

参数mode& umask

umask: 一种文件保护机制

新建文件的初始存取权限

image-20210611171522883

2. Open 函数

1
2
3
4
5
6
7
8
Open and possibly create a file or device 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
(Return: a new file descriptor if success; -1 if failure)

2.8设计一个程序,要求在当前目录下以可读写方式打开一个名为“2-8file”的文件。如果该文件不存在,则创建此文件;如果存在,将文件清空后关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>			
#include<stdlib.h>
#include<fcntl.h>
int main ()
{ int fd;
if((fd=open("./2-8file",O_CREAT|O_TRUNC|O_WRONLY,0600))<0)
{ perror("打开文件出错");
exit(1);
}
else
{ printf("打开(创建)文件\"2-8file\",文件描述符为:%d\n",fd);
}
if(close(fd)<0)
{ perror("关闭文件出错");
exit(1);
}
system("ls ./2-8file -l");
return 0;
}

参数 flags

1
2
3
4
5
6
7
8
“flags”: 指定文件存取方式
One of O_RDONLY, O_WRONLY or O_RDWR which request opening the file read-only, write-only or read/write, respectively, bitwise-or’d with zero or more of the following: ( All defined in /usr/include/fcntl.h)
O_APPEND: 追加方式打开
O_TRUNC:文件存在清空
O_CREAT: 文件不存在创建.
O_EXCL: 和 O_CREAT一起用时, 文件存在则出错,打开失败

“creat” function: 等价于open函数中指定 O_CREAT|O_WRONLY|O_TRUNC

3. close 函数

1
2
3
4
Close a file descriptor
#include <unistd.h>
int close(int fd);
(Return: 0 if success; -1 if failure)

4. read/write 函数

1
2
3
4
5
6
7
8
Read from a file descriptor
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
(返回值: 读到的字节数,若已到文件尾为0,若出错为-1)
Write to a file descriptor
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
(返回值: 若成功为已写的字节数,若出错为-1)

2.9设计一个程序,完成文件的复制工作。要求通过read函数和write函数复制“/etc/passwd”文件到目标文件中,目标文件名在程序运行时从键盘输入。

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<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{ int fdsrc,fddes,nbytes; int z; char buf[20], des[20];
int flags=O_CREAT | O_TRUNC | O_WRONLY;
printf("请输入目标文件名:");
scanf("%s",des);
fdsrc=open("/etc/passwd",O_RDONLY);
if(fdsrc<0) { exit(1); }
fddes=open(des,flags,0644);
if(fddes<0) { exit(1); }
while((nbytes=read(fdsrc,buf,20))>0)
{ z=write(fddes,buf,nbytes);
if(z<0) { perror("写目标文件出错"); } }
close(fddes);
close(fdsrc);
printf("复制\"/etc/passwd\"文件为\"%s\"文件成功!\n",des);
exit(0);
}

5. lseek 函数

read/write 定位文件指针

1
2
3
4
5
6
7
#include <sys/types.h>

#include <unistd.h>

off_t lseek(int fildes, off_t offset, int whence);

(Return: the resulting offset location if success; -1 if failure)

该指令的“那里”:

​ SEEK_SET: the offset is set to “offset” bytes指针位移量为设定值

​ SEEK_CUR: the offset is set to its current location plus “offset” bytes指针位移量为当前位移加设定值

​ SEEK_END: the offset is set to the size of the file plus “offset” bytes指针位移量为文件尾加设定值

image-20210611213210597

空洞文件

使用lseek修改文件偏移量后,当前文件偏移量有可能大于文件的长度

在这种情况下,对该文件的下一次写操作,将加长该文件

这样文件中形成了一个空洞。对空洞区域进行读,均返回0

image-20210611213233714

6. dup/dup2 函数

1
2
3
4
5
复制文件描述符
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
(Return: the new file descriptor if success; -1 if failure)

image-20210611213303221

image-20210611213316880

假设进程已打开文件描述符0、1、2

调用dup2(1, 6),dup2返回值是多少?

然后再调用dup(6),dup返回值是多少?

image-20210611213407986

image-20210611213417786

7. fcntl函数

1
2
3
4
5
6
查看、修改打开的文件描述符
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
int result=fcntl(int fd,int cmd);
int result=fcntl(int fd,int cmd,long arg,…);

lcmd取值:

F_DUPFD 复制文件描述符

F_GETFD 获得文件描述符

F_SETFD 设置文件描述符

F_GETFL 获取文件描述符当前模式

F_SETFL设置文件描述符当前模式

F_GETLK 获得记录锁

F_SETLK 设置记录锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
分析以下程序运行情况
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
char buf1[]="abcdefghij";
char buf2[]="ABCDEFGHEIJ";
main()
{int fd;
if((fd=open("file.hole",O_WRONLY|O_CREAT/*|O_APPEND*/,0644))<0)
perror("creat error");
if(write(fd,buf1,10)!=10)
perror("buf1 write error");
if(lseek(fd,40,SEEK_SET)==-1)
perror("lseek error");
if(write(fd,buf2,10)!=10)
perror("buf2 write error");
exit(0);
}
1)ls –l 查看文件大小多少字节?
2)cat 查看文件内容?
3)od –c 查看文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
分析以下程序运行结果
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
main()
{ int fd;
if((fd=open("myout",O_WRONLY|O_CREAT,0644))==-1)
perror("myout open error");
if(dup2(fd,STDOUT_FILENO)==-1)
perror("redirect stand output failed");
printf("this is a test program for redirect\n");
close(fd);
}
1)查看程序运行结果?
2)cat 查看文件myout的内容

3.1 标准I/O

为什么要设计标准I/O库?

​ 直接使用API进行文件访问时,需要考虑许多细节问题

​ 例如:read、write时,缓冲区的大小该如何确定,才能使效率最优

标准I/O库封装了诸多细节问题,包括缓冲区分配

I/O效率示例

1
2
3
4
5
6
7
8
9
10
11
12
13
#define BUFFSIZE 4096
#include<stdio.h>
#include<unistd.h>
int main()
{
int n;
char buf[BUFFSIZE];
while((n = read(STDIN_FILENO, buf, BUFFSIZE))>0)
if(write(STDOUT_FILENO, buf,n)!= n)
perror("write error");
return 0;
}
程序中,影响效率的关键:BUFFSIZE的取值

image-20210611213732273

原因

  • Linux文件系统采用了某种预读技术
  • 当检测到正在进行顺序读取时,系统就试图读入比应用程序所要求的更多数据
  • 并假设应用程序很快就会读这些数据
  • 当BUFFSIZE增加到一定程度后,预读就停止了

标准 I/O 缓冲

标准I/O库提供缓冲的目的:尽可能减少使用read、write调用的次数,以提高I/O效率。

通过标准I/O库进行的读写操作,数据都会被放置在标准I/O库缓冲中中转。

3.2 文件流

1
2
3
4
5
6
7
8
9
typedef struct  {
..............................
char fd; /* File descriptor */
short bsize; /* Buffer size */
unsigned char *buffer; /* Data transfer buffer */
....................................
} FILE;
标准I/O库
管理的缓冲区

3.3 标准 I/O 函数

  • 流 open/close
  • 流 read/write
    • ​ 每次一个字符的I/O:fgetc,fputc
    • ​ 每次一行的I/O: fgets,fputs,gets,puts
    • ​ 直接I/O(二进制I/O): fread,fwrite
    • ​ 格式化I/O:scanf,printf,fscanf,fprintf
  • 流定位:fseek,ftell,frewind
  • 流刷新:fflush

1. 流打开/关闭

2.10设计一个程序,要求用流文件I/O操作打开文件“2-10file”,如果该文件不存在,则创建文件。

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE * fp;
if((fp=fopen("./2-10file","a+"))==NULL)
{
printf("打开(创建)文件出错"); exit(0);
}
fclose(fp);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Open a stream
#include <stdio.h>
FILE *fopen(const char *filename, const char *mode);
Parameter “mode”
“r”: Open text file for reading.
“w”: Truncate file to zero length or create text file for writing.
“a”: Open for appending.
“r+”: Open for reading and writing.
“w+”: Open for reading and writing. The file is created if it does not exist, otherwise it is truncated.
“a+”: Open for reading and appending. The file is created if does not exist.

Close a stream
#include <stdio.h>
int fclose(FILE *fp);
(Return: 0 if success; -1 if failure)

**2.流读/**写

对流有三种读写方式

  • 每次读写一个字符

  • 每次读写一行

  • 每次读写任意长度的内容

1)输入/出一个字符

1
2
3
4
5
6
7
8
9
10
11
输入
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
输出
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
(Return: the character if success; -1 if failure)

(2) 输入/出一行

1
2
3
4
5
6
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
char *gets(char *s);
#include <stdio.h>
int fputs(const char *s, FILE *stream);
int puts(const char *s);

(3) 二进制流输入/输出

1
2
3
4
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
(Return: the number of a items successfully read or written.)

例2.11设计两个程序,要求一个程序把三个人的姓名和账号余额信息通过一次流文件I/O操作写入文件“2-11file”,另一个格式输出账号信息,把每个人的账号和余额一一对应显示输出。

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>
#define nmemb 3
struct test
{ char name[20];
int pay;
}s[nmemb];
int main( )
{ FILE * fp;
int i;
fp = fopen("2-11file", "r");
fread(s,sizeof(struct test),nmemb,fp);
fclose(fp);
for(i=0;i<nmemb;i++)
printf("帐号[%d]:%-20s余额[%d]:%d\n",i,s[i].name,i,s[i].pay);
return 0;
}

/*程序:把帐号信息写入文件*/
#include<stdio.h>
#include<string.h>
#define set_s(x,y,z) {strcpy(s[x].name,y);s[x].pay=z;} /*自定义宏,用于赋值*/
#define nmemb 3
struct test
{ char name[20];
int pay;
}s[nmemb];
int main()
{ FILE * fp;
set_s(0,"张三",12345);
set_s(1,"李四",200);
set_s(2,"王五",50000);
fp=fopen("2-11file","a+"); /*打开(创建)文件*/
fwrite(s,sizeof(struct test),nmemb,fp);
fclose(fp);
return 0;
}

(4) 格式化 I/O

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);

3.流定位

1
2
3
4
#include <stdio.h>
int fseek(FILE *stream, long int offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);

例2.12 设计一程序,要求用fopen函数打开系统文件“/etc/passwd”,先把位置指针移动到第10个字符前,再把位置指针移动到文件尾,最后把位置指针移动到文件头,输出三次定位的文件偏移量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
int main()
{
FILE *stream;
long offset;
fpos_t pos;
stream=fopen("/etc/passwd","r");
fseek(stream,10,SEEK_SET);
printf("文件流的偏移量:%d\n",ftell(stream));
fseek(stream,0,SEEK_END);
printf("文件流的偏移量:%d\n",ftell(stream));
rewind(stream);
printf("文件流的偏移量:%d\n",ftell(stream));
fclose(stream);
return 0;
}

image-20210617212155113

4. 流刷新

刷新文件流。把流里的数据立刻写入文件—fork前使用fflush
#include <stdio.h>
int fflush(FILE *stream);

自动刷新:

  • 流关闭fclose;
  • exit终止;
  • 行缓冲“\n”;
  • 缓冲区满;
  • 执行输入操作读文件:printf(“hello”);scanf(“%d”,&a)

5. 流缓冲

  • 三种类型的缓冲
  • 块(全)缓冲block buffered (fully buffered):一般C库函数写入文件是全缓冲的
  • 行缓冲line buffered:引用标准交互设备的流stdin,stdout
    例:    for(i=1;i<=10;i++) fputc(c,stdout);
               fputc("\n",stdout);
    
  • 无缓冲Unbuffered:标准错误流stderr

全缓冲

  • 在填满标准I/O缓冲区后,才进行实际I/O操作(例如调用write函数)
  • 调用fflush函数也能强制进行实际I/O操作

行缓冲

  • 在输入和输出遇到换行符时,标准I/O库执行I/O操作
  • 因为标准I/O库用来收集每一行的缓存的长度是固定的,所以,只要填满了缓存,即使没有遇到新行符,也进行I/O操作
  • 终端(例如标准输入和标准输出),使用行缓冲

不带缓冲

  • 标准I/O库不对字符进行缓冲存储
  • 标准出错是不带缓冲的,为了让出错信息尽快显示出来

6. 流和文件描述符

确定流使用的底层文件描述符
#include <stdio.h>
int fileno(FILE *fp);
根据已打开的文件描述符创建一个流
#include <stdio.h>
FILE *fdopen(int fildes, const char *mode);

4.1 目录文件

mkdir/rmdir
chdir/fchdir, getcwd
读目录操作
opendir/closedir
readdir
telldir
seekdir

例2.13设计一程序,要求读取当前目录文件中所有的目录结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<sys/types.h>
#include<dirent.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
DIR * dir;
struct dirent * ptr;
int i;
dir=opendir("./");
while((ptr = readdir(dir))!=NULL)
{
printf("目录: %s\n",ptr->d_name);
}
closedir(dir);
}

image-20210618151814819

1. 读目录

数据结构
DIR, struct dirent
操作函数
opendir/closedir
readdir
telldir
seekdir

2. 数据结构

DIR

​ 目录流对象的数据结构

​ in <dirent.h>
​ typedef struct __dirstream DIR;

struct dirent

​ 目录项

​ Defined in <dirent.h>

​ ino_t d_ino; /* inode number /
​ char d_name[NAME_MAX + 1]; /
file name */

3. 操作函数

目录的打开、关闭、读、定位
#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);
int closedir(DIR *dir);
struct dirent *readdir(DIR *dir);
off_t telldir(DIR *dir);
void seekdir(DIR *dir, off_t offset);

目录扫描程序——ls -R命令

1
2
3
4
5
6
7
8
9
10
11
12
13
DIR *dp;
struct dirent *entry;

if ( (dp = opendir(dir)) == NULL )
err_sys(…);
while ( (entry = readdir(dp)) != NULL ) {
lstat(entry->d_name, &statbuf);
if ( S_ISDIR(statbuf.st_mode) )

else

}
closedir(dp);

mkdir/rmdir 函数

创建一个空目录
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
(Return: 0 if success; -1 if failure)
删除一个空目录
#include <unistd.h>
int rmdir(const char *pathname);
(Return: 0 if success; -1 if failure)

chdir/fchdir 函数

Change working directory
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
(Return: 0 if success; -1 if failure)
当前工作目录是进程的属性,所以该函数只影响调用chdir的进程本身
cd(1) command

4.2 链接文件

ln 命令
link/unlink 函数
给一个文件创建一个链接.
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
(Return: 0 if success; -1 if failure)
删除文件链接
#include <unistd.h>
int unlink(const char *pathname);
(Return: 0 if success; -1 if failure)

ln –s命令
创建一个符号链接
#include <unistd.h>
int symlink(const char *oldpath, const char *newpath);
(Return: 0 if success; -1 if failure)
读取符号链接的值
#include <unistd.h>
int readlink(const char *path, char *buf, size_t bufsiz);
(Return: the count of characters placed in the buffer if success; -1 if failure)

例2.14设计一程序,要求为“/etc/passwd”文件建立软链接“2-14link”,并查看此链接文件和“/etc/passwd”文件。

1
2
3
4
5
6
7
#include<unistd.h>
int main()
{
symlink("/etc/passwd","2-14link");
system("ls 2-14link -l");
system("ls /etc/passwd -l");
}

image-20210618152519176

例2.15设计一程序,要求为“/etc/passwd”文件建立硬链接“2-15link”,并查看此链接文件和“/etc/passwd”文件。

1
2
3
4
5
6
7
#include<unistd.h>
int main()
{
link("./myfile","2-15link");
system("ls 2-15link -l");
system("ls /etc/passwd -l");
}

4.3 设备文件

1.设备文件名

ls –C 列出当前系统加载的设备对应的文件

ls –li 列出当前终端设备的属性

2.设备文件读写

open,read,write,close,stat

例2-16 向终端pts1写入100个“unix”。

1
2
3
4
5
6
7
8
#include<fcntl.h>
int main()
{ int i,fd;
fd=open("/dev/pts/1",O_WRONLY);
for(i=0;i<100;i++)
write(fd, "Unix",4);
close(fd);
}

扩展

fseek()

C 库函数 int fseek(FILE *stream, long int offset, int whence) 设置流 stream 的文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。

声明

下面是 fseek() 函数的声明。

1
int fseek(FILE *stream, long int offset, int whence)

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • offset – 这是相对 whence 的偏移量,以字节为单位。
  • whence – 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
常量 描述
SEEK_SET 文件的开头
SEEK_CUR 文件指针的当前位置
SEEK_END 文件的末尾

rewind()

将文件指针重新指向文件开头

readdir()

头文件:#include <sys/types.h> #include <dirent.h>

定义函数:struct dirent * readdir(DIR * dir);

函数说明:readdir()返回参数dir 目录流的下个目录进入点。结构dirent 定义如下:
struct dirent
{
ino_t d_ino; //d_ino 此目录进入点的inode
ff_t d_off; //d_off 目录文件开头至此目录进入点的位移
signed short int d_reclen; //d_reclen _name 的长度, 不包含NULL 字符
unsigned char d_type; //d_type d_name 所指的文件类型 d_name 文件名
har d_name[256];
};

返回值:成功则返回下个目录进入点. 有错误发生或读取到目录文件尾则返回NULL.

fdopen()

头文件:#include <stdio.h>

定义函数:FILE * fdopen(int fildes, const char * mode);

**函数说明:fdopen()会将参数fildes 的文件描述词, 转换为对应的文件指针后返回.参数mode 字符串则代表着文件指针的流形态, 此形态必须和原先文件描述词读写模式相同. 关于mode 字符串格式请参考fopen(). **

返回值:转换成功时返回指向该流的文件指针. 失败则返回NULL, 并把错误代码存在errno 中.

opendir()

头文件:#include <sys/types.h> #include <dirent.h>

函数:DIR *opendir(const char *name);

含义: opendir()用来打开参数name 指定的目录, 并返回DIR*形态的目录流, 和open()类似, 接下来对目录的读取和搜索都要使用此返回值.

readdir()

头文件:#include<sys/types.h> #include <dirent.h>

函数:struct dirent *readdir(DIR *dir);

含义:readdir()返回参数dir 目录流的下个目录进入点。

struct dirent
{
ino_t d_ino; //d_ino 此目录进入点的inode
ff_t d_off; //d_off 目录文件开头至此目录进入点的位移
signed short int d_reclen; //d_reclen _name 的长度, 不包含NULL 字符
unsigned char d_type; //d_type d_name 所指的文件类型 d_name 文件名
har d_name[256];
};

fileno()

功 能:把文件流指针转换成文件描述符
相关函数:open, fopen
表头文件:#include <stdio.h>
定义函数:int fileno(FILE *stream)
函数说明:fileno()用来取得参数stream指定的文件流所使用的文件描述词
返回值 :返回和stream文件流对应的文件描述符。如果失败,返回-1。
范例:
#include <stdio.h>
main()
{
FILE *fp;
int fd;
fp = fopen(“/etc/passwd”, “r”);
fd = fileno(fp);
printf(“fd = %d\n”, fd);
fclose(fp);
}

文件描述词是Linux编程中的一个术语。当一个文件打开后,系统会分配一部分资源来保存该文件的信息,以后对文件的操作就可以直接引用该部分资源了。文件描述词可以认为是该部分资源的一个索引,在打开文件时返回。在使用fcntl函数对文件的一些属性进行设置时就需要一个文件描述词参数。
以前知道,当程序执行时,就已经有三个文件流打开了,它们分别是标准输入stdin,标准输出stdout和标准错误输出stderr。和流式文件相对应的是,也有三个文件描述符被预先打开,它们分别是0,1,2,代表标准输入、标准输出和标准错误输出。需要指出的是,上面的流式文件输入、输出和文件描述符的输入输出方式不能混用,否则会造成混乱。

telldir()

头文件:#include <dirent.h>

定义函数:off_t telldir(DIR *dir);

函数说明:telldir()返回参数dir 目录流目前的读取位置. 此返回值代表距离目录文件开头的偏移量返回值返回下个读取位置, 有错误发生时返回-1.

exit()和_eixt()区别

_exit()函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;exit() 函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序。
exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。

文件流指针

在应用编程层面,程序对流的操作体现在文件流指针FILE上,在操作一个文件前,需要打开该文件,而使用ANSI C库函数fopen()打开一个文件后,将返回一个文件流指针与该文件关联,所有针对该文件的读写操作都通过该文件流指针完成,以下是应用层所能访问的FILE结构体,因此,结构体成员可以在用户空间中访问。
typedef struct _IO_FILE FILE;
struct _IO_FILE{
int _flags;
char* _IO_read_ptr; //如果以读打开,当前读指针
char* _IO_read_end; //如果以读打开,读区域结束位置
char* _IO_read_base; //Start of putback+get area
char* _IO_write_base; //如果以写打开,写区起始区
char* _IO_write_ptr; //如果以写打开,当前写指针
char* _IO_write_end; //如果以写打开,写区域结束位置
char* _IO_buf_base; //如果显示设置缓冲区,其起始位置
char* _IO_buf_end; //如果显示设置缓冲区,其结束位置。

int _fileno; //文件描述符

}
在此结构体中,包含了I/O库为管理该流所需要的所有信息,如用于实现I/O的文件描述符、指向流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数和出错标志等。

Linux文件存储结构

大部分的Linux文件系统(如ext2、ext3)规定,一个文件由目录项、inode和数据块组成

  • 目录项:包括文件名和inode节点号
  • Inode:又称文件索引节点,包含文件的基础信息以及数据块的指针
  • 数据块:包含文件的具体内容。

Linux系统中,目录(directory)也是一种文件。打开目录,实际上就是打开目录文件。

目录文件的结构非常简单,就是一系列目录项(dirent)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode号码

雨课堂题目整理

image-20210610214812080

image-20210610214821927

image-20210610214834210

image-20210610214845524

image-20210610214854641

image-20210610214906327

image-20210610214915564

!==[image-20210610214926804]==(D:\SyncDisk\笔记整理\Linux\image-20210610214926804.png)

image-20210610214936008

image-20210610214944544

image-20210610215005400

image-20210610215953437

image-20210610220011507

image-20210610220021904

==image-20210610220032690

image-20210610220042546

image-20210610220052131

image-20210610220104981

image-20210610220118232

image-20210610220131685

image-20210610214618550

image.pngimage.pngimage.png

image-20210610214629301

image-20210610214639221

image-20210618143645924

image-20210618143654774

image-20210618143702180

image-20210618143710258

image-20210618143719274

image-20210618143727064

进程编程

1.1. 进程

进程:一个或多个线程执行的地址空间,线程执行时需要系统资源

1.2. 进程启动

System call “fork” 系统调用fork
Process resources
struct task_struct
System space stack

System call “exec”系统调用exec
The entry of C programs C程序入口

1.3. 进程终止

进程终止的五种方式
Normal termination 正常终止
Return from “main” function 从main函数返回
Call “exit” function 调用exit函数
Call “_exit” function 调用_exit函数
Abnormal termination 异常终止
Call “abort” function 调用abort函数
Terminated by a signal 信号终止

1.4. 进程分类

Foreground process前台进程
要求用户启动它们或与它们交互的进程称为前台进程。
前台进程不结束,终端就不会出现系统提示符,直到进程终止。
缺省情况下,程序和命令作为前台进程运行。

Background process 后台进程
独立于用户运行的进程称为后台进程。
用户在输入命令行后加上“&”字符然后按键就启动了后台进程。
Shell不等待命令终止,就立即出现系统提示符,让该命令进程在后台运行,用户可以继续执行新的命令。

Daemon 守护进程
总是运行在后台的系统进程。
守护程序通常在系统启动时启动,并且它们一直运行到系统停止
守护进程常常用于向用户提供各种类型的服务和执行系统管理任务。
守护程序进程由 root 用户或 root shell 启动,并只能由 root 用户停止。

2.1. 进程标识符

image-20210618161417909

2.2. 进程创建

1.fork

fork: create a child process
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
returned value:
pid of child (in the current (parent) process),
0 (in child process),
-1 (failure)

fork创建子进程代码结构

……

pid = fork();

if (pid<0) {perror(“fork()”);exit(1);}

else if (pid==0) { child process }

else { parent process }

文件共享

所有由父进程打开的描述符都被复制到子进程中。父、子进程每个相同的打开描述符共享一个文件表项

image-20210618161638801

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
int main(int argc,char *argv[]) 
{
pid_t pid;
int fd;
int i=1;
int status;
char *ch1="hello";
char *ch2="world";
char *ch3="IN";
if((fd=open("test.txt",O_RDWR|O_CREAT,0644))==-1)
{ perror("parent open"); exit(EXIT_FAILURE); }
if(write(fd,ch1,strlen(ch1))==-1)
{ perror("parent write"); exit(EXIT_FAILURE); }
if((pid=fork())==-1)
{ perror("fork"); exit(EXIT_FAILURE); }
else
if(pid==0)
{ i=2;
printf("in child\n"); //打印 i 值,以示与父亲进程区别
printf("i=%d\n",i);
if(write(fd,ch2,strlen(ch2))==-1) //写文件 test.txt,与父进程共享
perror("child write");
return 0; }
else
{ sleep(1); //等待子进程先执行
printf("in parent\n");
printf("i=%d\n",i); //打印 i 值,以示与子进程区别
if(write(fd,ch3,strlen(ch3))==-1) //写操作,结果添加到文件后
perror("parent,write");
wait(&status); //等待子进程结束
return 0; }
}
main()
{ int i,j,mark;
for (i=LEFT;i<=RIGHT;i++)
{ mark=1;
for (j=2;j<i/2;j++)
{ if (i%j==0)
{ mark=0;
break;
}
}
if (mark)
printf("%d is a primer!\n",i);
}
}
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
单个进程
#define LEFT 30000000
#define RIGHT 30000200main()
{ int i,j,mark;
for (i=LEFT;i<=RIGHT;i++)
{ mark=1;
for (j=2;j<i/2;j++)
{ if (i%j==0)
{ mark=0;
break;
}
}
if (mark)
printf("%d is a primer!\n",i);
}
}
多个进程
main()
{ int i,j,mark;
pid_t pid;
for (i=LEFT;i<=RIGHT;i++)
{ mark=1;
pid = fork();
if (pid==0)
{ for (j=2;j<i/2;j++)
{ if (i%j==0) { mark=0; break; }
}
if (mark) printf("%d is a primer!\n",i); exit(0);
}
}
exit(0);

fork 应用场合

进程复制自己,使父子进程同一时刻执行不同的代码——网络服务
进程要执行另一个不同的程序:fork-exec——shell
Question:效率问题?父子进程各自占一段逻辑地址空间,fork之后立即exec,地址空间浪费。
“写—复制”

2. vfork

vfork

#include <sys/types.h>

#include <unistd.h>

pid_t vfork(void);

功能:类似fork,创建一个新进程,效率髙。

与fork区别:

(1) vfork创建的进程与父进程共用地址空间

(2) vfork创建子进程后,阻塞父进程,直到子进程调用exec或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
int global=5;
main()
{int pid; char *string="I am father:"; int local=10;
printf("before vfork\n");
if((pid=fork())<0) {perror("vfork failed");exit(0);}
if(pid==0)
{string="I am child.";
global++;
printf("%s global=%d, local=%d\n",string,global,local);
exit(0); }
else
{local++;
printf("%s my pid is %d \n" "global=%d\n local=%d\n", string,getpid(),global,local);
exit(0);
}}


int global=5;
main()
{int pid; char *string="I am father:"; int local=10;
printf("before vfork\n");
if((pid=vfork())<0) {perror(“vfork failed”);exit(0);}
//共用进程空间,子进程先执行
if(pid==0)
{string="I am child.";
global++;
printf("%s global=%d, local=%d\n",string,global,local);
exit(0); }
else
{local++;
printf("%s my pid is %d \n" "global=%d\n local=%d\n", string,getpid(),global,local);
exit(0);
}}

sleep函数

函数原型:

#include <unistd.h>

unsigned int sleep(unsigned int seconds);

seconds:暂停时间(秒)

3.exec 系列函数

用一个新的进程映像替换当前的进程映像,执行新程序的进程保持原进程的一系列特征:

pid, ppid, uid, gid, working directory, root directory …

#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, …);

int execlp(const char *file, const char *arg, …);

int execle(const char *path, const char *arg, …, char * const envp[]);

int execv(const char *path, char * const argv[]);

int execvp(const char *file, char * const argv[]);

int execve(const char *filename, char * const argv[], char * const envp[]);

1.两大类:

execl开头:参数以列表形式arg0,arg1…NULL结束。

execv开头:参数以指向字符串数组argv[]指针形式,arg[0]必须为程序名。

2.含有字母p的函数可以使用相对路径,根据环境变量PATH查找文件;其他函数必须使用绝对路径;

3.含有字母e的函数,需要通过指向envp[]数组的指针指明新的环境变量,其他函数使用当前环境变量。

1
2
3
4
5
6
7
8
9
10
11
用exec函数使新进程执行“/bin/ps” 程序。
#include<unistd.h>
const char*ps_argv[]={“ps”,”-af”, NULL};
const char *ps_envp[] = {“PATH=/bin:/usr/bin”, “TERM=console”, NULL};
六种情况:
execl(“/bin/ps”,”ps”,”-af”,NULL);
execlp(“ps”,”ps”,”-af”,NULL);
execle(“/bin/ps”,”ps”,”-af”,NULL,ps_envp);
execv(“/bin/ps”,ps_argv);
execvp(“ps”,ps_argv);
execve(“/bin/ps”,ps_argv,ps_envp);

fork和exec一起使用

父子进程各自执行不同的代码,进程要执行另一个程序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
printf("%% ");	/* print prompt  */
while (fgets(buf, MAXLINE, stdin) != NULL) {
buf[strlen(buf) - 1] = 0; /* replace newline with null */
if ( (pid = fork()) < 0 )
err_sys(“fork error”);
else if ( pid == 0 ) { /* child */
execlp(buf, buf, (char *) 0);
fprintf(stderr, "couldn't execute: %s", buf);
exit(127);
}
if ( (pid = waitpid(pid, &status, 0)) < 0 ) /* parent */
err_sys(“waitpid error”);
printf("%% ");
}

2.3 进程终止

1.exit函数

函数原型:

#include <stdlib.h>

void exit(int status);

status:进程状态

功能:正常终止目前进程的执行,把参数status返回给父进程,进程所有的缓冲区数据会自动写回并关闭未关闭的文件。

2._exit函数

函数原型:

#include <stdlib.h>

void ——exit(int status);

status:进程状态

功能:立刻终止目前进程的执行,把参数status返回给父进程,并关闭未关闭的文件。不处理标准I/O缓冲区。

2.4 父子进程关系

1. 两种进程概念

父进程在子进程前终止——孤儿进程

​ Orphan process——init

子进程在父进程前终止——可能成为僵尸进程

​ SIGCHLD signal 忽略SIGCHLD信号

​ Handled by wait/waitpid in parent 父进程中用wait/waitpid处理

​ Not handled by wait/waitpid in parent -> zombie父进程没有用wait/waitpid处理->僵尸进程

2. 僵尸进程

僵尸进程:已终止运行,但尚未被清除的进程。

子进程运行结束后(正常或异常),它并没有马上从系统的进程分配表中被删掉,而是进入僵死状态(Zombie),一直等到父进程来回收它的结束状态信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
main()
{ int pid; char *message; int n;
printf("fork program starting\n");
pid=fork();
switch(pid)
{ case -1: exit(0);
case 0: message="this is child"; n=2; break;
default: n=5; message="this is parent"; sleep(60); break;
}
for(;n>0;n--)
{ puts(message);
sleep(n-1);
}
exit(0);
}

**3.**僵尸进程解决方法

僵尸进程的proc结构一直存在直到父进程正常结束或系统重启

如何消除僵尸进程?

方法一:wait/waitpid阻塞父进程,子进程先终止

方法二:父进程不阻塞,两次fork

方法三:使用signal信号处理

方法一:wait example

例3.5设计一个程序,要求复制进程,子进程显示自己的进程号后暂停一段时间,父进程等待子进程正常结束,打印显示等待的进程号和等待的进程退出状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
main()
{ int i,j,mark; pid_t pid;
for (i=LEFT;i<=RIGHT;i++)
{ mark=1;
pid = fork();
if (pid==0)
{
for (j=2;j<i/2;j++)
if (i%j==0) { mark=0; break; }
if (mark) printf("%d is a primer!\n",i);
sleep(100);
exit(0);
} }
for (i=LEFT;i<=RIGHT;i++)
wait(NULL);
exit(0);
}
wait & waitpid functions

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *status);

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

waitpid的第一个参数pid的意义:

pid > 0: 等待进程id为pid的子进程。

pid == 0: 等待与自己同组的任意子进程。

pid == -1: 等待任意一个子进程

pid < -1: 等待进程组号为-pid的任意子进程。因此,wait(&stat)等价于waitpid(-1, &stat, 0)

waitpid例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>			
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main ()
{ pid_t pid,wpid; int status,i;
pid=fork();
if(pid==0)
{ printf("这是子进程,进程号(pid)是:%d\n",getpid());
sleep(5); exit(6); }
else
{ printf("这是父进程,正在等待子进程……\n");
wpid=waitpid(pid,&status,0);
i=WEXITSTATUS(status);
printf("等待的进程的进程号(pid)是:%d ,结束状态:%d\n",wpid,i);
}}

方法二:两次fork Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(void)
{pid_t pid;
if((pid=fork())<0) perror("fork error");
else if(pid==0) /*first child*/
{ if((pid=fork())<0) perror("fork error");
else if(pid>0) exit(0); /*parent of second child=first child*/
sleep(10); /*second child*/
printf("second child,parent pid=%d\n",getppid());
exit(0);
}
if (waitpid(pid,NULL,0)!=pid) /*wait for first child,parent(the original process)*/
perror("waitpid error");
printf("parent exit");
exit(0);
}

方法三:singnal函数处理

Singnal函数处理:

(1)设置信号处理函数

​ signal(SIGCHLD,fun)

(2)忽略子进程终止信号

​ signal(SIGCHLD,SIG_IGN)

​ 内核回收

4.1 登录方式

终端登录

image-20210618204829654

网络登录

image-20210618204928040

4.2 进程组和会话期

进程组

一个或多个过程的集合

getpgrp/setpgid functions

会话期Session

一个或多个进程组的集合。

getsid/setsid function

setsid函数

image-20210618211351096

image-20210618211443043

4.3 守护进程Daemon

精灵进程或守护进程

后台执行, 没有控制终端或登录 Shell 的进程

ps –aux 命令查看

Init:进程1,启动系统服务

Keventd:为内核中运行的函数提供进程上下文

Kswapd:页面调出守护进程

bdflush,kupdated:调整缓存中的数据写到磁盘

portmap:将RPC程序号映射为端口号

inetd(xinetd):侦听网络接口,获取网络服务进程请求

注意:大多数守护进程都以超级用户(用户ID为0)特权运行。没有一个守护进程具有控制终端,其终端名设置为问号(?)。

ps axj命令查看

daemon特征:

sid,pid,pgid相同,均为pid

ppid为1

tty为?

daemon进程实现规则

编程规则
首先调用fork,然后使父进程exit
调用setsid创建一个新的会话期 setsid()
将当前工作目录更改为特定目录chdir(./)
进程的umask设为0 umask(0)
关闭不需要的文件描述符 close()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int daemon_init(void) {
pid_t pid;

if ( (pid = fork()) < 0)
return(-1);
else if (pid != 0)
exit(0); /* parent goes bye-bye */

/* child continues */
setsid(); /* become session leader */
chdir("/tmp"); /* change working directory */
umask(0); /* clear our file mode creation mask */
for(i=0;i<MAXFILE;i++)
close(i); /* close file */
return(0);
}

编写守护进程的要点

(1)创建子进程,终止父进程

pid=fork();

if(pid>0)

{exit(0);} /终止父进程/

(2)在子进程中创建新会话

setsid函数用于创建一个新的会话,并担任该会话组的组长,其作用:

①让进程摆脱原会话的控制;

②让进程摆脱原进程组的控制;

③让进程摆脱原控制终端的控制。

而setsid函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

(3)改变工作目录

改变工作目录的常见函数是chdir。

(4)重设文件创建掩码

文件创建掩码是指屏蔽掉文件创建时的对应位。

把文件创建掩码设置为0,可以大大增强该守护进程的灵活性。

设置文件创建掩码的函数是umask。

(5)关闭文件描述符

通常按如下方式关闭文件描述符:

for(i=0;i<NOFILE;i++)

close(i);

或者也可以用如下方式:

for(i=0;i<MAXFILE;i++)

close(i);

守护进程编写

例3.7 设计两个程序,主程序和初始化程序。要求主程序每隔10秒向/tmp目录中的日志报告运行状态。初始化程序中的init_daemon函数负责生成守护进程。

image-20210618211952186

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
/*主程序每隔一分钟向/tmp目录中的日志3-7.log报告运行状态*/
#include <unistd.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <time.h>
#include<stdlib.h>
#include<unistd.h>

void init_daemon(void);
int main()
{
FILE *fp;
time_t t;
init_daemon();
while(1)
{ sleep(10);
if((fp=fopen("./3-7.log","a+")) >=0
{ t=time(0);
fprintf(fp,"守护进程还在运行,时间是: %s",asctime(localtime(&t)) );
fclose(fp);
} } }
void init_daemon(void)
{ pid_t child1,child2; int i;
child1=fork();
if(child1>0) exit(0);
else if(child1< 0)
{ perror("创建子进程失败"); exit(1); }
setsid();
chdir("/tmp");
umask(0);
for(i=0;i< NOFILE;++i) close(i);
return;
}

注意:fopen函数必须具有root权限。如果没有root权限,可以看到守护进程的运行,但不会在文件里写入任何字符。

例3-8:设计一个程序,要求运行后成为守护进程,守护进程又复制出一个子进程,守护进程和它的子进程都调用syslog函数,把结束前的状态写入系统日志文件。

image-20210618212139554

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
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<syslog.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/stat.h>
int main()
{
pid_t child1,child2;
int i;
child1=fork();
if(child1>0) /*(1)创建子进程,终止父进程*/
exit(0); /*这是第一子进程,后台继续执行*/
else if(child1< 0)
{
perror("创建子进程失败"); /*fork失败,退出*/
exit(1);
}
setsid(); /*(2)在子进程中创建新会话*/
chdir("/"); /*(3)改变工作目录到“/”*/
umask(0); /*(4)重设文件创建掩码*/
for(i=0;i< NOFILE;++i) /*(5)关闭文件描述符*/
close(i);
openlog("例3-8程序信息",LOG_PID,LOG_DAEMON);/* 调用openlog,打开日志文件*/
child2=fork();
if(child2==-1)
{ perror("创建子进程失败");
exit(1);
}
else if(child2==0)
{ syslog(LOG_INFO,"第二子进程暂停5秒!");
sleep(5); /*睡眠5秒钟*/
syslog(LOG_INFO,"第二子进程结束运行。");
exit(0);
}
else {
waitpid(child2,NULL,0);
syslog(LOG_INFO, "第一子进程在等待第二子进程结束后,也结束运行。");
closelog(); /*调用closelog,关闭日志服务*/
while(1) { sleep(10); }
}
}

注意:调用openlog、syslog函数,操作的系统日志文件“/var/log/syslog”或“/var/log/messages ”,必须具有root权限。

openlog函数说明

image-20210618212254955

syslog函数说明

image-20210618212315332

5.1 信号概念

信号
Software interrupt软件中断
Mechanism for handling asynchronous events异步事件
Having a name (beginning with SIG)
Defined as a positive integer (in <signal.h>)
信号产生
按终端键,硬件异常,kill(2)函数,kill(1)命令,软件条件,…

SIG信号(1-31)是从UNIX系统中继承下来的称为不可靠信号(也称为非实时信号)。
SIGRTMIN~SIGRTMAX是为了解决前面“不可靠信号”问题而进行更改和扩充的信号,称为可靠信号(也称为实时信号)。

可靠信号(实时信号):支持排队,发送用户进程一次就注册一次,发现相同信号已经在进程中注册,也要再注册。
不可靠信号(非实时信号):不支持排队,发送用户进程判断后注册,发现相同信号已经在进程中注册,就不再注册,忽略该信号。

名称 **说明 ** 默认操作
SIGABRT 进程异常终止(调用abort函数产生此信号)
SIGALRM 超时(alarm 终止
SIGFPE 算术运算异常(除以0,浮点溢出等)
SIGHUP 连接断开 终止
SIGILL 非法硬件指令
SIGINT 终端终端符(Clt**-C)** 终止
SIGKILL 立即结束程序运行(不能被捕捉、阻塞或忽略) 终止
SIGPIPE 向没有读进程的管道写数据
SIGQUIT 终端退出符(Clt-\) 终止
SIGTERM 终止(由kill命令发出的系统默认终止信号) 退出
SIGUSR1 用户定义信号 退出
SIGUSR2 用户定义信号 退出
SIGSEGV 无效存储访问(段违例)
SIGCHLD 子进程停止或退出 忽略
SIGCONT 使暂停进程继续 继续/忽略
SIGSTOP 暂停一个进程(不能被捕捉、阻塞或忽略) 终止
SIGTSTP 终端挂起符(Clt-Z) 停止进程
SIGTTIN 后台进程请求从控制终端读
SIGTTOUT 后台进程请求向控制终端写

信号处理

忽略信号
不能忽略的信号:
SIGKILL, SIGSTOP
一些硬件异常信号
执行系统默认动作
捕捉信号

5.2 信号相关命令

Kill

• 暂停 kill –STOP

• 恢复 kill –CONT

• 终止 kill –KILL

函数 功能
kill 发送信号给进程或进程组
raise 发送信号给进程自身
alarm 定时器时间到,向进程发送SIGALRM信号
pause 没有捕捉信号前一直将进程挂起
signal 捕捉信号SIGINT, SIGQUIT时执行信号处理函数
sigemptyset 初始化信号集合为空
sigfillset 初始化信号集合为所有信号集合
sigaddset 将指定信号加入到指定集合
sigdelset 将指定信号从信号集中删除
sigprocmask 判断检测或更改信号屏蔽字

1 信号发送——kill & raise

kill: send signal to a process

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int sig);

(Returned Value: 0 if success, -1 if failure)

The “pid” parameter

pid > 0: 发送信号给进程id为pid的进程。

pid =0:发送信号给与自己同组的所有进程。

pid = -1: 发送系统内所有进程

pid < -1: 发送给进程组号为-pid的所有进程。

raise: send a signal to the current process

#include <signal.h>

int raise(int sig);

(Returned Value: 0 if success, -1 if failure)

例3-9 设计一程序,要求用户进程复制出一个子进程,父进程向子进程发出SIGKILL信号,子进程收到此信号,结束子进程的运行。

分析:用户进程fork子进程后,子进程使用raise函数发送SIGSTOP信号,使自己暂停;父进程使用kill函数向子进程发送SIGKILL信号,子进程收到信号结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main ()             	
{ pid_t result; int ret; int newret;
result=fork();
if(result<0)
{ perror("创建子进程失败");
exit(1); }
else if (result==0)
{ raise(SIGSTOP); /*调用raise函数,发送SIGSTOP 使子进程暂停*/
exit(0); }
else
{ printf("子进程的进程号(PID)是:%d\n",result);
if((waitpid(result,NULL,WNOHANG))==0)
{ if((ret=kill(result,SIGKILL))==0)
printf("用kill函数返回值是:%d,发出的SIGKILL信号结束的进程进程号:%d\n",ret,result);
else { perror("kill函数结束子进程失败"); waitpid(result,NULL,0); }
} }
}
Waitpid(pid,NULL,WNOHANG)没有子进程终止立即返回,返回值为0
由此例可知,系统调用kill函数和raise函数,都是简单地向某一进程发送信号。kill函数用于给特定的进程或进程组发送信号,raise函数用于向一个进程自身发送信号。

2 信号处理—— “signal” 函数

为编号为sgn的信号安装一个新的信号处理程序。

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

(Returned Value: the previous handler if success, SIG_ERR if error)

The “handler” parameter

​ a user specified function, or

​ SIG_DFL, or

​ SIG_IGN

1
2
3
4
5
6
7
8
9
10
11
static void sig_usr(int);
int main(void)
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR1");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR2");

for ( ; ; )
pause();
}

3 alarm & pause 函数

alarm: 为信号的传送设置闹钟

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

(Returned value: 0, or the number of seconds remaining of previous alarm)

pause: wait for a signal

#include <unistd.h>

int pause(void);

(Returned value: -1, errno is set to be EINTR)

**4.**信号阻塞

有时既不希望进程在接收到信号时立刻中断进程的执行,也不希望此信号完全被忽略掉,而是延迟一段时间再去调用信号处理函数,这个时候就需要信号阻塞来完成。

信号集处理函数

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set,int signum);

int sigdelset(sigset_t *set,int signum);

int sigismember(const sigset_t *set,int signum);

参数:set 信号集 ;signum 信号

返回值:若成功返回0,若出错返回-1。

sigismember若真返回1,若假返回0,若出错返回-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <signal.h>
#include<stdlib.h>
main()
{ sigset_t *set;
set=(sigset_t*)malloc(sizeof(set));
sigemptyset(set);
sigaddset(set,SIGUSR1);
sigaddset(set,SIGINT);
if((sigismember(set,SIGUSR1))==1) printf("SIGUSR1\n");
if((sigismember(set,SIGUSR2))==1) printf("SIGUSR2\n");
if((sigismember(set,SIGINT))==1) printf("SIGINT\n");
}

信号集处理函数sigprocmask

#include <signal.h>

功能:检测或更改信号屏蔽字

函数:int sigprocmask(int how,const sigsett_t *set,sigset_t *oldset);

参数:how 操作方式set

how决定函数的操作方式。

SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中。

SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合。

SIG_SETMASK:将当前的信号集合设置为信号阻塞集合。

返回值:若成功返回0,若出错返回-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <signal.h>
#include<stdlib.h>
main()
{
sigset_t *set;
set=(sigset_t*)malloc(sizeof(set));
sigemptyset(set);
sigaddset(set,SIGINT);
sigprocmask(SIG_SETMASK,set,NULL);
//也可以使用:sigprocmask(SIG_BLOCK,set,NULL);
while(1);
}

程序先定义信号集set,然后把信号SIGINT添加到set信号集中,最后把set设置为信号阻塞集合。运行程序时,进程进入死循环。按“ctrl+c”系统并没有中断程序,SIGINT信号屏蔽掉了。按”ctrl+z”来结束程序。

信号实例

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
void handler() 
{ alarm(1); }
main(int argc,char *argv[])
{ char buf[BUFSIZE];
signal(SIGALRM,handler);
alarm(1);
sfd = open(argv[1],O_RDONLY);
/*do
{ sfd = open(argv[1],O_RDONLY);
if (sfd<0)
{ if (errno!= EINTR)
{ perror("open()");
exit(1); } }
}while(sfd<0);*/
while(1)
{ pause();
len = read(sfd,buf,10);
/* while((len = read(sfd,buf,BUFSIZE))<0)
{ if (errno == EINTR)
continue;
perror("read()");
break;
}
if (len==0) break;*/

ret = write(1,buf,len);
/*while(len>0)
{ ret = write(1,buf,len);
if (ret <0)
{ if (errno == EINTR)
continue;
perror("write");
exit(1);
}
len -= ret;
}*/
}
}

6.1 进程间通信

IPC: Inter-Process Communication 进程间通信

IPC机制

​ shared file

​ signal

​ pipe, FIFO (named pipe), message queue, semaphore, shared memory

​ socket

image-20210618215455459

Simple Client-Server or IPC model

image-20210618215509159

6.2 pipe 概念

Pipe

Pipe mechanism in a shellShell中的管道机构

​ e.g. cmd1 | cmd2

Pipe is half-duplex 半双工

管道只能在共同祖先的进程间使用

管道也是文件

命名管道(FIFO)

pipe 函数

The pipe function: create a pipe

#include <unistd.h>

int pipe(int filedes[2]);

(Returned value: 0 if success, -1 if failure)

A pipe: First In, First Out

filedes[0]:read, filedes[1]: write

单个进程使用管道

image-20210619121308293

父子进程使用管道

image-20210619121321411

管道使用(1):单个进程

类似共享文件

pipe—write——read

管道使用(2)多进程

父-子进程/子-子进程

使用模式:

pipe——fork——write(写进程)

​ ——read(读进程)

注意:pipe-fork顺序

管道使用(3)用于标准输入输出

管道:shell中的形式

​ cmd1 | cmd2

​ 重定向 cmd > file

实现代码

​ 执行cmd1前

​ if (fd[1] != STDOUT_FILENO) {

​ if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)

​ err_sys(“dup2 error to stdout);

​ }

执行cmd2前

​ if (fd[0] != STDIN_FILENO) {

​ if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO)

​ err_sys(“dup2 error to stdin);

​ }

image-20210619121557088

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main()
{ int data_processed; int file_pipes[2];
const char some_data[] = "123"; pid_t fork_result;
if (pipe(file_pipes) == 0)
{ fork_result = fork();
if (fork_result == (pid_t)-1)
{ fprintf(stderr, "Fork failure");
exit(EXIT_FAILURE); }
if (fork_result == (pid_t)0)
{ close(0);
dup(file_pipes[0]);
close(file_pipes[0]);
close(file_pipes[1]);
execlp("od", "od", "-c", (char *)0);
exit(EXIT_FAILURE);
}
else
{ close(file_pipes[0]);
data_processed = write(file_pipes[1], some_data,strlen(some_data));
close(file_pipes[1]);
printf("%d - wrote %d bytes\n", (int)getpid(),data_processed);
}
} exit(EXIT_SUCCESS);
}

od –c file 以字符方式显示文件内容,如果没指定文件则以标准输入作为默认输入

6.3 popen & pclose 函数

#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

image-20210619121713195

fp=popen(cmd,”r”)的pipe实现:
FILE * popen(char * cmd,char * type)
{ int fd; int p;
pipe(fd);
p=fork();
if(p==0) { close(1);dup(fd[1]);
execl(cmd,cmd,NULL); }
else { wait();
fp=fdopen(fd[0],”r”); return fp; }
}

image-20210619121805879

例3-13 设计一程序,要求用popen创建管道,实现“ls –l|grep fifo”的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main ()	
{ FILE *fp; int num; char buf[500];
memset(buf,0,sizeof(buf));
printf("建立管道……\n");
fp=popen("ls -l","r");
if(fp!=NULL)
{ num=fread(buf,sizeof(char),500,fp);
if(num>0)
{ printf("第一个命令是“ls–l”,运行结果如下:\n");
printf("%s\n",buf); }
pclose(fp); }
else
{ printf("用popen创建管道错误\n");
return 1; }
fp=popen(“grep fifo","w");
printf("第二个命令是“grep fifo”,运行结果如下:\n");
fwrite(buf,sizeof(char),500,fp);
pclose(fp);
return 0;
}

使用popen函数读写管道,实际上也是调用pipe函数建立一个管道,再调用fork函数建立子进程,接着会建立一个shell环境,并在这个shell环境中执行参数指定的进程。

例3-14 在程序中获得另一个程序的输出

popen_two.c

gcc myuclc.c -o myuclc

gcc popen_two.c -o two

输入大写字母

按Ctrl+D结束程序

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 <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
char line[MAXLINE]; FILE *fpin;
fpin = popen("./myuclc", "r");
if( NULL == fpin ) error_quit("popen error");
while( 1 )
{
fputs("prompt> ", stdout);
fflush(stdout);
if( fgets(line, MAXLINE, fpin) == NULL ) break;
if( fputs(line, stdout) == EOF ) perror("fputs error to pipe");
}
if( pclose(fpin) == -1 ) perror ("pclose error");
putchar('\n');
return 0;
}
//Myuclc.c
int main()
{
int c;
while( 1 )
{
c = getchar();
if( EOF == c )
break;
if( isupper(c) )
c = tolower(c);
putchar(c);
if( '\n' == c )
fflush(stdout);
}
return 0;
}

6.4 FIFO: named pipe命名管道

管道和命名管道
相同点
不同点
文件系统中
内存传输数据
同步:一个重要的考虑
mkfifo(1), mkfifo(3), mknod(1), mknod(2)

创建FIFO

命令行方式:mknod filename p

​ mkfifo filename

程序方式:mkfifo: make a FIFO special file (a named pipe)

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

(Returned value: 0 if success, -1 if failure)

例: mkfifo(“/tmp/myfifo”,0666)

访问FIFO

命令行方式

(1)cat < /tmp/myfifo 读FIFO文件

(2)echo hello >/tmp/myfifo 向FIFO写数据

(3)cat < /tmp/myfifo &

​ echo “hello” >/tmp/myfifo

程序方式:用open打开一个FIFO

Review: “open” system call

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

“flags” parameter

​ 必须指定的互斥模式:

​ O_RDONLY, O_WRONLY, O_NONBLOCK

​ O_RDONLY:若无进程写方式打开FIFO,open阻塞

​ O_RDONLY |O_NONBLOCK:若无进程写方式打开FIFO,open立即返回文件描述符

​ O_WRONLY:若无进程读方式打开FIFO,open阻塞

​ O_WRONLY| O_NONBLOCK:若无进程读方式打开FIFO,open返回ENXIO错误,-1

例3-15 两个程序通过FIFO传递数据,一个生产者程序创建并打开一个FIFO,向管道中写入数据。(3-15fifo_p.c)一个消费者程序,从FIFO中读取数据(3-15fifo_c.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
#define FIFO_NAME "/tmp/my_fifo“
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024 * 1024 * 10)
int main()
{ int pipe_fd; int res; int open_mode = O_WRONLY;
int bytes_sent = 0; char buffer[BUFFER_SIZE + 1];
if (access(FIFO_NAME, F_OK) == -1)
/*int access(const char *filenpath, int mode)
R_OK 只判断是否有读权限
W_OK 只判断是否有写权限
X_OK 判断是否有执行权限
F_OK 只判断是否存在
有效,则函数返回0,否则函数返回-1
*/
{ res = mkfifo(FIFO_NAME, 0777);
if (res != 0) { fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME); exit(EXIT_FAILURE); }
}
printf("Process %d opening FIFO O_WRONLY\n", getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d result %d\n", getpid(), pipe_fd);
if (pipe_fd != -1)
{ while(bytes_sent < TEN_MEG)
{ res = write(pipe_fd, buffer, BUFFER_SIZE);
if (res == -1) { fprintf(stderr, "Write error on pipe\n"); exit(EXIT_FAILURE); }
bytes_sent += res; }
(void)close(pipe_fd); }
else { exit(EXIT_FAILURE); }
printf("Process %d finished\n", getpid());
exit(EXIT_SUCCESS);
}

#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF
int main()
{ int pipe_fd; int res; int open_mode = O_RDONLY; char buffer[BUFFER_SIZE + 1];
int bytes_read = 0;
memset(buffer, '\0', sizeof(buffer));
printf("Process %d opening FIFO O_RDONLY\n", getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d result %d\n", getpid(), pipe_fd);
if (pipe_fd != -1) {
do { res = read(pipe_fd, buffer, BUFFER_SIZE);
bytes_read += res;
} while (res > 0);
(void)close(pipe_fd);
}
else {
exit(EXIT_FAILURE); }
printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
exit(EXIT_SUCCESS);
}

FIFO的应用(1)

用FIFO复制输出流

image-20210619124043625

mkfifo fifo1

prog3 < fifo1 &

prog1 < infile | tee fifo1 | prog2

tee命令读取标准输入,把这些内容同时输出到标准输出和(多个)文件中

image-20210619124101884

FIFO的应用(2)

C/S应用程序

image-20210619124145265

例3-16 client.c, server.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
56
57
58
59
60
61
62
63
//Client.h
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#define SERVER_FIFO_NAME "/tmp/serv_fifo"
#define CLIENT_FIFO_NAME "/tmp/cli_%d_fifo"
#define BUFFER_SIZE 20
struct data_to_pass_st {
pid_t client_pid;
char some_data[BUFFER_SIZE - 1];
};
int main() // 客户端程序
{ int server_fifo_fd, client_fifo_fd; struct data_to_pass_st my_data;
int times_to_send; char client_fifo[256];
server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY);
if (server_fifo_fd == -1) {
fprintf(stderr, "Sorry, no server\n");
exit(EXIT_FAILURE); }
my_data.client_pid = getpid();
sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);
if (mkfifo(client_fifo, 0777) == -1) {
fprintf(stderr, "Sorry, can't make %s\n", client_fifo);
exit(EXIT_FAILURE); }
for (times_to_send = 0; times_to_send < 5; times_to_send++) {
sprintf(my_data.some_data, "Hello from %d", my_data.client_pid);
printf("%d sent %s, ", my_data.client_pid, my_data.some_data);
write(server_fifo_fd, &my_data, sizeof(my_data));
client_fifo_fd = open(client_fifo, O_RDONLY);
if (client_fifo_fd != -1) {
if (read(client_fifo_fd, &my_data, sizeof(my_data)) > 0) {
printf("received: %s\n", my_data.some_data); }
close(client_fifo_fd); } }
close(server_fifo_fd);
unlink(client_fifo);
exit(EXIT_SUCCESS);}
int main() //服务端程序
{ int server_fifo_fd, client_fifo_fd; struct data_to_pass_st my_data; int read_res; char client_fifo[256]; char *tmp_char_ptr;
mkfifo(SERVER_FIFO_NAME, 0777);
server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY);
if (server_fifo_fd == -1) {
fprintf(stderr, "Server fifo failure\n");
exit(EXIT_FAILURE); }
sleep(10); /* lets clients queue for demo purposes */
do { read_res = read(server_fifo_fd, &my_data, sizeof(my_data));
if (read_res > 0) {
tmp_char_ptr = my_data.some_data;
while (*tmp_char_ptr) {
*tmp_char_ptr = toupper(*tmp_char_ptr);
tmp_char_ptr++; }
sprintf(client_fifo, CLIENT_FIFO_NAME, my_data.client_pid);
client_fifo_fd = open(client_fifo, O_WRONLY);
if (client_fifo_fd != -1) {
write(client_fifo_fd, &my_data, sizeof(my_data));
close(client_fifo_fd); }
} } while (read_res > 0);
close(server_fifo_fd);
unlink(SERVER_FIFO_NAME);
exit(EXIT_SUCCESS);}

例3-17 l设计两个程序,要求用命名管道FIFO实现简单的聊天功能。

Zhang.c

Li.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
int main()
{ int i, rfd,wfd,len=0,fd_in; char str[32]; int flag,stdinflag; fd_set write_fd,read_fd; struct timeval net_timer;
mkfifo("fifo1",S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
mkfifo("fifo2",S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
wfd =open("fifo1",O_WRONLY); rfd =open("fifo2",O_RDONLY); if(rfd<=0||wfd<=0)return 0; printf(“这是张三端!\n");
while(1)
{ FD_ZERO(&read_fd);
FD_SET(rfd,&read_fd);
FD_SET(fileno(stdin),&read_fd); /**/
net_timer.tv_sec=5;
net_timer.tv_usec=0;
memset(str,0,sizeof(str));
if(i=select(rfd+1, &read_fd,NULL, NULL, &net_timer) <= 0) continue;
if(FD_ISSET(rfd,&read_fd))
{ read(rfd,str,sizeof(str)); printf("----------------------------\n");
printf("李四:%s\n",str); }
if(FD_ISSET(fileno(stdin),&read_fd))
{ printf("----------------------------\n"); fgets(str,sizeof(str),stdin);
len=write(wfd,str,strlen(str));
} }
close(rfd);
close(wfd);}

int main()
{ int i, rfd,wfd,len=0,fd_in; char str[32]; int flag,stdinflag;
fd_set write_fd,read_fd; struct timeval net_timer;
mkfifo("fifo1",S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
mkfifo("fifo2",S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
rfd =open("fifo1",O_RDONLY); wfd =open("fifo2",O_WRONLY);
if(rfd<=0||wfd<=0)return 0; printf("这是李四端!\n");
while(1)
{ FD_ZERO(&read_fd); FD_SET(rfd,&read_fd);
FD_SET(fileno(stdin),&read_fd); net_timer.tv_sec=5;
net_timer.tv_usec=0; memset(str,0,sizeof(str));
if(i=select(rfd+1,&read_fd,NULL, NULL, &net_timer) <= 0) continue;
if(FD_ISSET(rfd,&read_fd))
{ read(rfd,str,sizeof(str));
printf("----------------------------\n");
printf("张三:%s\n",str); }
if(FD_ISSET(fileno(stdin),&read_fd))
{ printf("----------------------------\n");
fgets(str,sizeof(str),stdin);
len=write(wfd,str,strlen(str)); /*写入管道*/ }
} close(rfd);
close(wfd);
}

7.1 System V IPC的共同特征

进程间通信(interprocess communication)

IPC objects

  • 信号量(semaphore set)

  • 消息队列(message queue)

  • 共享内存(shared memory)

shell命令

  • ipcs -q/-m/-s

  • ipcrm –q/-m/-s

  • ipcrm -Q/-M/-S

标识符与关键字
创建IPC对象时指定关键字(key_t key;)
key的选择;预定义常数IPC_PRIVATE;ftok函数
引用IPC对象:标识符
内核将关键字转换成标识符
许可权结构
和文件类比
struct ipc_perm

SV IPC System Calls Overview

功能 消息队列 信号量 共享内存
分配一个IPC对象,获得对IPC的访问 msgget semget shmget
IPC操作: 发送/接收消息,信号量操作,连接/释放共享内存 msgsnd/ msgrcv semop shmat/ shmdt
IPC控制:获得/修改状态信息,取消IPC msgctl semctl shmctl

ftok函数

创建函数

key_t ftok( char * filename, int id);

功能说明

将一个已存在的文件名(该文件必须是存在而且可以访问的)和一个整数标识符id转换成一个key_t值

在Linux系统实现中,调用该函数时,系统将文件的索引节点号取出,并在前面加上子序号,从而得到key_t的返回值

创建IPC对象

key:可由ftok()函数产生或定义为IPC_PRIVATE常量

flag:包括读写权限,还可包含IPC_CREATE和IPC_EXCL标志位,组合效果如下

image-20210619133437323

7.2 Message queue

消息队列
消息队列是消息的链表,存放在内核中并由消息队列标识符标识。
First in, first out
message type: 优先级=
块数据

消息队列——程序结构

proto.h:约定的消息队列通信格式
send.c
receive.c

proto.h:约定的消息队列通信格式:

#define KEYPATH “/etc/services”

#define KEYPROJ ‘a’

struct msg_st {

​ char * msg;

​ long type;

​ …

}

消息队列——系统函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int flag);
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
int msgctl(int msqid, int cmd, struct shmid_ds *buf);

msgget

Ø函数原型 int msgget(key_t key, int flag);

Ø参数说明

ü key:待创建/打开队列的键值

ü flag:创建/打开方式

​ 常取IPC_CREAT|IPC_EXCL|0666

​ 若不存在key值的队列,则创建;否则,若存在,则打开队列

​ 0666表示与一般文件权限一样

Ø返回值

​ 成功返回消息队列描述字,否则返回-1

Ø说明

IPC_CREAT一般由服务器程序创建消息队列时使用

若是客户程序,须打开现有消息队列,而不用IPC_CREAT

msgsnd

Ø函数原型

int msgsnd(int msqid, struct msgbuf *msgp, size_t size, int flag);

Ø说明

flag有意义的标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待

Ø内核须对msgsnd( )函数完成的工作

ü检查消息队列描述符、许可权及消息长度

​ 若合法,继续执行;否则,返回

ü内核为消息分配数据区,将消息正文拷贝到消息数据区

ü分配消息首部,并将它链入消息队列的末尾

ü修改消息队列头数据,如队列中的消息数、字节总数等

ü唤醒等待消息的进程

msgrcv

Øint msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);

Ø参数说明

l msgid:消息队列描述字

l msgp:消息存储位置

l size:消息内容的长度

l type:请求读取的消息类型

l flag:规定队列无消息时内核应做的操作

​ IPC_NOWAIT:无满足条件消息时返回,此时errno=ENOMSG

​ IPC_EXCEPT:type>0时使用,返回第一个类型不为type的消息

​ IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将丢失

msgctl: message control operations

函数原型
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
“cmd” 参数
IPC_STAT: 把msqid_ds结构中的数据置为消息队列的当前关联值
IPC_SET: 在进程有足够权限的前提下,把消息队列的当前关联值置为msqid_ds结构给出的值
IPC_RMID: 删除消息队列

消息队列——Example

A C/S application
一台服务器,多个客户机:只需要一个队列。与FIFO实现相比

消息队列属性

消息队列创建后,操作系统在内核中分配了一个名称为msqid_ds的数据结构用于管理该消息队列。
在程序中可以通过函数msgctl对该结构进行读写,从而实现对消息队列的控制功能。

成员说明:
1)msg_perm:IPC许可权限结构。
2)msg_stime:最后一次向该消息队列发送消息(msgsnd)的时间。
3)msg_rtime:最后一次从该消息队列接收消息(msgrcv)的时间。
4)msg_ctime:最后一次调用msgctl的时间。
5)msg_cbytes:当前该消息队列中的消息长度,以字节为单位。
6)msg_qnum:当前该消息队列中的消息条数。
7)msg_qbytes:该消息队列允许存储的最大长度,以字节为单位。
8)msg_lspid:最后一次向该消息队列发送消息(msgsnd)的进程ID。
9)msg_lrpid:最后一次从该消息队列接收消息(msgrcv)的进程ID。

使用:
msg_sinfo.msg_qbytes = 1666;
msgctl(msgqid,IPC_SET,&msg_sinfo)

例3-18 msg_stat.c

image-20210619133908134

image-20210619133920223

image-20210619133947032

image-20210619133957944

image-20210619134026407

7.3 Shared memory

共享内存是内核为进程创建的一个特殊内存段,它可连接(attach)到自己的地址空间,也可以连接到其它进程的地址空间

​ 最快的进程间通信方式

​ 不提供任何同步功能

image-20210619134117718

共享内存实现途径

mmap()系统调用

将普通文件在不同的进程中打开并映射到内存

不同进程通过访问映射来达到共享内存目的

POSIX共享内存机制(Linux 2.6未实现)

System V共享内存

在内存文件系统tmpfs中建立文件

文件映射到不同进程空间

mmap()

lmmap()系统调用使得==进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。==

#include < unistd.h >

#include <sys/mman.h >

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

int munmap( void * addr, size_t len )

int msync ( void * addr , size_t len, int flags)

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

addr:指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。

len:映射到调用进程地址空间的字节数,从被映射文件开头offset个字节开始算起。

prot :指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。

flags;指定映射对象的类型,映射选项和映射页是否可以共享。由以下几个常值指定:MAP_SHARED , MAP_PRIVATE 必选其一。

fd:为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANONYMOUS,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,只能用于具有亲缘关系的进程间通信)。

offset参数一般设为0,表示从文件头开始映射。

函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。

mmap()用于共享内存的两种方式 :

(1)使用普通文件提供的内存映射:适用于任何进程之间;需要打开或创建一个文件,然后再调用mmap();调用代码如下:

fd=open(name, flag, mode);

if(fd>0)

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE,

​ MAP_SHARED , fd , 0);

(2) 使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;

l调用代码如下:

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS ,-1 , 0);

由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。在调用fork()之后,子进程继承父进程匿名映射后的地址空间,也继承mmap()返回的地址

不必指定具体的文件,只要设置相应的标志即可

munmap()

int munmap( void * addr, size_t len )
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

msync()

int msync ( void * addr , size_t len, int flags)
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

例3-19mmap.c l设计一个程序,要求复制进程,父子进程通过匿名映射实现共享内存。

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
typedef struct     	
{ char name[4];
int age;
}people;
main(int argc, char** argv)
{ pid_t result; int i; people *p_map; char temp;
p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
result=fork();
if(result<0)
{ perror("创建子进程失败"); exit(0); }
else if (result==0)
{ sleep(2);
for(i = 0;i<5;i++)
printf("子进程读取: 第 %d 个人的年龄是: %d\n",i+1,(*(p_map+i)).age);
(*p_map).age = 110;
munmap(p_map,sizeof(people)*10); /*解除内存映射关系*/
exit(0);
}
else {
temp = 'a';
for(i = 0;i<5;i++)
{ temp += 1;
memcpy((*(p_map+i)).name, &temp,1);
(*(p_map+i)).age=20+i;
}
sleep(5);
printf( "父进程读取: 五个人的年龄和是: %d\n",(*p_map).age );
printf("解除内存映射……\n");
munmap(p_map,sizeof(people)*10);
printf("解除内存映射成功!\n");
}
}

使用特殊文件提供匿名内存映射,适用于具有亲缘关系的进程之间。==一般而言,子进程单独维护从父进程继承下来的一些变量。而mmap函数的返回地址,由父子进程共同维护。==

System V IPC共享内存的实现

通过映射到tmpfs中的shm文件对象实现共享主存

  • 每个共享主存区对应tmpfs中的一个文件(通过shmid_kernel关联)

创建过程

  1. 从主存申请共享主存管理结构,初始化相应shmid_kernel结构
  2. 在tmpfs中创建并打开一个同名文件
  3. 在主存中建立与该文件对应的dentry及inode结构
  4. 返回相应标识符

System V shared memory

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

int shmget(key_t key, int size, int flag);

void *shmat(int shmid, void *addr, int flag);

int shmdt(void *addr);

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

mmap和System V

1.System V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。

2.System V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。

7.4 信号量

用来协调进程对共享资源的访问

相关函数semget,semop,semctl

所需头文件

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

相关函数——semget

创建一个新信号量或取得一个已有信号量,原型为:int semget(key_t key, int num_sems, int sem_flags);

参数key****:整数值

参数num_sems:指定需要的信号量数目,几乎总是1。

参数sem_flags:一组标志,信号量不存在时创建一个新的信号量,指定IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。

指定IPC_CREAT | IPC_EXCL,创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

返回值:成功返回一个相应信号标识符(非零),失败返回-1.

相关函数——semop

操作一个或一组信号 ,原型为:

int semop(int sem_id, struct sembuf *sem_opa, size_t nsops);

​ semid:信号集的识别码,可通过semget获取。

​ sem_opa:指向存储信号操作结构的数组指针,信号操作结构的原型如下

struct sembuf{

short sem_num; //信号量集中的信号量编号0,1……

short sem_op; //信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,一个是+1。

short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号并在进程没有释放该信号量而终止时,操作系统释放信号量

};

lnsops:信号量操作结构的数量,大于或等于1

相关函数——semctl

该函数用来直接控制信号量信息,它的原型为

int semctl(int sem_id, int sem_num, int command, …);

第四个参数,它通常是一个union semum结构,定义如下:

union semun{

int val;

struct semid_ds *buf;

unsigned short *arry;

};

例3-20sem1.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(int argc, char *argv[])  
{ char message = 'X'; int i = 0;
if(argc > 1)
message = argv[1][0];
for(i = 0; i < 10; ++i)
{ printf("%c", message);
fflush(stdout);
sleep(rand() % 3);
printf("%c", message);
fflush(stdout);
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
exit(EXIT_SUCCESS);
}

例3-21sem2.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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
static int set_semvalue()  
{ union semun sem_union;
sem_union.val = 1; //用于初始化信号量
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}

static void del_semvalue()
{
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\n");
}
static int semaphore_p()
{ //对信号量做减1操作,即等待P(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;//P()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}

static int semaphore_v()
{
//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;//V()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
union semun
{ int val; struct semid_ds *buf; unsigned short *arry; };
static int sem_id = 0;
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
int main(int argc, char *argv[])
{ char message = 'X'; int i = 0;
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT); //创建信号量
if(argc > 1)
{ if(!set_semvalue()) //程序第一次被调用,初始化信号量
{ fprintf(stderr, “Failed to initialize semaphore\n”);
exit(EXIT_FAILURE); }
message = argv[1][0]; //设置要输出到屏幕中的信息,即其参数的第一个字符
sleep(2);
}
for(i = 0; i < 10; ++i)
{ if(!semaphore_p()) //进入临界区 前执行P操作
exit(EXIT_FAILURE);
printf("%c", message); fflush(stdout); sleep(rand() % 3);
printf("%c", message); //离开临界区前再一次向屏幕输出数据
fflush(stdout);
if(!semaphore_v())
exit(EXIT_FAILURE); //离开临界区,休眠随机时间后继续循环
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
if(argc > 1) { sleep(3); del_semvalue(); }
//如果程序是第一次被调用,则在退出前删除信号量
exit(EXIT_SUCCESS);
}

雨课堂题目整理

image-20210610220213435

image-20210610220229500

image-20210610220240603

image-20210610220250059

image-20210610220300339

image-20210610220310461

image-20210610220321107

image-20210610220333714

image-20210610220410424

image-20210610220418478

image-20210610220429370

image-20210610220438832

image-20210610220449449

image-20210610220502760

image-20210610220526628

image-20210610220536612

image-20210610220544837

image-20210610220603324

image-20210610220612511

image-20210610220619429

image-20210610220627025

image-20210610220637248

image-20210610220655591

image-20210610220705258

image-20210610220715535

image-20210610220730081

image-20210610220748981

image-20210610220804988

image-20210610220811818

image-20210610220821845

image-20210610220829105

image-20210610220836629

image-20210610220844183

image-20210610220851954

image-20210610220908753

image-20210610220915232

image-20210610220927300

image-20210610220935403

image-20210610220943592

image-20210610220952495

image-20210610220959961

image-20210610221006902

image-20210610221014835

image-20210610221021551

image-20210610221029470

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <unistd.h>

#include <sys/mman.h>

#include <fcntl.h>

#include <stdio.h>

#include<string.h>

#include<stdlib.h>



typedef struct

{

char name[4];

int num;

} number;



main(int argc, char** argv)

{

FILE *fp;

pid_t result;

int i;

number *p_map;

char temp;

fp=fopen("/etc/passwd","r");

p_map=(number*)mmap(NULL,sizeof(number)*10000,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS ,fp,0);

result=fork();

if(result<0)

{

perror("创建子进程失败");

exit(0);

}

else if (result==0)

{

printf("子进程开始读取");

char tempC;

int temp=1,length=0;

while(1)

{

temp = read(fp, &tempC, sizeof(char));

printf("%c",tempC);

if(temp==0)

{



break;

}

else if(tempC=='a')

{

readSize = readSize+1;

printf("子进程读取: 第 %d 个a\n",++i);

}



}

printf("子进程读取wan: 第 %d 个a\n",++i);

(*p_map).num = readSize;

munmap(p_map,sizeof(number)*10);

exit(0);

}

else

{

sleep(5);

printf( "父进程读取: a的个数是: %d\n",(*p_map).num );

printf("解除内存映射……\n");

munmap(p_map,sizeof(number)*10000);

printf("解除内存映射成功!\n");

}

}

网络编程

0 服务器和客户机的信息函数

**1.**字节序列转换

每一台机器内部对变量的字节存储顺序不同,而网络传输的数据是一定要统一顺序的。所以对内部字节表示顺序与网络字节顺序(大端)不同的机器,一定要对数据进行转换

真正转换还是不转换是由系统函数自己来决定的。

头文件:include <arpa/inet.h>

unsigned short int htons(unsigned short int hostshort):
主机字节顺序转换成网络字节顺序,对无符号短型进行操作4bytes
unsigned long int htonl(unsigned long int hostlong):
主机字节顺序转换成网络字节顺序,对无符号长型进行操作8bytes
unsigned short int ntohs(unsigned short int netshort):
网络字节顺序转换成主机字节顺序,对无符号短型进行操作4bytes
unsigned long int ntohl(unsigned long int netlong):
网络字节顺序转换成主机字节顺序,对无符号长型进行操作8bytes

**2.**地址格式转换

头文件:#include <sys/types.h>   

​ #include <sys/socket.h>   

​ #include <arpa/inet.h>

int inet_pton(int family, const char *strptr, void *addrptr);

转换字符串到网络地址 。 返回:1成功;-1出错

const char* inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

转换网络二进制结构到ASCII类型的地址

返回:成功,指向结果的指针;出错,NULL

1 Socket 基础

socket是网络编程的一种接口,它是一种特殊的I/O,用socket函数建立一个Socket连接,此函数返回一个整型的socket描述符,随后进行数据传输。

一个IP地址,一个通讯端口,就能确定一个通讯程序的位置。为此开发人员专门设计了一个套接结构,就是把网络程序中所用到的网络地址和端口信息放在一个结构体中。

一般套接口地址结构都以“sockaddr”开头。socket根据所使用的协议的不同可以分TCP套接口和UDP套接口,又称为流式套接口和数据套接口。

UDP是一个无连接协议,TCP是个可靠的端对端协议。传输UDP数据包时,LINUX不知道也不关心它们是否已经安全到达目的地,而传输TCP数据包时,则应先建立连接以保证传输的数据被正确接收。

三种类型套接字

流套接字(SOCK_STREAM)

​ 可靠的、面向连接的通信。

​ 使用TCP协议

数据报套接字(SOCK_DGRAM)

​ 无连接服务

​ 使用UDP协议

原始套接字(SOCK_RAW)

​ 允许对底层协议如IP、ICMP直接访问

1. socket套接字的数据结构

两个重要的数据类型:sockaddr和sockaddr_in,这两个结构类型都是用来保存socket信息的,如IP地址、通信端口等。

sockaddr——虚拟定义的地址(取决于协议):

1
2
3
4
5
6
struct sockaddr
{ unsigned short sa_family;
/*可以为AF_INET,代表TCP/IP地址族*/
char sa_data[14];
/*14个字节,包含IP地址和端口号*/
};

sockaddr_in(AF_INET中使用的地址定义):

1
2
3
4
5
6
7
8
9
struct sockaddr_in
 { short sin_family; /*AF_INET(地址族)*/
 unsigned short sin_port;
/*端口号(必须要采用网络数据格式)*/
 struct in_addr sin_addr;
/*网络字节序的IP地址*/
  unsigned char sin_zero[8];
/*与SOCKADDR结构保持同样大小*/
 };

2. 基于连接的服务

image-20210619170114870

Server程序的作用

程序初始化

持续监听一个固定的端口

收到Client的连接后建立一个socket连接

与Client进行通信和信息处理

​ 接收Client通过socket连接发送来的数据,进行相应处理并返回处理结果

​ 通过socket连接向Client发送信息

通信结束后中断与Client的连接

Client程序的作用

程序初始化

连接到某个Server上,建立socket连接

与Server进行通信和信息处理

​ 接收Server通过socket连接发送来的数据,进行相应处理

​ 通过socket连接向Server发送请求信息

通信结束后中断与Server的连接

3. 无连接的服务

image-20210619170325054

UDP编程的适用范围

部分满足以下几点要求时,应该用UDP

​ 面向数据报

​ 网络数据大多为短消息

​ 拥有大量Client

​ 对数据安全性无特殊要求

​ 网络负担非常重,但对响应速度要求高

例子:ICQ、视频点播

具体编程时的区别

socket()的参数不同

UDP Server不需要调用listen和accept

UDP收发数据用sendto/recvfrom函数

TCP:地址信息在connect/accept时确定

UDP:在sendto/recvfrom函数中每次均需指定地址信息

UDP:shutdown函数无效

2 TCP编程

基于TCP协议的编程,其最主要的特点是建立完连接后,才进行通信。

常用的基于TCP网络编程函数及功能

头文件

#include <sys/types.h>

#include <sys/socket.h>

image-20210619170614129

1.基于TCP网络编程函数

socket: 创建用于通信的端点并返回描述符.

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

“domain” parameter

​ 指定通信域,即选择协议族,如 AF_INET,AF_INET6 …

“type” parameter

​ 指定通信语义。 三种主要类型: SOCK_STREAM, SOCK_DGRAM, SOCK_RAW

“protocol” parameter

​ usually 0 (default).

bind: binds a name to a socket

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

struct sockaddr {

sa_family_t sa_family;

char sa_data[14];

}

inet_aton & inet_ntoa

互联网地址操作例程

​ int inet_aton(const char *cp, struct in_addr *inp);

​ char* inet_ntoa (struct in_addr in);

inet_ntoa将一个32位数字表示的IP地址转换成点分十进制IP地址字符串

listen

listen: listen for connections on a socket

int listen(int s, int backlog);

参数说明:

s:socket()返回的套接口文件描述符。

backlog:进入队列中允许的连接的个数。进入的连接请求在使用系统调用accept()应答之前要在进入队列中等待。这个值是队列中最多可以拥有的请求的个数。大多数系统的缺省设置为20。

accept

accept()函数将响应连接请求,建立连接

int accept(int sockfd,struct sockaddr *addr,int *addrlen);

accept缺省是阻塞函数,阻塞直到有连接请求

sockfd: 被动(倾听)的socket描述符

如果成功,返回一个新的socket描述符(connected socket descriptor)来描述该连接。这个连接用来与特定的Client交换信息

addr将在函数调用后被填入连接对方的地址信息,如对方的IP、端口等。

connect

connect: initiate a connection on a socket (connect to a server).

int connect(int sockfd, struct sockaddr *servaddr, int addrlen);

主动的socket

servaddr是事先填写好的结构,Server的IP和端口都在该数据结构中指定。

**send/**recv

send/recv: 面向连接

int send(int s, const void *msg, size_t len, int flag);

s:发送数据的套接口文件描述符。

msg:发送的数据的指针

len:数据的字节长度

flag:标志设置为0。

int recv(int s, void *buf, size_t len, int flag);

s:读取的套接口文件描述符。

buf:保存读入信息的地址。

len:缓冲区的最大长度。

flag:设置为0。

sendto/recvfrom

int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socketlen_t tolen);

int recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

close & shutdown

close

​ int close(int sockfd);

shutdown

​ int shutdown(int sockfd, int how);

​ how: SHUT_RD, SHUT_WR, SHUT_RDWR

shutdown直接对TCP连接进行操作,close只是对套接字描述符操作。

例4.1:服务器通过socket连接后,向客户端发送字符串“连接上了”。在服务器上显示客户端的IP地址或域名。

image-20210619171521320

主要语句说明:
服务端
建立socket:socket(AF_INET, SOCK_STREAM, 0);
绑定bind:bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr);
建立监听listen:listen(sockfd, BACKLOG);
响应客户请求:accept(sockfd,(struct sockaddr *)&remote_addr, &sin_size);
发送数据send:send(client_fd, “连接上了 \n”, 26, 0);
关闭close:close(client_fd);

客户端:
建立socket:socket(AF_INET, SOCK_STREAM, 0);
请求连接connect:connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr));
接收数据recv:recv(sockfd, buf, MAXDATASIZE, 0);
关闭close:close(sockfd);

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<stdlib.h>
#include<errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#define SERVPORT 3333
#define BACKLOG 10
int main()
{ int sockfd,client_fd; int sin_size;
struct sockaddr_in my_addr; struct sockaddr_in remote_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0) ;
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(SERVPORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(my_addr.sin_zero),8);
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) == -1)
{ perror("bind 出错!"); exit(1); }
if (listen(sockfd, BACKLOG) == -1)
{ perror("listen 出错!"); exit(1); }
while(1)
{
sin_size = sizeof(struct sockaddr_in);
if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, &sin_size)) == -1)
{ perror("accept error"); continue; }
printf("收到一个连接来自: %s\n", inet_ntoa(remote_addr.sin_addr));
if (!fork())
{
if (send(client_fd, "连接上了 \n", 26, 0) == -1) error("send 出错!"); close(client_fd); exit(0);
}
close(client_fd);
}
}
//客户端源程序代码 :
#include<stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h> #define SERVPORT 3333
#define MAXDATASIZE 100
int main(int argc, char *argv[]){
int sockfd, recvbytes;
char buf[MAXDATASIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
if (argc < 2) { fprintf(stderr,"Please enter the server's hostname!\n");
exit(1); }
if((host=gethostbyname(argv[1]))==NULL)
{ herror("gethostbyname error!"); exit(1); }
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{ perror("socket create error!"); exit(1); }
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1) {
perror("connect error!");
exit(1);
}
if ((recvbytes=recv(sockfd, buf, MAXDATASIZE, 0)) ==-1) {
perror("connect 出错!");
exit(1);
}
buf[recvbytes] = '\0';
printf("收到: %s",buf);
close(sockfd);
}

3 UDP编程

基于UDP协议的编程,其最主要的特点是不需要用函数bind把本地IP地址与端口号进行绑定,也能进行通信。

常用的基UDP网络编程函数及功能:

image-20210619171733752

例4.2:服务器端接受客户端发送的字符串。客户端将打开liu文件,读取文件中的3个字符串,传送给服务器端,当传送给服务端的字符串为”stop”时,终止数据传送并断开连接。

image-20210619171749586

主要语句说明:
服务端:
建立socket:socket(AF_INET,SOCK_DGRAM,0)
绑定bind:bind(sockfd,(struct sockaddr *)&adr_inet,sizeof(adr_inet));
接收数据recvfrom:recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_clnt,&len);
关闭close:close(sockfd);

客户端:
建立socket:socket(AF_INET, SOCK_STREAM, 0);
读取liu文件:fopen(“liu”,”r”);
发送数据sendto:sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_srvr,sizeof(adr_srvr));
关闭close:close(sockfd);

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
76
77
78
79
80
81
82
83
//服务端源程序代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<errno.h>
#include<sys/types.h>

int port=8888;
int main()
{ int sockfd; int len; int z;
struct sockaddr_in adr_inet;
struct sockaddr_in adr_clnt; char buf[256];
printf("等待客户端....\n");
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);
len=sizeof(adr_clnt);
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd==-1)
{ perror("socket 出错"); exit(1); }
z=bind(sockfd,(struct sockaddr *)&adr_inet,sizeof(adr_inet));
if(z==-1)
{ perror("bind 出错"); exit(1); }
while(1)
{
z=recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_clnt,&len);
if(z<0)
{ perror("recvfrom 出错"); exit(1); }
buf[z]=0;
printf("接收:%s",buf);
if(strncmp(buf,"stop",4)==0)
{ printf("结束....\n"); break; }
}
close(sockfd);
exit(0);
}
//客户端源程序代码 :
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<errno.h>
#include<sys/types.h>
int port=8888;

int main()
{
int sockfd; int i=0; int z;
char buf[80],str1[80]; struct sockaddr_in adr_srvr;
FILE *fp;
printf("打开文件......\n");
fp=fopen("liu","r");
if(fp==NULL)
{ perror("打开文件失败"); exit(1); }
printf("连接服务端...\n");
adr_srvr.sin_family=AF_INET;
adr_srvr.sin_port=htons(port);
adr_srvr.sin_addr.s_addr = htonl(INADDR_ANY);
bzero(&(adr_srvr.sin_zero),8);
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd==-1)
{ perror("socket 出错"); exit(1); }
printf("发送文件 ....\n");
for(i=0;i<3;i++)
{ fgets(str1,80,fp); printf("%d:%s",i,str1);
sprintf(buf,"%d:%s",i,str1);
z=sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_srvr, sizeof(adr_srvr));
}
printf("发送.....\n"); sprintf(buf,"stop\n");
z=sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_srvr,
sizeof(adr_srvr));
if(z<0)
{ perror("sendto 出错"); exit(1); }
fclose(fp); close(sockfd); exit(0);
}

4 网络高级编程

I/O Models
Block mode 阻塞模式
Non-block mode 非阻塞模式
I/O multiplexing I/O多路复用
多进程并发
多线程并发

阻塞方式

阻塞方式:在数据通信中,当服务器运行函数accept() 时,假设没有客户机连接请求到来,那么服务器就一直会停止在accept()语句上,等待客户机连接请求到来,出现这样的情况就称为阻塞。

非阻塞方式

阻塞与非阻塞方式的比较
errno - EWOULDBLOCK
非阻塞的实现
int flags;
if ( (flags=fcntl(sock_fd, F_GETFL, 0)) < 0)
err_sys();
flags |= O_NONBLOCK;
if ( fcntl(sock_fd, F_SETFL, flags) < 0)
err_sys();

I/O 多路复用

基本思想:

先构造一张有关描述符的表,然后调用一个函数(如select),该函数到这些描述符中的一个已准备好进行I/O时才返回,返回时告诉进程哪个描述符已准备好进行I/O.

“select”

select: synchronous I/O multiplexing.

#include <sys/select.h>

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

FD_ZERO(fd_set *set);

FD_SET(int fd, fd_set *set);

FD_CLR(int fd, fd_set *set);

FD_ISSET(int fd, fd_set *set);

image-20210619172130327

多进程并发

1
2
3
4
5
6
7
8
9
10
listenfd = socket(...);
bind(...);
listen(...);
while(1) {
    connectfd = accpet(...);
     if(fork() == 0)    { close(listenfd);
     process(...);
     close(...);
     exit();    }
else { close(connectfd); ... continue;}    close(...); }

雨课堂题目整理

image-20210611101124982

image-20210611101135283

image-20210611101144547

image-20210611101151866

image-20210611101159284

image-20210611101205637

image-20210611101212054

image-20210611101220777

image-20210611101228856