Linux系统: “所见皆文件”

一个比较好的博客

一.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

二.vim基础知识

显示行数:

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指令

----------------------------------------------------------------

三.gcc  gdb  Makefile

gcc编译:

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调试

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     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.outa.out:hello.o add.o sub.o div1.o #mul.ogcc hello.o add.o sub.o div1.o -o a.outadd.o:add.cgcc -c add.c -o add.o
sub.o:sub.cgcc -c sub.c -o sub.o
hello.o:hello.cgcc -c hello.c -o hello.o
div1.o:div1.cgcc -c div1.c -o div1.o#mul.o:mul.c
#    gcc -c mul.c -o mul.osrc = $(wildcard *.c)    # add.c sub.c div1.c hello.c
obj = $(patsubst %.c, %.o, $(src)) # add.o sub.o div1.o hello.oALL:a.outa.out: $(obj)gcc $(obj) -o a.outadd.o:add.cgcc -c add.c -o add.o
sub.o:sub.cgcc -c sub.c -o sub.o
hello.o:hello.cgcc -c hello.c -o hello.o
div1.o:div1.cgcc -c div1.c -o div1.osrc = $(wildcard *.c)  # add.c sub.c div1.c hello.c
obj = $(patsubst %.c, %.o, $(src)) # add.o sub.o div1.o hello.oALL:a.outa.out: $(obj)gcc $(obj) -o a.outadd.o:add.cgcc -c add.c -o add.o
sub.o:sub.cgcc -c sub.c -o sub.o
hello.o:hello.cgcc -c hello.c -o hello.o
div1.o:div1.cgcc -c div1.c -o div1.oclean:-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.oALL:a.outa.out: $(obj)gcc $^ -o $@add.o:add.cgcc -c $< -o $@
sub.o:sub.cgcc -c $< -o $@
hello.o:hello.cgcc -c $< -o $@
div1.o:div1.cgcc -c $< -o $@clean:-rm -rf $(obj) a.outsrc = $(wildcard *.c)    # add.c sub.c div1.c hello.c
obj = $(patsubst %.c, %.o, $(src)) # add.o sub.o div1.o hello.oALL:a.outa.out: $(obj)gcc $^ -o $@%.o:%.cgcc -c $< -o $@clean:-rm -rf $(obj) a.outsrc = $(wildcard ./src/*.c)  # ./src/add.c ./src/sub.c ...
obj = $(patsubst ./src/%.c, ./obj/%.o, $(src))  # ./obj/add.o ./obj/sub.o ...inc_path=./incmyArgs= -Wall -g ALL:a.outa.out: $(obj)gcc $^ -o $@ $(myArgs) $(obj):./obj/%.o:./src/%.cgcc -c $< -o $@ $(myArgs) -I $(inc_path)  clean:-rm -rf $(obj) a.out.PHONY: clean ALL

作业:编写一个 makefile 可以将其所在目录下的所有独立 .c 文件编译生成同名可执行文件。

四.基本IO

open函数:

int open(char *pathname, int flags)    #include <unistd.h>

参数:
        pathname: 欲打开的文件路径名

flags:文件打开方式:    #include <fcntl.h>  file control

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

close函数:

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");

read函数:

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

参数:
        fd:文件描述符

buf:存数据的缓冲区

count:缓冲区大小

返回值:

0:读到文件末尾。

成功;    > 0 读到的字节数。

失败:    -1, 设置 errno

-1: 并且 errno = EAGIN 或 EWOULDBLOCK, 说明不是read失败,而是read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据。

write函数:

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

参数:
        fd:文件描述符

buf:待写出数据的缓冲区

count:数据大小

返回值:

成功;    写入的字节数。

失败:    -1, 设置 errno

cp命令实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>int main(int argc, char *argv[])
{char buf[1024];int n  = 0;int fd1 = open(argv[1], O_RDONLY);  // readif (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 非阻塞状态。(默认为阻塞状态)

fcntl:

int (int fd, int cmd, ...)

int flgs = fcntl(fd,  F_GETFL);

flgs |= O_NONBLOCK

fcntl(fd,  F_SETFL, flgs);

获取文件状态: F_GETFL

设置文件状态: F_SETFL

lseek函数:

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();
 }

stat/lstat 函数:

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 <sys/socket.h>

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 <arpa/inet.h>

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);

