Linux系统: “所见皆文件”
一个比较好的博客
Linux系统目录:
bin:存放二进制可执行文件
boot:存放开机启动程序
dev:存放设备文件: 字符设备、块设备
home:存放普通用户
etc:用户信息和系统配置文件 passwd、group
lib:库文件:libc.so.6
root:管理员宿主目录(家目录)
usr:用户资源管理目录
Linux系统文件类型: 7/8 种
普通文件:-
目录文件:d
字符设备文件:c
块设备文件:b
软连接:l
管道文件:p
套接字:s
未知文件。
show files:
list show :ls -l(list) -a(all)
? match a character
* match some chars
create files:
touch file_name
copy files:
cp sourse destination
but directories
rename:
cp source source_new_name
-l hard linck
-s symbo linck
less 一页一页读取,切不一次性读取所有文件
head -n 数字 取文件前几行
tail -nf 数字取文件最后几行, f代表持续探测
od 二进制读取
软连接:快捷方式
为保证软连接可以任意搬移, 创建时务必对源文件使用 绝对路径。
硬链接:
ln file file.hard
操作系统给每一个文件赋予唯一的 inode,当有相同inode的文件存在时,彼此同步。
删除时,只将硬链接计数减一。减为0时,inode 被释放。
创建用户:
sudo adduser 新用户名 --- useradd
修改文件所属用户:
sudo chown 新用户名 待修改文件。
sudo chown wangwu a.c
删除用户:
sudo deluser 用户名
创建用户组:
sudo addgroup 新组名
修改文件所属用户组:
sudo chgrp 新用户组名 待修改文件。
sudo chgrp g88 a.c
删除组:
sudo delgroup 用户组名
使用chown 一次修改所有者和所属组:
sudo chown 所有者:所属组 待操作文件。
find命令:找文件
-type 按文件类型搜索 d/p/s/c/b/l/ f:文件
-name 按文件名搜索
find ./ -name "*file*.jpg"
-maxdepth 指定搜索深度。应作为第一个参数出现。
find ./ -maxdepth 1 -name "*file*.jpg"
-size 按文件大小搜索. 单位:k、M、G
find /home/itcast -size +20M -size -50M
-atime、mtime、ctime 天 amin、mmin、cmin 分钟。
-exec:将find搜索的结果集执行某一指定命令。
find /usr/ -name '*tmp*' -exec ls -ld {} \;
-ok: 以交互式的方式 将find搜索的结果集执行某一指定命令
-xargs:将find搜索的结果集执行某一指定命令。 当结果集数量过大时,可以分片映射。
find /usr/ -name '*tmp*' | xargs ls -ld
-print0:
find /usr/ -name '*tmp*' -print0 | xargs -0 ls -ld
grep命令:找文件内容
grep -r 'copy' ./ -n
-n参数::显示行号
ps aux | grep 'cupsd' -- 检索进程结果集。
软件安装:
1. 联网
2. 更新软件资源列表到本地。 sudo apt-get update
3. 安装 sudo apt-get install 软件名
4. 卸载 sudo apt-get remove 软件名
5. 使用软件包(.deb) 安装: sudo dpkg -i 安装包名。
tar压缩:
1. tar -zcvf 要生成的压缩包名 压缩材料。
tar zcvf test.tar.gz file1 dir2 使用 gzip方式压缩。
tar jcvf test.tar.gz file1 dir2 使用 bzip2方式压缩。
tar解压:
将 压缩命令中的 c --> x
tar zxvf test.tar.gz 使用 gzip方式解压缩。
tar jxvf test.tar.gz 使用 bzip2方式解压缩。
rar压缩:
rar a -r 压缩包名(带.rar后缀) 压缩材料。
rar a -r testrar.rar stdio.h test2.mp3
rar解压:
unrar x 压缩包名(带.rar后缀)
zip压缩:
zip -r 压缩包名(带.zip后缀) 压缩材料。
zip -r testzip.zip dir stdio.h test2.mp3
zip解压:
unzip 压缩包名(带.zip后缀)
unzip testzip.zip
显示行数:
set nu(末行模式)
跳转到指定行:
1. 88G (命令模式)
2. :88 (末行模式)
跳转文件首:
gg (命令模式)
跳转文件尾:
G(命令模式)
自动格式化程序:
gg=G(命令模式)
大括号对应:
% (命令模式)
光标移至行首:
0 (命令模式)执行结束,工作模式不变。
光标移至行尾:
$ (命令模式)执行结束,工作模式不变。
删除单个字符:
x (命令模式)执行结束,工作模式不变。
替换单个字符:
将待替换的字符用光标选中, r (命令模式),再按欲替换的字符
删除一个单词:
dw(命令模式)光标置于单词的首字母进行操作。
删除光标至行尾:
D 或者 d$(命令模式)
删除光标至行首:
d0 (命令模式)
删除指定区域:
按 V (命令模式)切换为 “可视模式”,使用 hjkl挪移光标来选中待删除区域。 按 d 删除该区域数据。
删除指定1行:
在光标所在行,按 dd (命令模式)
删除指定N行:
在光标所待删除首行,按 Ndd (命令模式)
复制一行:
yy
粘贴:
p:向后、P:向前。
查找:
1. 找 设想 内容:
命令模式下, 按 “/” 输入欲搜索关键字,回车。使用 n 检索下一个。
2. 找 看到的内容:
命令模式下,将光标置于单词任意一个字符上,按 “*”/ “#”
单行替换:
将光标置于待替换行上, 进入末行模式,输入 :s /原数据/新数据
通篇替换:
末行模式, :%s /原数据/新数据/g g:不加,只替换每行首个。 sed
指定行的替换:
末行模式, :起始行号,终止行号s /原数据/新数据/g g:不加,只替换每行首个。
:29,35s /printf/println/g
撤销、反撤销:
u、ctrl+r(命令模式)
分屏:
sp:横屏分。 Ctrl+ww 切换。
vsp:竖屏分。Ctrl+ww 切换。
跳转至 man 手册:
将光标置于待查看函数单词上,使用 K(命令模式)跳转。 指定卷, nK
查看宏定义:
将光标置于待查看宏定义单词上,使用 [d 查看定义语句。
在末行模式执行shell命令:
:!命令 :! ls -l
可能会用到fg指令
----------------------------------------------------------------
4步骤: 预处理、编译、汇编、连接。
-I: 指定头文件所在目录位置。
-c: 只做预处理、编译、汇编。得到 二进制 文件!!!
-g: 编译时添加调试语句。 主要支持 gdb 调试。
-Wall: 显示所有警告信息。
-D: 向程序中“动态”注册宏定义。 #define NAME VALUE
静态库制作及使用步骤:
1. 将 .c 生成 .o 文件
gcc -c add.c -o add.o
2. 使用 ar 工具制作静态库
ar rcs lib库名.a add.o sub.o div.o
3. 编译静态库到可执行文件中:
gcc test.c lib库名.a -o a.out
头文件守卫:防止头文件被重复包含
#ifndef _HEAD_H_
#define _HEAD_H_
......
#endif
动态库制作及使用:
1. 将 .c 生成 .o 文件, (生成与位置无关的代码 -fPIC)
gcc -c add.c -o add.o -fPIC
2. 使用 gcc -shared 制作动态库
gcc -shared -o lib库名.so add.o sub.o div.o
3. 编译可执行程序时,指定所使用的动态库。 -l:指定库名(去掉lib前缀和.so后缀) -L:指定库路径。
gcc test.c -o a.out -lmymath -L./lib
4. 运行可以执行程序 ./a.out 出错!!!! --- ldd a.out --> "not found"
error while loading shared libraries: libxxx.so: cannot open shared object file: No such file or directory
原因:
链接器: 工作于链接阶段, 工作时需要 -l 和 -L
动态链接器: 工作于程序运行阶段,工作时需要提供动态库所在目录位置。
解决方式:
【1】 通过环境变量: export LD_LIBRARY_PATH=动态库路径
./a.out 成功!!! (临时生效, 终端重启环境变量失效)
【2】 永久生效: 写入 终端配置文件。 .bashrc 建议使用绝对路径。
1) vi ~/.bashrc
2) 写入 export LD_LIBRARY_PATH=动态库路径 保存
3). .bashrc/ source .bashrc / 重启 终端 ---> 让修改后的.bashrc生效
4)./a.out 成功!!!
【3】 拷贝自定义动态库 到 /lib (标准C库所在目录位置)
【4】 配置文件法
1)sudo vi /etc/ld.so.conf
2) 写入 动态库绝对路径 保存
3)sudo ldconfig -v 使配置文件生效。
4)./a.out 成功!!!--- 使用 ldd a.out 查看
拓展:数据段合并
gdb调试工具: 大前提:程序是你自己写的。 ---逻辑错误
基础指令:
-g:使用该参数编译可以执行文件,得到调试表。
gdb ./a.out
list: list 1 列出源码。根据源码指定 行号设置断点。
b: b 20 在20行位置设置断点。
run/r: 运行程序
n/next: 下一条指令(会越过函数)
s/step: 下一条指令(会进入函数)
p/print:p i 查看变量的值。
continue:继续执行断点后续指令。
finish:结束当前函数调用。
quit:退出gdb当前调试。
其他指令:
run:使用run查找段错误出现位置。
set args: 设置main函数命令行参数 (在 start、run 之前)
run 字串1 字串2 ...: 设置main函数命令行参数
info b: 查看断点信息表
b 20 if i = 5: 设置条件断点。
ptype:查看变量类型。
bt:列出当前程序正存活着的栈帧。
frame: 根据栈帧编号,切换栈帧。
display:设置跟踪变量
undisplay:取消设置跟踪变量。 使用跟踪变量的编号。
makefile: 管理项目。
命名:makefile Makefile --- make 命令
1 个规则:
目标:依赖条件
(一个tab缩进)命令
1. 目标的时间必须晚于依赖条件的时间,否则,更新目标
2. 依赖条件如果不存在,找寻新的规则去产生依赖条件。
ALL:指定 makefile 的终极目标。
2 个函数:
src = $(wildcard ./*.c): 匹配当前工作目录下的所有.c 文件。将文件名组成列表,赋值给变量 src。 src = add.c sub.c div1.c
obj = $(patsubst %.c, %.o, $(src)): 将参数3中,包含参数1的部分,替换为参数2。 obj = add.o sub.o div1.o
clean: (没有依赖)
-rm -rf $(obj) a.out “-”:作用是,删除不存在文件时,不报错。顺序执行结束。
3 个自动变量:
$@: 在规则的命令中,表示规则中的目标。
$^: 在规则的命令中,表示所有依赖条件。
$<: 在规则的命令中,表示第一个依赖条件。如果将该变量应用在模式规则中,它可将依赖条件列表中的依赖依次取出,套用模式规则。
模式规则:
%.o:%.c
gcc -c $< -o %@
静态模式规则:
$(obj):%.o:%.c
gcc -c $< -o %@
伪目标:
.PHONY: clean ALL
参数:
-n:模拟执行make、make clean 命令。
-f:指定文件执行 make 命令。 xxxx.mk
hello.c是主函数, 其余时加减乘除四个文件.
makefile的进化之旅
ALL:a.out
a.out:hello.o add.o sub.o div1.o #mul.o
gcc hello.o add.o sub.o div1.o -o a.out
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
hello.o:hello.c
gcc -c hello.c -o hello.o
div1.o:div1.c
gcc -c div1.c -o div1.o
#mul.o:mul.c
# gcc -c mul.c -o mul.o
src = $(wildcard *.c) # add.c sub.c div1.c hello.c
obj = $(patsubst %.c, %.o, $(src)) # add.o sub.o div1.o hello.o
ALL:a.out
a.out: $(obj)
gcc $(obj) -o a.out
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
hello.o:hello.c
gcc -c hello.c -o hello.o
div1.o:div1.c
gcc -c div1.c -o div1.o
src = $(wildcard *.c) # add.c sub.c div1.c hello.c
obj = $(patsubst %.c, %.o, $(src)) # add.o sub.o div1.o hello.o
ALL:a.out
a.out: $(obj)
gcc $(obj) -o a.out
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
hello.o:hello.c
gcc -c hello.c -o hello.o
div1.o:div1.c
gcc -c div1.c -o div1.o
clean:
-rm -rf $(obj) a.out #"-"出错依然执行
src = $(wildcard *.c) # add.c sub.c div1.c hello.c
obj = $(patsubst %.c, %.o, $(src)) # add.o sub.o div1.o hello.o
ALL:a.out
a.out: $(obj)
gcc $^ -o $@
add.o:add.c
gcc -c $< -o $@
sub.o:sub.c
gcc -c $< -o $@
hello.o:hello.c
gcc -c $< -o $@
div1.o:div1.c
gcc -c $< -o $@
clean:
-rm -rf $(obj) a.out
src = $(wildcard *.c) # add.c sub.c div1.c hello.c
obj = $(patsubst %.c, %.o, $(src)) # add.o sub.o div1.o hello.o
ALL:a.out
a.out: $(obj)
gcc $^ -o $@
%.o:%.c
gcc -c $< -o $@
clean:
-rm -rf $(obj) a.out
src = $(wildcard ./src/*.c) # ./src/add.c ./src/sub.c ...
obj = $(patsubst ./src/%.c, ./obj/%.o, $(src)) # ./obj/add.o ./obj/sub.o ...
inc_path=./inc
myArgs= -Wall -g
ALL:a.out
a.out: $(obj)
gcc $^ -o $@ $(myArgs)
$(obj):./obj/%.o:./src/%.c
gcc -c $< -o $@ $(myArgs) -I $(inc_path)
clean:
-rm -rf $(obj) a.out
.PHONY: clean ALL
作业:编写一个 makefile 可以将其所在目录下的所有独立 .c 文件编译生成同名可执行文件。
int open(char *pathname, int flags) #include
参数:
pathname: 欲打开的文件路径名
flags:文件打开方式: #include
O_RDONLY | O_WRONLY | O_RDWR O_CREAT | O_APPEND | O_TRUNC | O_EXCL | O_NONBLOCK ....
返回值:
成功: 打开文件所得到对应的 文件描述符(整数)
失败: -1, 设置errno
int open(char *pathname, int flags, mode_t mode) 123 775
参数:
pathname: 欲打开的文件路径名
flags:文件打开方式: O_RDONLY|O_WRONLY|O_RDWR O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK ....
mode: 参数3使用的前提, 参2指定了 O_CREAT。 取值8进制数,用来描述文件的 访问权限。 rwx 0664
创建文件最终权限 = mode & ~umask
返回值:
成功: 打开文件所得到对应的 文件描述符(整数)
失败: -1, 设置errno
int close(int fd);
错误处理函数: 与 errno 相关。
printf("xxx error: %d\n", errno);
char *strerror(int errnum);
printf("xxx error: %s\n", strerror(errno));
void perror(const char *s);
perror("open error");
ssize_t read(int fd, void *buf, size_t count);
参数:
fd:文件描述符
buf:存数据的缓冲区
count:缓冲区大小
返回值:
0:读到文件末尾。
成功; > 0 读到的字节数。
失败: -1, 设置 errno
-1: 并且 errno = EAGIN 或 EWOULDBLOCK, 说明不是read失败,而是read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据。
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd:文件描述符
buf:待写出数据的缓冲区
count:数据大小
返回值:
成功; 写入的字节数。
失败: -1, 设置 errno
cp命令实现
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
char buf[1024];
int n = 0;
int fd1 = open(argv[1], O_RDONLY); // read
if (fd1 == -1) {
perror("open argv1 error");
exit(1);
}
int fd2 = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, 0664);
if (fd2 == -1) {
perror("open argv2 error");
exit(1);
}
while ((n = read(fd1, buf, 1024)) != 0) {
if (n < 0) {
perror("read error");
break;
}
write(fd2, buf, n);
}
close(fd1);
close(fd2);
return 0;
}
PCB进程控制块:本质 结构体。
成员:文件描述符表。
文件描述符:0/1/2/3/4。。。。/1023 表中可用的最小的。
0 - STDIN_FILENO
1 - STDOUT_FILENO
2 - STDERR_FILENO
阻塞、非阻塞: 是设备文件、网络文件的属性。
产生阻塞的场景。 读设备文件。读网络文件。(读常规文件无阻塞概念。)
/dev/tty -- 终端文件。
open("/dev/tty", O_RDWR|O_NONBLOCK) --- 设置 /dev/tty 非阻塞状态。(默认为阻塞状态)
int (int fd, int cmd, ...)
int flgs = fcntl(fd, F_GETFL);
flgs |= O_NONBLOCK
fcntl(fd, F_SETFL, flgs);
获取文件状态: F_GETFL
设置文件状态: F_SETFL
off_t lseek(int fd, off_t offset, int whence);
参数:
fd:文件描述符
offset: 偏移量
whence:起始偏移位置: SEEK_SET/SEEK_CUR/SEEK_END
返回值:
成功:较起始位置偏移量
失败:-1 errno
应用场景:
1. 文件的“读”、“写”使用同一偏移位置。
2. 使用lseek获取文件大小
3. 使用lseek拓展文件大小:要想使文件大小真正拓展,必须引起IO操作。
使用 truncate 函数,直接拓展文件。 int ret = truncate("dict.cp", 250);
传入参数:
1. 指针作为函数参数。
2. 同常有const关键字修饰。
3. 指针指向有效区域, 在函数内部做读操作。
传出参数:
1. 指针作为函数参数。
2. 在函数调用之前,指针指向的空间可以无意义,但必须有效。
3. 在函数内部,做写操作。
4。函数调用结束后,充当函数返回值。
传入传出参数:
1. 指针作为函数参数。
2. 在函数调用之前,指针指向的空间有实际意义。
3. 在函数内部,先做读操作,后做写操作。
4. 函数调用结束后,充当函数返回值。
void aaa();
int aaa(int *p, struct stat *p2, strcut student *p3);
bbb()
{
aaa();
}
int stat(const char *path, struct stat *buf);
参数:
path: 文件路径
buf:(传出参数) 存放文件属性。
返回值:
成功: 0
失败: -1 errno
获取文件大小: buf.st_size
获取文件类型: buf.st_mode
获取文件权限: buf.st_mode
符号穿透:stat会。lstat不会。
link/unlink:
隐式回收。
DIR * opendir(char *name);
int closedir(DIR *dp);
struct dirent *readdir(DIR * dp);
struct dirent {
inode
char dname[256];
}
./a.out ls.c /home/itcast/28_Linux ./abc/
/home/itcast/28_Linux/testdir/
递归遍历目录:ls-R.c
1. 判断命令行参数,获取用户要查询的目录名。 int argc, char *argv[1]
argc == 1 --> ./
2. 判断用户指定的是否是目录。 stat S_ISDIR(); --> 封装函数 isFile() { }
3. 读目录: read_dir() {
opendir(dir)
while (readdir()){
普通文件,直接打印
目录:
拼接目录访问绝对路径。sprintf(path, "%s/%s", dir, d_name)
递归调用自己。--》 opendir(path) readdir closedir
}
closedir()
}
read_dir() --> isFile() ---> read_dir()
dup 和 dup2:
int dup(int oldfd); 文件描述符复制。
oldfd: 已有文件描述符
返回:新文件描述符。
int dup2(int oldfd, int newfd); 文件描述符复制。重定向。
fcntl 函数实现 dup:
int fcntl(int fd, int cmd, ....)
cmd: F_DUPFD
参3: 被占用的,返回最小可用的。
未被占用的, 返回=该值的文件描述符。
===================================================================================================
进程:
程序:死的。只占用磁盘空间。 ——剧本。
进程;活的。运行起来的程序。占用内存、cpu等系统资源。 ——戏。
PCB进程控制块:
进程id
文件描述符表
进程状态: 初始态、就绪态、运行态、挂起态、终止态。
进程工作目录位置
*umask掩码
信号相关信息资源。
用户id和组id
fork函数:
pid_t fork(void)
创建子进程。父子进程各自返回。父进程返回子进程pid。 子进程返回 0.
getpid();getppid();
循环创建N个子进程模型。 每个子进程标识自己的身份。
父子进程相同:
刚fork后。 data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式
父子进程不同:
进程id、返回值、各自的父进程、进程创建时间、闹钟、未决信号集
父子进程共享:
读时共享、写时复制。———————— 全局变量。
1. 文件描述符 2. mmap映射区。
gdb调试:
设置父进程调试路径:set follow-fork-mode parent (默认)
设置子进程调试路径:set follow-fork-mode child
exec函数族:
使进程执行某一程序。成功无返回值,失败返回 -1
int execlp(const char *file, const char *arg, ...); 借助 PATH 环境变量找寻待执行程序
参1: 程序名
参2: argv0
参3: argv1
...: argvN
哨兵:NULL
int execl(const char *path, const char *arg, ...); 自己指定待执行程序路径。
int execvp();
ps ajx --> pid ppid gid sid
孤儿进程:
父进程先于子进终止,子进程沦为“孤儿进程”,会被 init 进程领养。
僵尸进程:
子进程终止,父进程尚未对子进程进行回收,在此期间,子进程为“僵尸进程”。 kill 对其无效。
wait函数: 回收子进程退出资源, 阻塞回收任意一个。
pid_t wait(int *status)
参数:(传出) 回收进程的状态。
返回值:成功: 回收进程的pid
失败: -1, errno
函数作用1: 阻塞等待子进程退出
函数作用2: 清理子进程残留在内核的 pcb 资源
函数作用3: 通过传出参数,得到子进程结束状态
获取子进程正常终止值:
WIFEXITED(status) --》 为真 --》调用 WEXITSTATUS(status) --》 得到 子进程 退出值。
获取导致子进程异常终止信号:
WIFSIGNALED(status) --》 为真 --》调用 WTERMSIG(status) --》 得到 导致子进程异常终止的信号编号。
waitpid函数: 指定某一个进程进行回收。可以设置非阻塞。 waitpid(-1, &status, 0) == wait(&status);
pid_t waitpid(pid_t pid, int *status, int options)
参数:
pid:指定回收某一个子进程pid
> 0: 待回收的子进程pid
-1:任意子进程
0:同组的子进程。
status:(传出) 回收进程的状态。
options:WNOHANG 指定回收方式为,非阻塞。
返回值:
> 0 : 表成功回收的子进程 pid
0 : 函数调用时, 参3 指定了WNOHANG, 并且,没有子进程结束。
-1: 失败。errno
总结:
wait、waitpid 一次调用,回收一个子进程。
想回收多个。while
===========================
进程间通信的常用方式,特征:
管道:简单
信号:开销小
mmap映射:非血缘关系进程间
socket(本地套接字):稳定
管道:
实现原理: 内核借助环形队列机制,使用内核缓冲区实现。
特质; 1. 伪文件
2. 管道中的数据只能一次读取。
3. 数据在管道中,只能单向流动。
局限性:1. 自己写,不能自己读。
2. 数据不可以反复读。
3. 半双工通信。
4. 血缘关系进程间可用。
pipe函数: 创建,并打开管道。
int pipe(int fd[2]);
参数: fd[0]: 读端。
fd[1]: 写端。
返回值: 成功: 0
失败: -1 errno
管道的读写行为:
读管道:
1. 管道有数据,read返回实际读到的字节数。
2. 管道无数据: 1)无写端,read返回0 (类似读到文件尾)
2)有写端,read阻塞等待。
写管道:
1. 无读端, 异常终止。 (SIGPIPE导致的)
2. 有读端: 1) 管道已满, 阻塞等待
2) 管道未满, 返回写出的字节个数。
pipe管道: 用于有血缘关系的进程间通信。 ps aux | grep ls | wc -l
父子进程间通信:
兄弟进程间通信:
fifo管道:可以用于无血缘关系的进程间通信。
命名管道: mkfifo
无血缘关系进程间通信:
读端,open fifo O_RDONLY
写端,open fifo O_WRONLY
文件实现进程间通信:
打开的文件是内核中的一块缓冲区。多个无血缘关系的进程,可以同时访问该文件。
共享内存映射:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 创建共享内存映射
参数:
addr: 指定映射区的首地址。通常传NULL,表示让系统自动分配
length:共享内存映射区的大小。(<= 文件的实际大小)
prot: 共享内存映射区的读写属性。PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE
fd: 用于创建共享内存映射区的那个文件的 文件描述符。
offset:默认0,表示映射文件全部。偏移位置。需是 4k 的整数倍。
返回值:
成功:映射区的首地址。
失败:MAP_FAILED (void*(-1)), errno
int munmap(void *addr, size_t length); 释放映射区。
addr:mmap 的返回值
length:大小
使用注意事项:
1. 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”。
2. 用于创建映射区的文件大小为 0,实际制定0大小创建映射区, 出 “无效参数”。
3. 用于创建映射区的文件读写属性为,只读。映射区属性为 读、写。 出 “无效参数”。
4. 创建映射区,需要read权限。当访问权限指定为 “共享”MAP_SHARED是, mmap的读写权限,应该 <=文件的open权限。 只写不行。
5. 文件描述符fd,在mmap创建映射区完成即可关闭。后续访问文件,用 地址访问。
6. offset 必须是 4096的整数倍。(MMU 映射的最小单位 4k )
7. 对申请的映射区内存,不能越界访问。
8. munmap用于释放的 地址,必须是mmap申请返回的地址。
9. 映射区访问权限为 “私有”MAP_PRIVATE, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。
10. 映射区访问权限为 “私有”MAP_PRIVATE, 只需要open文件时,有读权限,用于创建映射区即可。
mmap函数的保险调用方式:
1. fd = open("文件名", O_RDWR);
2. mmap(NULL, 有效文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
父子进程使用 mmap 进程间通信:
父进程 先 创建映射区。 open( O_RDWR) mmap( MAP_SHARED );
指定 MAP_SHARED 权限
fork() 创建子进程。
一个进程读, 另外一个进程写。
无血缘关系进程间 mmap 通信: 【会写】
两个进程 打开同一个文件,创建映射区。
指定flags 为 MAP_SHARED。
一个进程写入,另外一个进程读出。
【注意】:无血缘关系进程间通信。mmap:数据可以重复读取。
fifo:数据只能一次读取。
匿名映射:只能用于 血缘关系进程间通信。
p = (int *)mmap(NULL, 40, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
信号共性:
简单、不能携带大量信息、满足条件才发送。
信号的特质:
信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。
所有信号的产生及处理全部都是由【内核】完成的。
信号相关的概念:
产生信号:
1. 按键产生
2. 系统调用产生
3. 软件条件产生
4. 硬件异常产生
5. 命令产生
概念:
未决:产生与递达之间状态。
递达:产生并且送达到进程。直接被内核处理掉。
信号处理方式: 执行默认处理动作、忽略、捕捉(自定义)
阻塞信号集(信号屏蔽字): 本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
未决信号集:本质:位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理。
信号4要素:
信号使用之前,应先确定其4要素,而后再用!!!
编号、名称、对应事件、默认处理动作。
kill命令 和 kill函数:
int kill(pid_t pid, int signum)
参数:
pid: > 0:发送信号给指定进程
= 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。
< -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
= -1:发送信号给,有权限发送的所有进程。
signum:待发送的信号
返回值:
成功: 0
失败: -1 errno
alarm 函数:使用自然计时法。
定时发送SIGALRM给当前进程。
unsigned int alarm(unsigned int seconds);
seconds:定时秒数
返回值:上次定时剩余时间。
无错误现象。
alarm(0); 取消闹钟。
time 命令 : 查看程序执行时间。 实际时间 = 用户时间 + 内核时间 + 等待时间。 --》 优化瓶颈 IO
setitimer函数:
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数:
which: ITIMER_REAL: 采用自然计时。 ——> SIGALRM
ITIMER_VIRTUAL: 采用用户空间计时 ---> SIGVTALRM
ITIMER_PROF: 采用内核+用户空间计时 ---> SIGPROF
new_value:定时秒数
类型:struct itimerval {
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
}it_interval;---> 周期定时秒数
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
}it_value; ---> 第一次定时秒数
};
old_value:传出参数,上次定时剩余时间。
e.g.
struct itimerval new_t;
struct itimerval old_t;
new_t.it_interval.tv_sec = 0;
new_t.it_interval.tv_usec = 0;
new_t.it_value.tv_sec = 1;
new_t.it_value.tv_usec = 0;
int ret = setitimer(&new_t, &old_t); 定时1秒
返回值:
成功: 0
失败: -1 errno
其他几个发信号函数:
int raise(int sig);
void abort(void);
信号集操作函数:
sigset_t set; 自定义信号集。
sigemptyset(sigset_t *set); 清空信号集
sigfillset(sigset_t *set); 全部置1
sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中
sigdelset(sigset_t *set, int signum); 将一个信号从集合中移除
sigismember(const sigset_t *set,int signum); 判断一个信号是否在集合中。 在--》1, 不在--》0
设置信号屏蔽字和解除屏蔽:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how: SIG_BLOCK: 设置阻塞
SIG_UNBLOCK: 取消阻塞
SIG_SETMASK: 用自定义set替换mask。
set: 自定义set
oldset:旧有的 mask。
查看未决信号集:
int sigpending(sigset_t *set);
set: 传出的 未决信号集。
【信号捕捉】:
signal();
【sigaction();】 重点!!!
信号捕捉特性:
1. 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask
2. 捕捉函数执行期间,本信号自动被屏蔽(sa_flgs = 0).
3. 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!
借助信号完成 子进程回收。
守护进程:
daemon进程。通常运行与操作系统后台,脱离控制终端。一般不与用户直接交互。周期性的等待某个事件发生或周期性执行某一动作。
不受用户登录注销影响。通常采用以d结尾的命名方式。
守护进程创建步骤:
1. fork子进程,让父进程终止。
2. 子进程调用 setsid() 创建新会话
3. 通常根据需要,改变工作目录位置 chdir(), 防止目录被卸载。
4. 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限。 022 -- 755 0345 --- 432 r---wx-w- 422
5. 通常根据需要,关闭/重定向 文件描述符
6. 守护进程 业务逻辑。while()
=============================================================
线程概念:
进程:有独立的 进程地址空间。有独立的pcb。 分配资源的最小单位。
线程:有独立的pcb。没有独立的进程地址空间。 最小单位的执行。
ps -Lf 进程id ---> 线程号。LWP --》cpu 执行的最小单位。
线程共享:
独享 栈空间(内核栈、用户栈)
共享 ./text./data ./rodataa ./bsss heap ---> 共享【全局变量】(errno)
线程控制原语:
pthread_t pthread_self(void); 获取线程id。 线程id是在进程地址空间内部,用来标识线程身份的id号。
返回值:本线程id
检查出错返回: 线程中。
fprintf(stderr, "xxx error: %s\n", strerror(ret));
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void *), void *arg); 创建子线程。
参1:传出参数,表新创建的子线程 id
参2:线程属性。传NULL表使用默认属性。
参3:子线程回调函数。创建成功,ptherad_create函数返回时,该函数会被自动调用。
参4:参3的参数。没有的话,传NULL
返回值:成功:0
失败:errno
循环创建N个子线程:
for (i = 0; i < 5; i++)
pthread_create(&tid, NULL, tfn, (void *)i); // 将 int 类型 i, 强转成 void *, 传参。
void pthread_exit(void *retval); 退出当前线程。
retval:退出值。 无退出值时,NULL
exit(); 退出当前进程。
return: 返回到调用者那里去。
pthread_exit(): 退出当前线程。
int pthread_join(pthread_t thread, void **retval); 阻塞 回收线程。
thread: 待回收的线程id
retval:传出参数。 回收的那个线程的退出值。
线程异常借助,值为 -1。
返回值:成功:0
失败:errno
int pthread_detach(pthread_t thread); 设置线程分离
thread: 待分离的线程id
返回值:成功:0
失败:errno
int pthread_cancel(pthread_t thread); 杀死一个线程。 需要到达取消点(保存点)
thread: 待杀死的线程id
返回值:成功:0
失败:errno
如果,子线程没有到达取消点, 那么 pthread_cancel 无效。
我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();
成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。
线程控制原语 进程控制原语
pthread_create() fork();
pthread_self() getpid();
pthread_exit() exit(); / return
pthread_join() wait()/waitpid()
pthread_cancel() kill()
pthread_detach()
线程属性:
设置分离属性。
pthread_attr_t attr 创建一个线程属性结构体变量
pthread_attr_init(&attr); 初始化线程属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 设置线程属性为 分离态
pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 设置线程属性 创建为分离态的新线程
pthread_attr_destroy(&attr); 销毁线程属性
线程同步:
协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。
锁的使用:
建议锁!对公共数据进行保护。所有线程【应该】在访问公共数据前先拿锁再访问。但,锁本身不具备强制性。
使用mutex(互斥量、互斥锁)一般步骤:
pthread_mutex_t 类型。
1. pthread_mutex_t lock; 创建锁
2 pthread_mutex_init; 初始化 1
3. pthread_mutex_lock;加锁 1-- --> 0
4. 访问共享数据(stdout)
5. pthrad_mutext_unlock();解锁 0++ --> 1
6. pthead_mutex_destroy;销毁锁
初始化互斥量:
pthread_mutex_t mutex;
1. pthread_mutex_init(&mutex, NULL); 动态初始化, 适用于局部变量。
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr)
参 1:传出参数,调用时应传 &mutex
参 2:互斥量属性。是一个传入参数,通常传 NULL,选用默认属性(线程间共享)。
2. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 静态初始化, 适用于静态变量或者全局变量。
注意事项:
尽量保证锁的粒度, 越小越好。(访问共享数据前,加锁。访问结束【立即】解锁。)
互斥锁,本质是结构体。 我们可以看成整数。 初值为 1。(pthread_mutex_init() 函数调用成功。)
加锁: --操作, 阻塞线程。
解锁: ++操作, 换醒阻塞在锁上的线程。
try锁:尝试加锁,成功--。失败,返回, 不阻塞。同时设置错误号 EBUSY
restrict关键字:
用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
【死锁】:
是使用锁不恰当导致的现象:
1. 对一个锁反复lock。
2. 两个线程,各自持有一把锁,请求另一把。
读写锁:
锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。
读共享,写独占。
写锁优先级高。
相较于互斥量而言,当读线程多的时候,提高访问效率
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
pthread_rwlock_rdlock(&rwlock); try
pthread_rwlock_wrlock(&rwlock); try
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);
条件变量:
本身不是锁! 但是通常结合锁来使用。 mutex
pthread_cond_t cond;
初始化条件变量:
1. pthread_cond_init(&cond, NULL); 动态初始化。
2. pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 静态初始化。
阻塞等待条件:
pthread_cond_wait(&cond, &mutex);
作用: 1) 阻塞等待条件变量满足
2) 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex))
3) 当条件满足,函数返回时,重新加锁信号量 (相当于, pthread_mutex_lock(&mutex);)
pthread_cond_signal(): 唤醒阻塞在条件变量上的 (至少)一个线程。
pthread_cond_broadcast(): 唤醒阻塞在条件变量上的 所有线程。
【要求,能够借助条件变量,完成生成者消费者】
信号量:
应用于线程、进程间同步。
相当于 初始化值为 N 的互斥量。 N值,表示可以同时访问共享数据区的线程数。
函数:
sem_t sem; 定义类型。
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem: 信号量
pshared: 0: 用于线程间同步
1: 用于进程间同步
value:N值。(指定同时访问的线程数)
sem_destroy();
sem_wait(); 一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 -- 就会阻塞。 (对比 pthread_mutex_lock)
sem_post(); 一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。(对比 pthread_mutex_unlock)
协议:
一组规则。
分层模型结构:
OSI七层模型: 物、数、网、传、会、表、应
TCP/IP 4层模型:网(链路层/网络接口层)、网、传、应
应用层:http、ftp、nfs、ssh、telnet。。。
传输层:TCP、UDP
网络层:IP、ICMP、IGMP
链路层:以太网帧协议、ARP
c/s模型:
client-server
b/s模型:
browser-server
C/S B/S
优点: 缓存大量数据、协议选择灵活 安全性、跨平台、开发工作量较小
速度快
缺点: 安全性、跨平台、开发工作量较大 不能缓存大量数据、严格遵守 http
网络传输流程:
数据没有封装之前,是不能在网络中传递。
数据-》应用层-》传输层-》网络层-》链路层 --- 网络环境
以太网帧协议:
ARP协议:根据 Ip 地址获取 mac 地址。
以太网帧协议:根据mac地址,完成数据包传输。
IP协议:
版本: IPv4、IPv6 -- 4位
TTL: time to live 。 设置数据包在路由节点中的跳转上限。每经过一个路由节点,该值-1, 减为0的路由,有义务将该数据包丢弃
源IP: 32位。--- 4字节 192.168.1.108 --- 点分十进制 IP地址(string) --- 二进制
目的IP:32位。--- 4字节
IP地址:可以在网络环境中,唯一标识一台主机。
端口号:可以网络的一台主机上,唯一标识一个进程。
ip地址+端口号:可以在网络环境中,唯一标识一个进程。
UDP:
16位:源端口号。 2^16 = 65536
16位:目的端口号。
TCP协议:
16位:源端口号。 2^16 = 65536
16位:目的端口号。
32序号;
32确认序号。
6个标志位。
16位窗口大小。 2^16 = 65536
网络套接字: socket
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)
在通信过程中, 套接字一定是成对出现的。
网络字节序:
小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678
大端法:(网络存储) 高位存低地址。地位存高地址。
htonl --> 本地--》网络 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序
htons --> 本地--》网络 (port)
ntohl --> 网络--》 本地(IP)
ntohs --> 网络--》 本地(Port)
IP地址转换函数:
int inet_pton(int af, const char *src, void *dst); 本地字节序(string IP) ---> 网络字节序
af:AF_INET、AF_INET6
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址。
返回值:
成功: 1
异常: 0, 说明src指向的不是一个有效的ip地址。
失败:-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序 ---> 本地字节序(string IP)
af:AF_INET、AF_INET6
src: 网络字节序IP地址
dst:本地字节序(string IP)
size: dst 的大小。
返回值: 成功:dst。
失败:NULL
sockaddr地址结构: IP + port --> 在网络环境中唯一标识一个进程。
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6 man 7 ip
addr.sin_port = htons(9527);
int dst;
inet_pton(AF_INET, "192.157.22.45", (void *)&dst);
addr.sin_addr.s_addr = dst;
【*】addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系统中有效的任意IP地址。二进制类型。
bind(fd, (struct sockaddr *)&addr, size);
socket函数:
#include
int socket(int domain, int type, int protocol); 创建一个 套接字
domain:AF_INET、AF_INET6、AF_UNIX
type:SOCK_STREAM、SOCK_DGRAM
protocol: 0
返回值:
成功: 新套接字所对应文件描述符
失败: -1 errno
#include
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个 地址结构 (IP+port)
sockfd: socket 函数返回值
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr: 传入参数(struct sockaddr *)&addr
addrlen: sizeof(addr) 地址结构的大小。
返回值:
成功:0
失败:-1 errno
int listen(int sockfd, int backlog); 设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
sockfd: socket 函数返回值
backlog:上限数值。最大值 128.
返回值:
成功:0
失败:-1 errno
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。
sockfd: socket 函数返回值
addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)
socklen_t clit_addr_len = sizeof(addr);
addrlen:传入传出。 &clit_addr_len
入:addr的大小。 出:客户端addr实际大小。
返回值:
成功:能与客户端进行数据通信的 socket 对应的文件描述。
失败: -1 , errno
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用现有的 socket 与服务器建立连接
sockfd: socket 函数返回值
struct sockaddr_in srv_addr; // 服务器地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致。
inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);
addr:传入参数。服务器的地址结构
addrlen:服务器的地址结构的大小
返回值:
成功:0
失败:-1 errno
如果不使用bind绑定客户端地址结构, 采用"隐式绑定".
TCP通信流程分析:
server:
1. socket() 创建socket
2. bind() 绑定服务器地址结构
3. listen() 设置监听上限
4. accept() 阻塞监听客户端连接
5. read(fd) 读socket获取客户端数据
6. 小--大写 toupper()
7. write(fd)
8. close();
client:
1. socket() 创建socket
2. connect(); 与服务器建立连接
3. write() 写数据到 socket
4. read() 读转换后的数据。
5. 显示读取结果
6. close()
三次握手:
主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。
被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。
主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带确认序号。
四次挥手:
主动关闭连接请求端, 发送 FIN 标志位。
被动关闭连接请求端, 应答 ACK 标志位。 ----- 半关闭完成。
被动关闭连接请求端, 发送 FIN 标志位。
主动关闭连接请求端, 应答 ACK 标志位。 ----- 连接全部关闭
滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
错误处理函数:
封装目的:
在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。
【wrap.c】 【wrap.h】
存放网络通信相关常用 自定义函数 存放 网络通信相关常用 自定义函数原型(声明)。
命名方式:系统调用函数首字符大写, 方便查看man手册
如:Listen()、Accept();
函数功能:调用系统调用函数,处理出错场景。
在 server.c 和 client.c 中调用 自定义函数
联合编译 server.c 和 wrap.c 生成 server
client.c 和 wrap.c 生成 client
readn:
读 N 个字节
readline:
读一行
read 函数的返回值:
1. > 0 实际读到的字节数
2. = 0 已经读到结尾(对端已经关闭)【 !重 !点 !】
3. -1 应进一步判断errno的值:
errno = EAGAIN or EWOULDBLOCK: 设置了非阻塞方式 读。 没有数据到达。
errno = EINTR 慢速系统调用被 中断。
errno = “其他情况” 异常。
多进程并发服务器:server.c
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1) {
cfd = Accpet(); 接收客户端连接请求。
pid = fork();
if (pid == 0){ 子进程 read(cfd) --- 小-》大 --- write(cfd)
close(lfd) 关闭用于建立连接的套接字 lfd
read()
小--大
write()
} else if (pid > 0) {
close(cfd); 关闭用于与客户端通信的套接字 cfd
contiue;
}
}
5. 子进程:
close(lfd)
read()
小--大
write()
父进程:
close(cfd);
注册信号捕捉函数: SIGCHLD
在回调函数中, 完成子进程回收
while (waitpid());
多线程并发服务器: server.c
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1) {
cfd = Accept(lfd, );
pthread_create(&tid, NULL, tfn, (void *)cfd);
pthread_detach(tid); // pthead_join(tid, void **); 新线程---专用于回收子线程。
}
5. 子线程:
void *tfn(void *arg)
{
// close(lfd) 不能关闭。 主线程要使用lfd
read(cfd)
小--大
write(cfd)
pthread_exit((void *)10);
}
TCP状态时序图:
结合三次握手、四次挥手 理解记忆。
1. 主动发起连接请求端: CLOSE -- 发送SYN -- SEND_SYN -- 接收 ACK、SYN -- SEND_SYN -- 发送 ACK -- ESTABLISHED(数据通信态)
2. 主动关闭连接请求端: ESTABLISHED(数据通信态) -- 发送 FIN -- FIN_WAIT_1 -- 接收ACK -- FIN_WAIT_2(半关闭)
-- 接收对端发送 FIN -- FIN_WAIT_2(半关闭)-- 回发ACK -- TIME_WAIT(只有主动关闭连接方,会经历该状态)
-- 等 2MSL时长 -- CLOSE
3. 被动接收连接请求端: CLOSE -- LISTEN -- 接收 SYN -- LISTEN -- 发送 ACK、SYN -- SYN_RCVD -- 接收ACK -- ESTABLISHED(数据通信态)
4. 被动关闭连接请求端: ESTABLISHED(数据通信态) -- 接收 FIN -- ESTABLISHED(数据通信态) -- 发送ACK
-- CLOSE_WAIT (说明对端【主动关闭连接端】处于半关闭状态) -- 发送FIN -- LAST_ACK -- 接收ACK -- CLOSE
重点记忆: ESTABLISHED、FIN_WAIT_2 <--> CLOSE_WAIT、TIME_WAIT(2MSL)
netstat -apn | grep 端口号
2MSL时长:
一定出现在【主动关闭连接请求端】。 --- 对应 TIME_WAIT 状态。
保证,最后一个 ACK 能成功被对端接收。(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)
端口复用:
int opt = 1; // 设置端口复用。
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
半关闭:
通信双方中,只有一端关闭通信。 --- FIN_WAIT_2
close(cfd);
shutdown(int fd, int how);
how: SHUT_RD 关读端
SHUT_WR 关写端
SHUT_RDWR 关读写
shutdown在关闭多个文件描述符应用的文件时,采用全关闭方法。close,只关闭一个。
select多路IO转接:
原理: 借助内核, select 来监听, 客户端连接、数据通信事件。
void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。
fd_set rset;
FD_ZERO(&rset);
void FD_SET(int fd, fd_set *set); --- 将待监听的文件描述符,添加到监听集合中
FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);
void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。
FD_CLR(4, &rset);
int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。
返回值: 在:1;不在:0;
FD_ISSET(4, &rset);
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1
readfds: 读 文件描述符监听集合。 传入、传出参数
writefds:写 文件描述符监听集合。 传入、传出参数 NULL
exceptfds:异常 文件描述符监听集合 传入、传出参数 NULL
timeout: > 0: 设置监听超时时长。
NULL: 阻塞监听
0: 非阻塞监听,轮询
返回值:
> 0: 所有监听集合(3个)中, 满足对应事件的总数。
0: 没有满足监听条件的文件描述符
-1: errno
思路分析:
int maxfd = 0;
lfd = socket() ; 创建套接字
maxfd = lfd;
bind(); 绑定地址结构
listen(); 设置监听上限
fd_set rset, allset; 创建r监听集合
FD_ZERO(&allset); 将r监听集合清空
FD_SET(lfd, &allset); 将 lfd 添加至读集合中。
while(1) {
rset = allset; 保存监听集合
ret = select(lfd+1, &rset, NULL, NULL, NULL); 监听文件描述符集合对应事件。
if(ret > 0) { 有监听的描述符满足对应事件
if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。
cfd = accept(); 建立连接,返回用于通信的文件描述符
maxfd = cfd;
FD_SET(cfd, &allset); 添加到监听通信描述符集合中。
}
for (i = lfd+1; i <= 最大文件描述符; i++){
FD_ISSET(i, &rset) 有read、write事件
read()
小 -- 大
write();
}
}
}
select优缺点:
缺点: 监听上限受文件描述符限制。 最大 1024.
检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
优点: 跨平台。win、linux、macOS、Unix、类Unix、mips
多路IO转接:
select:
poll:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:监听的文件描述符【数组】
struct pollfd {
int fd: 待监听的文件描述符
short events: 待监听的文件描述符对应的监听事件
取值:POLLIN、POLLOUT、POLLERR
short revnets: 传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR
}
nfds: 监听数组的,实际有效监听个数。
timeout: > 0: 超时时长。单位:毫秒。
-1: 阻塞等待
0: 不阻塞
返回值:返回满足对应监听事件的文件描述符 总个数。
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。
拓展 监听上限。 超出 1024限制。
缺点:
不能跨平台。 Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。
read 函数返回值:
> 0: 实际读到的字节数
=0: socket中,表示对端关闭。close()
-1: 如果 errno == EINTR 被异常终端。 需要重启。
如果 errno == EAGIN 或 EWOULDBLOCK 以非阻塞方式读数据,但是没有数据。 需要,再次读。
如果 errno == ECONNRESET 说明连接被 重置。 需要 close(),移除监听队列。
错误。
突破 1024 文件描述符限制:
cat /proc/sys/fs/file-max --> 当前计算机所能打开的最大文件个数。 受硬件影响。
ulimit -a ——> 当前用户下的进程,默认打开文件描述符个数。 缺省为 1024
修改:
打开 sudo vi /etc/security/limits.conf, 写入:
* soft nofile 65536 --> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】
* hard nofile 100000 --> 命令修改上限。
epoll:
int epoll_create(int size); 创建一棵监听红黑树
size:创建的红黑树的监听节点数量。(仅供内核参考。)
返回值:指向新创建的红黑树的根节点的 fd。
失败: -1 errno
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作监听红黑树
epfd:epoll_create 函数的返回值。 epfd
op:对该监听红黑数所做的操作。
EPOLL_CTL_ADD 添加fd到 监听红黑树
EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。
EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
fd:
待监听的fd
event: 本质 struct epoll_event 结构体 地址
成员 events:
EPOLLIN / EPOLLOUT / EPOLLERR
成员 data: 联合体(共用体):
int fd; 对应监听事件的 fd
void *ptr;
uint32_t u32;
uint64_t u64;
返回值:成功 0; 失败: -1 errno
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞监听。
epfd:epoll_create 函数的返回值。 epfd
events:传出参数,【数组】, 满足监听条件的 哪些 fd 结构体。
maxevents:数组 元素的总个数。 1024
struct epoll_event evnets[1024]
timeout:
-1: 阻塞
0: 不阻塞
>0: 超时时间 (毫秒)
返回值:
> 0: 满足监听的 总个数。 可以用作循环上限。
0: 没有fd满足监听事件
-1:失败。 errno
epoll实现多路IO转接思路:
lfd = socket(); 监听连接事件lfd
bind();
listen();
int epfd = epoll_create(1024); epfd, 监听红黑树的树根。
struct epoll_event tep, ep[1024]; tep, 用来设置单个fd属性, ep 是 epoll_wait() 传出的满足监听事件的数组。
tep.events = EPOLLIN; 初始化 lfd的监听属性。
tep.data.fd = lfd
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep); 将 lfd 添加到监听红黑树上。
while (1) {
ret = epoll_wait(epfd, ep,1024, -1); 实施监听
for (i = 0; i < ret; i++) {
if (ep[i].data.fd == lfd) { // lfd 满足读事件,有新的客户端发起连接请求
cfd = Accept();
tep.events = EPOLLIN; 初始化 cfd的监听属性。
tep.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);
} else { cfd 们 满足读事件, 有客户端写数据来。
n = read(ep[i].data.fd, buf, sizeof(buf));
if ( n == 0) {
close(ep[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL); // 将关闭的cfd,从监听树上摘下。
} else if (n > 0) {
小--大
write(ep[i].data.fd, buf, n);
}
}
}
}
epoll 事件模型:
ET模式:
边沿触发:
缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
LT模式:
水平触发 -- 默认采用模式。
缓冲区剩余未读尽的数据会导致 epoll_wait 返回。
结论:
epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 --- 忙轮询。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
int flg = fcntl(cfd, F_GETFL);
flg |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flg);
优点:
高效。突破1024文件描述符。
缺点:
不能跨平台。 Linux。
epoll 反应堆模型:
epoll ET模式 + 非阻塞、轮询 + void *ptr。
原来: socket、bind、listen -- epoll_create 创建监听 红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--
-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足
-- read() --- 小->大 -- write回去。
反应堆:不但要监听 cfd 的读事件、还要监听cfd的写事件。
socket、bind、listen -- epoll_create 创建监听 红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--
-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足
-- read() --- 小->大 -- cfd从监听红黑树上摘下 -- EPOLLOUT -- 回调函数 -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听写事件
-- 等待 epoll_wait 返回 -- 说明 cfd 可写 -- write回去 -- cfd从监听红黑树上摘下 -- EPOLLIN
-- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听读事件 -- epoll_wait 监听
eventset函数:
设置回调函数。 lfd --》 acceptconn()
cfd --> recvdata();
cfd --> senddata();
eventadd函数:
将一个fd, 添加到 监听红黑树。 设置监听 read事件,还是监听写事件。
网络编程中: read --- recv()
write --- send();
struct threadpool_t {
pthread_mutex_t lock; /* 用于锁住本结构体 */
pthread_mutex_t thread_counter; /* 记录忙状态线程个数de琐 -- busy_thr_num */
pthread_cond_t queue_not_full; /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
pthread_cond_t queue_not_empty; /* 任务队列里不为空时,通知等待任务的线程 */
pthread_t *threads; /* 存放线程池中每个线程的tid。数组 */
pthread_t adjust_tid; /* 存管理线程tid */
threadpool_task_t *task_queue; /* 任务队列(数组首地址) */
int min_thr_num; /* 线程池最小线程数 */
int max_thr_num; /* 线程池最大线程数 */
int live_thr_num; /* 当前存活线程个数 */
int busy_thr_num; /* 忙状态线程个数 */
int wait_exit_thr_num; /* 要销毁的线程个数 */
int queue_front; /* task_queue队头下标 */
int queue_rear; /* task_queue队尾下标 */
int queue_size; /* task_queue队中实际任务数 */
int queue_max_size; /* task_queue队列可容纳任务数上限 */
int shutdown; /* 标志位,线程池使用状态,true或false */
};
typedef struct {
void *(*function)(void *); /* 函数指针,回调函数 */
void *arg; /* 上面函数的参数 */
} threadpool_task_t; /* 各子线程任务结构体 */
rear = 5 % 5
线程池模块分析:
1. main();
创建线程池。
向线程池中添加任务。 借助回调处理任务。
销毁线程池。
2. pthreadpool_create();
创建线程池结构体 指针。
初始化线程池结构体 { N 个成员变量 }
创建 N 个任务线程。
创建 1 个管理者线程。
失败时,销毁开辟的所有空间。(释放)
3. threadpool_thread()
进入子线程回调函数。
接收参数 void *arg --》 pool 结构体
加锁 --》lock --》 整个结构体锁
判断条件变量 --》 wait -------------------170
4. adjust_thread()
循环 10 s 执行一次。
进入管理者线程回调函数
接收参数 void *arg --》 pool 结构体
加锁 --》lock --》 整个结构体锁
获取管理线程池要用的到 变量。 task_num, live_num, busy_num
根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。
5. threadpool_add ()
总功能:
模拟产生任务。 num[20]
设置回调函数, 处理任务。 sleep(1) 代表处理完成。
内部实现:
加锁
初始化 任务队列结构体成员。 回调函数 function, arg
利用环形队列机制,实现添加任务。 借助队尾指针挪移 % 实现。
唤醒阻塞在 条件变量上的线程。
解锁
6. 从 3. 中的wait之后继续执行,处理任务。
加锁
获取 任务处理回调函数,及参数
利用环形队列机制,实现处理任务。 借助队头指针挪移 % 实现。
唤醒阻塞在 条件变量 上的 server。
解锁
加锁
改忙线程数++
解锁
执行处理任务的线程
加锁
改忙线程数——
解锁
7. 创建 销毁线程
管理者线程根据 task_num, live_num, busy_num
根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。
如果满足 创建条件
pthread_create(); 回调 任务线程函数。 live_num++
如果满足 销毁条件
wait_exit_thr_num = 10;
signal 给 阻塞在条件变量上的线程 发送 假条件满足信号
跳转至 --170 wait阻塞线程会被 假信号 唤醒。判断: wait_exit_thr_num > 0 pthread_exit();
---------------------------------------------------
TCP通信和UDP通信各自的优缺点:
TCP: 面向连接的,可靠数据包传输。对于不稳定的网络层,采取完全弥补的通信方式。 丢包重传。
优点:
稳定。
数据流量稳定、速度稳定、顺序
缺点:
传输速度慢。相率低。开销大。
使用场景:数据的完整型要求较高,不追求效率。
大数据传输、文件传输。
UDP: 无连接的,不可靠的数据报传递。对于不稳定的网络层,采取完全不弥补的通信方式。 默认还原网络状况
优点:
传输速度块。相率高。开销小。
缺点:
不稳定。
数据流量。速度。顺序。
使用场景:对时效性要求较高场合。稳定性其次。
游戏、视频会议、视频电话。 腾讯、华为、阿里 --- 应用层数据校验协议,弥补udp的不足。
UDP实现的 C/S 模型:
recv()/send() 只能用于 TCP 通信。 替代 read、write
accpet(); ---- Connect(); ---被舍弃
server:
lfd = socket(AF_INET, STREAM, 0); SOCK_DGRAM --- 报式协议。
bind();
listen(); --- 可有可无
while(1){
read(cfd, buf, sizeof) --- 被替换 --- recvfrom() --- 涵盖accept传出地址结构。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
sockfd: 套接字
buf:缓冲区地址
len:缓冲区大小
flags: 0
src_addr:(struct sockaddr *)&addr 传出。 对端地址结构
addrlen:传入传出。
返回值: 成功接收数据字节数。 失败:-1 errn。 0: 对端关闭。
小-- 大
write();--- 被替换 --- sendto()---- connect
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd: 套接字
buf:存储数据的缓冲区
len:数据长度
flags: 0
src_addr:(struct sockaddr *)&addr 传入。 目标地址结构
addrlen:地址结构长度。
返回值:成功写出数据字节数。 失败 -1, errno
}
close();
client:
connfd = socket(AF_INET, SOCK_DGRAM, 0);
sendto(‘服务器的地址结构’, 地址结构大小)
recvfrom()
写到屏幕
close();
本地套接字:
IPC: pipe、fifo、mmap、信号、本地套(domain)--- CS模型
对比网络编程 TCP C/S模型, 注意以下几点:
1. int socket(int domain, int type, int protocol); 参数 domain:AF_INET --> AF_UNIX/AF_LOCAL
type: SOCK_STREAM/SOCK_DGRAM 都可以。
2. 地址结构: sockaddr_in --> sockaddr_un
struct sockaddr_in srv_addr; --> struct sockaddr_un srv_adrr;
srv_addr.sin_family = AF_INET; --> srv_addr.sun_family = AF_UNIX;
·
srv_addr.sin_port = htons(8888); strcpy(srv_addr.sun_path, "srv.socket")
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); len = offsetof(struct sockaddr_un, sun_path) + strlen("srv.socket");
bind(fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)); --> bind(fd, (struct sockaddr *)&srv_addr, len);
3. bind()函数调用成功,会创建一个 socket。因此为保证bind成功,通常我们在 bind之前, 可以使用 unlink("srv.socket");
4. 客户端不能依赖 “隐式绑定”。并且应该在通信建立过程中,创建且初始化2个地址结构:
1) client_addr --> bind()
2) server_addr --> connect();
对比本地套 和 网络套。
网络套接字 本地套接字
server: lfd = socket(AF_INET, SOCK_STREAM, 0); lfd = socket(AF_UNIX, SOCK_STREAM, 0);
bzero() ---- struct sockaddr_in serv_addr; bzero() ---- struct sockaddr_un serv_addr, clie_addr;
serv_addr.sin_family = AF_INET; serv_addr.sun_family = AF_UNIX;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(8888); strcpy(serv_addr.sun_path, "套接字文件名")
len = offsetof(sockaddr_un, sun_path) + strlen();
bind(lfd, (struct sockaddr *)&serv_addr, sizeof()); unlink("套接字文件名");
bind(lfd, (struct sockaddr *)&serv_addr, len); 创建新文件
Listen(lfd, 128); Listen(lfd, 128);
cfd = Accept(lfd, ()&clie_addr, &len); cfd = Accept(lfd, ()&clie_addr, &len);
client:
lfd = socket(AF_INET, SOCK_STREAM, 0); lfd = socket(AF_UNIX, SOCK_STREAM, 0);
" 隐式绑定 IP+port" bzero() ---- struct sockaddr_un clie_addr;
clie_addr.sun_family = AF_UNIX;
strcpy(clie_addr.sun_path, "client套接字文件名")
len = offsetof(sockaddr_un, sun_path) + strlen();
unlink( "client套接字文件名");
bind(lfd, (struct sockaddr *)&clie_addr, len);
bzero() ---- struct sockaddr_in serv_addr; bzero() ---- struct sockaddr_un serv_addr;
serv_addr.sin_family = AF_INET; serv_addr.sun_family = AF_UNIX;
inet_pton(AF_INT, "服务器IP", &sin_addr.s_addr)
strcpy(serv_addr.sun_path, "server套接字文件名")
serv_addr.sin_port = htons("服务器端口");
len = offsetof(sockaddr_un, sun_path) + strlen();
connect(lfd, &serv_addr, sizeof()); connect(lfd, &serv_addr, len);