Welcome to unix_c's documentation!¶
编程基础 - d01¶
第一课 编程基础
第二课 内存管理
第三课 文件系统
第四课 进程管理
第五课 信号处理
第六课 进程通信
第七课 网络通信
第八课 线程管理
第九课 线程同步
第十课 综合案例
1、 unix下c开发,和前边学习的C编程数据结构有什么区别
C学习,C编程语法、C标准库函数 :printf malloc
strcpy strcmp
memcpy fgets
fopen fclose
fread fwrite
数据结构是把学会C语法(遣词造句),如何表达我们的思想(算法)
uc学习的内容?
开放给应用编程者的函数(系统调用)如何使用
200~300个系统调用
2、UC课程学什么?
学习系统调用
开发环境
内存管理
文件操作
进程管理/进程间通信
网络通信
线程管理
线程同步
操作系统原理的内容
《linux程序设计》 陈健
《unix环境高级编程》
3、怎么学?
注意听原理
man手册 + 编程练习 + 网络搜索
关于某些函数man不到如何解决?
man-patch.tar.gz
步骤:tar xf man-patch.tar.gz
rm manpages-zh_1.5.2-1_all.deb
sudo dpkg -i *.deb
4、unix/linux发展历程
1970年出现的unix系统
C语言 用C语言改写了unix
1975 unix第6版发布了,已经具有现代操作系统的几乎所有特征,最后一版免费版本的unix。
后续商业版本的unix, AIX(IBM) 银行
Solaris(sun) 电信
HPUX
1991 在unix发展起来的linux 0.0.1版 开源
fedora ubuntu redhat debain 红旗...
POSIX: 统一的系统编程接口规范
GNU计划: 开源共享
5、重新审视gcc工具
gcc hello.c >a.out
1) 预编译 :处理所有的伪指令(以#开头)
gcc hello.c -E -o hello.i
2)编译: 把hello.i中的C语言变成汇编语言
gcc hello.i -S -o hello.s
3)汇编:由汇编变成目标代码(二进制)
gcc hello.s -c -o hello.o
4)链接 : 将多个.o生成一个可执行文件
gcc hello.o -o hello
把前三歩统称为编译阶段,最后一步称为链接阶段,
编译阶段检查的是语法错误。
gcc的一些重要参数
-c: 只编译不链接
-o: 目标文件名称
-E:预编译
-S:产生汇编文件
-Wall: 产生尽可能多的警告信息(建议加上该选项)
-g: 生成的目标文件中包含调试信息
-v: 显示编译阶段的过程以及编译器的版本号
-O0/1/2/3:目标代码的优化级别
6、重新审视.c和.h文件
6.1 编译多个文件生成一个可执行文件
gcc xxx.c yyy.c -o
6.2 头文件的作用?什么内容放入头文件?
本模块对外的清单,各中声明都应该放在头文件中。
1)声明外部的函数、变量
2)自定义类型 strcut 宏定义
3)包含其他的头文件
注意:防止头文件的重复包含机制
6.3 头文件的使用注意事项
1) #include <>:在系统目录下找文件
#include "":先在当前目录找指定的头文件, 如果找不到再到系统目录下找
问题1: 系统目录是哪个目录?
gcc hello.c -v
/usr/include
/usr/include/i386-linux-gnu
问题2:自己写一个.h文件,写一个.c文件,该文件中包含该.h ,如果.h和.c不在同一个目录怎么办?
解决方案一:#include 时指定头文件所在的路径
相对路径(推荐)
绝对路径
解决方案二: 把xxx.h扔到系统目录下去(不推荐)
解决方案三: 使用gcc -I指定附加的头文件搜索路径
6.4 预编译指示符
#error
各种版本控制工具: SVN CVS git
error.c
#line
#pragma
#pragma pack(1) (重点)
pack.c
对齐:每个成员,必须放在自己大小整数倍的位置
(该成员大于4字节按4字节计算)
补齐:每个结构体的大小应该是最大成员大小的整数倍(最大成员超过4个字节按4字节算)
#pragma GCC poison
poison.c
6.5 预定义宏
__FILE__
__LINE__
__FUNCTION__
predef.c
一般用于程序调试阶段
7、重新审视a.out
问题一:为什么./a.out可以执行 而a.out 不可以执行
ls clear统统可以执行?
答案:环境变量中的PATH在起作用
环境变量一般是操作系统中用来指定操作系统运行环境的一些参数,比如说临时文件夹的位置。
环境变量有很多,对应了不同的用途。
命令 env,可以用来显示当前用户的环境变量
其中PATH用来指定可执行程序的搜索路径。
PATH=/home/tarena/workdir/Android2.3/android-source/jdk1.6.0_16/bin:/bin:/usr/bin:/usr/X11R6/bin:/usr/local/bin:/home/tarena/workdir/linux-x86/sdk/android-sdk_eng.root_linux-x86/tools
问题二: printf()函数的实现代码在哪?
ldd a.out: 显示可执行程序需要的共享库文件
printf()函数的实现代码位于libc.so共享库文件中
7.1 静态库文件
就是一些目标文件的集合,通常以.a结尾。静态库在程序链接的时候使用,连接器会将程序中使用到的函数的代码从库文件中拷贝到应用程序中。一旦链接完成,在执行应用程序时就不再需要静态库文件。
由于每个使用静态库的应用程序都需要拷贝所用函数的代码,所以静态链接的文件会比较大。
7.1.1创建静态库文件
1)编辑源程序
2)编译生成目标文件 gcc -c xxx.c -o xxx.o
gcc -c calc.c
gcc show.c -c
3)打包生成静态库文件 ar -r libzzz.a xxx.o yyy.o
ar -r libmath.a calc.o show.o
ar [选项] 静态库文件名称 目标文件列表
选项:
-r, 将目标文件插入到静态库中(重点)
-d, 从静态库中删除目标文件
-t, 以列表方式显示静态库中的目标文件
7.1.2 静态库如何使用
1) 直接法
gcc main.c libmath.a
2)环境变量法
export LIBRARY_PATH=$LIBRARY_PATH:./
gcc main.c -lmath
3)参数法 (推荐使用)
gcc main.c -lmath -L./
作业:写两个函数, 分别打印空/实心菱形,把这两个函数封装成静态库,通过main.c调用这两个库函数
数据结构作业:在内存中建立一个班上同学的电话簿
struct
{
name
age
number
vip
}
增删改查
输入姓名找到电话,姓名重复的,把连个重复姓名的电话都列出来。
7.2 共享库文件
编程基础 - d02¶
回顾:
unix/linux发展历史:
1970 unix 第一版
1975 unix 第六版 以后版本不再免费
HPUX AIX solaris
GNU计划: 自由软件 操作系统 之上的应用软件
1991 linux 0.0.1 GNU/linux
fedora ubuntu redhat debain suse 红旗
POSIX:统一的用户编程接口规范
1、重新认识编译器
.c ---> a.out
1) 预编译
2)编译
3)汇编
4)链接
gcc 编译器的参数: -o -g -c -E -S -I -v -l -L -Wall
2、.c .h文件
预处理指令 pragma pack
pragma GCC poison
预定义宏 __LINE__ __FILE__ __FUNCTION__ (调试)
头文件的使用:本模块对外的清单
#include <>
#include ""区别
如果头文件和.c不在同一个目录的解决方案
1)#include "相对路径/xxx.h"
2) #include "xxx.h"
gcc -I "相对路径"
3、重新审视a.out
环境变量: 系统运行时需要的一系列参数
PATH: 当去执行某个可执行程序未指定路径,在PATH中的路径挨个寻找是否存在要执行的可执行程序
如何定义 赋值 环境变量
export PATH=$PATH:. (不推荐将当前目录添加)
env: 当前用户的所有环境变量
4、静态库
1)静态库的制作
gcc -c xxx.c yyy.c
ar -r libxxxxx.a xxx.o yyy.o
2) 使用方式
gcc main.c -lxxxxx -L./
3) 运行./a.out
5、共享库
在程序链接时并不像静态库那样拷贝库中函数的代码,而是做标记。然后在程序启动运行的时候,动态加载所需要的模块。
所以,应用程序在执行时仍需要共享库的支持。
使用共享库生成的可执行文件要比使用静态库生成的可执行文件小的多。
1)共享库的制作
a) 编辑.c .h文件
b) 编译生成目标文件 gcc -c -fpic xxx.c
fpic作用:生成位置无关码
gcc -c -fpic calc.c
gcc -c -fpic show.c
c) 链接成共享库文件 gcc -shared xxx.o... libxxx.so
gcc -shared calc.o show.o -o libmy.so
b、c步骤可以合并为
gcc -shared -fpic calc.c show.c -o libmy.so
2)使用方式
a) 直接法
gcc main.c libmy.so
blibc.) 环境变量法
export LIBRARY_PATH=$LIBRARY_PATH:.
gcc main.c -lmy
c)参数法 (推荐使用)
gcc main.c -lmy -L./ ---》a.out
3)运行
LD_LIBRARY_PATH: 加载共享库的路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
./a.out
总结:静态库和动态库的优缺点
使用静态库占用空间非常大, 执行效率高,如果静态库函数修改,a.out需要重新编译
使用动态库占用空间比较小, 执行效率偏低,如果动态库中的函数进行了修改,只需要重新编译生成库文件,不需要重新编译a.out文件。
4)共享库的加载方式
a)隐式加载
./a.out 所需要的共享库由加载器加载到内存中去
b)显式加载
编程者通过程序手工加载所需要的库文件。
dlopen
dlsym
dlerror
dlclose
load.c
6、工具
ldd: 可以用来察看可执行文件和共享库文件的动态依赖
ldd a.out
ldd libmy.so
nm: 察看目标文件、可执行文件、静态库、共享库文件的
符号列表
所谓的符号:函数名称 全局变量名称
objdump: 反汇编 , 将二进制翻译成汇编程序
7、C语言的出错处理
错误处理解决不了所有的问题,只是可控的范围内,对错误的情况进行编码处理。
错误的处理对所有的语言都是必须的,后期学习的C++都使用异常机制进行错误处理。
C语言中通过返回值的方式来描述出错原因,判断返回值进行出错处理。
7.1 通过函数的返回值来表示错误
1)返回合法值表示成功,返回非法值表示失败。
2)返回有效指针表示成功,返回空指针表示失败
3)返回0表示成功,返回-1表示失败
4)永远成功. 如: bzero
练习:
写4个函数,分别是:
取1~10的随机值,并返回该值, 假如随机到5代表出错
字符串拷贝,拷贝成功返回目标字符串地址,失败返回null
strcopy(char *dest, char *src, int len)
传入一个字符串,如果该字符串为"error",返回出错,否则返回0
求两个整数的平均值,
void func(int a, int b , int *av)
7.2 标C库函数 系统调用如何判断是否执行成功
errno.c
在标C中,对出错情况提供了一个全局变量和3个函数
errno-----外部的全局变量,用于存储错误的编号
strerror/perror/printf:把错误的编号转换成对应出错原因字符串信息
strerror
perror
printf("%m") :打印当前的出错原因(自动查找errno)
问题:为什么sudo ./a.out 读写打开/etc/passwd文件成功,
而./a.out读写打开失败?
答:当a.out 开始执行时会获取一张环境变量表,这张表记录了当前是哪个用户(权限)来执行的a.out
8、环境变量的操作
env命令
在C语言中环境变量存在于环境变量表中。环境表就是一个字符串的数组(字符指针数组)。
通过外部的全局变量 environ --->环境表的地址
char **environ
编程:打印所有的环境变量的值
env.c
练习:将”LANG=zh_CN.UTF-8“中的 ”zh_CN.UTF-8“过滤出来,并保存到buf[],最后打印buf
思路:通过environ变量取得环境变量表中的每个环境变量字符串 ,通过strncmp(字符串 , ”LANG=“, ...)
如果相等字符串中就含有字串”zh_CN.UTF-8“,
将该字串拷贝到buf[],并打印
操作环境变量的函数:getenv setenv unsetenv clearenv
putenv
内存管理 malloc/brk/sbrk/mmap
makefile 三要素
目标:依赖
(TAB) 命令
工具《跟我一起写makefile》
内存管理 - d03¶
回顾:
C语言的程序怎么写? - 包括:C基本语法、数据结构、C标准函数库、常用的算法
UC - C程序员的错误处理、环境变量、Unix的静态库和共享库
今天:
UC - Unix/Linux系统开发(以C语言为主)
环境变量收尾
Unix系统的内存管理(纯UC)
C语言没有直接的字符串(文本)类型,字符串用 char* 或者 char[ ]去代表,因此 字符串 可以看出 字符指针,字符串数组就是 字符指针数组 (或char** / char [][])。
环境表就是一个字符串数组,char* [],用 外部全局变量environ代表。 extern char** environ;
操作环境表就有两种方式:
1 用指针操作的方式(C程序员的基本功)
2 C提供的标准函数
getenv() - 获取一个环境变量,用名字找到值
putenv()/setenv() - 设置环境变量(区别在于如果存在,putenv必定替换,而setenv可以选择)
unsetenv() - 删除一个环境变量(按名字)
clearenv() - 删除所有环境变量
#include <stdio.h>
#include <my.h>
export CPATH=.:/home/tarena/uc/day03/
环境变量PATH(Windows中是Path)是系统路径,默认情况下,所有的可执行文件/命令 都需要带上路径才能运行,但如果把命令所在的路径放入系统路径,系统就能自动识别,命令就不需要带路径而是直接运行。
去掉./a.out中的路径,可以把 export PATH=.:$PATH 写入 /home/tarena/.bashrc 文件中。
重启机器 或者 source .bashrc 就生效了。
Unix/Linux系统函数 - 内存管理机制
1 六个内存分配/回收 函数
malloc()/free() sbrk()/brk() mmap()/munmap()
2 进程内存空间的划分
一个进程启动后,都有 哪些内存区域组成
3 Unix/Linux系统的内存管理机制(原理)
虚拟内存地址空间
1 Unix/Linux遵循相同的规范,POSIX规范,因此UC和Linux C在编程上是通用的。细微的差别会讲清楚。
2 分配/回收内存的方式:
STL容器、JAVA - 自动管理(分配和回收)
|
C++ - new分配内存 delete回收内存
|
C语言 - malloc()分配 free()回收
|
UC系统函数 - sbrk() brk() 分配、回收
|
UC系统函数 - mmap()分配 munmap()回收
| (用户层)
------------------------------------------------------------
| (内核层)
UC系统函数 - kmalloc() vmalloc()
3 进程内存空间的划分
程序 是源代码编译连接的产物,是硬盘上的文件。
进程 运行在内存中的程序,程序一旦运行起来就叫进程。
严格来说,内存空间是针对进程的,与程序无关。程序加载到内存中就是进程了。在很多的情况下,也把进程叫程序。
Unix/Linux查看系统进程的命令:
ps -aux (Linux专用) 或 ps -ef(通用)
一个进程的内存空间包括以下部分:
3.1 代码区 - 程序的代码(以函数的形式)存入代码区,函数指针就是函数在代码区的地址,代码区 是 只读区域。
3.2 全局区 - 存放全局变量和static的局部变量,读写权限。
3.3 BSS段 - 存放未初始化的全局变量(没有写=的),读写权限。
全局区和BSS段的区别就在于:虽然两者都是在main()运行前创建,但BSS段会在main()执行之前自动清一次0,而全局区不会。
3.4 栈区(stack) - 也叫堆栈区,存放局部变量(没有static的),函数的形参也是在栈区。系统自动管理。
3.5 堆区(heap) - 也叫自由区,是程序员完全管理的区域,系统不会管理这个区域。malloc()/sbrk()/mmap()主要针对这个区域。内存泄露的重灾区。
3.6 只读常量区 - 很多的书上都把只读常量区归入代码区,存放字符串的字面值("abc")和const修饰的全局变量。
(超重点)
4 Unix/Linux系统的内存管理机制(针对32位系统)
Unix系统采用虚拟内存地址空间 管理内存。虚拟内存地址其实就是一个整数,可以看成是内存的编号,但不是内存自带的物理地址。每个进程在启动后 都先天具备 0-4G的虚拟内存地址(0-4G编号),这个编号不代表任何的物理内存,也存储不了任何的数据;只有做了内存映射(用虚拟地址 映射 物理内存或硬盘文件),虚拟内存地址才能存储数据。程序员接触到的全部都是虚拟内存地址。不同的进程虽然使用相同的虚拟内存地址,但映射的物理内存是不同的,因此互相不会影响。
其中,0-3G是用户使用的,叫用户层(用户空间),3G-4G是内核使用的,叫内核层(空间)。用户层的程序只能使用用户层的内存,无法直接访问内核空间,除非使用系统提供的相关函数进入内核空间。
内存的管理是以 字节为单位的,一个字节 等于 8个 二进制位。内存映射 以 字节作为单位 效率太低,内存映射 以内存页作为基本单位,一次映射N个内存页,一个 内存页 是4096(4K)字节。函数getpagesize()可以获取内存页的大小。
所谓的内存分配其实包括两部分:
1 分配虚拟内存地址
2 映射物理内存/硬盘文件
如果使用了未映射的虚拟内存地址,就会引发段错误。
段错误的常见原因:
1 使用了未映射的虚拟内存地址操作数据
2 对内存区域进行没有权限的操作(比如:修改只读区)
Linux系统中,几乎一切都可以看成文件,内存也可以用文件的方式进行操作,也对应着文件。比如:进程对应 /proc/PID 目录。
如果想查看进程的内存信息,可以用
cat /proc/PID/maps 查看。 (PID是 打印出来的进程号)
内存管理 - d04¶
回顾:
环境变量和环境表的相关函数 - getenv() setenv() putenv() unsetenv() clearenv()
Unix/Linux的内存管理 - C程序员能用的6个函数
进程的内存空间划分:6个部分
代码区、只读常量区、全局区、BSS段、堆、栈
Unix/Linux内存管理机制 - 虚拟内存地址
程序员目前接触都是 虚拟内存地址,是 地址的编号,本身不能存储任何的数据,需要做内存映射才能存储数据。内存映射就是把虚拟内存地址和 物理内存/硬盘文件 对应起来。内存的分配是以 字节作为基本单位,但内存映射 是以 内存页 作为基本单位,一个内存页 4096字节。虚拟内存地址在使用时会被分配,内存分配过程包括两步:
1 分配虚拟内存地址(未使用的)
2 映射物理内存/硬盘文件
今天:
关于 C程序员的基本功 - 操作字符串的代码 (超重点)
6个函数 - malloc() free() sbrk() brk() mmap() munmap()
malloc()函数分配的是堆区内存,在第一次申请内存时会映射33个内存页(申请的是小块内存,如果是大块内存会映射比申请的多一点的页数),在33个内存页之内不会再次映射,只需要分配虚拟内存地址。
malloc()除了分配申请的内存之外,还需要额外的空间去存储一些附加数据,比如:分配了多少字节,附加数据以双向链表的形式存在分配内存的前面,还会留出一些空白。因此,使用malloc()分配的内存时,不要超界使用,否则可能导致后面的内存出现问题。
free()一定会释放虚拟内存地址,但不一定会 解除内存映射。最后的33页 free()不会解除映射,直到进程结束。free()不一定会擦除数据。
sbrk()和brk()
sbrk()和brk()是UC函数,因此机制和malloc()、free()完全不同。
sbrk()和brk()依靠 系统维护的一个位置实现内存的分配和回收,sbrk()和brk()都可以 分配 也可以回收内存。但sbrk()更适合分配内存,brk()更适合回收。
void* sbrk(int increment)
参数是一个整数,代表内存的增量,其实是位置移动的值
参数为正数时,分配内存,返回分配内存的首地址
参数为负数时,回收内存,返回没有实际意义
参数为0时,不分配也不回收内存,取当前的位置
返回移动之前的位置。如果出错,返回 -1。
作业:
用sbrk()分配内存,用brk()回收内存方式 改良 brk.c代码。
要求新建一个文件。
文件系统 - d05¶
回顾:
string的操作 - 代码要熟练(超重点)
内存分配的几个函数 - malloc() free() sbrk() brk()
zhanglm@tarena.com.cn
今天:
mmap()和munmap() - 针对物理内存的映射(不包括映射文件)
Unix/Linux系统函数 - 关于文件读写的(重点)
经验:在应用开发中,多个权限或者多个选项 之间用 位或 | 连接。
r - 100
w - 010
x - 001
rw rx -> rwx
110 101 -> 111
110
101 (位或|)
--------
111
void* mmap(void* addr,size_t size,int prot,int flags,
int fd,off_t offset)
功能:创建一个新的 内存映射(可能映射 文件或内存)
参数:addr 允许程序员指定映射的首地址,一般为NULL交给内核指定。size 就是分配内存的大小,最好是 4096的整数倍,如果不是,也会映射 内存页的整数倍。prot是 内存的访问权限,一般写:PROT_READ|PROT_WRITE 即可。flags是映射的选项,必须包含以下两个之一:
MAP_PRIVATE - 只有本进程使用
MAP_SHARED - 运行其他进程使用,但只对 映射文件有效。
默认情况下,mmap()映射的是文件,想映射物理内存,必须在flags指定 MAP_ANONYMOUS .
fd和offset 只对 映射文件有效,映射物理内存时给 0 即可。
返回 映射成功返回首地址,失败 返回 (void*)-1 (MAP_FAILED)
系统调用 - 用户层的程序不能直接使用内核空间,而从功能上需要内核空间完成,因此,Unix系统提供了大量的系统函数,用户程序通过调用这些系统函数进入内核空间,完成功能。这些函数 统称为 系统调用。(system call)
Linux系统中,几乎一切都可以看成文件。目录是文件,内存是文件,各种设备也都是文件。因此,操作文件的函数 可以 操作各种设备。
关于文件的系统函数:
通用的
open() - 打开一个文件,返回 文件描述符
read() - 读文件
write() - 写文件
close() - 关闭 打开的文件
文件描述符 其实就是一个整数,对应一个 打开的文件。
读写文件时,有几个步骤:
1 首先要在硬盘上找到文件,查找文件 其实 是依靠 i节点(inode),i节点就是 文件在硬盘上的地址,也是整数。ls -i就可以看到i节点。
2 CPU 不能直接操作 硬盘上的文件,因此硬盘上的文件 必须放入内存中才能使用。
3 文件描述符 就是文件读入内存后的代表,是 内存中代表一个打开的文件的整数。
4 程序通过 操作文件描述符 实现对文件的读写操作。
文件描述符 其实 没有存储任何文件的信息,就是一个非负的整数编号,文件描述符 其实 在内存中对应了一张文件表,文件的信息存在文件表中。
每个进程都有一个文件描述符 总表,总表中 记录了已经使用的文件描述符和文件表的对应关系。文件描述符的0 1 2 被系统占用,代表标准输入、标准输出和标准错误,因此文件描述符其实从3开始。
open()函数 其实 是先打开一个文件,把文件的信息存入文件表,然后去文件描述符总表中 查找未使用的最小值,然后把对应关系写入总表,并把文件描述符的值返回。
close()函数 其实 就是从 文件描述符总表中 删除 对应关系,但不一定删除文件表。只有 没有关系的 文件表 才会被删除。
int open(char* filename,int flags,...)
功能:打开一个文件,返回文件的描述符
参数:filename 就是文件名(带路径的),flags是打开的方式,包括
O_RDONLY / O_WRONLY / O_RDWR - 3选1,代表打开权限
O_CREAT - 如果文件不存在会创建新文件,不写 就不会新建文件
O_EXCL - 和O_CREAT一起用,代表文件如果不存在新建,存在不打开而是直接 返回错误 -1
O_TRUNC - 清空已存在文件的内容(小心使用,可能误删数据)
O_APPEND - 以追加的方式打开文件,文件的读写位置不是文件头,而是文件的末尾。
... 代表0-N个参数,在新建文件时需要第三个参数,是新文件在硬盘上的权限,采用 8进制。
返回: 成功返回文件描述符,失败返回 -1 。
read() 和 write()
都是三个参数一个返回值,用于读写文件
参数:第一个参数是 文件描述符,open()的返回值。
第二个参数就是 读/写的 首地址。
第三个参数 read() 是 buf的大小,而 write() 是 数据的大小
返回:
成功都返回实际读/写的字节数,失败都返回 -1 。
read() 用返回0 代表读到了文件尾。
练习:
员工管理系统的模块 - 增加员工模块、查询员工信息模块
员工的信息包括:员工编号id、员工姓名name、员工薪水sal
做成一个结构体。
增加员工的流程:用户输入(scanf())员工的id、name和sal,然后放入结构体中,把结构体写入文件中。
查询员工信息的流程:就是把所有员工信息从文件中读出来,读到结构体中并打印出来。
提示&要求: 把结构体写入 头文件 .h 中。
文件系统 - d06¶
闵卫
minwei@tarena.com.cn
标C UC
fopen open
fclose close
fread read
fwrite write
一、系统I/O与标准I/O
1.当系统调用函数被执行时,需要切换用户态和内核态,
频繁调用会导致性能损失。
2.标准库做了必要的优化,内部维护一个缓冲区,
只有满足特定条件时才将缓冲区与系统内核同步,
借此降低执行系统调用的频率,
减少进程在用户态和内核态之间来回切换的次数,
提高运行性能。
二、lseek
1.每个打开文件都有一个与之相关的“文件位置”。
2.文件位置通常是一个非负的整数,
用以度量从文件头开始计算的字节数。
3.读写操作都是从当前文件位置开始,
并且根据所读写的字节数,增加文件位置。
4.打开一个文件,除非指定了O_APPEND,
否则文件位置一律被设为0。
5.lseek函数根据所提供的参数认为地设置文件位置,
并不引发任何文件I/O动作。
6.在超越文件尾的文件位置写入数据,
将在文件中形成空洞。
7.文件空洞并不占用磁盘空间,但是被算在文件大小内。
#include <sys/types.h>
#include <unistd.h>
off_t lseek (
int fd, // 文件描述符
off_t offset, // 偏移量
int whence // 起始位置
);
成功返回当前文件位置,失败返回-1。
whence取值:
SEEK_SET - 从文件头
(文件的第一个字节)
SEEK_CUR - 从当前位置
(上一次读写的最后一个字节的下一个位置)
SEEK_END - 从文件尾
(文件的最后一个字节的下一个位置)
获取当前文件位置:lseek (fd, 0, SEEK_CUR)
获取文件大小:lseek (fd, 0, SEEK_END)
复位到文件头:lseek (fd, 0, SEEK_SET)
三、打开文件的内核数据结构
1.每个进程在内存中都有一个进程表条目(Process Table Entry)
与之对应。
2.每个进程表条目中都有一个文件描述符表的结构体数组。
3.文件描述符就是文件描述表条目在文件描述符表中的下标。
4.每个文件描述符表条目中包含了一个文件表指针,
指向一个特定的文件表结构(File Table)。
5.文件表中包含了若个数据,如:文件状态标志、文件偏移、
v节点指针等。
6.v节点指针指向v节点结构体,v节点结构体中保存了i节点号。
7.通过i节点号,对应磁盘上一个具体的i节点,
通过i节点中记录的文件位置,
就可以确定文件数据在磁盘上的存储位置。
四、dup/dup2
#include <unistd.h>
int dup (int oldfd);
将参数文件描述符oldfd所对应的文件描述符表条目复制到文件描述
符表中的最小未用文件描述符的位置,返回该位置所对应的下标。
如果失败,返回-1。
int dup2 (int oldfd, int newfd);
通过newfd指定文件描述符复制的目标。如果newfd对应的是一个
正在打开的文件,该函数会先将该文件关闭,然后再复制。如果该
函数成功返回第二参数,失败返回-1。
无论dup还是dup2都只是复制文件描述符,文件表始终都只有一个。
int fd1 = open ("a.txt", ...);
int fd2 = dup (fd1);
fd1 \
> 文件表 -> v节点 -> i节点 -> 文件
fd2 /
两次open同一个文件,会得到两张文件表,对应同一个v节点。
int fd1 = open ("a.txt", ...);
int fd2 = open ("a.txt", ...);
fd1 -> 文件表1 \
> v节点 -> i节点 -> 文件
fd2 -> 文件表2 /
五、sync/fsync/fdatasync
1.大多数磁盘I/O都是通过缓冲区进行的,
写入文件其实只是写入缓冲区,直到缓冲区满,
才将其排入写队列,等待I/O子系统空闲时将其同步到设备。
2.延迟写降低访问磁盘设备的频率,提高写操作的效率,
但可能导致磁盘文件与缓冲区数据不同步。
void sync (void);
将调用进程所有被修改过的缓冲区排入写队列。
int fsync (int fd);
针对特定文件做写同步,只到被修改过的缓冲区确实被同步到设备上
才返回。
int fdatasync (int fd);
功能和fsync一样,但只同步文件数据,不同步文件属性。
应用程-fwrite->标准库缓冲-fflush->内核-sync----->磁盘
序内存-------------write------------>缓冲 fsync 设备
fdatasync
六、fcntl
#include <fcntl.h>
int fcntl (
int fd, // 文件描述符
int cmd, // 操作指令
... // 可变参数,因操作指令而异
);
对fd文件执行cmd指令,某些指令需要提供参数。
int fcntl (int fd, int cmd);
int fcntl (int fd, int cmd, long arg);
返回值因cmd而异,失败返回-1。
cmd取值:
F_DUPFD - 复制文件描述符fd为不小于arg的文件描述符
若arg文件描述符已用,
该函数会选择一个比arg大的最小的未用值,
而不是象dup2那样关闭之。
F_GETFD - 获取文件描述符标志
F_SETFD - 设置文件描述符标志
截止目前文件描述符标志只包含一个位:FD_CLOEXEC
0: 在通过exec函数所创建进程中,该文件描述符保持打开,缺省。
1: 在通过exec函数所创建进程中,该文件描述符将被关闭。
F_GETFL - 获取文件状态标志
不能获取O_CREAT/O_EXCL/O_TRUNC
F_SETFL - 追加文件状态标志
只能追加O_APPEND/O_NONBLOCK
int fd = open (...);
int flags = fcntl (fd, F_GETFL);
if (flags & O_WRONLY)
只写文件
fcntl (fd, F_SETFL, O_APPEND | O_NONBLOCK);
文件系统 - d07¶
六、fcntl
...
文件锁
int fcntl (int fd, int cmd, struct flock* lock);
其中:
struct flock {
short int l_type; // 锁的类型
// F_RDLCK/F_WRLCK/F_UNLCK
// 读锁/写锁/无锁
short int l_whence; // 偏移起点
// SEEK_SET/SEEK_CUR/SEEK_END
// 文件头/当前位置/文件尾
off_t l_start; // 锁区偏移,相对于l_whence计算
off_t l_len; // 锁区长度,0表示锁到文件尾
pid_t l_pid; // 加锁进程,-1表示自动设置
};
文件fd, 从相对于文件头10个字节处开始,对20个字节加写锁。
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 10;
lock.l_len = 20;
lock.l_pid = -1;
fcntl (fd, F_SETLK, &lock);
cmd取值:
F_GETLK - 测试锁
测试lock所表示的锁是否可加,
若可加则将lock.l_type置为F_UNLCK,
否则通过lock返回当前锁的信息
F_SETLK - 设置锁,成功返回0,失败返回-1
若因为其它进程持有锁而导致的失败,
errno = EACCESS/EAGAIN
F_SETLKW - 以阻塞模式设置锁,成功返回0,失败返回-1
若有其它进程持有锁,该函数会阻塞,直到获得锁为止
把刚才20个字节中的前5个字节解锁。
struct flock lock;
lock.l_type = F_UNLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 10;
lock.l_len = 5;
lock.l_pid = -1;
fcntl (fd, F_SETLK, &lock);
此时,另一个进程试图:
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 10;
lock.l_len = 6;
lock.l_pid = -1;
fcntl (fd, F_SETLKW, &lock);
这个进程会怎么样?阻塞...
这时第一个进程
struct flock lock;
lock.l_type = F_UNLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 15;
lock.l_len = 1;
lock.l_pid = -1;
fcntl (fd, F_SETLK, &lock);
第二个进程立即获得所要求锁,同时从fcntl中返回。
七、stat/fstat/lstat
#include <sys/stat.h>
int stat (
char const* path, // 文件路径
struct stat* buf // 文件属性
);
int fstat (
int fd, // 文件描述符
struct stat* buf // 文件属性
);
int lstat ( // 不跟踪软链接
char const* path, // 文件路径
struct stat* buf // 文件属性
);
文件:a.txt [我要吃饭]
软链接:b.txt [a.txt]
软链接:c.txt [b.txt]
stat ("b.txt", ...); // 获取a.txt的属性
stat ("c.txt", ...); // 获取a.txt的属性
lstat ("b.txt", ...); // 获取b.txt软链接文件本身的属性
lstat ("c.txt", ...); // 获取c.txt软链接文件本身的属性
成功返回0,失败返回-1。
struct stat {
dev_t st_dev; // 设备ID
ino_t st_ino; // i节点号
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 用户ID
gid_t st_gid; // 组ID
dev_t st_rdev; // 特殊设备ID
off_t st_size; // 总字节数
blksize_t st_blksize; // I/O块字节数
blkcnt_t st_blocks; // 占用块(512字节)数
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态改变时间
};
文件元数据/文件属性/文件状态
用八进制标书st_mode成员:TTSUGO
TT - 文件类型
S_IFDIR - 目录
S_IFREG - 普通文件
S_IFLNK - 软链接
S_IFBLK - 块设备
S_IFCHR - 字符设备
S_IFSOCK - UNIX域套接字
S_IFIFO - 有名管道
S - 设置与粘滞位
S_ISUID - 设置用户ID
S_ISGID - 设置组ID
S_ISVTX - 粘滞
-r--r----- ... tarena soft ...
mw
a.out suid tarena
/usr/bin/passwd:可执行程序,改口令
/etc/passwd:数据文件,存口令
执行具有S_ISUID/S_ISGID位的可执行文件所产生的进程,
其有效用户ID/有效组ID,
并不取自由其父进程(比如登录shell)所决定的,
实际用户ID/实际组ID,
而是取自该可执行文件的用户ID/组ID。
S_ISUID位对于目录而言没有任何意义。
具有S_ISGID的目录,
在该目录下所创建的文件,继承该目录的组ID,
而非其创建者进程的有效组ID。
具有S_IVTX位的可执行文件,
在其首次执行并结束后,
其代码区被连续地保存在磁盘交换区中,
因此,下次执行该程序可以获得较快的载入速度。
具有S_IVTX位的目录,
该目录下的文件或子目录,
只有其拥有者或者超级用户才能更名或删除。
U - 用户权限
S_IRUSR - 用户可读
S_IWUSR - 用户可写
S_IXUSR - 用户可执行
G - 同组权限
S_IRGRP - 同组可读
S_IWGRP - 同组可写
S_IXGRP - 同组可执行
O - 其它用户权限
S_IROTH - 其它用户可读
S_IWOTH - 其它用户可写
S_IXOTH - 其它用户可执行
文件系统 - d08¶
八、access
#include <unistd.h>
int access (
char const* pathname, // 文件路径
int mode // 访问模式
);
根据调用进程的用户ID和组ID进行指定访问模式测试。
测试通过返回0,否则返回-1。
mode取值:
R_OK - 读
W_OK - 写
X_OK - 执行
F_OK - 存在
access ("a.txt", F_OK) -> 0 : 存在,-1 : 不存在
access ("a.txt", R_OK) -> 0 : 可读,-1 : 不可读
九、umask
#include <sys/stat.h>
mode_t umask (mode_t cmask);
为调用进程设置文件权限屏蔽字,并返回以前的值。
所谓文件权限屏蔽字,就是此进程所创建的文件都不会包含屏蔽字中
的权限。
当前屏蔽字:U 0002
设置权限字:M 0666 & ~0002 = 0664
110110110 000000010
111111101
110110100
6 6 4
实际文件权限:M&~U 0664
十、chmod/fchmod
修改文件的权限。
#include <sys/stat.h>
int chmod (
char* const pathname, // 文件路径
mode_t mode // 文件权限
);
int fchmod (
int fd, // 文件描述符号
mode_t mode // 文件权限
);
成功返回0,失败返回-1。
十一、chown/fchown/lchown
修改文件的用户和组。
#include <unistd.h>
int chown (
char const* pathname, // 文件路径
uid_t owner, // 用户ID
gid_t group // 组ID
);
int fchown (
int fd, // 文件描述符
uid_t owner, // 用户ID
gid_t group // 组ID
);
int lchown ( // 不跟踪软链接
char const* pathname, // 文件路径
uid_t owner, // 用户ID
gid_t group // 组ID
);
成功返回0,失败返回-1。
某个文件a,原来是tarena(1000):soft(2000),
1) 希望改成csd1408(1001):beijing(2001)
chown ("a", 1001, 2001);
2) 希望改成csd1408(1001):soft(2000)
chown ("a", 1001, -1);
3) 希望改成tarena(1000):beijing(2001)
chown ("a", -1, 2001);
注意:只有超级用户(root)才可以任意修改文件的用户和组,
普通用户只能修改原来属于自己的文件的用户和组。
相当于执行命令:$ chown cs1408:beijing a
十二、truncate/ftruncate
从文件尾部截短或增长,增加的部分用0填充。
#include <unistd.h>
int truncate (
char const* pathname, // 文件路径
off_t length // 文件长度
);
int ftruncate (
int fd, // 文件描述符
off_t length // 文件长度
);
成功返回0,失败返回-1。
注意:对于文件映射,
私有映射(MAP_PRIVATE)
只是将虚拟内存映射到文件缓冲区而非文件中。
对于内存映射,私有映射(MAP_PRIVATE)和
公有映射(MAP_SHARED),没有任何区别。
十三、link/unlink/remove/rename
#include <unistd.h>
1.创建硬链接
int link (
char const* path1, // 文件路径
char const* path2 // 链接路径
);
成功返回0,失败返回-1。
已有文件/usr/home/tarena/a.txt,
希望为其建立硬链接/usr/home/soft/b.txt。
link ("/usr/home/tarena/a.txt",
"/usr/home/soft/b.txt");
硬链接的本质就是目录文件中的一个条目:
<文件路径> <i节点号>
a.txt <- b.txt
<- c.txt
目录文件中:
a.txt 1234
b.txt 1234
c.txt 1234
2.删除硬链接
int unlink (
char const* path // 路径
);
成功返回0,失败返回-1。
如果与一个文件相对应的所有的硬链接都被删除了,
那么该文件在磁盘上的存储区域即被释放。
unlink ("a.txt"); -> 2
unlink ("b.txt"); -> 1
unlink ("c.txt"); -> 0 -> 释放磁盘空间
如果此刻文件正在被打开,
并不会立即释放磁盘空间,而是对该文件做标记,
当该文件的最后一个描述符被关闭时,
检查此标记,若为删除状态,则释放磁盘空间。
3.删除文件及空目录
int remove (
char const* path // 路径
);
成功返回0,失败返回-1。
4.移动或更名
int rename (
char const* old, // 原路径
char const* new // 新路径
);
成功返回0,失败返回-1。
在/usr/home/tarena目录下有文件abc.txt,
希望将其改名为123.txt。
rename ("/usr/home/tarena/abc.txt",
"/usr/home/tarena/123.txt");
在/usr/home/tarena目录下有文件abc.txt,
希望将该文件移动到/usr/home/soft目录下。
rename ("/usr/home/tarena/abc.txt",
"/usr/home/soft/abc.txt");
在/usr/home/tarena目录下有文件abc.txt,
希望将该文件移动到/usr/home/soft目录下,
同时更名为123.txt。
rename ("/usr/home/tarena/abc.txt",
"/usr/home/soft/123.txt");
十四、symlink/readlink
#include <unistd.h>
1.建立符号链接(软链接)
int symlink (
char const* oldpath, // 文件路径(可以不存在)
char const* newpath // 链接路径
);
成功返回0,失败返回-1。
已有文件a.txt,建立该文件的符号链接b.txt。
symlink ("a.txt", "b.txt");
2.读取符号链接文件
ssize_t readlink (
char const* path, // 符号链接文件路径
char* buf, // 缓冲区
size_t bufsize // 缓冲区大小
);
成功返回实际拷入buf缓冲区中符号链接文件内容的字节数,
失败返回-1。
注意,该函数不追加结尾空字符。
十五、mkdir/rmdir
#include <sys/stat.h>
1.创建目录
int mkdir (
char const* path, // 目录路径
mode_t mode // 访问权限
);
成功返回0,失败返回-1。
2.删除空目录
int rmdir (
char const* path, // 目录路径
);
成功返回0,失败返回-1。
十六、chdir/fchdir/getcwd
当前目录<==>工作目录
#include <unistd.h>
1.获取工作目录
char* getcwd (
char* buf, // 缓冲区
size_t size // 缓冲区大小
);
将当前工作目录拷贝入buf缓冲区,同时返回其首地址。
失败返回NULL。
该函数会追加空字符。
#include <limits.h>
char buf[PATH_MAX+1];
getcwd (buf, sizeof (buf));
2.改变工作目录
int chdir (char const* path);
int fchdir (int fd);
成功返回0,失败返回-1。
十七、opendir/fopendir/closedir/readdir/
rewinddir/telldir/seekdir
#include <sys/types.h>
#include <dirent.h>
1.打开目录流
DIR* opendir (
char const* pathname // 目录路径
);
成功返回目录流指针,失败返回NULL。
DIR* fopendir (
int fd // 目录的文件描述符(open返回)
);
成功返回目录流指针,失败返回NULL。
2.关闭目录流
int closedir (DIR* dirp);
成功返回0,失败返回-1。
3.读取目录流
struct dirent* readdir (DIR* dirp);
struct dirent {
ino_t d_ino; // i节点号
off_t d_off; // 下一个条目在目录流中的位置
unsigned short d_reclen; // 目录条目记录长度
unsigned char d_type; // 目录/文件类型
char d_name[256]; // 目录/文件名字
};
d_type取值:
DT_DIR - 目录
DT_REG - 普通文件
DT_LNK - 符号链接
DT_BLK - 块设备文件
DT_CHR - 字符设备文件
DT_SOCK - UNIX域套接字文件
DT_FIFO - 有名管道文件
DT_UNKNOWN - 不知道是什么文件
成功返回下一个目录条目结构体的指针,
到达目录尾(不设置errno)或者失败(设置errno)返回NULL。
4.设置目录流位置
void seekdir (
DIR* dirp, // 目录流指针
long offset // 目录流位置(相对于目录头)
);
5.获取目录流位置
long telldir (DIR* dirp);
6.复位目录流
void rewinddir (DIR* dirp);
作业:根据命令行指定的路径,打印一棵目录树。
进程管理 - d09¶
第一课 编程基础
第二课 内存管理
第三课 文件系统
第四课 进程管理
第五课 信号处理
第六课 进程通信
第七课 网络通信
第八课 线程管理
第九课 线程同步
第十课 综合案例
------------------
一、基本概念¶
1.进程与程序
1)进程就是运行中的程序。一个运行着的程序,
可能有多个进程。进程在操作系统中完成特定的任务。
进程是一个内存的概念,就是内存中的一个数据块,其中包括代码和数据。
2)程序是存储在磁盘上,
包含可执行的机器指令和数据的静态实体。
进程或任务是处于活动状态的程序。
程序是一个文件的概念,其中包含了代码和数据。
2.进程的分类
1)交互进程:通过一个人机界面与用户交换信息。
2)批量进程:在无人值守的情况下,集中执行一个复杂而且耗时的任务。
3)守护进程(幽灵进程/精灵进程)
3.查看进程
1)简单形式
$ ps
以简略的格式显示当前用户拥有控制终端的进程信息。
2)BSD风格常用选项
$ ps axu
a - 所有用户有控制终端的进程
x - 包括无控制终端的进程
u - 详尽方式显示
w - 增加列宽
3)SVR4风格常用选项
$ ps -efl
-e/-A - 所用用户的进程
-a - 当前终端的进程
-u 用户名/用户ID - 特定用户的进程
-g 组名/组ID - 特定组的进程
-f - 按完整格式显示
-F - 按更完整格式显示
-l - 按长格式显示
4)进程信息列表
USER/UID:进程的实际用户ID
PID:进程ID
%CPU/C:处理器使用率
%MEM:内存使用率
VSZ:占用虚拟内存大小(KB)
RSS:占用半导体内存大小(KB)
TTY:终端机次设备号,“?”表示无控制终端
STAT/S:进程状态
O - 就绪。等待被调度。
R - 运行。正在被处理机执行。 Linux下没有O状态,
处于就绪状态的进程也用R表示。
S - 可唤醒的睡眠。系统中断、获得资源,收到信号,都可被唤醒,
转入运行状态。
D - 不可唤醒的睡眠。只能被wake_up系统调用唤醒。
T - 停止。收到SIGSTOP信号转入停止状态。
收到SIGCONT信号重新转入运行状态。
W - 等待内存分页。2.6版以后的Linux内核以取消此状态。
X - 终止。死亡。不可见。
Z - 僵尸。进程已经终止,但其父进程尚未回收该进程的状态
(尸体)。
< - 高优先级。
N - 低优先级。
L - 存在被锁入内存的分页。实时进程。
s - 会话首进程。
l - 多线程化的进程。
+ - 在前台进程组中的进程。
START/STIME:进程开始时间
COMMAND/CMD:进程启动命令
F:进程标志,1 - 通过fork产生但是没有调用exec。
4 - 拥有超级用户权限。
PPID:父进程的PID。
NI:进程nice值,-20到19,衡量进程对处理器而言的友好程度。
越小越友好。
PRI:进程实际优先级。一个进程的实际优先级由两部分组成:
静态优先级 = 80 + nice,60到99,值越小优先级越高
动态优先级,系统内核可以根据进程表现进行奖惩。
对于处理机消耗型进程,惩罚,降低优先级。
对于I/O消耗型进程,奖励,提升优先级。
ADDR:内核进程的起始地址。普通进程显示“-”。
SZ:虚拟内存页数。
WCHAN:进程正在阻塞的系统调用。
PSR:进程当前正在被哪个处理器执行。
4.父进程、子进程、孤儿进程、僵尸进程
1) 在一个进程中通过特定的系统调用可以创建另一个进程,
创建进程的进程就被成为父进程,被创建的进程被称为子进程。
进程A创建进程B,进程B创建了进程C。
A是B的父进程,B是C的父进程。
C是B的子进程,B是A的子进程。
内核进程(PID=0)
->init进程(PID=1)
->xinetd
->in.telnetd <- 远程用户登录
->login
->bash
->vi
->gcc
->a.out
->...
每个父进程可能会有1到多个子进程,
但是每个子进程只有一个父进程――树状结构。
2)父进程先于子进程结束,子进程就成为孤儿进程,
同时被init进程收养,即成为init进程的子进程。
3)子进程先于父进程结束,但是父进程没有及时回收子进程的状态(尸体),此时该子进程即为僵尸进程。
如果父进程在结束前一直都没有回收子进程的尸体,该尸体就会被init进程回收。
5.进程标识符(PID)
1)系统中的每个进程都以一个唯一的标识号,即PID。
2)任何时刻,不能存在PID相同的进程。进程一旦终止,而且没有成为僵尸,该进程的PID就可以被其它进程复用。
3)延迟重用。
pid_t getpid (void);
获取调用进程的PID。永远不会失败。
二、进程ID相关的函数:getxxxid¶
#include <unistd.h>
getpid - 获取进程标识
getppid - 获取父进程标识
getuid - 获取实际用户ID,即登录shell的用户ID
getgid - 获取实际组ID,即登录shell的组ID
geteuid - 获取有效用户ID
getegid - 获取有效组ID
进程有效用户/组ID默认情况下,取自其实际用户/组ID。
但是,如果该进程所对应的可执行文件带有设置用户/组ID位,
那么该进程的有效用户/组ID,就取自可执行文件的用户/组。
假设a.out文件的用户和组都是root,以tarena用户的身份登录系统,
并执行a.out程序,其运行结果:
实际用户ID:1000
实际组ID:1000
有效用户ID:1000
有效组ID:1000
以root修改a.out文件的权限:
# chmod u+s a.out
# chmod g+s a.out
# ls -l a.out
-rwsr-sr-x 1 root root ... a.out
再次以tarena用户的身份登录系统,并执行a.out程序,其运行结果:
实际用户ID:1000
实际组ID:1000
有效用户ID:0
有效组ID:0
三、fork¶
#include <unistd.h>
pid_t fork (void);
1.创建一个子进程,失败返回-1。
2.调用一次,返回两次。
分别在父子进程中返回子进程的PID和0。
利用该函数返回值的不同,分别父子进程编写处理代码。
3.子进程是父进程的副本,
子进程获得父进程的数据段和堆栈段(包括I/O流缓冲区)的拷贝,
但是父子进程共享同一个代码段。
4.fork函数被调用以后父子进程各自独立地运行,
其被执行的先后顺序无法确定。
某些UNIX实现,会先调度子进程。
5.fork函数被调用以后父进程的文件描述符表,
也会被复制一份给子进程,二者同一个文件表。
6.如果当前线程数已经达到系统的最大线程数,
或者当前用户的进程数已经达到该用户的最大进程数,
那么fork函数会失败。
$ cat /proc/sys/kernel/threads-max
$ ulimit -u
ioctl
进程管理 - d10¶
回顾:
进程控制
在Unix/Linux系统中,查看进程的命令:
ps - 只能查看当前终端启动的进程
ps -aux Linux专用的,Unix系统不直接支持
ps -ef 通用的
某些Unix系统有两个ps命令,可以用whereis 查看一下。
kill -9 进程的PID 杀死对应的进程
其实就是发送9信号给对应进程
如何创建子进程?
fork() - 通过复制 父进程创建子进程
vfork() + execl() - 不复制父进程的任何东西,创建子进程,子进程会执行完全独立的代码(与父进程无关)
fork() 会复制除了代码区之外的所有内存区域,代码区和父进程共享。如果父进程有文件描述符,子进程会复制描述符,但不复制文件表(父子进程使用相同的文件表)。子进程也会复制父进程的输入输出缓冲区。
今天:
wait()和waitpid() 让父进程等待子进程的结束进程结束的方式
vfork()和execl()
信号
进程的结束方式
正常结束
1 在main()函数中,执行了return语句(没有写的,系统会在最后自动加上return语句,返回内存中的任意值)
2 执行exit(int)函数(任意函数中都可以),exit()中的参数是退出状态码,一般是用非负数代表功能已经完成,用负数代表功能没有完成
3 _exit() 或 _Exit() 也能退出进程
4 最后一个线程正常结束
非正常结束
1 被信号终止
2 最后一个线程被其他线程取消
exit() 、 _Exit()和_exit()的区别
1 _Exit()和_exit()几乎没有区别,_Exit()是标C函数,另外一个是Unix C函数。
2 _Exit()是立即退出,不会有多余的时间。
3 exit()不是立即退出,甚至可以执行另外一个函数后再退出。另外的函数可以用atexit()注册。
函数wait()和waitpid()可以让父进程等待子进程的结束,从而取到子进程的退出状态码(exit()或return)。
wait()函数等待任意一个子进程的结束,没法指定等待哪个子进程;也必须以阻塞的方式等待。
waitpid()更加灵活,可以指定等待哪个/哪些 子进程,甚至可以设置以 非阻塞的方式等待(可以不等)。
pid_t waitpid(pid_t pid , int* status, int options)
函数功能:让父进程等待子进程的结束,并取得退出码。
参数: pid就是父进程等待哪个/哪些 子进程
>0 - 等待 进程ID 等于 pid的那个子进程
-1 - 等待任意一个子进程
0 - 等待本组子进程(和父进程同一个进程组的)
<-1 - 等待进程组id=-pid的子进程
status 可以取得结束子进程的退出情况和退出状态码,需要借助宏函数实现数据的筛选(WIFEXITED() 和 WEXITSTATUS())
options 可以设置是否等待,0 代表等待,WNOHANG 不等待。
返回: 大于0 就是 结束子进程的PID
0 就是WNOHANG时 没有子进程结束直接返回 0
-1 代表出错。
注: wait()和waitpid() 都可以回收僵尸子进程的资源。(殓尸工)
vfork() - 从语法上说,vfork()和fork() 没有任何的区别。区别在于vfork() 不会复制 父进程的任何资源,子进程会直接占用父进程的资源,
父进程被阻塞(暂停运行), 只有在以下两种情况下父进程才会解除阻塞继续运行:
1 子进程运行结束,把资源还给父进程,父进程继续运行。
2 子进程执行了 exec 系列函数(比如execl()),也会把资源还给父进程,子进程将加载 execl() 所指定的进程资源。
其中,情况1 是没有现实意义。
fork() 之后 父子进程 谁先运行不一定,vfork() 之后一定是子进程先运行(父进程没资源)。
注: vfork()创建的子进程,如果没有execl()或execl()失败,一定要用exit() 函数 显式退出。否则可能导致出错或者死循环。
vfork()+execl() 创建和执行子进程,组合方式如下:
vfork() 负责启动新的子进程,但不负责提供子进程的代码和数据。
execl() 无法启动新进程,但可以提供新的代码和数据 供当前进程更新。
int execl(char* path, char* cmd, ...)
函数功能: 启动一个全新的程序,替换当前的进程
参数: path 就是 新程序在硬盘对应的文件路径
cmd 就是 执行程序的命令
... 可以包括 选项、参数,以NULL 作为 结束。
execl("/bin/ls","ls","-l",NULL) 调用系统的ls
返回: 成功就 执行全新的代码,因此 无返回。失败返回 -1 。
失败最可能的情况就是 第一个参数写错了。
练习: vfork()+execl()的用法
用vfork()+execl()方式创建子进程,其中子进程实现的功能是:
在 execl()之前打印子进程 PID,然后在子进程中启动新的程序,新的程序功能 也是打印 当前进程的PID (这两个PID应该是一样的)。
提示:需要写两个程序,连接成两个不同的名字。
重点在于 如何创建子进程,fork() 和 vfork()+execl() 。
信号(signal)
在Unix系统中,信号是最常见的 软件中断方式之一。大多数的 软件中断都借助信号实现。
中断 是中止当前正在执行的程序,转而执行其他的代码。中断分为硬件中断和软件中断,一般可控的 都是采用软件中断。有些硬件的故障也会转换成软件中断进行处理,一般都是信号。
信号 本质是非负整数,Unix系统 信号是 0-48,Linux系统是0-64,中间不保证连续。其中,信号0 有特殊的用途,没有 实际的意义。真正使用时,信号 从1 开始。
命令 kill 就是用来处理信号的命令,kill 可以给进程发信号。
$ kill -l 可以查看 都有哪些信号。
每个信号除了值,都还有一个宏名称,比如:信号2 叫SIGINT,
所有的宏名称都以 SIG 开头。信号分为两类,可靠信号和不可靠信号。
不可靠信号 就是 1-31 ,特点是 不支持排队,因此多次发动时可能丢失。
可靠信号 就是 34-64 ,特点是 支持排队,因此不会丢失。
使用信号编程时,用宏名称有更好的通用性。
信号的处理方式 (默认、忽略和自定义) - 重点
1 当信号到来时,如果不做任何的设置,将采用默认处理。默认处理一般 就是退出进程。
2 忽略处理,当该信号没有来过,不做任何的处理。
3 自定义处理,程序员按照自己的方式,写函数处理信号。(必须会)
注: 不是所有的信号都能忽略和自定义的,信号9 只能采用默认处理
每个用户只能给自己的进程发信号,其他用户的进程无权限发送信号。但root 用户可以给所有的用户进程发信号。
Unix系统/Linux系统提供了设置信号处理方式的函数:
signal() - 功能简单,但实用主讲
sigaction() - 功能复杂,有些功能用不到
void (*fa) (int) signal(int signum, void (*fa)(int) )
函数功能:指定某个信号的处理方式
参数: signum 就是 哪个信号,一般用宏名称
第二个参数是一个函数指针,支持三个值:
1 SIG_IGN 处理方式为 忽略
2 SIG_DFL 处理方式为 默认
3 自定义一个函数,把函数名放在这里 (自定义处理)
返回: 正常会返回之前的信号处理方式,失败返回 SIG_ERR。
进程通信 - d11¶
回顾:¶
进程的退出方式 - 主函数执行完毕,遇到return语句
执行了 exit() 或 _Exit() 函数
用信号强行退出进程
wait()和waitpid()函数 - 让父进程等待子进程结束,区别在于wait()等待任意一个子进程的结束;waitpid()可以选择等待的子进程,也可以选择不等待,直接返回。
vfork()+execl() 方式创建进程,子进程不会复制父进程的任何资源,而是直接使用父进程的资源,导致父进程阻塞,直到调用execl()函数为止。换而言之:execl()会让父子进程并行。
信号 - Unix系统的软件中断的方式。自定义信号处理的代码结构:
#include <signal.h> //信号专用的头文件
void fa(int signo){ .... } //信号处理函数
int main(){
...;
signal(SIGXXX,fa); //设置信号处理方式
...;
}
今天:
信号 - 父子进程的信号处理方式继承性、信号发送函数、
信号集和信号屏蔽、信号的相关函数(sleep()/usleep())、计时器
IPC - 进程间通信(两个或多个进程间的数据交互)
父子进程之间的信号处理方式:
fork()创建的子进程对于信号的处理方式 完全继承 父进程的信号处理方式(与父进程的信号处理方式一样)。
vfork()+execl()创建的子进程,部分继承父进程的信号处理方式。父进程是默认或忽略的,子进程会继承下来;
父进程是自定义处理函数,子进程会改为默认处理(因为子进程中没有 父进程处理函数的代码的)。
fork()之前的代码只有父进程执行一次,fork()之后的代码 父子进程分别执行一次(执行两次)。
信号的发送方式
1 键盘发送信号(部分)
ctrl+c -> 信号2
ctrl+\ -> 信号3
2 硬件故障发送信号(部分)
内存没有映射 -> 段错误
硬盘上的文件映射出错 -> 总线错误
3 kill 命令发送信号 (全部)
kill -信号值 进程PID
4 信号发送函数(程序员的方式)
raise() alarm() kill() sigqueue() ...
讲 kill()和alarm(),严格来说,alarm()不算一个信号发送函数
int kill(pid_t pid,int signo)
// 函数功能: 给进程发送信号
// 参数: pid 代表某个或某些进程,使用方式和waitpid()一样
// >0 代表某个进程
// -1 代表给所有 有发送权限的进程发送信号
// 0 代表给本组进程发送信号
// <-1 代表给进程组ID是-pid的进程发送信号
// signo 就是发送信号的值
// 成功返回0,失败返回 -1 。
// 注: 可以用 0 信号 测试 是否有发送信号的权限,而不会有任何附带的问题。
/*
sleep() 和 usleep()
功能都是休眠,区别在于 sleep() 以秒作为单位,usleep()以微秒为单位。
sleep()的结束可能是时间到了,也可能是被 未忽略的信号打断,此时将返回休眠的剩余秒数。
/*
信号的产生和到来时间都是无法控制的。因此在执行一些关键代码时,信号有可能带来负面影响。
解决方案:使用信号屏蔽技术。信号屏蔽 不是 阻止 信号的到来,而是 暂时忽略信号的处理,直到解除信号屏蔽后再 处理信号。
信号屏蔽 需要使用 信号集,信号集 就是 信号的集合。不是所有信号都可以被屏蔽,比如信号 9 。
long long int -> 64位整数
信号集 可以看出一个 超大的整数。类型是:sigset_t 。一个数据结构包括:逻辑结构、物理结构、运算结构。通常的运算结构包括:
创建、销毁、增加、删除、修改、查询,其他功能。其中,修改不是必须的,先删除后增加 也可以实现修改的效果。
信号集提供了以下功能函数:
sigaddset() - 增加信号
sigdelset() - 删除信号
sigfillset() - 增加所有信号
sigemptyset() - 删除所有信号
sigismember() - 查询某个信号是否存在
在信号集中,倒数第n位代表 信号n,权重是 2的n-1次方。
信号屏蔽 由 sigprocmask() 实现,解除信号屏蔽也使用这个函数。
int sigprocmask(int how,sigset_t* new,sigset_t* old)
// 函数功能: 屏蔽信号 或者 解除信号屏蔽
// 参数: how 是屏蔽的算法,包括三个值,实际上使用SIG_SETMASK即可。就是 直接使用 new作为新的屏蔽
// new 新的信号屏蔽集
// old 屏蔽之前的 信号屏蔽集 ,用于 解除屏蔽,如果不需要考虑解除屏蔽,old 用 NULL 即可。
// 返回 0 代表成功,-1 代表失败。
sigaction() 是 signal()的增强版,拥有更多的功能。
计时器 - 一段时间以后开始运行某个功能,每隔一段时间再次运行
相对于alarm(),计时器精确到 微秒。(纯了解内容)
计时器的原理就是 一段时间后 每隔一段时间 产生一个信号,然后在信号处理时执行 相应的代码。计时器包括:真实、虚拟和实用三种,一般使用的是 真实计时器。
计时器函数: setitimer()设置计时器 、getitimer()获取当前计时器
setitimer(ITIMER_REAL,&timer,0);
进程间通信 - IPC¶
常见的IPC包括:
文件
信号 (signal)
管道
共享内存
消息队列
信号量集(semaphore arrays)
网络编程socket
........
其中,共享内存、消息队列和信号量集 遵循相同的规范,因此统称为XSI IPC。
IPC 的原理都是一样的,都是通过 媒介进行数据交互,这个媒介基本上是 内存或者硬盘上的文件。
管道( pipe )
管道的交互媒介是 一种特殊的文件,叫 管道文件。
管道 分为 有名管道和无名管道,无名管道 只能用于 fork()创建的父子进程之间IPC,有名管道没有限制。
命令 touch 、函数open() 等 都不能创建出管道文件,管道文件的创建 必须使用 mkfifo命令 或 mkfifo() 函数。但 读写管道文件的函数和 普通文件 一样。
如果只对管道文件进行读操作 或者 写操作,会 卡住(阻塞),数据无法交互。必须 同时 具备 读操作和写操作 ,数据 才能正常传输,传输完成后,文件中 不会存留 数据。
管道是最古老的IPC方式之一,但 目前较少使用。
mkfifo b 创建管道文件
ls > b
另外一个终端 : cat b
练习&作业:
用管道文件实现IPC,从pipea.c 发送100个 int 到 pipeb.c。和读写普通文件的方式一样。
example¶
/*
* /alarm.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void fa(int signo){
printf("时间到了,该起床了\n");
//exit(0);
alarm(1);
}
int main(){
signal(SIGALRM,fa);
alarm(3);//3秒后发送信号SIGALRM
//alarm(0);//取消闹钟
while(1);
}
/*
* fork.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void fa(int signo){
printf("捕获了信号%d\n",signo);
}
int main(){
signal(SIGINT,fa);
signal(SIGQUIT,SIG_IGN);
//printf("begin\n");
pid_t pid = fork();
//printf("end\n");
if(pid==0){
//signal(SIGINT,SIG_DFL);
printf("cpid=%d\n",getpid());
while(1);
}
printf("father over\n");
}//练习: 测试vfork()+execl() 信号继承效果
/*
* itimer.c
*/
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/time.h>
void fa(int signo){
printf("I am superman!\n");
}
int main(){
signal(SIGALRM,fa);
struct itimerval timer;
timer.it_interval.tv_sec = 1;//间隔秒数
timer.it_interval.tv_usec = 100000;//微秒数
timer.it_value.tv_sec = 5;//开始秒数
timer.it_value.tv_usec = 0;
setitimer(ITIMER_REAL,&timer,0);
while(1);
}
/*
* kill.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void fa(int signo){
printf("捕获了信号%d\n",signo);
}
int main(){
pid_t pid = fork();
if(pid == 0){
signal(SIGINT,fa);
sleep(10);
printf("child over\n");
exit(0);
}
sleep(1);
kill(pid,SIGINT);
printf("father over\n");
}
/*
* proc.c
*/
int main(){
while(1);
}
/*
* sigprocmask.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void fa(int signo){
printf("捕获了信号%d\n",signo);
}
int main(){
printf("pid=%d\n",getpid());
signal(SIGINT,fa); signal(50,fa);
printf("执行普通代码,没有屏蔽信号\n");
sleep(15);
printf("执行关键代码,开始屏蔽信号\n");
sigset_t set,old; sigemptyset(&set);
sigaddset(&set,2);sigaddset(&set,50);
int res=sigprocmask(SIG_SETMASK,&set,&old);
if(res==-1) perror("sigmask"),exit(-1);
sleep(15);
/*sigset_t pend;
sigpending(&pend);
if(sigismember(&pend,2))
printf("信号2来过\n");*/
printf("关键代码结束,解除信号屏蔽\n");
sigprocmask(SIG_SETMASK,&old,NULL);
sleep(15);
}
/*
* sigset.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(){
sigset_t set;
printf("size=%d\n",sizeof(set));
printf("%d\n",set);//有警告
sigemptyset(&set);printf("%d\n",set);//0
sigaddset(&set,2);printf("%d\n",set);//+2
sigaddset(&set,3);printf("%d\n",set);//+4
sigaddset(&set,7);printf("%d\n",set);//+64
sigdelset(&set,3);printf("%d\n",set);
if(sigismember(&set,2) == 1)
printf("信号2存在\n");
else printf("信号2不存在\n");
return 0;
}
/*
* sleep.c
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void fa(int signo){
printf("捕获了信号%d\n",signo);
}
int main(){
signal(SIGINT,fa);
printf("开始休眠\n");
int res = sleep(10);
if(res == 0) printf("睡的好\n");
else printf("被打断了,剩余%d秒\n",res);
usleep(10000);//0.01秒
printf("很快\n");
}
/*
* vfork.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void fa(int signo){
printf("捕获了信号%d\n",signo);
}
int main(){
signal(SIGINT,fa);
signal(SIGQUIT,SIG_IGN);
pid_t pid = vfork();
if(pid==0){
printf("cpid=%d\n",getpid());
execl("./proc","proc",NULL);
}
printf("father over\n");
}
all:
gcc -m32 -o alarm alarm.c
gcc -m32 -o fork fork.c
gcc -m32 -o itimer itimer.c
gcc -m32 -o kill kill.c
gcc -m32 -o proc proc.c
gcc -m32 -o sigprocmask sigprocmask.c
gcc -m32 -o sigset sigset.c
gcc -m32 -o sleep sleep.c
gcc -m32 -o vfork vfork.c
clean:
rm -rf alarm \
fork \
itimer \
kill \
proc \
sigprocmask \
sigset \
sleep \
vfork \
进程通信 - d12¶
回顾:¶
信号 -
父子进程之间 信号的继承关系:
fork()创建的子进程 信号处理方式 和 父进程完全一样。
vfork()+execl()创建的子进程 信号处理方式 继承 默认和忽略,自定义处理函数的会改为默认。
信号的发送函数 - kill()
信号集和信号屏蔽
sigset_t : sigaddset()/sigdelset()/sigfillset()/sigemptyset()/sigismember()
sigprocmask()
IPC - 管道(有名管道)
创建管道文件必须使用 mkfifo()函数或者mkfifo命令,touch命令或者open()函数 无法创建管道文件。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
//int fd=open("aa",O_RDWR);//权限不能是读写
int fd = open("aa",O_WRONLY);
if(fd == -1) perror("open"),exit(-1);
int i;
for(i=0;i<100;i++){
write(fd,&i,sizeof(i));
usleep(100000);//休眠0.1秒
}
close(fd);
printf("write over\n");
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("aa",O_RDONLY);
if(fd == -1) perror("open"),exit(-1);
while(1){
int i;
int res = read(fd,&i,sizeof(i));
if(res == 0) break;
if(res == -1){ perror("read"); break; }
printf("i=%d\n",i);
}
close(fd);
}
今天:
XSI IPC - 共享内存、消息队列(重点)¶
XSI IPC是遵循相同规范的进程间通信,包括:共享内存、消息队列、信号量集。
共享内存的媒介就是一块系统管理的物理内存,允许多个进程挂接(映射),实现进程间通信,使用完毕后脱接(解除映射)。
共享内存是最快的IPC方式。
消息队列的媒介就是 系统管理的内存中的一个队列,进程可以把数据放入队列,或者从队列中取出数据。
消息队列是一个比较快的IPC方式。
XSI IPC都有固定的使用套路,记住步骤即可。
XSI IPC的共同规范:
1 设计套路是一样的,首先都需要一个外部的key,然后用外部的key创建/获取内部ID,
这个内部的ID 就可以代表 内核管理的IPC 结构(共享内存、消息队列、信号量集的统称)。
2 外部的key有三种获取方式:
2.1 宏 IPC_PRIVATE 做key,但这种方式基本不被使用。因为IPC_PRIVATE 禁止其他进程使用。(知道即可,用不上)
2.2 写一个通用的头文件,把所有的key定义在这个头文件。因为这个key 其实就是一个整数,类型是 key_t。(集中管理)
2.3 使用 ftok()函数 生成 外部的key。(单独生成)
3 用key创建/获取内部ID时,函数名都是: xxxget(),比如:
共享内存 shmget()
消息队列 msgget()
4 使用 xxxget()创建IPC结构时,都有一个flag参数,其值都是:
权限|宏,比如: 0666|IPC_CREAT
5 都有一个xxxctl()函数,提供查询、修改和删除功能。
比如: shmctl() msgctl(),函数中利用宏参数实现功能。
IPC_STAT : 查询IPC结构
IPC_SET : 修改IPC结构
IPC_RMID : 删除IPC结构(按ID)
共享内存的使用步骤:¶
- 1 使用ftok()或者从头文件中 获得 key 。
- 2 使用key 创建/获取共享内存,函数是 shmget()。
- 3 使用shmat()函数 挂接(映射) 共享内存。
- 4 使用共享内存中的数据。
- 5 使用shmdt()函数 脱接(解除映射) 共享内存。
- 6 如果确保不再使用,可以用 shmctl(IPC_RMID) 删除。
系统现有的XSI IPC结构 可以用命令操作:
# ipcs 查看IPC结构
ipcs -a #查看所有IPC结构
ipcs -m #查看共享内存
ipcs -q #查看消息队列
ipcs -s #查看信号量集
#ipcrm 删除IPC结构(按ID删除)
ipcrm -m 内部ID # 删除共享内存
ipcrm -q 内部ID # 删除消息队列
ipcrm -s 内部ID # 删除信号量集
# 注: 共享内存的删除必须保证没有进程挂接,否则只做删除标志而不真正删除,挂接数为0 时 在删除(延后删除)。
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key = ftok(".",100);//确保文件存在 1
int shmid = shmget(key,4,IPC_CREAT|0666);//2
if(shmid == -1) perror("shmget"),exit(-1);
int* pi = shmat(shmid,0,0);//3
*pi = 100;//4
shmdt(pi);//5
}//练习:写shmb.c,把100从共享内存读出来并打印
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key = ftok(".",100);//确保文件存在 1
int shmid = shmget(key,0,0);//2
if(shmid == -1) perror("shmget"),exit(-1);
int* pi = shmat(shmid,0,0);//3
printf("*pi=%d\n",*pi);;//4
shmdt(pi);//5
}//练习:写shmb.c,把100从共享内存读出来并打印
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key = ftok(".",100);
int shmid = shmget(key,0,0);
if(shmid == -1) perror("shmget"),exit(-1);
struct shmid_ds ds;
shmctl(shmid,IPC_STAT,&ds);//查询
printf("size=%d\n",ds.shm_segsz);//大小
printf("mode=%o\n",ds.shm_perm.mode);//权限
printf("key=%x\n",ds.shm_perm.__key);
ds.shm_segsz = 400;//修改,大小不能修改
ds.shm_perm.mode = 0644;//能修改
shmctl(shmid,IPC_SET,&ds);
//shmctl(shmid,IPC_RMID,0);//删除
}
共享内存的优缺点:
优点是 最快的IPC,效率最高。缺点是 如果多个进程写,会产生互相覆盖的问题,数据 没法保证。
消息队列能很好的解决这个问题。
消息队列 其实就是系统 管理了一个队列,每个进程都可以获取这个队列,然后把数据封入消息(结构体)中,再把消息放入队列中。
消息队列的使用步骤:¶
- 1 使用 ftok() 或 从 头文件中 获取 外部的key。
- 2 使用 msgget() 创建/获取 消息队列。
- 3 使用msgsnd()/ msgrcv() 发送/接收 消息到队列中。
- 4 如果确保消息队列不再使用,使用msgctl(IPC_RMID)删除。
msgsnd()和msgrcv() 可以发送或者接收消息。
消息分为: 有类型消息和无类型消息,
无类型消息可以是任意的类型,数据本身就是消息。
有类型消息必须是一个结构,格式如下:
struct 消息名{ //消息名可以随便起,是合法标识符
long mtype; //第一个必须是消息类型,其值必须大于0
char buf[]; //数据区,类型一般是数组 或者 结构体,名字随意
};
int msgsnd(int msgid,void* buf,size_t size,int flag)
函数功能:向消息队列中 放入消息。
参数: msgid 就是消息队列的内部ID;
buf 就是消息,可以是有类型的,也可以是无类型的;
size 是数据的大小,
对于无类型消息,就是数据的大小,
而对于有类型消息,不是消息结构体的大小,而是数据区的大小(不算数据类型);
flag 为0 就是满了会阻塞,为IPC_NOWAIT 就是满了直接返回-1 不阻塞。
返回值: 0 代表成功,-1 代表失败。
int msgrcv(int msgid,void* buf,size_t size,long mtype,int flag)
函数功能:从消息队列中 按类型 取出消息
参数: msgid 就是消息队列的内部ID;
buf就是用于接收消息的首地址,可以是有类型消息,也可以是无类型消息;
size 是数据的大小,
对于无类型消息,就是数据的大小,
而对于有类型消息,不是消息结构体的大小,而是数据区的大小(不算数据类型);
mtype代表接收消息的类型,其值可以有三种:
> 0 就取类型为mtype的消息。
==0 取任意类型的消息( 什么类型都可以,先入先出)
< 0 取类型小于等于 mtype绝对值的消息,从小到大取。
flag和上面一样;
返回:失败返回-1 ,成功返回 实际接收到的字节数。
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main()
{
key_t key = ftok(".",100);
int msgid = msgget(key,0666|IPC_CREAT);
if(msgid == -1) perror("msgget"),exit(-1);
int res = msgsnd(msgid,"hello",5,0);//IPC_NOWAIT满了不会等待
if(res == -1) {
perror("msgsnd");
} else {
printf("send ok\n");
}
}//练习:写msgb.c,从队列中读出hello并打印
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main()
{
key_t key = ftok(".",100);
int msgid = msgget(key,0);
if(msgid == -1) perror("msgget"),exit(-1);
char buf[50] = { };
int res = msgrcv( msgid, buf, sizeof(buf), 0,
IPC_NOWAIT);//IPC_NOWAIT空了不会等待
if(res == -1) {
perror("msgsnd");
}else {
printf("%s\n",buf);
}
}
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msg{ //名字程序员随便起
long mtype;//固定的,消息类型
char name[100];//数据区
};
int main()
{
key_t key = ftok(".",100);
int msgid = msgget(key,0666|IPC_CREAT);
if(msgid == -1) perror("msgget"),exit(-1);
struct msg msg1,msg2;
msg1.mtype = 1;strcpy(msg1.name,"zhangfei");
msgsnd(msgid,&msg1,sizeof(msg1.name),0);
msg2.mtype = 2;strcpy(msg2.name,"guanyu");
msgsnd(msgid,&msg2,sizeof(msg2.name),0);
}//练习:写msgtypeb.c,用消息类型取出所有的关羽
//并打印
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msg{ //名字程序员随便起
long mtype;//固定的,消息类型
char name[100];//数据区
};
int main()
{
key_t key = ftok(".",100);
int msgid = msgget(key,0);
if(msgid == -1) perror("msgget"),exit(-1);
struct msg msg1;
int res = msgrcv(msgid,&msg1,
sizeof(msg1.name),2,0);
if(res==-1) perror("msgrcv"),exit(-1);
printf("res=%d,name=%s\n",res,msg1.name);
}
all:
gcc -m32 -o msga msga.c
gcc -m32 -o msgb msgb.c
gcc -m32 -o msgtypea msgtypea.c
gcc -m32 -o msgtypeb msgtypeb.c
gcc -m32 -o pipea pipea.c
gcc -m32 -o pipeb pipeb.c
gcc -m32 -o shma shma.c
gcc -m32 -o shmb shmb.c
gcc -m32 -o shmctl shmctl.c
clean:
rm -rf msga \
msgb \
msgtypea \
msgtypeb \
pipea \
pipeb \
shma \
shmb \
shmctl
商业公司做项目的流程:
1 需求分析 - 弄清用户要做什么软件,功能有哪些。
2 系统分析和系统设计 - 搭建软件的结构和模块,具体如何实现。
3 编码 - 程序员编程把设计实现。
4 测试 - 程序员进行单元测试,测试工程师进行其他测试。
5 安装和维护 - 把软件安装配置到用户服务器上,并且做日常维护和调整,包括功能增加和调整。
综合案例:
涉及的知识点:文件读写、文件操作、进程管理、信号处理、消息队列。
模拟银行ATM机(了解一下银行业务)
ATM 提供6个功能:
开户、销户、存钱、取钱、查询余额、转账
明天做一个开户就可以,基础好的全做。
开户的流程:输入账户信息
(卡号ID,名字name,密码passwd,金额money),然后把这些信息写入文件中,如果没有出错,则开户成功,否则开户失败。
项目设计:
至少要写两个程序,客户端和服务器端。先启动服务器端,服务器端先创建2个消息队列,分别是服务器发给客户端 和 客户端发给服务器的,然后服务器就 等待接收 来自客户端的消息。
接下来客户端启动,发送消息给服务器(通过消息队列)。消息采用有类型消息,共8种类型:开户、销户、存钱、取钱、查询、转账、操作成功、操作失败。消息的数据 就写一个 账户结构体:
struct Account{
int id;
char name[40];
char passwd[40];
double money;
};
服务器收到消息后进行处理,把处理结果通过消息队列返回给客户端
开户时客户端输入name、passwd(两次)、money,然后封入消息中传递到服务器,服务器负责生成 无重复的id,把这个结构写入文件中。根据写入的结果返回给客户端。
注意问题:
1 服务器读取来自客户端的消息时,应该是死循环,用ctrl+c(信号)退出。退出时应该删除消息队列。
2 如何生成无重复的ID?ID要从文件中读取,自增后做新的ID,然后再把新ID 写回到文件中。
3 把账户结构体写入文件中,每个账户生成一个文件。
进程通信 - d13¶
- 1.简单进程间通信:命令行参数、环境变量、信号、文件
- 2.传统进程间通信:管道(pipe/mkfifo)
- 3.现代进程间通信:共享内存、消息队列、信号量
- 4.网络进程间通信:套接字
练习:本地银行(ATM)¶
#ifndef _BANK_H
#define _BANK_H
#include <sys/types.h>
#define KEY_REQUEST 0x12345678 // 请求队列
#define KEY_RESPOND 0x87654321 // 响应队列
#define TYPE_OPEN 8001 // 开户
#define TYPE_CLOSE 8002 // 清户
#define TYPE_SAVE 8003 // 存款
#define TYPE_WITHDRAW 8004 // 取款
#define TYPE_QUERY 8005 // 查询
#define TYPE_TRANSFER 8006 // 转账
// 账户
typedef struct tag_Account {
int id; // 账号
char name[256]; // 户名
char passwd[9]; // 密码
double balance; // 余额
} ACCOUNT;
// 开户请求
typedef struct tag_OpenRequest {
long type; // 类型
pid_t pid; // 进程ID
char name[256]; // 户名
char passwd[9]; // 密码
double balance; // 余额
} OPEN_REQUEST;
// 开户响应
typedef struct tag_OpenRespond {
long type; // 类型
char error[512]; // 错误
int id; // 账号
} OPEN_RESPOND;
// 查询请求
typedef struct tag_QueryRequest {
long type; // 类型
pid_t pid; // 进程ID
int id; // 账号
char name[256]; // 户名
char passwd[9]; // 密码
} QUERY_REQUEST;
// 查询响应
typedef struct tag_QueryRespond {
long type; // 类型
char error[512]; // 错误
double balance; // 余额
} QUERY_RESPOND;
// ...
#endif // _BANK_H
#ifndef _DAO_H
#define _DAO_H
#include "../inc/bank.h"
int dao_unique (void);
int dao_insert (ACCOUNT const* acc);
int dao_select (int id, ACCOUNT* acc);
int dao_update (ACCOUNT const* acc);
int dao_delete (int id);
#endif // _DAO_H
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <fcntl.h>
#include "../inc/dao.h"
char const* ID_FILE = "../db/id.dat";
int dao_unique (void) {
int id = 1000;
int fd = open (ID_FILE, O_RDWR | O_CREAT,
0644);
if (fd == -1) {
perror ("open");
return -1;
}
if (read (fd, &id, sizeof (id)) == -1) {
perror ("read");
return -1;
}
++id;
if (lseek (fd, 0, SEEK_SET) == -1) {
perror ("lseek");
return -1;
}
if (write (fd, &id, sizeof (id)) == -1) {
perror ("write");
return -1;
}
close (fd);
return id;
}
int dao_insert (ACCOUNT const* acc) {
char pathname[PATH_MAX+1];
sprintf (pathname, "../db/%d.acc",
acc->id);
int fd = creat (pathname, 0644);
if (fd == -1) {
perror ("creat");
return -1;
}
if (write (fd, acc, sizeof (*acc)) == -1) {
perror ("write");
return -1;
}
close (fd);
return 0;
}
int dao_select (int id, ACCOUNT* acc) {
char pathname[PATH_MAX+1];
sprintf (pathname, "../db/%d.acc", id);
int fd = open (pathname, O_RDONLY);
if (fd == -1) {
perror ("open");
return -1;
}
if (read (fd, acc, sizeof (*acc)) == -1) {
perror ("read");
return -1;
}
close (fd);
return 0;
}
int dao_update (ACCOUNT const* acc) {
char pathname[PATH_MAX+1];
sprintf (pathname, "../db/%d.acc",
acc->id);
int fd = open (pathname, O_WRONLY);
if (fd == -1) {
perror ("open");
return -1;
}
if (write (fd, acc, sizeof (*acc)) == -1) {
perror ("write");
return -1;
}
close (fd);
return 0;
}
int dao_delete (int id) {
char pathname[PATH_MAX+1];
sprintf (pathname, "../db/%d.acc", id);
if (unlink (pathname) == -1) {
perror ("unlink");
return -1;
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/msg.h>
#include "../inc/bank.h"
static int g_reqid = -1; // 请求队列
static int g_resid = -1; // 响应队列
void menu_loop (int (*menu) (void),
int (*on_menu[]) (void), size_t size) {
for (;;) {
int id = menu ();
if (id < 0 || size <= id)
printf ("无效选择!\n");
else if (on_menu[id] () == -1)
break;
}
}
int main_menu (void) {
printf ("--------\n");
printf ("本地银行\n");
printf ("--------\n");
printf ("[1] 开户\n");
printf ("[2] 清户\n");
printf ("[3] 存款\n");
printf ("[4] 取款\n");
printf ("[5] 查询\n");
printf ("[6] 转账\n");
printf ("[0] 退出\n");
printf ("--------\n");
printf ("请选择:");
int id = -1;
scanf ("%d", &id);
scanf ("%*[^\n]");
scanf ("%*c");
return id;
}
int on_quit (void) {
printf ("谢谢使用,再见!\n");
return -1;
}
int on_open (void) {
pid_t pid = getpid ();
OPEN_REQUEST req = {TYPE_OPEN, pid};
printf ("户名:");
scanf ("%s", req.name);
printf ("密码:");
scanf ("%s", req.passwd);
printf ("金额:");
scanf ("%lf", &req.balance);
if (msgsnd (g_reqid, &req, sizeof (req) -
sizeof (req.type), 0) == -1) {
perror ("msgsnd");
return 0;
}
OPEN_RESPOND res;
if (msgrcv (g_resid, &res, sizeof (res) -
sizeof (res.type), pid, 0) == -1) {
perror ("msgrcv");
return 0;
}
if (strlen (res.error)) {
printf ("%s\n", res.error);
return 0;
}
printf ("账号:%d\n", res.id);
return 0;
}
int on_close (void) {
printf ("此业务暂未开通。\n");
return 0;
}
int on_save (void) {
printf ("此业务暂未开通。\n");
return 0;
}
int on_withdraw (void) {
printf ("此业务暂未开通。\n");
return 0;
}
int on_query (void) {
pid_t pid = getpid ();
QUERY_REQUEST req = {TYPE_QUERY, pid};
printf ("账号:");
scanf ("%d", &req.id);
printf ("户名:");
scanf ("%s", req.name);
printf ("密码:");
scanf ("%s", req.passwd);
if (msgsnd (g_reqid, &req, sizeof (req) -
sizeof (req.type), 0) == -1) {
perror ("msgsnd");
return 0;
}
QUERY_RESPOND res;
if (msgrcv (g_resid, &res, sizeof (res) -
sizeof (res.type), pid, 0) == -1) {
perror ("msgrcv");
return 0;
}
if (strlen (res.error)) {
printf ("%s\n", res.error);
return 0;
}
printf ("余额:%.2lf\n", res.balance);
return 0;
}
int on_transfer (void) {
printf ("此业务暂未开通。\n");
return 0;
}
int main (void) {
if ((g_reqid = msgget (KEY_REQUEST,
0)) == -1) {
perror ("msgget");
return -1;
}
if ((g_resid = msgget (KEY_RESPOND,
0)) == -1) {
perror ("msgget");
return -1;
}
int (*on_menu[]) (void) = {
on_quit,
on_open,
on_close,
on_save,
on_withdraw,
on_query,
on_transfer};
menu_loop (main_menu, on_menu,
sizeof (on_menu) / sizeof (on_menu[0]));
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/msg.h>
#include "../inc/bank.h"
#include "../inc/dao.h"
void sigint (int signum) {
printf ("开户服务:即将停止。\n");
exit (0);
}
int main (void) {
if (signal (SIGINT, sigint) == SIG_ERR) {
perror ("signal");
return -1;
}
int reqid = msgget (KEY_REQUEST, 0);
if (reqid == -1) {
perror ("msgget");
return -1;
}
int resid = msgget (KEY_RESPOND, 0);
if (resid == -1) {
perror ("msgget");
return -1;
}
printf ("开户服务:启动就绪。\n");
for (;;) {
OPEN_REQUEST req;
if (msgrcv (reqid, &req, sizeof (req) -
sizeof (req.type), TYPE_OPEN, 0) == -1){
perror ("msgrcv");
return -1;
}
OPEN_RESPOND res = {req.pid, ""};
ACCOUNT acc;
if ((acc.id = dao_unique ()) == -1) {
sprintf (res.error, "创立账户失败!");
goto send_respond;
}
strcpy (acc.name, req.name);
strcpy (acc.passwd, req.passwd);
acc.balance = req.balance;
if (dao_insert (&acc) == -1) {
sprintf (res.error, "保存账户失败!");
goto send_respond;
}
res.id = acc.id;
send_respond:
if (msgsnd (resid, &res, sizeof (res) -
sizeof (res.type), 0) == -1) {
perror ("msgsnd");
continue;
}
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/msg.h>
#include "../inc/bank.h"
#include "../inc/dao.h"
void sigint (int signum) {
printf ("查询服务:即将停止。\n");
exit (0);
}
int main (void) {
if (signal (SIGINT, sigint) == SIG_ERR) {
perror ("signal");
return -1;
}
int reqid = msgget (KEY_REQUEST, 0);
if (reqid == -1) {
perror ("msgget");
return -1;
}
int resid = msgget (KEY_RESPOND, 0);
if (resid == -1) {
perror ("msgget");
return -1;
}
printf ("查询服务:启动就绪。\n");
for (;;) {
QUERY_REQUEST req;
if (msgrcv (reqid, &req, sizeof (req) -
sizeof (req.type), TYPE_QUERY, 0) ==-1){
perror ("msgrcv");
return -1;
}
QUERY_RESPOND res = {req.pid, ""};
ACCOUNT acc;
if (dao_select (req.id, &acc) == -1) {
sprintf (res.error, "无效账号!");
goto send_respond;
}
if (strcmp (req.name, acc.name)) {
sprintf (res.error, "无效户名!");
goto send_respond;
}
if (strcmp (req.passwd, acc.passwd)) {
sprintf (res.error, "密码错误!");
goto send_respond;
}
res.balance = acc.balance;
send_respond:
if (msgsnd (resid, &res, sizeof (res) -
sizeof (res.type), 0) == -1) {
perror ("msgsnd");
continue;
}
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <sys/msg.h>
#include <wait.h>
#include "../inc/bank.h"
static int g_reqid = -1; // 请求队列
static int g_resid = -1; // 响应队列
typedef struct tag_Service {
char srv_path[PATH_MAX+1]; // 服务路径
pid_t srv_pid; // 服务进程ID
} SERVICE;
static SERVICE g_srv[] = {
{"./open", -1},
{"./query", -1}
// ...
};
int init (void) {
printf ("服务器初始化...\n");
if ((g_reqid = msgget (KEY_REQUEST,
0600 | IPC_CREAT | IPC_EXCL)) == -1) {
perror ("msgget");
return -1;
}
printf ("创建请求消息队列成功!\n");
if ((g_resid = msgget (KEY_RESPOND,
0600 | IPC_CREAT | IPC_EXCL)) == -1) {
perror ("msgget");
return -1;
}
printf ("创建响应消息队列成功!\n");
return 0;
}
void deinit (void) {
printf ("服务器终结化...\n");
if (msgctl (g_reqid, IPC_RMID, NULL) == -1)
perror ("msgctl");
else
printf ("销毁请求消息队列成功!\n");
if (msgctl (g_resid, IPC_RMID, NULL) == -1)
perror ("msgctl");
else
printf ("销毁响应消息队列成功!\n");
}
void start (void) {
printf ("启动业务服务...\n");
size_t i;
for (i = 0; i < sizeof (g_srv) /
sizeof (g_srv[0]); ++i) {
g_srv[i].srv_pid = vfork ();
if (g_srv[i].srv_pid == -1) {
perror ("vfork");
continue;
}
if (g_srv[i].srv_pid == 0)
if (execl (g_srv[i].srv_path,
g_srv[i].srv_path, NULL) == -1) {
perror ("execl");
_exit (EXIT_FAILURE);
}
}
}
void stop (void) {
printf ("停止业务服务...\n");
size_t i;
for (i = 0; i < sizeof (g_srv) /
sizeof (g_srv[0]); ++i) {
if (g_srv[i].srv_pid == -1)
continue;
if (kill (g_srv[i].srv_pid, SIGINT) == -1)
perror ("kill");
}
for (;;)
if (wait (0) == -1) {
if (errno == ECHILD)
break;
perror ("wait");
}
}
int main (void) {
atexit (deinit);
if (init () == -1)
return -1;
start ();
sleep (1);
printf ("按<回车>退出...\n");
getchar ();
stop ();
return 0;
}
一、信号量¶
1.基本特点¶
1)资源计数器,用于限制多个进程对有限共享资源的访问。
2)多个进程获取有限共享资源的操作模式
A.测试控制该资源的信号量;
B.若信号量大于0,则进程可以使用该资源,
为了表示此进程已获得该资源,需将信号量减1;
C.若信号量等于0,则进程休眠等待该资源,
直到信号量大于0,进程被唤醒,执行步骤A;
D.当某进程不再使用该资源时,信号量增1,
正在休眠中等待该资源的进程将被唤醒。
2.常用函数¶
#include <sys/sem.h>
/* 1)创建/获取信号量(集合) */
int semget (key_t key, int nsems, int semflg);
// A.该函数 以key参数为键值创建一个信号量集合
// (nsems参数表示该集合中信号量的个数),
// 或获取已有信号量集合(nsems取0)。
// B.semflg取值
// 0 - 获取,不存在即失败。
// IPC_CREAT - 创建,不存在即创建,
// 已存在即获取,除非...
// IPC_EXCL - 排斥,已存在即失败。
// C.成功返回信号量集合的标识,失败返回-1。
/* 2)操作信号量 */
int semop (int semid, struct sembuf* sops, unsigned int nsops);
struct sembuf {
unsigned short sem_num; // 信号量下标
short sem_op; // 操作数
short sem_flg; // 操作标记
};
// A.该函数对semid参数所标识的信号量集合中,
// 由sops参数所指向的包含nsops个元素的,
// 结构体数组中的每个元素,依次执行如下操作:
//
// a)若sem_op大于0,
// 则将sem_op加到信号量集合中第sem_num个信号量的计数值上,
// 以表示对资源的释放。
// b)若sem_op小于0,
// 则从信号量集合中第sem_num个信号量的计数值中减去其绝对值,
// 以表示对资源的获取。
// c)若信号量集合中第sem_num个信号量的计数值不够减(信号量的计数值不能为负),
// 此函数会阻塞,直到够减为止,以表示对资源的等待。
// d)若sem_flg包含IPC_NOWAIT位,即使被操作的信号量不够减,也不会阻塞,
// 而是返回-1,同时将errno置为EAGAIN。
// 以允许进程在等待资源的同时,完成其它任务。
// 5本三国演义,4本水浒传,3本西游记
int semid = semget (key, 3, IPC_CREAT | IPC_EXEL);
// 将所创建信号量集合中的3个元素依次初始化为:5、4和3
// ...
// 某人欲借2本三国演义和1本西游记
// struct sembuf sops[2];
// sops[0].sem_num = 0;
// sops[0].sem_op = -2;
// sops[1].sem_num = 2;
// sops[1].sem_op = -1;
// semop (semid, sops, 2);
// 还1本三国演义同时借2本水浒传
// struct sembuf sops[2];
// sops[0].sem_num = 0;
// sops[0].sem_op = 1;
// sops[1].sem_num = 1;
// sops[1].sem_op = -2;
// semop (semid, sops, 2);
/* 3)销毁/控制信号量 */
int semctl (int semid, int semnum, int cmd);
int semctl (int semid, int semnum, int cmd, union semun arg);
union semun {
int val; // cmd取SETVAL
struct semid_ds* buf; // cmd取IPC_STAT/IPC_SET
unsigned short* array; // cmd取GETALL/SETALL
struct seminfo* __buf; // cmd取IPC_INFO
};
// A.cmd取值
// IPC_STAT - 获取信号量集合的属性,通过arg.buf输出
// union semun arg;
// struct semid_ds buf;
// arg.buf = &buf;
semctl (semid, 0, IPC_STAT, arg);
struct semid_ds {
struct ipc_perm sem_perm; // 权限信息
time_t sem_otime; // 最后操作时间
time_t sem_ctime; // 最后改变时间
unsigned shrot sem_nsems; // 信号量个数
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 用户ID
gid_t gid; // 组ID
uid_t cuid; // 创建者用户ID
gid_t cgid; // 创建者组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
// IPC_SET - 设置信号量集合的属性,通过arg.buf输入。
// 只有以下三个属性可以设置:
// semid_ds::sem_perm.uid; // 用户ID
// semid_ds::sem_perm.gid; // 组ID
// semid_ds::sem_perm.mode; // 权限字
// IPC_RMID - 删除信号量集合。
semctl (semid, 0, IPC_RMID);
// 此时所有阻塞在该信号量集合上的semop函数都会立即返回失败,
// errno为EIDRM。
// GETALL - 获取信号量集合中所有信号量计数值。
short array[3];
union semun arg;
arg.array = array;
semctl (semid, 0, GETALL, arg);
// SETALL - 设置信号量集合中所有信号量计数值。
short array[3] = {5, 4, 3};
union semun arg;
arg.array = array;
semctl (semid, 0, SETALL, arg);
// GETVAL - 获取一个信号量的计数值。
int shuihuzhuan = semctl (semid, 1, GETVAL);
// SETVAL - 设置一个信号量的计数值。
union semun arg;
arg.val = 6;
semctl (semid, 1, SETVAL, arg);
// 注意:只有针对信号量集合中某个具体信号量的操作,
// 才会使用semnum参数。凡是针对信号量集合整体的操作,
// 该参数会被忽略。
// 成功返回值因cmd而异,失败返回-1。
网络通信 - d14¶
一、基本概念¶
1.协议模型(OSI模型)¶
应用层 - 应用程序
表示层 - 数据的组织
会话层 - 数据的交换
传输层 - 数据的封包
网络层 - 网络的拓扑结构
数据链路层 - 数字信号的电子化
物理层 - 网络设备
2.TCP/IP协议族¶
- 1)TCP(Transmission Control Protocol),传输控制协议
面向连接的协议。
逻辑上模拟为电话业务。
- 2)UDP(User Datagram Protocol),用户数据报文协议
面向无连接的协议。
逻辑上模拟为邮政业务。
- 3)IP(Internet Protocol),互联网协议
工作在TCP和UDP协议的底层。
实现在互联网上传递信息的基本机制。
ISO TCP/IP
应用层
表示层 应用层 telnet/ftp/http
会话层
------------------------
传输层 传输层 TCP/UDP
网络层 互联网层 IP
------------------------
数据链路层 网络接口层 硬件/驱动
物理层
3.消息流和协议栈¶
有协议模型的不同层次构成了一个自下而上的协议栈, 所发送数据的数据沿着协议栈自上而下层层打包, 最终变成可以在物理介质上传输的电子信号。 接收数据时再将物理介质上的电子信号,沿着协议栈自下向上, 层层解包,最终变成应用可以处理的数据内容。
4.IP地址¶
- 1)IP地址是Internet中唯一标识一台计算机的地址。
A.IPv4:32位,IPv6:128位
B.点分十进制字符串表示:0x01020304 -> 1.2.3.4
- 2)IP地址分级
A级:0XXXXXXX | XXXXXXXX XXXXXXXX XXXXXXXX
网络地址 本机地址
B级:10XXXXXX XXXXXXXX | XXXXXXXX XXXXXXXX
网络地址 本机地址
C级:110XXXXX XXXXXXXX XXXXXXXX | XXXXXXXX
网络地址 本机地址
172.40.0.10
D级:1110XXXX XXXXXXXX XXXXXXXX XXXXXXXX
多播地址
- 3)子网掩码
IP地址 & 子网掩码 = 网络地址
IP地址 & ~子网掩码 = 本机地址
IP地址:192.168.182.48
子网掩码:255.255.255.0
网络地址:192.168.182.0
本机地址:0.0.0.48
二、套接字(Socket)¶
1.接口¶
QQ \
LeapFTP > socket -> TCP/UDP -> IP -> 网络驱动 -> 网络硬件
IE /
2.异构¶
Java@UNIX@大型机<->socket<->C@Windows@PC机
3.模式¶
- 1)点对点(Peer-to-Peer,P2P),一对一通信。
- 2)客户机/服务器(Client/Server,C/S),一对多通信。
- 3)浏览器/服务器(Browser/Server,B/S),一个服务器为多个浏览器提供服务。
4.绑定¶
将逻辑的套接字对象与物理的通信载体关联起来。
5.函数¶
1)创建套接字
#include <sys/socket.h>
int socket (int domain, int type, int protocol);
- domain - 域
AF_UNIX/AF_LOCAL/AF_FILE,本地通信
AF_INET,基于IPv4的网络通信, 对于BSD是AF_INET,对于POSIX是PF_INET. 两者本质一样。
AF_INET6,基于IPv6的网络通信
AF_PACKET,基于IP的底层协议进行网络通信
- type - 类型
SOCKET_STREAM,流式套接字,基于TCP协议通信
SOCKET_DGRAM,数据报套接字,基于UDP协议通信
- protocal - 特殊协议,目前置0即可
成功返回套接字描述符,失败返回-1。
套接字描述符类似于文件描述符,UNIX把网络当成文件看待。
发送数据即写文件,接收数据即读文件,一切皆文件。
- 2)通信地址
A.基本地址结构
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址值
};
B.网络地址结构
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; /* 地址族,AF_INET/AF_INET6 */
in_port_t sin_port; /* unsigned short,网络字节序的端口号
逻辑上表示一个参与通信的进程,
0-1024,公知端口,WWW-80
FTP-21,TELNET-23,...
自己编写的应用程序使用1024以上的端口号 */
struct in_addr sin_addr; /* 网络字节序的IP地址 */
};
小低低:小端字节序,低位低地址
int i = 0x12345678;
小端机器:低地址-------->高地址
78 56 34 12 -> 0x78563412
大端机器:低地址-------->高地址
12 34 56 78
主机字节序->网络字节序->发送
接收->网络字节序->主机字节序
网络字节序就是大端字节序
欲从小端机器发送0x12345678:L 78 56 34 12 H
现将其转换为网络字节序:L 12 34 56 78 H
发送到大端机器上:L 12 34 56 78 H
从网络字节序转到大端字节序:L 12 34 56 78 H -> 0x12345678
struct in_addr {
in_addr_t s_addr;
};
typedef uint32_t in_addr_t;
typedef unsigned int uint32_t;
- 3)将套接字和通信地址绑定
int bind (int sockfd, const string sockaddr* addr, socklen_t addrlen);
// 成功返回0,失败返回-1。
- 4)将套接字和对方通信地址连接
int connect (int sockfd, const string sockaddr* addr, socklen_t addrlen);
// 成功返回0,失败返回-1。
- 5)用读写文件的方式通信:read/write
- 6)关闭套接字:close
- 7)字节序转换
#include <arpa/inet.h>
// 主机字节序->网络字节序
uint32_t htonl (uint32_t hostlong);
uint16_t htons (uint16_t hostshort);
// 网络字节序->主机字节序
uint32_t ntohl (uint32_t netlong);
uint16_t ntohs (uint16_t netshort);
- 8)IP地址转换
//点分十进制字符串->网络字节序整数形式的IP地址
in_addr_t inet_addr (const char* cp);
//网络字节序整数形式的IP地址->点分十进制字符串
char* inet_ntoa (struct in_addr in);
6.编程¶
服务器:创建套接字 -> 准备地址结构并绑定 -> 接收数据 -> 关闭套接字
客户机:创建套接字 -> 准备地址结构并连接 -> 发送数据 -> 关闭套接字
三、基于TCP协议的客户机/服务器模型¶
1.TCP协议的基本特征¶
- 1)面向连接:虚拟电路,建立可靠的连接,稳定地传输数据,人为断开连接。
- 2)传输可靠:发送数据、等待确认、丢包重传。
ABCDEF
A-> -+
B-> |
C-> | 时间窗口
D-> |
E-> <-A OK -+
F-> <-B OK
<-C OK
<-D OK
<-E OK
<-F OK
每个发送都有应答,若在一定的时间窗口内没有收到应答,
重新再发。
- 3)保证顺序:有序发送,有序接收,丢弃重复。
- 4)流量控制:接收端实时通知发送端接收窗口大小,防止溢出。
- 5)传输速度慢。
2.编程模型¶
服务器 客户机
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
创建套接字 socket 创建套接字 socket
准备地址 sockaddr_in 准备地址 sockaddr_in
绑定地址 bind
监听套接字 listen
接受连接 accept 建立连接 connect
接收数据 read/recv 发送数据 write/send
发送数据 write/send 接收数据 read/recv
关闭套接字 close 关闭套接字 close
3.常用函数¶
#include <sys/socket.h>
int listen (
int sockfd, // 套接字描述符
int backlog // 未决连接请求队列的最大长度,1024
);
//成功返回0,失败返回-1。
int accept (int sockfd, struct sockaddr* addr, socklen_t* addrlen);
/* 从sockfd参数所标识套接字的未决连接请求队列中, 提取第一个连接请求,同时创建一个新的套接字,
用于在该连接中通信,返回该套接字描述符。
同时通过后两个参数向调用者输出客户机的地址信息。
失败返回-1。
*/
ssize_t recv (int sockfd, void* buf, size_t len, int flags);
// 返回实际接收到的字节数。
// 如果在该函数执行过程中,对方关闭了连接,返回0。
// 失败返回-1。
ssize_t send (int sockfd, void const* buf, size_t len, int flags);
//返回kk实际发送的字节数。失败返回-1。
example¶
- UDP
/*
* file : netcli.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main (int argc, char* argv[]) {
if (argc < 3) {
printf ("用法:%s <IP地址> <端口号>\n",
argv[0]);
return -1;
}
printf ("客户机:创建套接字...\n");
int sockfd = socket (AF_INET, SOCK_DGRAM,
0);
if (sockfd == -1) {
perror ("socket");
return -1;
}
printf ("客户机:准备地址并连接...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[2]));
addr.sin_addr.s_addr = inet_addr (argv[1]);
if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
perror ("connect");
return -1;
}
printf ("客户机:发送数据...\n");
for (;;) {
printf ("> ");
char buf[1024];
gets (buf);
if (! strcmp (buf, "!"))
break;
if (write (sockfd, buf, strlen (buf) + 1) == -1) {
perror ("write");
return -1;
}
if (! strcmp (buf, "!!"))
break;
}
printf ("客户机:关闭套接字...\n");
close (sockfd);
return 0;
}
/*
* filename : netsvr.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main (int argc, char* argv[]) {
if (argc < 2) {
printf ("用法:%s <端口号>\n", argv[0]);
return -1;
}
printf ("服务器:创建套接字...\n");
int sockfd = socket (AF_INET, SOCK_DGRAM,0);
if (sockfd == -1) {
perror ("socket");
return -1;
}
printf ("服务器:准备地址并绑定...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[1]));
addr.sin_addr.s_addr = INADDR_ANY;
if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
perror ("bind");
return -1;
}
printf ("服务器:接收数据...\n");
for (;;) {
char buf[1024];
ssize_t rb = read (sockfd, buf, sizeof (buf));
if (rb == -1) {
perror ("read");
return -1;
}
if (! strcmp (buf, "!!"))
break;
printf ("< %s\n", buf);
}
printf ("服务器:关闭套接字...\n");
close (sockfd);
return 0;
}
- TCP
/*
* filename : tcpsvr.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main (int argc, char* argv[]) {
if (argc < 2) {
printf ("用法:%s <端口号>\n", argv[0]);
return -1;
}
printf ("服务器:创建套接字...\n");
int sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror ("socket");
return -1;
}
printf ("服务器:准备地址并绑定...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[1]));
addr.sin_addr.s_addr = INADDR_ANY;
if (bind (sockfd, (struct sockaddr*)&addr,
sizeof (addr)) == -1) {
perror ("bind");
return -1;
}
printf ("服务器:监听套接字...\n");
if (listen (sockfd, 1024) == -1) {
perror ("listen");
return -1;
}
printf ("服务器:等待连接请求...\n");
struct sockaddr_in addrcli = {};
socklen_t addrlen = sizeof (addrcli);
int connfd = accept (sockfd,
(struct sockaddr*)&addrcli, &addrlen);
printf ("服务器:接受来自%s:%u客户机的"
"连接请求。\n",
inet_ntoa (addrcli.sin_addr),
ntohs (addrcli.sin_port));
printf ("服务器:收发数据...\n");
for (;;) {
char buf[1024];
ssize_t rb = recv (connfd, buf,
sizeof (buf), 0);
if (rb == -1) {
perror ("recv");
return -1;
}
if (rb == 0) {
printf ("服务器:客户机已关闭连接。\n");
break;
}
if (send (connfd, buf, rb, 0) == -1) {
perror ("send");
return -1;
}
}
printf ("服务器:关闭套接字...\n");
close (connfd);
close (sockfd);
return 0;
}
- 信号量 sem
/*
* filename : csem.c
*/
#include <stdio.h>
#include <errno.h>
#include <sys/sem.h>
int pmenu (void) {
printf ("------------------\n");
printf (" 迷你图书馆\n");
printf ("------------------\n");
printf ("[1] 借《三国演义》\n");
printf ("[2] 还《三国演义》\n");
printf ("[3] 借《水浒传》\n");
printf ("[4] 还《水浒传》\n");
printf ("[5] 借《红楼梦》\n");
printf ("[6] 还《红楼梦》\n");
printf ("[7] 借《西游记》\n");
printf ("[8] 还《西游记》\n");
printf ("[0] 退出\n");
printf ("------------------\n");
printf ("请选择:");
int sel = -1;
scanf ("%d", &sel);
return sel;
}
int pleft (int semid, unsigned short semnum){
int val = semctl (semid, semnum, GETVAL);
if (val == -1) {
perror ("semctl");
return -1;
}
printf ("还剩%d册。\n", val);
return 0;
}
int borrow (int semid, unsigned short semnum){
struct sembuf sop = {semnum, -1,
/*0*/IPC_NOWAIT};
if (semop (semid, &sop, 1) == -1) {
if (errno != EAGAIN) {
perror ("semop");
return -1;
}
printf ("暂时无书,下回再试。\n");
return 0;
}
printf ("借阅成功。\n");
return pleft (semid, semnum);
}
int revert (int semid, unsigned short semnum){
struct sembuf sop = {semnum, 1, 0};
if (semop (semid, &sop, 1) == -1) {
perror ("semop");
return -1;
}
printf ("归还成功。\n");
return pleft (semid, semnum);
}
int main (void) {
printf ("创建信号量...\n");
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
int semid = semget (key, 4, 0644 |
IPC_CREAT | IPC_EXCL);
if (semid == -1) {
perror ("semget");
return -1;
}
printf ("初始化信号量...\n");
unsigned short semarr[] = {5, 5, 5, 5};
if (semctl (semid, 0, SETALL,
semarr) == -1) {
perror ("semctl");
return -1;
}
int quit = 0;
while (! quit) {
int sel = pmenu ();
switch (sel) {
case 0:
quit = 1;
break;
case 1:
case 3:
case 5:
case 7:
if (borrow (semid, sel / 2) == -1)
return -1;
break;
case 2:
case 4:
case 6:
case 8:
if (revert (semid, (sel-1)/2) == -1)
return -1;
break;
default:
printf ("无效选择!\n");
scanf ("%*[^\n]");
scanf ("%*c");
break;
}
}
printf ("销毁信号量...\n");
if (semctl (semid, 0, IPC_RMID) == -1) {
perror ("semctl");
return -1;
}
printf ("再见!\n");
return 0;
}
/*
* gsem.c
*/
#include <stdio.h>
#include <errno.h>
#include <sys/sem.h>
int pmenu (void) {
printf ("------------------\n");
printf (" 迷你图书馆\n");
printf ("------------------\n");
printf ("[1] 借《三国演义》\n");
printf ("[2] 还《三国演义》\n");
printf ("[3] 借《水浒传》\n");
printf ("[4] 还《水浒传》\n");
printf ("[5] 借《红楼梦》\n");
printf ("[6] 还《红楼梦》\n");
printf ("[7] 借《西游记》\n");
printf ("[8] 还《西游记》\n");
printf ("[0] 退出\n");
printf ("------------------\n");
printf ("请选择:");
int sel = -1;
scanf ("%d", &sel);
return sel;
}
int pleft (int semid, unsigned short semnum){
int val = semctl (semid, semnum, GETVAL);
if (val == -1) {
perror ("semctl");
return -1;
}
printf ("还剩%d册。\n", val);
return 0;
}
int borrow (int semid, unsigned short semnum){
struct sembuf sop = {semnum, -1,
/*0*/IPC_NOWAIT};
if (semop (semid, &sop, 1) == -1) {
if (errno != EAGAIN) {
perror ("semop");
return -1;
}
printf ("暂时无书,下回再试。\n");
return 0;
}
printf ("借阅成功。\n");
return pleft (semid, semnum);
}
int revert (int semid, unsigned short semnum){
struct sembuf sop = {semnum, 1, 0};
if (semop (semid, &sop, 1) == -1) {
perror ("semop");
return -1;
}
printf ("归还成功。\n");
return pleft (semid, semnum);
}
int main (void) {
printf ("创建信号量...\n");
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
int semid = semget (key, 0, 0);
if (semid == -1) {
perror ("semget");
return -1;
}
int quit = 0;
while (! quit) {
int sel = pmenu ();
switch (sel) {
case 0:
quit = 1;
break;
case 1:
case 3:
case 5:
case 7:
if (borrow (semid, sel / 2) == -1)
return -1;
break;
case 2:
case 4:
case 6:
case 8:
if (revert (semid, (sel-1)/2) == -1)
return -1;
break;
default:
printf ("无效选择!\n");
scanf ("%*[^\n]");
scanf ("%*c");
break;
}
}
printf ("再见!\n");
return 0;
}
线程管理 - d15¶
一、基本概念¶
- 1.线程就是进程中代码的执行路线,即进程内部的控制序列。
程序――磁盘文件,纸面上的旅行日程表
进程――内存数据,头脑中的旅行计划
线程――执行路径,按照计划旅行
- 2.线程只是一个执行路径,并不拥有独立的内存资源,
共享进程的代码区、数据区、堆区、命令行参数和环境变量。
- 3.线程拥有自己独立的栈,因此用户自己独立的局部变量。
- 4.一个进程可以同时拥有多个线程,但是至少有一个主线程。
二、POSIX线程¶
IEEE POSIX 1003.1c指定了POSIX线程标准。
// pthread
#include <pthread.h>
# libpthread.so
gcc ... -lpthread
三、POSIX线程库的功能¶
- 1.线程管理:创建、销毁、分离、汇合、获取或者设置属性。
- 2.线程同步:解决并发冲突问题。
四、线程函数¶
1.创建(工作)线程¶
int pthread_create (
pthread_t* tid, // 线程ID
const pthread_attr_t* attr, // 线程属性,置NULL表示缺省属性
void* (*start_routine) (void*), // 线程过程函数
void* arg // 传递给线程过程函数的参数
);
/* 内核:开辟一个新的线程,在该线程中调用
start_routine (arg);
*/
// 成功返回0,失败返回错误码。
2.汇合线程¶
int pthread_join (
pthread_t tid, // 所要汇合的线程ID
void** retval // 线程过程函数的返回值
);
// 线程版的waitpid。
// 该函数会一直阻塞,直到tid参数所标识的线程结束(线程过程函数返回),成功返回0,失败返回错误码。
// 如果对线程过程函数的返回值不感兴趣,retval可以置NULL。
3.获取线程自身的ID¶
pthread_t pthread_self (void);
//成功返回调用线程的ID,不会失败。
4.比较线程ID¶
int pthread_equal (pthread_t t1, pthread_t t2);
// t1和t2相等返回非0,否则返回0。
5.终止线程(线程自杀)¶
void pthread_exit (void* retval);
// 线程版的exit。
/*
void fun2 (void) {
if (我实在受不了了)
exit (-1);
}
void fun1 (void) {
fun2 ();
}
int main (void) {
fun1 ();
return 0;
}
*/
6.分离线程¶
默认情况下的线程都是可汇合线程,即可以通过pthread_join汇合的线程。 这样的线程即便终止,系统仍然会为其保留部分资源(其中包括线程过程函数的返回值),直到调用pthread_join函数,这部分资源才会被回收。
调用如下函数:
pthread_detach (pthread_t tid);
// 将tid参数所标识的线程置为分离线程。这样的线程不可被pthread_join汇合,线程终止以后其全部资源被系统自动回收。
7.取消线程(线程他杀)¶
int pthread_cancel (pthread_t tid);
// 成功返回0,失败返回错误码。
// 设置调用线程的取消状态:
int pthread_setcancelstate (int state, int* oldstate);
/*
state取值:
PTHREAD_CANCEL_ENABLE - 可以取消(默认)
PTHREAD_CANCEL_DISABLE - 禁止取消
*/
// 设置调用线程的取消类型:
int pthread_setcanceltype (int type, int* oldtype);
/*
type取值:
PTHREAD_CANCEL_DEFERRED - 延迟取消(默认)
PTHREAD_CANCEL_ASYNCHRONOUS - 立即取消
*/
网络通信补遗¶
#include <sys/socket.h>
ssize_t recvfrom (int sockfd,
void* buf,
size_t len,
int flags,
struct sockaddr* addr,
socklen_t* addrlen);
// 输入数addr和addrlen表示发送方的地址信息。
ssize_t sendto ( int sockfd,
void* buf,
size_t len,
int flags,
struct sockaddr const* addr,
socklen_t addrlen);
// 输入参数addr和addrlen表示接收方的地址信息。
example¶
/*
* create.c
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread_proc (void* arg) {
for (;;) {
printf ("%c", (char)arg);
usleep (100000);
}
return NULL;
}
int main (void) {
setbuf (stdout, NULL);
pthread_t tid;
int error = pthread_create (&tid, NULL, thread_proc, (void*)'A');
if (error) {
printf ("pthread_create: %s\n", strerror (error));
return -1;
}
printf ("线程ID:%lu\n", tid);
error = pthread_create (&tid, NULL, thread_proc, (void*)'B');
if (error) {
printf ("pthread_create: %s\n", strerror (error));
return -1;
}
printf ("线程ID:%lu\n", tid);
thread_proc ((void*)'C');
//getchar ();
return 0;
}
/*
* join.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#define PI 3.1415926
double g_s;
void* area (void* arg) {
double r = *(double*)arg;
/*
static double s;
s = PI * r * r;
return &s;
*//*
double* s = malloc (sizeof (double));
*s = PI * r * r;
return s;
*/
g_s = PI * r * r;
return NULL;
}
int main (void) {
double r = 10;
pthread_t tid;
pthread_create (&tid, NULL, area, &r);
/*
double* s;
pthread_join (tid, (void**)&s); // *s = ...
printf ("%g\n", *s);
free (s);
*/
//sleep (1);
pthread_join (tid, NULL);
printf ("%g\n", g_s);
return 0;
}
/*
* exit.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#define PI 3.1415926
void* area (void* arg) {
double r = *(double*)arg;
double* s = malloc (sizeof (double));
*s = 2 * PI * r;
pthread_exit (s);
*s = PI * r * r;
return s;
}
int main (void) {
//pthread_exit (NULL);
double r = 10;
pthread_t tid;
pthread_create (&tid, NULL, area, &r);
double* s;
pthread_join (tid, (void**)&s); // *s = ...
printf ("%g\n", *s);
free (s);
return 0;
}
/*
* equal.c
*/
#include <stdio.h>
#include <string.h>
#include <pthread.h>
pthread_t g_main;
void* ismain (void* arg) {
if (pthread_equal (pthread_self (), g_main)){
printf ("这是主线程。\n");
}
else{
printf ("这是子线程。\n");
}
return NULL;
}
int main (void)
{
g_main = pthread_self ();
ismain (NULL);
pthread_t tid;
pthread_create (&tid, NULL, ismain, NULL);
pthread_join (tid, NULL);
return 0;
}
/*
* detach.c
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread_proc (void* arg) {
//pthread_detach (pthread_self ());
int i;
for (i = 0; i < 200; ++i) {
putchar ('-');
usleep (50000);
}
return NULL;
}
int main (void) {
setbuf (stdout, NULL);
pthread_t tid;
pthread_create (&tid, NULL, thread_proc, NULL);
pthread_detach (tid);
int error = pthread_join (tid, NULL);
if (error){
printf ("pthread_join: %s\n", strerror (error));
}
int i;
for (i = 0; i < 200; ++i) {
putchar ('+');
usleep (50000);
}
printf ("\n");
return 0;
}
/*
* cancle.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void elapse (void) {
size_t i;
for (i = 0; i < 800000000; ++i);
}
void* thread_proc (void* arg) {
/*
pthread_setcancelstate ( PTHREAD_CANCEL_DISABLE, NULL);
*/
pthread_setcanceltype ( PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
for (;;) {
printf ("我是快乐的子线程!\n");
elapse ();
}
return NULL;
}
int main (void) {
setbuf (stdout, NULL);
pthread_t tid;
pthread_create (&tid, NULL, thread_proc, NULL);
getchar ();
int error = pthread_cancel (tid);
if (error){
printf ("pthread_cancel: %s\n", strerror (error));
}
pthread_join (tid, NULL);
printf ("子线程终止!\n");
return 0;
}
/*
* udpcli.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main (int argc, char* argv[]) {
if (argc < 3) {
printf ("用法:%s <IP地址> <端口号>\n", argv[0]);
return -1;
}
printf ("客户机:创建套接字...\n");
int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror ("socket");
return -1;
}
printf ("客户机:准备地址并连接...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[2]));
addr.sin_addr.s_addr = inet_addr (argv[1]);
/*
if (connect (sockfd,
(struct sockaddr*)&addr,
sizeof (addr)) == -1) {
perror ("connect");
return -1;
}
*/
printf ("客户机:发送数据...\n");
for (;;) {
printf ("> ");
char buf[1024];
gets (buf);
if (! strcmp (buf, "!"))
break;
if (sendto (sockfd, buf,
strlen (buf) + 1, 0,
(struct sockaddr*)&addr,
sizeof (addr)) == -1) {
perror ("sendto");
return -1;
}
if (! strcmp (buf, "!!"))
break;
}
printf ("客户机:关闭套接字...\n");
close (sockfd);
return 0;
}
/*
* tcpcli.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main (int argc, char* argv[])
{
if (argc < 3) {
printf ("用法:%s <IP地址> <端口号>\n", argv[0]);
return 0;
}
printf ("客户机:创建套接字...\n");
int sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror ("socket");
return -1;
}
printf ("客户机:准备地址并连接...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[2]));
addr.sin_addr.s_addr = inet_addr (argv[1]);
if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
perror ("connect");
return -1;
}
printf ("客户机:收发数据...\n");
for (;;) {
printf ("> ");
char buf[1024];
gets (buf);
if (! strcmp (buf, "!")) break;
if (send (sockfd, buf, strlen (buf) + 1, 0) == -1) {
perror ("send");
return -1;
}
memset (buf, 0, sizeof (buf));
ssize_t rb = recv (sockfd, buf, sizeof (buf), 0);
if (rb == -1) {
perror ("recv");
return -1;
}
if (rb == 0) {
printf ("客户机:服务器已宕机!\n");
break;
}
printf ("< %s\n", buf);
}
printf ("客户机:关闭套接字...\n");
close (sockfd);
return 0;
}
/*
* tcpcli.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main (int argc, char* argv[])
{
if (argc < 3) {
printf ("用法:%s <IP地址> <端口号>\n", argv[0]);
return 0;
}
printf ("客户机:创建套接字...\n");
int sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror ("socket");
return -1;
}
printf ("客户机:准备地址并连接...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[2]));
addr.sin_addr.s_addr = inet_addr (argv[1]);
if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
perror ("connect");
return -1;
}
printf ("客户机:收发数据...\n");
for (;;) {
printf ("> ");
char buf[1024];
gets (buf);
if (! strcmp (buf, "!")) break;
if (send (sockfd, buf, strlen (buf) + 1, 0) == -1) {
perror ("send");
return -1;
}
memset (buf, 0, sizeof (buf));
ssize_t rb = recv (sockfd, buf, sizeof (buf), 0);
if (rb == -1) {
perror ("recv");
return -1;
}
if (rb == 0) {
printf ("客户机:服务器已宕机!\n");
break;
}
printf ("< %s\n", buf);
}
printf ("客户机:关闭套接字...\n");
close (sockfd);
return 0;
}
/*
* tcpsvr.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void sigchld (int signum)
{
wait (0);
printf ("回收了一个子进程的尸体!\n");
}
int main (int argc, char* argv[])
{
if (argc < 2) {
printf ("用法:%s <端口号>\n", argv[0]);
return -1;
}
if (signal (SIGCHLD, sigchld) == SIG_ERR) {
perror ("signal");
return -1;
}
printf ("服务器:创建套接字...\n");
int sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror ("socket");
return -1;
}
printf ("服务器:准备地址并绑定...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[1]));
addr.sin_addr.s_addr = INADDR_ANY;
if (bind (sockfd, (struct sockaddr*)&addr,
sizeof (addr)) == -1) {
perror ("bind");
return -1;
}
printf ("服务器:监听套接字...\n");
if (listen (sockfd, 1024) == -1) {
perror ("listen");
return -1;
}
for (;;) {
printf ("服务器:等待连接请求...\n");
struct sockaddr_in addrcli = {};
socklen_t addrlen = sizeof (addrcli);
int connfd = accept (sockfd, (struct sockaddr*)&addrcli, &addrlen);
printf ("服务器:接受来自%s:%u客户机的连接请求。\n",
inet_ntoa (addrcli.sin_addr),
ntohs (addrcli.sin_port));
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
if (pid == 0) {
printf ("服务器:收发数据...\n");
for (;;) {
char buf[1024];
ssize_t rb = recv (connfd, buf, sizeof (buf), 0);
if (rb == -1) {
perror ("recv");
return -1;
}
if (rb == 0) {
printf ("服务器:客户机已关闭连接。\n");
break;
}
if (send (connfd, buf, rb, 0) == -1) {
perror ("send");
return -1;
}
}
printf ("服务器:关闭套接字...\n");
close (connfd);
return 0;
}
}
close (sockfd);
return 0;
}
all:
gcc -m32 -o cancel cancel.c -lpthread
gcc -m32 -o create create.c -lpthread
gcc -m32 -o detach detach.c -lpthread
gcc -m32 -o equal equal.c -lpthread
gcc -m32 -o exit exit.c -lpthread
gcc -m32 -o join join.c -lpthread
gcc -m32 -o tcpcli tcpcli.c
gcc -m32 -o tcpsvr tcpsvr.c
gcc -m32 -o udpcli udpcli.c
gcc -m32 -o udpsvr udpsvr.c
clean:
rm -rf cancel \
create \
detach \
equal \
exit \
join \
tcpcli \
tcpsvr \
udpcli \
udpsvr
线程同步 - d16¶
一、并发冲突问题¶
g_pool = 0;
线程1 线程1
从内存中把0读取CPU寄存器
把CPU寄存器中的0累加为1
把CPU寄存器中1写回到内存
从内存中把1读取CPU寄存器
把CPU寄存器中的1累减为0
把CPU寄存器中0写回到内存
-------------------------------------------------------------
从内存中把0读取CPU寄存器
从内存中把0读取CPU寄存器
把CPU寄存器中的0累加为1
把CPU寄存器中的0累减为-1
把CPU寄存器中-1写回到内存
把CPU寄存器中1写回到内存
当多个线程同时访问共享资源时,
由于对共享资源的非原子化操作,
可以引发数据不一致的问题。
这种现象被称为并发冲突。
二、互斥量¶
多线程 X
并发访问共享资源 X
非原子化操作 V
将一组非原子化的指令变成原子化
――任何时候都只会有一个线程执行一组特定的指令。
线程1 {
锁定互斥量;
...... \
...... > 原子化操作
...... /
解锁互斥量;
}
线程2 {
锁定互斥量;
...... \
...... > 原子化操作
...... /
解锁互斥量;
}
// 初始化互斥量:
int pthread_mutex_init (pthread_mutex_t* mutex,
const pthread_mutexattr_t* mutexattr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 锁定互斥量:
int pthread_mutex_lock (pthread_mutex_t* mutex);
// 解锁互斥量:
int pthread_mutex_unlock (pthread_mutex_t* mutex);
// 销毁互斥量:
int pthread_mutex_destroy (pthread_mutex_t* mutex);
// 针对通过pthread_mutex_init初始化的互斥量。
三、资源分享问题¶
当多个线程竞争有限资源时,会出现某些线程得不到资源的情况。 这时得不到资源的线程应该等待那些获得资源的线程,在使用资源以后,主动放弃资源。
四、信号量¶
信号量是一个资源计数器,用于控制访问有限资源的线程数。
#include <semaphore.h>
/* 1.创建信号量 */
int sem_init (
sem_t* sem, // 输出,信号量ID
int pshared, // 0表示线程信号量,非零表示进程信号量
unsigned int value // 信号量初值
);
// 成功返回0,失败返回-1。
/* 2.等待信号量 */
int sem_wait (sem_t* sem);
// 如果信号量sem的计数值足够减1,立即返回0,
// 同时将信号量sem的计数值减1,不够减则阻塞,直到够减为止。
int sem_trywait (sem_t* sem);
// 如果信号量sem的计数值不够减,不阻塞而是立即返回-1,
// 同时将errno置为EAGAIN。
int sem_timedwait (sem_t* sem,
const struct timespec* abs_timeout /* 等待超时 */);
struct timespec {
time_t tv_sec; // 秒
long tv_nsec; // 纳秒(10^-9秒)
};
// 如果信号量sem的计数值不够减即阻塞,直到够减或超时返回,
// 如果因为超时返回,errno为ETIMEOUT。
/* 3.释放信号量 */
int sem_post (sem_t* sem);
// 将信号量sem的计数值加1。
/* 4.销毁信号量 */
int sem_destroy (sem_t* sem);
// 释放信号量对象本身的内核资源。
五、死锁问题¶
线程1 线程2
| |
获取A 获取B
| |
获取B 获取A <- 死锁
| \ / |
释放B X 释放A
| / \ |
释放A 释放B
/*
* bus.c
*/
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
int g_cn = 5; // 空座位
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
sem_t g_sem;
// 乘客线程
void* thread_proc (void* arg) {
sem_wait (&g_sem);
pthread_mutex_lock (&g_mutex);
printf ("%lu坐下,还剩%d个空座。\n", pthread_self (), --g_cn);
pthread_mutex_unlock (&g_mutex);
usleep (50000); // 坐车时间
pthread_mutex_lock (&g_mutex);
printf ("%lu下车,还剩%d个空座。\n", pthread_self (), ++g_cn);
pthread_mutex_unlock (&g_mutex);
sem_post (&g_sem);
return NULL;
}
int main (void) {
sem_init (&g_sem, 0, g_cn);
printf ("发车时共有%d个空座。\n", g_cn);
pthread_t tids[20];
int i;
for (i = 0; i < 20; ++i){
pthread_create (&tids[i], NULL, thread_proc, NULL);
}
for (i = 0; i < 20; ++i){
pthread_join (tids[i], NULL);
}
printf ("收车时共有%d个空座。\n", g_cn);
sem_destroy (&g_sem);
return 0;
}
/*
* dead.c
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t g_a = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t g_b = PTHREAD_MUTEX_INITIALIZER;
void* thread1 (void* arg) {
pthread_t tid = pthread_self ();
printf ("%lu线程:等待A...\n", tid);
pthread_mutex_lock (&g_a);
printf ("%lu线程:获得A!\n", tid);
sleep (1);
printf ("%lu线程:等待B...\n", tid);
pthread_mutex_lock (&g_b);
printf ("%lu线程:获得B!\n", tid);
// ...
pthread_mutex_unlock (&g_b);
printf ("%lu线程:释放B。\n", tid);
pthread_mutex_unlock (&g_a);
printf ("%lu线程:释放A。\n", tid);
return NULL;
}
void* thread2 (void* arg) {
pthread_t tid = pthread_self ();
printf ("%lu线程:等待B...\n", tid);
pthread_mutex_lock (&g_b);
printf ("%lu线程:获得B!\n", tid);
sleep (1);
printf ("%lu线程:等待A...\n", tid);
pthread_mutex_lock (&g_a);
printf ("%lu线程:获得A!\n", tid);
// ...
pthread_mutex_unlock (&g_a);
printf ("%lu线程:释放A。\n", tid);
pthread_mutex_unlock (&g_b);
printf ("%lu线程:释放B。\n", tid);
return NULL;
}
int main (void) {
pthread_t tid1;
pthread_create (&tid1, NULL, thread1, NULL);
pthread_t tid2;
pthread_create (&tid2, NULL, thread2, NULL);
pthread_join (tid1, NULL);
pthread_join (tid2, NULL);
return 0;
}
#include <stdio.h>
#include <string.h>
#include <pthread.h>
int g_pool = 0;
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
void* pool1 (void* arg) {
unsigned int i;
pthread_mutex_lock (&g_mutex);
for (i = 0; i < 100000000; ++i) {
// pthread_mutex_lock (&g_mutex);
++g_pool;
// pthread_mutex_unlock (&g_mutex);
}
pthread_mutex_unlock (&g_mutex);
return NULL;
}
void* pool2 (void* arg) {
unsigned int i;
pthread_mutex_lock (&g_mutex);
for (i = 0; i < 100000000; ++i) {
// pthread_mutex_lock (&g_mutex);
--g_pool;
// pthread_mutex_unlock (&g_mutex);
}
pthread_mutex_unlock (&g_mutex);
return NULL;
}
int main (void) {
//pthread_mutex_init (&g_mutex, NULL);
pthread_t tid1;
pthread_create (&tid1, NULL, pool1, NULL);
pthread_t tid2;
pthread_create (&tid2, NULL, pool2, NULL);
pthread_join (tid1, NULL);
pthread_join (tid2, NULL);
printf ("%u\n", g_pool);
//pthread_mutex_destroy (&g_mutex);
return 0;
}
all:
gcc -m32 -o bus bus.c -lpthread
gcc -m32 -o dead dead.c -lpthread
gcc -m32 -o pool pool.c -lpthread
clean:
rm bus dead pool
六、条件变量(DMS项目)¶
综合练习:局域网聊天室¶

// 单向线性链表
//
#ifndef _LIST_H
#define _LIST_H
#include <sys/types.h>
#include <stdbool.h>
// 节点
typedef struct ListNode {
void* data; // 数据
struct ListNode* next; // 后指针
} LIST_NODE;
// 链表
typedef struct List {
LIST_NODE* head; // 头指针
LIST_NODE* tail; // 尾指针
LIST_NODE* iter; // 迭代器
} LIST;
// 创建
LIST* list_create (void);
// 销毁
void list_destroy (LIST* list);
// 压入
void list_push (LIST* list, void* data);
// 弹出
void* list_pop (LIST* list);
// 删除
void list_remove (LIST* list, void* data);
// 清空
void list_clear (LIST* list);
// 判空
bool list_empty (LIST* list);
// 开始
void list_begin (LIST* list);
// 继续
void** list_next (LIST* list);
// 终止
bool list_end (LIST* list);
#endif // _LIST_H
#ifndef _CHATROOM_H
#define _CHATROOM_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "list.h"
#define MAX_NCK 256 // 最大昵称
#define MAX_TAG (4+MAX_NCK) // 最大标志
#define MAX_TXT 1024 // 最大文本
#define MAX_MSG (MAX_NCK+MAX_TXT+1) // 最大消息
#define MAX_ACK 256 // 最大应答
#endif // _CHATROOM_H
// 聊天室服务器
//
#include "chatroom.h"
// 发送器结构体
typedef struct Sender {
int connfd; // 发送器连接套接字
char nickname[MAX_NCK]; // 发送器昵称
} SENDER;
LIST* g_snds = NULL; // 发送器结构体列表
LIST* g_rcvs = NULL; // 接收器套接字列表
LIST* g_msgs = NULL; // 消息包队列
// 发送器结构体列表互斥量
pthread_mutex_t g_mtx_snds = PTHREAD_MUTEX_INITIALIZER;
// 接收器套接字列表互斥量
pthread_mutex_t g_mtx_rcvs = PTHREAD_MUTEX_INITIALIZER;
// 消息包队列互斥量
pthread_mutex_t g_mtx_msgs = PTHREAD_MUTEX_INITIALIZER;
// 消息包队列非空条件变量
pthread_cond_t g_cnd_msgs = PTHREAD_COND_INITIALIZER;
// SIGINT信号处理函数
void sigint (int signum) {
printf ("\n服务器:再见!\n");
exit (0);
}
// 从消息包队列弹出消息
char* pop_msg (void) {
pthread_mutex_lock (&g_mtx_msgs);
while (list_empty (g_msgs)){
pthread_cond_wait (&g_cnd_msgs, &g_mtx_msgs);
}
char* msg = (char*)list_pop (g_msgs);
pthread_mutex_unlock (&g_mtx_msgs);
return msg;
}
// 向消息包队列压入消息
void push_msg (char* msg) {
pthread_mutex_lock (&g_mtx_msgs);
list_push (g_msgs, msg);
pthread_cond_broadcast (&g_cnd_msgs);
pthread_mutex_unlock (&g_mtx_msgs);
}
// 发送线程过程函数
void* send_proc (void* arg) {
// 发送循环
for (;;) {
char* msg = pop_msg (); // 从消息包对列中弹出消息
pthread_mutex_lock (&g_mtx_rcvs);
// 遍历接收器套接字列表
list_begin (g_rcvs);
while (! list_end (g_rcvs)) {
int* connfd = (int*)list_next (g_rcvs); // 针对每一个接收器
// 向接收器发送消息包
if (send (*connfd, msg, (strlen (msg) + 1) * sizeof (msg[0]), 0) == -1) {
perror ("send");
close (*connfd);
*connfd = -1;
continue;
}
// 从接收器接收应答包
char ack[MAX_ACK];
ssize_t rb = recv (*connfd, ack, sizeof (ack), 0);
if (rb == -1) {
perror ("recv");
close (*connfd);
*connfd = -1;
continue;
}
if (rb == 0) {
printf ("服务器:接收器已关闭连接。\n");
close (*connfd);
*connfd = -1;
continue;
}
}
// 从接收器套接字列表中删除已失效的接收器套接字
list_remove (g_rcvs, (void*)-1);
pthread_mutex_unlock (&g_mtx_rcvs);
free (msg); // 销毁已发送的消息包
}
return NULL;
}
// 发送器登入
void login (const SENDER* sender) {
pthread_mutex_lock (&g_mtx_snds);
list_push (g_snds, (void*)sender);
pthread_mutex_unlock (&g_mtx_snds);
}
// 发送器登出
void logout (const SENDER* sender) {
pthread_mutex_lock (&g_mtx_snds);
list_remove (g_snds, (void*)sender);
pthread_mutex_unlock (&g_mtx_snds);
}
// 接收线程过程函数
void* recv_proc (void* arg) {
SENDER* sender = (SENDER*)arg;
login (sender);
// 向消息包队列压入欢迎包
char* msg = malloc (MAX_MSG * sizeof (char));
sprintf (msg, "系统> 热烈欢迎%s进入聊天室!", sender->nickname);
push_msg (msg);
// 接收循环
for (;;) {
// 从发送器接收文本包
char txt[MAX_TXT];
ssize_t rb = recv (sender->connfd, txt, sizeof (txt), 0);
if (rb == -1) {
perror ("recv");
break;
}
if (rb == 0) {
printf ("服务器:发送器已关闭连接。\n");
// 向消息包队列压入告别包
char* msg = malloc (MAX_MSG * sizeof (char));
sprintf (msg, "系统> %s挥一挥衣袖,不带走一片云彩...",
sender->nickname);
push_msg (msg);
break;
}
// 向消息包队列压入文本包
char* msg = malloc (MAX_MSG * sizeof (char));
sprintf (msg, "%s> %s", sender->nickname, txt);
push_msg (msg);
// 向发送器发送应答包
char ack[MAX_ACK] = "OK";
if (send (sender->connfd, ack, (strlen (ack) + 1) *
sizeof (ack[0]), 0) == -1) {
perror ("send");
break;
}
}
logout (sender);
close (sender->connfd);
free (sender);
return NULL;
}
// 启动发送线程
int start_send (void) {
printf ("服务器:启动发送线程...\n");
int error;
pthread_attr_t attr;
if ((error = pthread_attr_init (&attr)) != 0) {
fprintf (stderr, "pthread_attr_init: %s\n",
strerror (error));
return -1;
}
if ((error = pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED)) != 0) {
fprintf (stderr, "pthread_attr_setdetachstate: %s\n", strerror (error));
return -1;
}
pthread_t tid;
if ((error = pthread_create (&tid, &attr, send_proc, NULL)) != 0) {
fprintf (stderr, "pthread_create: %s\n", strerror (error));
return -1;
}
if ((error = pthread_attr_destroy (&attr)) != 0) {
fprintf (stderr, "pthread_attr_destroy: %s\n", strerror (error));
return -1;
}
return 0;
}
// 启动
int start (unsigned short port) {
if (signal (SIGINT, sigint) == SIG_ERR) {
perror ("signal");
return -1;
}
printf ("服务器:创建网络流套接字...\n");
int sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror ("socket");
return -1;
}
int on = 1;
if (setsockopt (sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on)) == -1) {
perror ("setsockopt");
return -1;
}
printf ("服务器:准备地址并绑定...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
perror ("bind");
return -1;
}
printf ("服务器:监听套接字...\n");
if (listen (sockfd, 1024) == -1) {
perror ("listen");
return -1;
}
g_snds = list_create ();
g_rcvs = list_create ();
g_msgs = list_create ();
// 启动发送线程
if (start_send () == -1)
return -1;
return sockfd; // 返回监听套接字
}
// 昵称可用否
int verify (const char* nickname) {
int ok = 1;
pthread_mutex_lock (&g_mtx_snds);
// 遍历发送器结构体列表
list_begin (g_snds);
while (! list_end (g_snds)) {
// 针对每一个发送器
if (! strcmp ((**(SENDER**)list_next (g_snds)).nickname, nickname)) {
ok = 0;
break;
}
}
pthread_mutex_unlock (&g_mtx_snds);
return ok;
}
// 等待客户机连接
int wait_client (int sockfd, char nickname[]) {
printf ("服务器:等待连接请求...\n");
// 接受来自客户机的连接请求
struct sockaddr_in addrcli;
socklen_t addrlen = sizeof (addrcli);
int connfd = accept (sockfd, (struct sockaddr*)&addrcli, &addrlen);
if (connfd == -1) {
perror ("accept");
return -1;
}
// 从客户机接收标志包
char tag[MAX_TAG];
if (recv (connfd, tag, MAX_TAG, 0) == -1) {
perror ("recv");
close (connfd);
return -2;
}
char ack[MAX_ACK] = "OK";
// 发送器
if (! strncmp (tag, "SNDR", 4))
if (verify (tag + 4)) {
printf ("服务器:接受来自%s:%u发送器的连接请求。\n",
inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));
strcpy (nickname, tag + 4);
}
else {
printf ("服务器:拒绝来自%s:%u发送器的连接请求。\n",
inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));
strcpy (ack, "更换昵称再试一次!");
}
// 接收器
else if (! strncmp (tag, "RCVR", 4))
printf ("服务器:接受来自%s:%u接收器的连接请求。\n",
inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));
// 非法客户机
else {
printf ("服务器:拒绝来自%s:%u客户机的连接请求。\n",
inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));
close (connfd);
return -2;
}
// 向客户机发送应答包
if (send (connfd, ack, (strlen (ack) + 1) * sizeof (ack[0]), 0) == -1) {
perror ("send");
close (connfd);
return -2;
}
if (strcmp (ack, "OK")) {
close (connfd);
return -2;
}
return connfd; // 返回连接套接字
}
// 启动接收线程
int start_recv (int connfd, const char* nickname) {
printf ("服务器:启动接收线程...\n");
int error;
pthread_attr_t attr;
if ((error = pthread_attr_init (&attr)) != 0) {
fprintf (stderr, "pthread_attr_init: %s\n", strerror (error));
return -1;
}
if ((error = pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED)) != 0) {
fprintf (stderr, "pthread_attr_setdetachstate: %s\n", strerror (error));
return -1;
}
// 初始化发送器结构体,填入发送器信息,
// 包括与之连接的套接字
SENDER* sender = malloc (sizeof (SENDER));
sender->connfd = connfd;
strcpy (sender->nickname, nickname);
// 每个接收线程持有一个发送器结构体,
// 为该发送器提供服务
pthread_t tid;
if ((error = pthread_create (&tid, &attr, recv_proc, sender)) != 0) {
fprintf (stderr, "pthread_create: %s\n", strerror (error));
return -1;
}
if ((error = pthread_attr_destroy (&attr)) != 0) {
fprintf (stderr, "pthread_attr_destroy: %s\n", strerror (error));
return -1;
}
return 0;
}
// 运行
int run (int sockfd) {
for (;;) {
// 等待客户机连接,返回与客户机连接的套接字
char nickname[MAX_NCK] = {};
int connfd = wait_client (sockfd, nickname);
if (connfd == -1)
return -1;
else
if (connfd == -2)
continue;
// 针对来自发送器的连接
if (strlen (nickname)) {
// 启动接收线程
if (start_recv (connfd, nickname) == -1) {
close (connfd);
continue;
}
}
// 针对来自接收器的连接
else {
// 将与接收器连接的套接字加入接收器套接字列表
pthread_mutex_lock (&g_mtx_rcvs);
list_push (g_rcvs, (void*)connfd);
pthread_mutex_unlock (&g_mtx_rcvs);
}
}
}
// 停止
int stop (int sockfd) {
list_destroy (g_msgs);
list_destroy (g_rcvs);
list_destroy (g_snds);
printf ("服务器:关闭套接字...\n");
if (close (sockfd) == -1) {
perror ("close");
return -1;
}
printf ("服务器:再见!\n");
return 0;
}
// 主函数
int main (int argc, char* argv[]) {
if (argc < 2) {
fprintf (stderr, "用法:%s <端口号>\n", argv[0]);
return -1;
}
printf ("********************\n");
printf ("* 狂聊聊天室服务器 *\n");
printf ("********************\n");
// 启动
int sockfd = start (atoi (argv[1]));
if (sockfd == -1)
return -1;
// 运行
if (run (sockfd) == -1)
return -1;
// 停止
return stop (sockfd);
}
// 聊天室发送器
//
#include "chatroom.h"
// 启动
int start (const char* ip, unsigned short port, const char* nickname) {
printf ("发送器:创建网络流套接字...\n");
int sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror ("socket");
return -1;
}
printf ("发送器:准备地址并连接...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (port);
addr.sin_addr.s_addr = inet_addr (ip);
if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
perror ("connect");
return -1;
}
// 向服务器发送标志包
char tag[MAX_TAG] = "SNDR";
strcat (tag, nickname);
if (send (sockfd, tag, (strlen (tag) + 1) * sizeof (tag[0]), 0) == -1) {
perror ("send");
return -1;
}
// 从服务器接收应答包
char ack[MAX_ACK];
ssize_t rb = recv (sockfd, ack, sizeof (ack), 0);
if (rb == -1) {
perror ("recv");
return -1;
}
if (rb == 0) {
printf ("发送器:服务器已宕机!\n");
return -1;
}
// 服务器返回错误信息
if (strncmp (ack, "OK", 2)) {
printf ("服务器:%s\n", ack);
return -1;
}
return sockfd;
}
// 运行
int run (int sockfd, const char* nickname) {
printf ("发送器:发送信息...\n");
for (;;) {
printf ("%s> ", nickname);
char txt[MAX_TXT];
gets (txt); // 读取控制台输入
if (strspn (txt, " \t\r\n") == strlen (txt)) {
printf ("发送器:请不要输入空白字符串!\n");
continue;
}
if (! strcmp (txt, "!"))
break;
// 向服务器发送文本包
if (send (sockfd, txt, (strlen (txt) + 1) * sizeof (txt[0]), 0) == -1) {
perror ("send");
return -1;
}
// 从服务器接收应答包
char ack[MAX_ACK];
ssize_t rb = recv (sockfd, ack, sizeof (ack), 0);
if (rb == -1) {
perror ("recv");
return -1;
}
if (rb == 0) {
printf ("发送器:服务器已宕机!\n");
break;
}
}
return 0;
}
// 停止
int stop (int sockfd) {
printf ("发送器:关闭套接字...\n");
if (close (sockfd) == -1) {
perror ("close");
return -1;
}
printf ("发送器:再见!\n");
return 0;
}
int main (int argc, char* argv[]) {
if (argc < 4) {
fprintf (stderr, "用法:%s <服务器IP地址> <端口号> <昵称>\n", argv[0]);
return -1;
}
printf ("********************\n");
printf ("* 狂聊聊天室发送器 *\n");
printf ("********************\n");
// 启动
int sockfd = start (argv[1], atoi (argv[2]), argv[3]);
if (sockfd == -1)
return -1;
// 运行
if (run (sockfd, argv[3]) == -1)
return -1;
// 停止
return stop (sockfd);
}
// 聊天室接收器
//
#include "chatroom.h"
// SIGINT信号处理函数
void sigint (int signum) {
printf ("\n接收器:再见!\n");
exit (0);
}
// 启动
int start (const char* ip, unsigned short port) {
if (signal (SIGINT, sigint) == SIG_ERR) {
perror ("signal");
return -1;
}
printf ("接收器:创建网络流套接字...\n");
int sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror ("socket");
return -1;
}
printf ("接收器:准备地址并连接...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (port);
addr.sin_addr.s_addr = inet_addr (ip);
if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
perror ("connect");
return -1;
}
// 向服务器发送标志包
char tag[MAX_TAG] = "RCVR";
if (send (sockfd, tag, (strlen (tag) + 1) * sizeof (tag[0]), 0) == -1) {
perror ("send");
return -1;
}
// 从服务器接收应答包
char ack[MAX_ACK];
ssize_t rb = recv (sockfd, ack, sizeof (ack), 0);
if (rb == -1) {
perror ("recv");
return -1;
}
if (rb == 0) {
printf ("接收器:服务器已宕机!\n");
return -1;
}
return sockfd;
}
// 运行
int run (int sockfd) {
printf ("接收器:接收信息...\n");
for (;;) {
// 从服务器接收消息包
char msg[MAX_MSG];
ssize_t rb = recv (sockfd, msg, sizeof (msg), 0);
if (rb == -1) {
perror ("recv");
return -1;
}
if (rb == 0) {
printf ("接收器:服务器已宕机!\n");
break;
}
// 打印消息包
printf ("%s\n", msg);
// 向服务器发送应答包
char ack[MAX_ACK] = "OK";
if (send (sockfd, ack, (strlen (ack) + 1) * sizeof (ack[0]), 0) == -1) {
perror ("send");
return -1;
}
}
return 0;
}
// 停止
int stop (int sockfd) {
printf ("接收器:关闭套接字...\n");
if (close (sockfd) == -1) {
perror ("close");
return -1;
}
printf ("接收器:再见!\n");
return 0;
}
int main (int argc, char* argv[]) {
if (argc < 3) {
fprintf (stderr, "用法:%s <服务器IP地址> <端口号>\n", argv[0]);
return -1;
}
printf ("********************\n");
printf ("* 狂聊聊天室接收器 *\n");
printf ("********************\n");
// 启动
int sockfd = start (argv[1], atoi (argv[2]));
if (sockfd == -1)
return -1;
// 运行
if (run (sockfd) == -1)
return -1;
// 停止
return stop (sockfd);
}
CC = gcc
RM = rm
CFLAGS = -c -Wall -I../inc
all: chatroom sender receiver
chatroom: chatroom.o list.o
$(CC) $^ -lpthread -o ../bin/$@
chatroom.o: chatroom.c
$(CC) $(CFLAGS) $^
list.o: list.c
$(CC) $(CFLAGS) $^
sender: sender.o
$(CC) $^ -o ../bin/$@
sender.o: sender.c
$(CC) $(CFLAGS) $^
receiver: receiver.o
$(CC) $^ -o ../bin/$@
receiver.o: receiver.c
$(CC) $(CFLAGS) $^
clean:
$(RM) *.o \
../bin/chatroom \
../bin/sender \
../bin/receiver