Linux/UNIX系统编程手册gg相关推荐

  1. linux/unix系统编程手册11-15

    title: linux/unix编程手册-11_15 date: 2018-05-27 11:53:07 categories: programming tags: tips linux/unix编 ...

  2. Linux/Unix系统编程手册 第三章:系统编程概念

    本章介绍系统编程的基础概念和一些后续章节用到的函数及头文件,并说明了可移植性问题. 系统调用是受控的内核入口,通过系统调用,进程可以请求内核以自己的名义去执行某些动作,比如创建子进程,执行I/O操作, ...

  3. Linux/Unix系统编程 五:进程

    进程是一个可执行程序的实例. 一.linux系统进程管理 1.进程管理的作用 判断机器健康状态 查看系统中所有进程 杀手进程 2.查看系统进程 1.ps -aux BSD操作系统格式: TTY说明: ...

  4. Linux系统编程手册-源码的使用

    Linux系统编程手册-源码的使用 转自:http://www.cnblogs.com/pluse/p/6296992.html 第三章后续部分重点介绍了后面章节所要使用的头文件及其实现,主要如下: ...

  5. 5w字总结 Unix系统编程学习笔记(面试向)(Unix环境高级编程/Unix环境程序设计)

    文章目录 一.计算 C语言的数据表示与处理 计算 C语言的基本运算操作 内存表和符号表 类型转换 函数类型的分析 指令 复合指令 句法 函数 函数激活(Activation Record) 函数激活定 ...

  6. LINUX C系统编程与PYTHON中的时间模块对比

    今天看python时间模块time的时候发现和LINUX系统编程中的时间调用函数基本一样,以前刚好没有好好学习LINUX C编程的时间模块就对比进行了学习. 本文只是给出函数接口和使用方式,详细了解请 ...

  7. ①Linux简明系统编程(嵌入式公众号的课)---总课时12h

    10.09 注意:这个是Linux高级编程的简明教程,是Linux应用程序的开发,而不是底层程序的开发. 内容是关于操作系统和网络编程的吗? Linux简明系统编程 〇.课程思维导图 〇.会用到的头文 ...

  8. 【Linux】系统编程之文件(标准I/O库)

    目录 一.文件I/O与标准I/O的区别(open与fopen) 1.来源 2.移植性 3.适用范围 4.文件IO层次 5.缓冲 二.函数fopen.fwrite.fread.fseek.fclose ...

  9. UNIX系统编程(1)

    注:本文来自"网易"博主,仅阅读,学习 第一章:什么是系统编程  UNIX系统编程,简单的说就是"C语言+系统调用(system call)",学会了C语言再知 ...

最新文章

  1. 简单的分级别写日志程序
  2. hdu 5212 : Code【莫比乌斯】
  3. 终于不会看不懂LSTM了
  4. PL/SQL如何设置 窗口列表默认显示
  5. r语言 xmlto html,使用R语言将XML转换为CSV(示例代码)
  6. bigdecimal判断等于0_shell 脚本中if判断的条件总结
  7. 盘式制动优于鼓式制动吗?
  8. 【CTF】题目名称:破译 题目类型:Web
  9. 批量修改注册表之Bat脚本
  10. 【上传文件至服务器】
  11. 小米(xiaomi)红米(Redmi)手机一开机就自动重启:find device closed unexpectedly
  12. 本地打印后台处理程序服务没有运行和windows无法连接到打印机拒绝访问
  13. 镜头的MTF曲线分析和原理
  14. android 排他button,排他思想案例
  15. 从安装过程品国产Linux操作系统的技术来源与异同之处
  16. pve万兆网卡驱动_PVE+lede+DSM网卡硬盘直通+win10
  17. Pentest Wiki Part5 提权
  18. OpenSSL BIO 自我扫盲
  19. 搜索引擎:检索技巧(Google谷歌,百度)+ 常用的资源网站及技巧
  20. 【STM32CubeIDE和ITM SWV实现printf调试信息的打印】

热门文章

  1. Ubuntu18.04装录屏软件OBS-Studio 21.1
  2. 【金融大屏项目】—— Echarts水滴图(echarts-liquidfill)
  3. 网易我的世界手机版服务器文件在哪个文件夹,网易我的世界手机版怎么导入地图...
  4. ingress 七层负载均衡器
  5. 用Python中的Numpy实现简单高效的扑克牌API(附代码)
  6. EasyExcel代码层面设置写出的Excel样式、以及拦截器策略的使用、自动列宽设置、EasyExcel默认设置详解
  7. Electron桌面应用打包流程详情
  8. XUPT 寒假算法集训第二周
  9. php 图片木马检测
  10. YOLO如何训练分类网络???