Linux/UNIX系统编程手册gg


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基础知识

Linux/UNIX系统编程手册gg_第1张图片

显示行数:

    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编译:

Linux/UNIX系统编程手册gg_第2张图片

    4步骤: 预处理、编译、汇编、连接。

    -I:    指定头文件所在目录位置。

    -c:    只做预处理、编译、汇编。得到 二进制 文件!!!

    -g:    编译时添加调试语句。 主要支持 gdb 调试。

    -Wall: 显示所有警告信息。

    -D:    向程序中“动态”注册宏定义。   #define NAME VALUE

库文件制作

Linux/UNIX系统编程手册gg_第3张图片


静态库制作及使用步骤:

    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 查看

拓展:数据段合并

Linux/UNIX系统编程手册gg_第4张图片

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.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 文件编译生成同名可执行文件。
        
 

四.基本IO


open函数:

    int open(char *pathname, int flags)    #include

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

        flags:文件打开方式:    #include   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 
#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 非阻塞状态。(默认为阻塞状态)

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

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


 

    


    
    

    

    

    


    
 

    


 

你可能感兴趣的:(Linux,linux,unix,运维)