经典数据:
APUE:unix环境高级编程3版
linux系统编程手册 德linux系统编程 oreily
unix内核源码剖析 日
windows核心编程
linux系统开发模式:
ssh远程登录即可;vi;
企业中:
版本控制:把代码写到版本库中(svn/git)由库提交到服务器
重点:shell 进程线程
进程概念:
程序进程线程
写了程序,启动起来就是进程。
并发
早期,单道,挂起
进程划分为多个片,时间轮转片
单道多道程序设计
mmu的理解: mmu最小页是4k
多进程为什么需要mmu:
程序经过4个步骤生成可执行程序,最后一步的链接需要指定链接地址。而在多任务时候,不同的任务可能指定相同的链接地址,
这就需要mmu把链接地址(链接地址是给CPU看的,所以是虚拟地址)映射到内存上(物理地址)
eg:ProgramA 被加载到地址 0x500-0x600 处, ProgramB 被加载到地址 0x700-0x800 处,同时建立了各自的地址翻译表,
当处理器要执行 ProgramB 时,会使用 ProgramB 对应的地址翻译表
综上:mmu的地址翻译实现多任务
mmu地址翻译功能好处:
MMU 的地址翻译功能还可以为用户提供比实际大得多的内存空间。用户在编写程序的时候并不知道运行该程序的计算机内存大小,
如果在链接的时候指定程序被加载到地址 Addr 处,
而运行该程序的计算机内存小于 Addr ,那么程序就无法执行,
有了 MMU 后,程序员就不用关心实际内存大小,可以认为内存大小就是“ 2^ 指令地址宽度”。
MMU 会将超过实际内存的虚拟地址翻译为物理地址进行访问。
《步步惊芯——软核处理器内部设计分析》一书的第10章 MMU剖析
内核区映射时pcb共用3G~4G 这是通信进程间通信基础
pcb描述进程,控制终端,当前工作目录,umask,文件描述符表,用户id组id
资源上限(ulimit -a)等
进程4态:就绪,运行,停止,挂起 通常忽略初始态
mmu映射的物理内存3~ 4G共用一块,因为3~4G中是内核区,用于管理各个进程等
环境变量:
man -f time 查找同名手册
1shell命令;2系统调用,即内核提供的函数;3库提供的库函数调用;4特殊文件/dev
5文件格式,配置文件格式 6游戏 7 杂项如man signal 7 man 8系统管理工具
which ls看ls的PATH
打印当前进程所有环境变量:
声明全局变量extern char ** environ;是一个指针数组
if(strncmp(environ[i],xPath,strlen(xPath))==0)
家目录下的新建bin文件里建立一个mksh
target=$1
gcc -o $target $target.c
chmod +x mksh
vi .bashrc 在44行 export PATH=$PATH:$HOME/bin
make test
getenv setenv看man 3 中的函数
PATH:是可执行文件的搜索路径,ls是可执行文件,a.out也是可执行文件。为什么必须得./a.out?
就是因为PATH里有ls的搜索路径,没有a.out的搜索路径
fork进程控制原语
ps -a可以看进程
程序是存放在存储介质上的一个可执行文件,而进程是程序执行的过程。进程的状态是变化的,其包括进程的创建、调度和消亡。程序是静态的,进程是动态的。
fork
进程是资源的分配单位,有独立的虚拟空间,线程共享这些虚拟空间,进程里包含很多线程,团队是进程,线程是人。
多进程健壮性好,因为有独立的空间,一个挂掉,其余不影响。多线程,一个挂掉会影响其他线程
进程间通信比较复杂 消息队列,共享内存,信号量来通信 线程间通信通过锁
返回两个返回值
成功:父进程返回子进程pid 子进程返回0
进程号pid最大值为65535
若写的是if并列语句 父子之间是竞争的
父子共用代码段(RO),而子进程把父进程的地址空间的数据段(全局变量)和堆栈进行复制(读时共享,写时复制,节省内存)
而父子进程的3~4G共用,这就是进程通信的基础 子进程所独有的只有进程号计时器等。
父子进程共享文件描述符
通过打印,知道谁先运行
sleep意味着 放弃cpu权限
为什么要子进程? ls -l:shell是父进程,ls是子进程
gdb调试 set follow-fork-mode child和set follow-fork-mode parent默认 注意得在fork之前设置
exec族函数:如果我们本来就运行着一个程序(进程),我们如何在这个进程内部启动一个外部程序,
由内核将这个外部程序读入内存,使其执行起来成为一个进程呢?这里我们通过 exec 函数族实现。
exec 函数族,顾名思义,就是一簇函数,在 Linux 中,并不存在 exec() 函数,exec 指的是一组函数,一共有 6 个
#include <unistd.h>
子进程加载一个进程
一旦执行,子进程就不回头了,除非错误发生才有返回值
a.out(exec)执行了b.out,则b.out就会覆盖(0~3G)了a.out,再从头执行
exclp("ls","ls","-l","-a",NULL)第一个参数得借助环境变量
execl("/bin/ls","ls","-l","-a",NULL)
execv换汤不换药v是argv的意思 excle()临时改变环境变量
exec +l(list)+p(有了环境变量可直接使用文件名)+e(environment)
dup2:是两个非常有用的系统调用,都是用来复制一个文件的描述符,
使新的文件描述符也标识旧的文件描述符所标识的文件。但是 dup2() 复制出来的新文件描述符
可以指定任意一个合法的数字。
常用于把本来通过 printf() 显示到屏幕上的内容,不显示在屏幕上,而让这些内容写入一个文件
fd2 = dup2(fd1, 1);
printf("fd2 ============== %d\n", fd2);
打印是库函数 最终还是会调用系统调用函数write(1),而现在1中是文件描述符fd1 所以打印到fd1中
shell命令中有很多内嵌了printf的函数,execlp("ps","ps","aux",NULL)也会打印出来
进程的控制:结束进程、等待进程结束。
#include <sys/types.h>
#include <sys/wait.h>
可以通过 exit() 或 _exit() 来结束当前进程。#include <stdlib.h>
当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号,相当于告诉父亲他哪个儿子挂了,
而父进程可以通过 wait() 或 waitpid() 函数等待子进程结束,
获取子进程结束时的状态,同时回收他们的资源(相当于,父亲听听死去儿子的遗言同时好好安葬它)。
调用 wait() 函数的进程会挂起(阻塞),(就是说父进程会阻塞等待)
直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒(相当于继续往下执行)
status: 进程退出时的状态信息。一般为NULL
wait返回值是成功清理的id 一个wait只能回收一个
宏的用法比较简单 固定的典型用法:
回收所有子进程:while(pid=wait(NULL)) 把成功回收的id 打印出来
while(pid=wait(NULL)>0)有没有必要?????
wait典型用法:
int status;
wait(&status);
if(WIFEXITED(&status)){正常返回,可接受子进程的返回值 返回值是WEXITSTATUS(status)}
if(WIFSIFNALED(status)){不正常返回(被某个信号杀死了),看哪个信号用WTERMSIG(status)}
waitpid典型用法:
wait(status) 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。
成功:返回清理掉的子进程ID;失败:-1(无子进程);
waitpid(pid_t pid, int *status, in options)
第一个参数:
> 0 回收指定ID的子进程;-1 回收任意子进程(相当于wait);0 回收和当前调用waitpid一个组的所有子进程;
< -1 回收指定进程组内的任意子进程
返回0:参3为WNOHANG,表明无子进程退出,且子进程正在运行;
返回>0有子进程退出;
返回-1所有子进程回收完毕
while(1) pid=waitpid(0,NULL,WNOHANG)
if(pid>0)有退出 if(pid<0)退出完毕
sleep 即sleep时间内轮询子进程有无退出
孤儿进程:父进程先被回收 子进程while(1); 孤儿会被init(root的init或user的init)回收 即孤儿的父亲变成了1
僵尸进程:进程已运行结束,但进程的占用的资源未被回收
子进程死了(僵尸) 父进程不回收while(1)不调用wait; 用户空间会释放,但是内核的PCB会残留
ps aux Z+ [没了]<没了>
ps -ef|grep defunct 后面尖括号里是 defunct 的都是僵尸进程
ps -e 查看所有进程
孤儿进程:
都写成while(1); ps ajx kill -9 父进程 杀死父进程后 子进程的ppid变成1 init是孤儿院.....
此时<CC>停止不了(因为cc发给的是shell,而非子进程) 只能kill
僵尸不好,占空间,用wait/waitpid 看死的原因
wait/waitpid和
wait作用:阻塞等待子进程退出 回收子进程资源 看死亡原因
ps ajx 看不到僵尸
程序一退出或父进程一退出,所有的都没了。也就没有僵尸了
IPC进程间通信:
pipe:使用简单 父子兄弟。。。 无名管道
fifo:非血缘关系间 命名管道
mmap:速度最快 效率最高
socket:本地套接字 稳定性最好
signal:开销小
还有消息队列(淘汰了),文件等进行进程间通信
pipe:伪文件 bcps
socket 双向全双工
一般情况下父进程读 子进程写+sleep 有阻塞态
一般先运行父进程,父进程读 通常会等待 等待子进程的写
ls |wc -l 把所有带l的个数列出来
子进程中把写的东西传到输出端
execlp(ls ,ls,null)
父进程把读给 标准输入,再执行wc
13号信号 管道写错误信号
mkfifo testfifo创建管道
计算机行业至少得3,4语言 从一门先开始,辅助操作系统。
首先把自己讲明白 自我解决问题
*****************************************
fifo:可用命令也可以函数创建既可以血缘关系又可以非血缘关系
mkfifo myfifo。mkfifo 库函数是man 3
eg:非血缘关系间
mkfifo("test_fifo",0644);
open只读或只写 fifo文件
while(1) 组包 write+strlen sleep1
while(1) read+sizeof sleep3
开两个终端 创建fifo文件 先写后读(读端关闭 pipe中会报错sig)
一个写端多个读端 多个写一个读 最好单向流动
pipe中:先pipe 后fork 这样可以继承父的pipe
fifo中:open(只读还是只写) 先确定读写
fifo有多大????????????????????????????
write的写时从当前位置写 别考虑覆盖问题
perror头文件:stdio中
open头文件: sys/types.h sys/stat.h fcntl.h
exit头文件:stdlib.h
fork头文件:unistd.h
wait头文件:sys/types.h sys/wait.h
mmap:文件用于进程间通信 sys/mman.h
磁盘上的文件映射到了内核区,此内核区对外开放
文件通信:1父子进程间 2非血缘关系间
之所以不用文件通信 因为读写指针每次都得打开 还得找,而且只有一个
所以用mmap 第一个参数一般传NULL 映射大小(字节)非0(malloc可以为0) 读写 标志位(对映射区的修改反应到磁盘上)
文件描述符 偏移一般写0映射全部必须是一个page的整数倍大小
成功返回地址 失败返回宏MAP_FAILED
eg:一般情况
open 644
ftruncate==lseek+写一个字符(引起IO操作)ftruncate的个数是实际个数( 若不够则有空洞符)
stat 获取文件大小
mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);//反应到磁盘上
strcpy 操作字符类 或 memcpy() 操作非字符类????
munmap(p,4)
close
mmap建立映射区,完成进程间通信
总结:创建映射区 若用MAP_SHARED mmap中必须得有读的操作,因为得反映到磁盘上,
创建映射区的过程中,隐含着一次对映射文件的读操作。所以open时 必须有read
映射区的权限应 <=文件打开的权限(出于对映射区的保护)
当以只读方式open时 MAP_SHARED只是充当占位的作用,恰当的写法最好写成MAP_PRIVATE***********
申请4字节 实际上申请的是4k的整数倍内存 可以越界操作(拷贝很多) 但是不严谨
若文件4字节只有 偏移写成4096 会出现错误
eg:父子进程间 :尤其注意读写指针,若不重新open 读写指针共用一根
通过文件版本:
因为内核区共享 所以进程间可以通信
与fifo一样得在fork之前得先映射好
open O_TRUNC 若存在则清空 建立一个hollo.c 生成dentry dentry里包括文件名和inode inode可以找到i节点 i节点里有很多属性和盘符地址 地址中存了代码
硬链接只是又创建一个dentry rm的时候只是删除dentry(硬链接计数)
unlink("temp") 删除dentry(文件目录项) 使之具备释放条件
ftruncate(fd,4)指定文件大小为4字节
mmap MAP_PRIVATE时 是另一层含义:父子间共享不了,所以还得写成MAP_SHARED
close 过河拆桥 卸磨杀驴
fork
父子进程都可以操作同一个mmap句柄
在父进程中关闭映射区
eg:父子间简洁版本:
直接mmap flag加上MAP_ANONYMOUS, 文件描述符为-1
fork
父进程中munmap 比上面轻松太多
但是 unix没有MAP_ANONYMOUS
无限的向 /dev/null 写数据
无限的向 /dev/zero 读数据 里面全是0
open zero
mmap fork ......
strace ./test 看都掉了哪些系统调用
eg:非血缘关系间
只能通过读写普通文件
open 0644
ftruncate sizeof(struct)
mmap close
while(1) memcpy
open 只读
mmap+close
while(1) 打印
MAP_ANON不行(文件描述符为-1) /dev/zero不可以 普通文件可以
flag为 MAP_SHARED
mmap可以重复读取 根本原因是他是数组而非队列
3匿名映射
练习:多进程的文件拷贝 简单的shell 简单的本地聊天室程序
read write复制文件 本来是一个进程
多进程 获得了更多的时间片 获取了cpu的跟多资源
偏移:lseek(系统编程 比较复杂综合起来不如mmap) fseek(库函数 也复杂)
父子进程 p+100
建议复习苏炳问给的例子 很值得反思!!!!!!!!!
老师讲的那个人
lxr
include->linux->fs.h->struct file{
文件操作结构体
文件描述结构体
*****************************************
复习:
文件血缘非血缘通信的机理:内存只有一份,文件描述符可有多分
读常规文件时 不会阻塞.c 等等。 读管道(伪文件) 设备文件会阻塞
fcntl文件杂货铺 什么都可以做 这里设置非阻塞 F_GETFL 和 F_SETFL 利用 O_NONBLOCK 设置非阻塞。
或open(O_NONBLOCK)
eg:
char flg=fcntl(fd/STDIN_FILENO,F_GETFL);flg|=O_NONBLOCK; fcntl(fd,F_SETFL,flg)
while(1){read}
非阻塞:读不到 (可以从read里出来 不会阻塞在read里) 则睡1s 读到了 则输出
阻塞:读不到 一直等待等待 (阻塞在read函数里面出不来了) 读到了输出
10ns滴答时钟到达 触发硬件中断 强制中断 切换进程
信号的优先级最高 内核软件产生
gdb直接run就可以到段错误的地方
read函数返回值:
1. 读到实际数据 :> 0
2. 读到文件末尾 := 0 或管道对端关闭 或套接字对端关闭
3. 读文件失败:-1
EAGAIN或 EWOULDBLOCK说明,文件被设置为非阻塞方式读取,此刻没有数据到达,应该轮询。
EINTR或ECONNABORTED(网络中) 慢速系统调用,信号被打断 ,应设置重启:goto again....
信号:线程的出现消弱了信号 但信号还是很强
本质:软中断,内核发送,处理
特性:延迟性(因为是软中断)
四要素:编号9;名称SIGKILL;默认处理动作term(终止进程) core(终止进程产生core文件) ign stop(暂停) cont;触发事件
推荐使用宏名
称呼:
递达(信号立即被OS处理)vs阻塞、屏蔽(处于未决状态)
OS处理有3种:1忽略(已经递达了) 2执行默认动作:内核中断 3捕捉:自己写的中断
未决信号集:pending(位图):记录信号是否产生 并且被处理
阻塞信号集/信号屏蔽字:mask:记录信号是否设置屏蔽
kill的前31个是普通信号 后33个是与驱动有关
cc使pending第二位变成1,递达内核处理,内核杀死进程,再由1变成0 一般情况递达==处理
............... 若mask的第二位为1,则一直处于未决态 因为屏蔽了 无法递达
用户只能通过设置屏蔽从而控制信号是否是未决态
信号操作函数:信号集set mask操作 pending操作
信号捕捉:signal signaction 内核实现信号的捕捉
产生信号:
1硬件 cc 2:发送sigint到当前程序
cz 20:sigtstp当前程序挂起,暂停 可以用bg使其后台继续运行,fg使其转入前台运行。
c\ 3: sigquit 并生产core文件
19 sigstop 特殊
9 sigkill 特殊
2system call:kill raise abort
kill向指定进程发送指定信号 kill(getpid(),SIGSEGV)
kill -9 1 是不行的:普通用户是无法向root的init进程发送信号的
raise向本进程发送指定信号 raise(SIGSEGV)
abort向本进程发送终止信号SIGABRT
3软件条件产生:alarm
alarm:惭愧 就是写不出来
alarm 进程不共享alarm 一个进程只有一个定时器
alarm5---3---alarm3 返回值上次定时剩余时间
取消闹钟:alarm0
采用自然定时 与运行状态无关
为什么会 嘛爪???
硬件异常:段错误 除0 内存对齐总线错误
setitimer函数:第一个参数ITIMER_REAL自然定时法
第二个参数itimerval类型结构体(里有it_interval结构体设置循环定时,it_valuse结构体设置第一次定时)
简单用法:与alarm一样 时间到了就发信号 终止进程
复杂用法:signal(SIGALRM,myfun); setitimer(ITIMER_REAL,&it,*old);时间一到就到捕捉函数里执行
4命令产生:kill
段错误:1访问非访问区域 ,1000 printf() 终极原因是mmu没有对该地址映射
2对只读区进行写操作 char*p="aaaaa" p[0]='o' mmu映射了但权限是1内核权限 用户访问不了
3内存溢出 char buf[10] buf[10]='9' buf[100]='q' mmu最小页是4k
信号集(set)操作:
sigset_t set;
sigemptyset(&set) 置空
sigfillset(&set) 置高
sigaddset(&set,待添加的信号名称) 添加信号到集合中
sigdelset(&set,待)
sigismember(&set,待)1在 0不在 -1错误
mask操作:上面也是然并卵 下面才能真正的写到mask中
位操作简单粗暴 下面方法温柔些
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
sigprocmask(哪种以下三种,&set,老的mask)
SIG_BLOCK:mask=mask|set 将set中的信号添加到mask屏蔽字中 即添加屏蔽位
SIG_UNBLOCK:mask=mask&~set 即解除set位为1的相应mask的屏蔽
SIG_SETMASK:简单粗暴
pending操作:只能读
int sigpending(sigset_t *set);
sigpending(&set) 只能传出未决信号集set
返回值:成0失败-1 errno
9kill,19stop不能捕捉,屏蔽和忽略 只能默认处理
cc-2init c\--3quit 通常捕捉2 3
信号捕捉: 一般主函数中都是while 捕捉函数执行完后还可以继续循环
signal简单 信号和回调函数
signal(SIGINT,do_sig) 一旦有信号来就执行捕捉函数
比较智能的一点是:do_sig(int signum) 可以直接判断if (signum == SIGINT)signum的值
返回值是SIG_ERR代表错误
***四部曲:句柄 flag mask 注册
sigaction复杂太多:
定义结构体sigaction act
这里只设置act属性的三个参数即可:
act.sa_handler=do_sig;还可传入宏:设置为默认或忽略
act.sa_flags=0; 这是本信号被屏蔽的根源!
sigemptyset(&act.sa_mask);设置所要屏蔽的信号集合(只在执行捕捉函数时有效!) 这里默认不设置 若要设置后面
加一句:“sigaddset(&act.sa_mask,SIGQUIT);
最后注册(install)新的act:sigaction(SIGINT,&act,NULL) 老的act保存到NULL里 即不关心老的
此安装貌似伪mask的操作
捕捉思想:由内核在信号产生,递达之后负责回调,调用结束返回内核 再返回用户空间
当前捕捉函数执行时!,mask根据设置屏蔽其他,让当前捕捉函数执行完,执行完后再恢复mask
信号捕捉函数执行期间,本信号自动被屏蔽:act.sa_flags=0;
信号捕捉函数执行期间,若本!!信号多次产生,只记录1此 信号不支持排队 而来了屏蔽信号则‘支持排队’....表达不是太清楚
由于执行捕捉函数后pending由1变0了
阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)
******************************************************
信号--线程同步---socket 难度依次递增
信号捕捉的特性比较常用
了解内核实现信号捕捉过程:
sigaction其他情况 ,使用sigaction函数捕捉信号
user因为信号(中断pending)有1陷入内核 内核调user捕捉函数 再次通过sc陷入内核(保存恢复现场) 内核可能再处理其它的未决信号 最终返回user
时序竞态,竟态条件:
pause函数:等待信号唤醒 返回-1并设置errno为EINTR
unistd.h 包含了sys/***.h
丢人!
一般:alarm到了 发出信号sigalarm回调sigalarm的函数返回内核(pending由1-->0) 唤醒pause即返回-1 继续向下执行
如果信号的默认处理动作是终止进程,则进程终止,pause函数么有机会返回。
忽略 继续挂起忽略
只有 捕捉,捕捉后返回-1.。。。。
竟态: alarm没到之前 cpu易主 执行其他100s 而alarm(自然定时与运行状态无关) 到了定时时间内核向进程发出!信号sigalarm (pending变为1)
因为cpu易主暂时无法执行进程捕捉函数,更别提pause了。(alarm的pending仍为1)。 100s之后 该程序获取cpu (先处理信号再处理pause)
获取cpu就处理这个信号(递达)调捕捉(因为pending是1)。捕捉函数执行完后 才返回user,执行pause()挂起等待,pause()等不到信号,因为信号已经被处理了,就卡
这里了
究其终极原因:发出信号的
时候当前进程没有获取cpu 虽然获取cpu的时候可以执行接下来的捕捉函数 但是pause没有得到信号 就卡那里了
也就是说 pause没有得到信号
简而言之:pause只有 捕捉到信号和执行捕捉函数后 才正常返回 而现在是发送捕捉信号和执行捕捉函数分离了
结论:pause不好
解决方法:设置alarm时候,屏蔽alarm信号 一旦时间到了pending就是1,但是1也是然并卵,因为屏蔽了,无法递达。
所以继续向下运行。
不管cpu在其他上运行多长时间,当回来时调用 sigsuspend()这个函数里设置的是不屏蔽alarm信号
并等待alarm信号到来 这个函数的作用是:解除屏蔽和等待alarm是一个原子操作 而且pending是1
所以就会执行,不会卡那里
不明白的一点:屏蔽 proc(block)为什么得屏蔽?如果不屏蔽,信号立即来,会立即调用捕捉函数,捕捉函数执行完后,
再回user sigsuspend就执行不了了 防止这种情况发生 所以得屏蔽。得保证让这种情况发生在suspend的里面
如果信号
竟态:提早预见 主动规避
全局变量的异步IO:
还是cpu易主的问题 解决方法:原子操作
SIGUSE1/2 自定义信号
1 多个进程操作同一个全局变量。
2 写操作 没有任何同步(保护)机制
可重入不可重入
有全局变量,static,malloc free的函数为 不可重入函数
信号捕捉函数应封装为可重入函数
strtok不可重入,第一次传字符串,第二次传NULL 里面有static变量。所以不可重入
可重入的系统函数 man 7 signal
SIGCHLD信号的应用
sigchld产生条件:只要子进程发生改变,就会向父进程发送
eg:
子进程终止时
子进程接收到SIGSTOP信号停止时
子进程处在停止态,接受到SIGCONT后唤醒时
以前的回收机制:通过用户调用:
wait作用:阻塞等待子进程退出 即回收子进程
waitpid:wnohang设置不阻塞,轮训 即回收子进程
父进程都得消耗很大资源(轮训和阻塞得实时判断)
现在的通过设置信号和捕捉函数在捕捉函数里wait 高效回收子进程
eg:父进程设置捕捉:sigaction(SIGCHLD,&act,NULL);捕捉函数里waitpid(0,&status,WNOHANG)
用while而不用if
???????????????????????????????????????
信号传参:发送少量的信号
sigqueue函数对应kill函数,但可在向指定进程发送信号的同时携带参数
sigqueue(pid,信号, const union sigval v);
union是这样的联合体union sigval{int sival_int(可以传int); void* sival_ptr(指针比较强大 可以传结构体字符串.....)}
有发送就有接受:接受还是用sigaction结构体 里面有5个成员变量 以前只用3个
这里介绍第二个参数:void (*sa_sigaction)(int ,siginfo_t *,void *);
siginfo_t是一个非常复杂的结构体:里面有发送信号(父进程)的各种信息:pid uid status.......
eg:父进程sigqueue(kill发信号sigusr1)
子进程注册sigusr1的捕捉函数:sigaction不用handler了用介绍的第二个参数:act.sa_sigaction=fun
fun有3个参数。sa_flags = SA_SIGINFO不能传0了
即:父进程给子进程发一个信号(带有参数),子进程捕捉到了后在捕捉函数中通过siginfo_t结构体把父进程发的各种信息搞出来
信号中断系统调用
慢速sc:可以永久阻塞的 read write pause wait .....
唤醒慢速系统调用的信号应该被 捕捉,不能屏蔽 忽略 默认
返回 -1 errno=EINTR
sa_flags:
0 不重启 即屏蔽本信号
1 SA_SIGINFO:捕捉函数有3惨的 sa_sigaction
2 SA_INTERRUPT:慢速系统调用,若信号被打断 不重启
3 SA_RESTART:。。。。。。重启
4 SA_NODEFER:捕捉期间,本信号不被屏蔽
创建进程组、会话
我们用的图形虚拟终端 封装了文字终端cc alt+.... 文字终端通过敲入命令,shell解析命令 进行人机交互
ssh 是网络终端
创建守护进程
1 位于linux后台,服务进程
2 独立控制终端,周期性执行某任务,等待事件
3 不随用户注销而终止
4 通常采用以d结尾的名字
1创建子进程同时 退出父进程
2调用setsid创建新会话 成为组长社长脱离了终端
3改变工作目录到不可卸载的路径上
4修改 umask(0022)
5关闭文件描述符012(重定向)
6编写守护进程核心程序
7退出
shift 2 K 我的是K 直接跳到那个地方
返回error number 与 set errno 的区别:返回error number 用strerror(number)打印。
set errno 可直接调perror函数打印
**********************************************
线程:
什么是线程:
light weight process
线程进程区别联系:
都有各自pcb,都在3~4G,在这个1G内核空间中,进程只能有1个PCB,线程可有很多pcb(不科学的表述)。即进程独享地址空间,线程共享
线程是最小的执行单位(时间片) 进程是最小分配资源单位(公用4G进程空间)
内核实现线程的原理:
PCB中有页目录-页表-物理页面。每个进程都有各自PCB,PCB中的页表也都不同,最终映射的地址也都不同
每个线程都有各自PCB,但都公用PCB中的页目录。这就是内存公用的根源
程序(进程)运行时得有2部分支撑:用户栈,内核栈(reg保存环境。处理,调度),当用户程序切换时候得保存恢复环境,就保存到内核栈(reg)中
毫无疑问 不同线程的内核栈各不相同
eg:ps -LF 2984 看2984线程中有多少线程LWP号 最终:LWP号是最小执行单位
不同进程内可有相同的线程id号
线程之间共享不共享:面试
(文工IP)除了stack 共享0~3G.text/ordata/(前面的只读,后面的rw).data(已初始化的全局变量)/.bass(未初始化的全局变量)/heap
共享:文件描述符表 信号处理方式(pending) 用户id组id 工作目录位置
(SMIEu) 不共享:mask stack(user kernel) 线程id errno 线程优先级
线程优缺点:
线程间通信方便---通过全局变量。(进程不共享全局变量,mmap映射才能共享)
开销小,节省room
提高并发:时间片,最小执行单位
第三方库函数不稳定,gdb不支持,对信号的支持不好
线程控制原语:重点
pthread_t tid;
pthread_create(&tid,NULL,tfn,NULL/(void*)i)......主控函数
return 0(主函数中)=exit(0) 退出进程
return 返回到调用者那里
pthread_exit(NULL) 退出线程 线程退出但malloc出的资源共享
pthread_exit(void *(10))-->pthread_join(void **)
pthread_exit(void *); -----》 pthread_join(viod **retval) ==> void * --- void **
exit(1)/ return 0; -----》 wait(int *status)===> int --- int *
pthread_join是阻塞等待 不是太好 所以用线程分离
pthread_cancel()只有当子线程陷入内核时 才能杀死,否则杀不掉。所以while(1)里至少加上一个testcancel()陷入内核
只要有pthread_detach这句话 后面有阻塞等待pthread_join() 返回(不成功)一个错误码 而非0,就自动略过此函数 并且返回值都是22号即是EINVAL无效的参数
游离后还想join是不可能的 游离自然退出当然不能接收线程的返回值
pthread_cancel()主函数发送给子线程 子线程死 主函数中阻塞等待可以等待成功join返回0 但是接受子线程返回值返回-1
线程进程控制原语对比:
pthread_create() fork()
pthread_self(void) getpid()...
pthread_join(tid,void **) waitpid(int *status)
pthread_exit(void *) exit(int)
pthread_cancel(tid) kill(pid,signum)
pthread_detach()
pthread_equal()
线程属性:
几乎用不到
stack是固定的8M
分离态:
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);
线程栈大小:
1个进程可以起500个线程 均分了8M
可以修改每个线程所均分的大小
线程使用注意事项:
getconf GNU_LIBPTHREAD_VERSION 看pthread的版本库2.19版
gcc 编译指定-lpthread
1 主控线程调用用 pthread_exit 别用exit否则全都结束
2 避免僵尸线程,回收、分离、借助属性分离线程。
3 线程主函数中避免返回局部变量地址值
4 malloc mmap可被其他线程释放
5 信号复杂 避免与线程混合使用
6 线程中避免fork ,除非立即使用exec. fork的子进程中只有。。。。。。其他线程pthread_exit()结束。。。。。。。。。
***********************************************************
同步 线程同步的概念
协同步调指定先后次序
资源共享 调度随机,所有多个控制流,共同操作同一共享数据的场景,都需要同步机制
线程同步方法:
互斥量(互斥锁):只有一把锁;失去cpu也不会失去锁;只要访问数据先判断锁;只有一个事件结束才解锁
是个建议锁(协同锁)。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,NULL);pthread_mutex_destroy(&mutex);
pthread_mutex_lock。加锁,加锁失败--阻塞
pthread_mutex_unlock。解锁,解锁成功---唤醒阻塞在mutex上面的所有线程。
restrict 只能通过本指针修改 内部内容
初始化后锁为1,而0代表忙的意思
加锁不成功就阻塞等待-------------这一点很不好所以有了trylock-加锁失败返回EBUSY---所以得轮询,但这样效率又不高
线程解锁后,唤醒全部的其它阻塞的锁,开始竞争
应该在访问共享数据之前加锁,访问共享数据之后解锁,锁的粒度越小越好!
同一个线程连续加锁两次必挂,不同的线程加锁,会等待第一把锁释放掉
死锁:不是线程同步的方式而是一种现象
死锁1:对同一互斥量加锁加锁两次 ,一直等第一个锁就阻塞那里了
死锁2:若果无法获取所有锁的时候,放弃已掌握的锁;规定访问共享线程数据的顺序;
根本原因是一个线程有两把锁 一旦一个阻塞全都阻塞
不同的锁锁了相同的东西
读写锁:读共享 写独占 写优先级高 eg: r r r w 后三个阻塞
pthread_rwlock_t rwlock;pthread_rwlock_init(&rwlock, NULL);_destroy()
_wrlock/_rdlock/_trywrlock/_tryrdlocd/_unlock;
当读线程多于写线程时,提高数据访问效率
也是建议锁
条件变量:本身不是锁,也能造成线程阻塞。记录公共资源里是否有产品 想象场景多个线程公用一个锁(互斥量)
pthread_cond_wait(&cond,&mutex);隐喻:之前是pthread_mutex_lock(),因为原子操作里有解锁;
原子操作:解锁的同时阻塞等待cond被唤醒(signal和broadcast)·
若唤醒 lock互斥量 wait结束。吃掉 再解锁
pthread_cond_t cond;pthread_cond_init(&cond, NULL);pthread_cond_destroy(&cond);
pthread_cond_signal(); 唤醒阻塞在条件变量上的一个线程。pthread_cond_broadcast(); 唤醒阻塞在条件变量上的所有线程。
pthread_cond_timedwait(); 指定时间等待条件变量满足。参数3: 绝对时间。 struct timespec { } 参考: man 3 sem_timedwait
生产者消费者模型:当东西放进仓库里时候,就会发出signal
锁和信号都是全局变量 最方便的用法是定义的时候也初始化(静态初始化),这就免了用的时候调用init的麻烦
生产者线程pid 线程函数中 加锁--添加到仓库--解锁 发信号 睡一段时间
消费者线程cid 加锁 wait 删除节点 解锁 free
公共区
此仓库用的链表 与个数没有关系所以不需要头节点 只需要一个头指针就可以了 最简单的数据结构
对公共区添加条件变量 互斥量 加以保护
if与while区别
那么什么时候会出现虚假唤醒呢?
在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程)。
条件变量可以提高效率:。。。。。。。。。。。。。。。。。。。
1把锁1个条件变量2个线程
初始化(mutex/cond): 动态初始化:pthread_xxxx_init(); 子线程主函数内。
静态初始化:pthread_xxxx_t var = PTHREAD_XXXX_INITIALIZER; 全局位置
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
信号量:进化版互斥锁,当成N看待
sem_t sem;
sem_init(&sem,1/0,n) 1进程间0线程间 n支持n个不能超过上限;sem_destroy(&sem);
sem_wait:==lock。先判断n>0,再--。若n=0,则阻塞
sem_post:==unlock。信号量++,同时唤醒阻塞在信号量上的线程
sem_trywait(&sem);尝试将信号量--, 当信号量为0时, 不阻塞。直接返回-1, 设置errno=EAGAIN
绝对时间: 从当前时间,到1970年1月1日 00:00:00 所经过的秒数。 Unix操作系统的计时元年。
time_t time = time(NULL); 系统当前时间
struct timespec t_var;
t_var.tv_sec = time+3; 在当前时间基础上+3秒。t_var.tv_nsec = t_var.tv_sec + 10;
sem_timedwait(&sem, &t_var);典型用法给这个函数套上while(1) 一旦时间到达就不会等待或sempost给信号量发信号也不会等待
生成者消费者模型:
两个线程1个生成1个消费。
time_t time=time(NULL)当前时间
2把锁2个信号量(多用锁)2个线程
进程同步:
信号量
一.什么是信号量
信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)
所拥有。
信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为0,说明
它被占用,测试的线程要进入睡眠队列中,等待被唤醒。
sem_init(&sem,1,N)
互斥量:在互斥锁函数的基础上增添pthread_mutexattr_t属性
pthread_mutexattr_t mutexattr; 得到mutex属性结构体变量
pthread_mutexattr_init(&mutexattr);pthread_mutexattr_destroy(&mutexattr);
pthread_mutexattr_setpshared(&mutexattr,宏)
宏:PTHREAD_PROCESS_PRIVATE线程间(缺省);PTHREAD_PROCESS_SHARED进程间
pthread_mutex_init(&mutex,&mutexattr) 初始化用于进程间的互斥量
文件锁
fcntl可以实现:可以open但无法read write,即是加锁
第二个参数是F_SETLKW(lock) F_SETLK(trylock) F_GETFL(获取文件锁的各个属性,不常用)
第三个参数是个结构体flock 可设置锁类型,分段加锁,起始位置,长度,加锁的进程号(只在get时用)
l_type: F_RDLCK,F_WRLCK,F_UNLCK;l_whence:SEEK_SET, SEEK_CUR, SEEK_END;l_start:l_len: 加锁的长度 (0:整个文件)
写独占 读共享
多线程间共享文件描述符,而给文件加锁,是通过修改文件描述符所指向的文件结构体中的成员变量来实现的。
因此,多线程中无法使用文件锁。
哲学家吃饭问题:
震荡死锁
创建5个线程 5把锁,每个线程传一个参数,传递完5个子函数一起竞争
都是:先给左手加锁,紧接着再尝试给右手加锁 成功则吃失败则去锁
线程同步可以写到简历上
********************************************************************
UNP
重点:cs模型实现的tcp 客户端服务器程序 for(i)
协议概念:
即规则 ftp协议
bs cs模型:
cs:QQ 1缓存数据到本机,提高通信效率 2协议的选择灵活,可自定义,裁剪
1对用户,安全性低 2开发工作量大 3受平台限制
bs:1工作量小 只开发服务器 2安全性高 3偷菜游戏并不受平台限制 4开发不了3D
5协议选择不灵活
提高通信效率用cs 降低工作量,跨平台用bs
分层模型 数据)应用层)传输层)网络层)数据链路层)
osi七层模型:应 表 会 传(tcp udp;端口) 网(ip地址) 数(速度) 物
tcpip 4层模型:应 传 网 网络接口层(重点)
点到点 端到端:
端到端是传输层的,你比如你要将数据从A传送到E,中间可能经过A->B->C->D->E,对于传输层
来说他并不知道b,c,d的存在,他只认为我的报文数据是从a直接到e的,这就叫做端到端。
总之,一句话概括就是端到端是由无数的点到点实现和组成的
协议格式:
传输层:传输协议和端口设置
网络层:通过ip找吓一跳 进行路径选择
链路层:通过ip找mac arp机制
以太网帧(数据包格式):数据链路层(位于最低层,通过ip寻找mac地址)
链路层:为什么会发到交换机 就是因为链路层
链路层的前6字节是目的地址(arp报通过目的ip获取目的mac地址),接着6字节是我的mac......
arp数据报格式:6目的 6源地址mac 2帧类型0806 。。。 发送端以太网地址(mac) 发送端ip 目的端以太网地址 目的ip
一开始目的mac是16个f 现已知对方ip为前提。交换机广播arp数据报 各主机对比ip 若符合 填充mac到16个f上
目标扔出去arp包(已交换ip mac) 找到我
填充以太网帧格式包的前6位
ip段格式:通过ip找吓一跳
网络层封装(路由表,吓一路由56跳比较多)
4位版本 4位首部长度 16位总长度 8位ttl 32位源/目的ip(终点IP,而非下一跳IP)
广域网通信方式:每个路由器中都有路由表 表中记录了所有的ip 会递归的寻路。跳到下一个路由(下一跳),TTL--;
TTL为0 则该路由器清除此包
tcp数据包格式:传输层 面向连接
得建立连接,16位源端口号,目的端口号 4位首部长度 6位标志位 16位窗口大小 32位序号 32位确认序号
udp数据包格式:传输层 无连接
16位源端口号,目的端口号。端口:在一台主机上唯一标示一个进程
ip地址:在网络中唯一标示一个主机
ip+端口:标示指定主机的指定进程。网络环境中唯一标示一个进程
端口号上限:65535
eg:QQ 微信 飞秋 发QQ只能对方的QQ接
私有ip:仅在局域网内有效
共网ip:才是真正的ip 在整个网络环境中可见
eg:新浪给你发消息:先找到你连的公网路由器,路由器里有nat映射表(你的私有ip+端口号)找到你的电脑
只要挂在公网服务器的路由器都是映射出来的。
打洞:视频聊天 直接通信快。公网服务器将两台私有的陌生的ip间直接通信 就不用公网服务器转发了
交换机:只有net映射表,没有路由表
路由器:两者都有
一层一层往下封装 最后形成以太网帧:以太网首部、ip首部、tcp首部、应用数据(6-1460字节)、尾部。
以太网帧:662 mac地址 帧类型:0800发送应用数据 0806是arp数据报
ip段:4位版本 8位ttl 32位源/目的ip(终点IP,而非下一跳IP)
tcp报(不说udp报):16位端口号 16窗口大小 32位序列号(1000 8000) 6个标志位
例如 HTTP 服务默认 TCP 协议的 80 端口, FTP 服务默认 TCP 协议的 21 端口, TFTP
服务默认 UDP 协议的 69 端口
三次握手:传送4个重要信息 syn 32位序列号 最大报文长度 窗口大小
socket编程:
socket:套接字 本质:内核提供的缓冲区 与管道一样 socket=ip+端口号+两个缓冲区
一定成对使用 双向全双工
网络字节序:
大端:高位存低地址
小端:高存高
网络环境:大端法 本地环境:小短发
操作函数:
htonl -- 本地---网络---32 -- IP地址
htons -- 本地---网络---16 -- Port号
ntohl -- 网络---本地---32
ntohs -- 网络---本地---16
ip地址转换:strIP <---> 网络字节序IP
man htonl:本地转到网络 32位 转IP地址
str--long--htonl--net:麻烦
用函数int inet_pton(int,const char *src,void *dest)直接把本地ip字符串转成网络字节序
af:ipv4时用 AF_INET ipv6时用AF_INET6
src:字符串类型的ip地址
dst:传出参数:转化后的网络字节序的ip地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af: ipv4: AF_INET;ipv6: AF_INET6;
src:网络字节序的IP地址。
dst:用来存储转化后的IP地址。
size: dst的大小。
是不是一般把字符串传出到一个数组中 都要指定这个数组的大小??? 是的
socketaddr地址结构:
定义变量: struct sockaddr_in saddr;此结构体封装了协议 端口号 ip地址
初始化:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
使用: bind(fd,(struct sockaddr_in*)&saddr,) 即把端口、ip绑定到socket上
cs模型实现的tcp 客户端服务器程序:
建立套接字socket(ip+port) bind; listen; accept阻塞等待客户端connect发送信号 3函数正常返回则表明建好了socket
int socket(int domain, int type, int protocol);
domain指定用哪种协议AF_INET/AF_INET6;socket的协议流式还是报式SOCK_STREAM--流式协议、SOCK_DGRAM--报试协议;
protocol:0流式默认TCP 1报式默认udp 返回套接字fd
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 绑定服务器:IP、服务器:端口号 到套接字上。
sockfd:socket函数的返回值。
addr:sockaddr_in 类型结构体变量地址。(IP、端口号)
addrlen: 地址结构长度。
int listen(int sockfd, int backlog); 设置同时向服务器发起连接的客户端数量 -- 10
sockfd:socket函数的返回值。
backlog:数量上限
设置同时向服务器发起链接(3次握手状态)的客户端最大量
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客户端发起连接请求。
只要有连接 就调用成功 上层应用就会从内核拿到一个连接??????????????????????????????
sockfd:socket函数的返回值。
addr:传出参数 返回客户端!的结构 ip和端口
addrlen:进来时是结构体大小,出来时是实际大小
返回值:返回一个新的fd,指向客户端的套接字。
返回一个新的非负fd(用于与客户端通信的,翻译上式被socke接受的,呵呵)用于与客户端通信,前面的fd是描述socket自己的
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 主动向服务器发起连接请求
sockfd:客户端socket函数的返回值。
addr:地址结构(服务器IP、服务器的port)
addrlen: 地址结构长度。
跟bind很像,都是绑定服务器的ip地址和端口号,其实,客户端socket内部也隐式绑定了客户端的ip和端口号
只有socket的传入的地址结构用的不是服务器ip 其他情况下都是用服务器ip
cc strace ./server看阻塞哪儿了
nc 127.0.0.1 6666 写一个小写 返回一个大小 即:nc:连接到服务器127.0.0.1 的6666端口
服务器一般就传那个参数 是因为它可以监听本地所有ip地址
socket ipv4,流式协议,TCP
bind 套接字lfd 套接字地址结构 大小
listen 套接字lfd,个数
accept 套接字lfd,客户端地址结构,客户端大小 成功则返回指向客户端的cfd
while
read cfd
write cfd
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。
*****************************************************
多道程序设计的 -- 分时复用 操作系统 ----- 贝尔实验室、AT&T、 -- 失败。
DOC ----- 单道程序设计。
肯。汤姆森: 小 而 精巧。
unics --- PDP7 --- 玩游戏。
汇编。
1969 ----- unix -- C改写。
丹尼斯。李琦。 --- C语言之父 1971。
美 苏 冷战。 ----美 国防部 出资 TCP/IP ---
大学:非商业发布 (Unix + C + TCP/IP)
客户端实现:
TCP 三次握手:建立连接:SYN ACK 嵌入式中都是2次握手
对应API 是connect accept的过程
四次握手:半关闭,关闭链接。对应API是close shutdown
FIN 我给你说的话说完了,你有事儿吗?
syn 1000(0):发一个占0个字节的数据包(这就是那个32位的序号)(1000)标志位SYN占1个字节
ack 1001:即1001之前的包的数据包都已经收到
mss最大报文长度:1460字节。一个数据包最大报文长度,都是各自的缓冲区大小
MTU最大传输单元:1500字节 在以太网帧里面包括有 IP头TCP头和数据
TCP有滑动窗口、MTU,面向连接。而UDP没有滑动窗口: MTU的1500字节是指 看以太网帧 可以推出最大报文长度为1460字节也就是所谓的数据长度
滑动窗口:win 4096: <mss 1460>是:一次发送不能超过1460字节 一般设为1024 而win 4096滑动窗口:
总缓存有4096个字节 也就是说最多连发4次,发满之后总缓存变为0 得等待服务器处理缓存
给对端提供一个发送数据的上限值,进行流量控制 win是16位的所以最大值不能超65535
manpage不区分大小写 bind() 可以封装为Bind()
7,8遍了才会这么熟 唉
封装思想:简化逻辑 首字母大写 浅封装
readn readline不是太明白
多进程并发服务器:
子进程负责与客户端通信 父进程:监听客户端连接请求 注册捕捉函数回收子进程
思路:
socket 初始化 bind listen accept() read () write() close();
while(1){accept fork}
多线程并发服务器。
1. socket() bind() listen() accept() read () write() close();
2. 子线程用来跟客户端通信。—— pthread_create();
3. 主线程。 监听客户端连接请求。 子线程1 :pthread_join(); pthread_detach();---修改线程。
4. 客户端通信---小写--大写: 子线程的主控函数。
************************************************************
回去之前 打听2年工作经验 多少钱 平均水平
北京8--14K 武汉9K挂掉就5K。 大城市 跳 小城市。
1天面2家 金9银10。5年工作经验回去。
****************************************************************
TCP状态转换图:非常重要---王者
主动发起连接:
Close --- SYN --- SYN_SEND(说明SYN标志位发送完成)---- 接收ACK ---- ESTABLISHED(数据通信状态)
主动关闭连接:
ESTABLISHED --- FIN --- FIN_WAIT_1 (说明FIN标志发送完成) --- 接收ACK --- FIN_WAIT_2(【*】半关闭状态)
--- 接收FIN、发ACK --- TIME_WAIT (2MSL) --- close
被动发起连接:
close --- LISTEN --- SYN --- SYN_RCVD (说明SYN标志位接收完成,ACK发送结束,SYN发送完成)
--- 接收ACK --- ESTABLISHED(数据通信状态, 3次握手建立完成)
被动关闭连接:
ESTABLISHED --- 接收FIN、回ACK --- CLOSE_WAIT (对端半关闭完成) --- FIN ---- LAST_ACK --- 接收ACK --- close
2MSL:netstat -apn|grep 8001 看正在占用的端口的各种信息:fin_wait_2 close_wait。
MSL只发生在主动关闭一方 CLOSE_WAIT (跟FIN_WAIT_2 成对出现)
大概40s 保证4次握手关闭连接有效、更可靠。
服务于 TIME_WAIT “主动关闭连接一方”, 在发送了最后一个ACK之后,处于 TIME_WAIT 状态。等待2MSL时长。
实现端口复用:端口等的时间40s很长,这就催生了端口复用
getsockopt()太复杂了 UNP第七章
int opt = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
放bind之前,粘贴上直接就可以用
自己看看端口状态 还是TIME_WAIT状态-------------------------------未实验
半关闭:对应API是close
主动关闭连接一方,处于 FIN_WAIT_2 。 4次握手--完成2次。
只能接收数据,不能发送数据(但能发ack,ack不是数据)。
shutdown:shutdown(cfd,SHUT_RD) 关闭了接受端
3,4,5文件描述符都指向同一个文件,close(3)时,4,5都还可以访问文件
但是shutdown 一次性关闭所有。
多路IO转接服务器:非常重要 只是一种机制
监听客户端连接请求(while(1){accept}) 交给了内核(select,poll)完成
select监听到有连接请求,有,则server,client间建立连接(accept connect)。
select监听是否有数据到达(write) 有 ,则server 读。
select:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds: 所监听所有的fd中,最大的fd+1
readfds: 所监听的fd的读事件集合。
timeout: 1. NULL 没有条件满足,永久等待。2. > 0 等待时长 3. 0不等待 --- 应配合轮询机制。
返回值: readfds、writefds、exceptfds集合中,满足条件的 【总】 个数。
select有自己的set读表,里面记录要监听的cfd和lfd select监听读 看是否有cfd向服务器写 和 连接请求
.................写表,。。。。。。。。cfd..... .........写.... ...................读。。。。。。
fd_set set;
FD_ZERO(&set);FD_SET(cfd1, &set);FD_CLR(lfd, &set);FD_ISSET(fd, &set);
操作函数:
监听、转接 实现方法,思想。
poll:
区分优缺点
select : 跨平台。 POSIX标准。
受1024文件描述符上限限制。
返回满足条件的总数。---- 轮询查找关心的fd --- 自定义数组管理fds
poll: Linux
不受1024文件描述符上限限制。 --修改参数。
返回满足条件的总数。 -----轮询查找关心的fd --- poll提供管理fd的数组。
********************************************************************
同步、异步:交给我来做是同步 交给内核做是异步
实际上同步与异步是针对应用程序与内核的交互而言的。同步过程中进程触发IO操作并等待或者轮询的去查看IO操作是否完成。
异步过程中进程触发IO操作以后,直接返回,做自己的事情,IO交给内核来处理,完成后内核通知进程IO完成。
cxy吃苦不算事儿,只要性能提升上去
epoll多路IO转接模型:一般情况下epoll占据了异步IO的大部分 但是不能跨平台
设置突破1024文件描述符限制,poll也可以。 而无论怎样改select依然受1024的限制!
ET LT
epoll操作函数:linux的典型代表
epoll_create(); 创建一个监听红黑树, 返回树根
参数size: 红黑树节点的个数(参考值,不具有强制性) 返回值:文件描述符, 红黑树的树根。
epoll_ctl(); 操作红黑树(添加、删除、修改节点)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数1: epoll_create 返回值---红黑树的根节点。
参数2:EPOLL_CTL_ADD 向红黑树中添加一个节点 EPOLL_CTL_MOD 修改 3. EPOLL_CTL_EDL 删除红黑树上的一个节点
参数3: fd指代待操作的文件描述符-----客户端socket
参数4: struct epoll_event结构体变量 &event
struct epoll_event {
__uint32_t events; //监听fd的事件 : EPOLLIN、EPOLLOUT、EPOLLERR
epoll_data_t data; // 一个联合体 --- 将满足监听条件的文件描述符直接返回。
}
epoll_data_t {
int fd; //应该与epoll_ctl()参3一致
void *ptr; uint32_t var; uint64_t var;}
返回值: 成功 0; 失败-1,errno
epoll_wait(); 阻塞等待 监听树上的fd条件满足。 --- select()、poll();
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
参数1:epoll_create 返回值---红黑树的根节点
参数2:数组地址 struct epoll_event(传出参数)--- 只用来记录满足监听事件的fd
参数3:数组容量
参数4:超时时长 -1:阻塞。0:不阻塞,立即返回 --- 轮询。> 0: 设置超时时长, 单位毫秒
返回值:成功:总的满足条件的监听个数。超时:时间到达,返回0. 失败: -1, errno
epoll实现多路IO转接服务器:
create:创建红黑树返回红黑树根节点efd;ctl:数上添加lfd(顺便设置事件);wait:监听树;
若事件发生,阻塞函数的传出数组里只记录 发生的event的fd
普通多路IO: 红黑树 —— 添加待监听的结点,epoll_ctl,EPOLLIN,fd
—— 监听,epoll_wait, 返回满足监听事件的fd的总个数,传出参数 events数组,内部元素,满足对应监听事件的fd
—— 判断对应事件, Accept、Read。
PS1=$ 好屌 把当前路径赋值给$
epoll-LT水平触发:
当 epoll_wait 被触发后,read 剩下的数据依然能触发 epoll_wait
epoll-ET边沿触发:
当 epoll_wait 被触发后,read 剩下的数据不能触发 epoll_wait, 需要等客户端,发送新的数据,epoll_wait才被触发。
event= EPOLLIN |
每个文件格式中都有一个header,里面有文件的总述,30K。只需先读30k。
LT是必须得读完所有数据 ET可以读一部分
默认水平触发
readn:实际传来了10字节 但n要读500字节 所以阻塞这里了 要想readn不阻塞就得设置fctl
结论:异步IO需求:
epoll-ET + 非阻塞 轮询
epoll反应堆模型:( libevent 网络编程开源库 核心思想)
普通多路IO转接服务器: 红黑树 —— 添加待监听的结点 —— epoll_ctl —— EPOLLIN —— fd —— 监听 —— epoll_wait ——
返回满足监听事件的fd的总个数 —— 传出参数 events数组 —— 内部元素 ——满足对应监听事件的fd
—— 判断对应事件 —— Accept、Read。——循环 epoll_wait 监听
epoll反应堆模型: 创建红黑树 —— 添加监听结点 —— epoll_ctl —— EPOLLIN —— fd —— 监听 —— epoll_wait —— 将结点从树上摘下
—— 大写转小写 —— 修改fd的监听事件 —— EPOLLOUT —— 重新添加到红黑树 —— 监听 —— epoll_wait —— 写数据到
客户端 —— 再将结点从树上摘下 —— 修改监听时间 —— EPOLLIN —— 挂上红黑树监听。
添加监听写事件的目的: “滑动窗口”已满,绕过写。epoll_wait满足后再进行写。
struct epoll_event { events, data } data -- ptr --》 {void (*func)(int fd, void *arg}
epoll反应堆模型:
心跳机制:从无信号阶段到有信号的时候,不用三次握手了。服务器知道你掉线了
客户端每隔300us向服务器发送心跳包 若发5次都连不上服务器 则三次握手
乒乓包:朋友圈多少评论 在心跳包基础上额外携带数据
问问服务器有没有人给我点赞 有 则服务器发送50个人给你点赞
两者都是维持长连接,进行必要的沟通。
探测分节:
判断对方是否还在。具有强制性:必须立即回答。
2小时11分15秒 几乎用不到。
线程池:系统编程难度顶峰!(半天~1天) 至少 2遍以上+研究代码
这就是底层开发的........
********************************************************************
建议:系统的学习内核的计划和复习计划。
大体脉络 干什么事 分模块 不要深入细节
迭代二次开发 学习方法 画图 干什么事
UDP数据包就比TCP包 简单。
UDP协议格式:针对不稳定的网络层,做行为。TCP —— 完全弥补97‰ 。 UDP —— 完全不弥补。
TCP:面向连接的可靠数据传输。—— 打电话。
优点: 1. 数据传输可靠。2. 稳定: 数据内容稳定。流量稳定。速率稳定。
缺点: 1. 效率低。开销大。2. 速度慢。
应用场景: 重要文件、数据传输。 大文件。
UDP:无连接的不可靠的报文传递。—— 发短信。
优点: 1. 速度快。开销小。2. 实时性强。效率高。
缺点: 1. 数据传输不可靠2. 稳定差
应用场景:对实时性要求较高场合:视频聊天、视频电话、分屏广播。
腾讯:TCP --- TCP + UDP ---- UDP + 应用层校验。———— 自行开发应用协议。
总结:
TCP传数据走的是固定的路线,数据包是按先后顺序传送的 而UDP不是,短信乱序,丢包97%%丢了就不会再次发送了
TCP握手 滑动窗口限制 ack回值机制 效率低。UDP不建立连接 没有回值机制
UDP C/S服务器模型:
UDP时没有窗口机制 所以得加大服务器缓冲区或应用层CRC
TCP:SBLAR SC
三次握手建立连接--Server————Socket()、Bind()、 Accept()、read()、转大写、write()
三次握手建立连接--Client————Socket()、隐式绑定、Connect()、write()、read()
UDP: SBR SS
无需建立连接--Server——Socket()、Bind()、recvfrom(sfd,buf,bufsize,0,客户端的地址结构(传出),地址结大小(传入传出)
无需建立连接--Client——Socket()、隐式绑定、sendto(cfd,buf,strlen,0,服务器端的地址结构(传入),地址结构大小)、recvfrom)
默认UDP支持 多路IO。--- 并发服务器。
结论:send:只能用于TCP连接的C、S模型。
比较差异:
UDP默认支持多路IO转接 而day08只能起一个客户端(因为accept在while(1)外面只执行了一次,后面的多进程服务器accept在while里面)
nc命令用于TCP
UDP客户端只能用sendto:sendto里面有绑定(服务器IP、服务器的port)但不等价send+bind(服务器IP、服务器的port)
UDP广播:广播是基于UDP的
路由器转发给各个ip+客户端端口号不能隐式绑定
server:绑定ip为255 协商好端口9000 给server进程分配广播权限:setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&flag,sizeof(flag));
client:本地ip设为0000或段内随意写(inaddr_any) 设进程端口 一个端口只能有一个进程
因为广播的机制 所以得设置端口一致
server:S(B)SS
1. 广播地址:192.168.22.255
2. PORT: 客户端的端口号 固定。 9000
3. 开发server广播权限。
int flag = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag));
4. sendto (,,, 广播地址结构);
client:SBR
1. socket() 创建套接字
2. 必须要bind(不能依赖隐式绑定)端口 要跟广播的端口号一致。 9000
3. recvfrom();
UDP组播:
239.0.0.0~239.255.255.255 应用于局域网通信的组播地址。
对广播分组,左边收A 右边收B。
239.0.0.2 这个组播地址是第二组。则客户端需设置自己的组(加入组播组)
ip ad看网卡 一般有线网卡编号是2
grep -r "struct ip_mreqn" /usr/include/ -n行号
struct ip_mreqn {
imr_multiaddr: 组播地址。(239.0.0.0--239.255.255.255任意一个)
imr_address:本机IP (INADDR_ANY == "0.0.0.0")
imr_ifindex: 网卡序号 (获取手段:1. 命令:ip ad 2: if_nametoindex() ) }
server:初始化结构体ip_mreqn{组播地址,本机ip(0000),网卡编号} 创建组播组setsockopt sendto协定号的端口
client:初始化结构体ip_mreqn{一模一样} 加入组播组setsockopt
只要是类广播 server的bind都可有可无 client必须bind
server:
1. socket() 创建套接字
2. bind()可有可无。server自己port。
4. struct ip_mreqn group结构体 初始化。
5. 创建一个组播组: setsockopt( ,IP_MULTICAST_IF,&group,)
3. 初始化地址结构,客户端PORT -- 9000
6. sendto();
client:
1. socket() 创建套接字
2. 必须要bind(不能依赖隐式绑定) 端口 要跟广播的端口号一致。 -- 9000
3. 初始化 struct ip_mreqn group地址结构。 imr_multiaddr: 组播地址。 imr_address:本机IP。imr_ifindex: 网卡编号
4. 加入组播组: setsockopt(, IP_ADD_MEMBERSHIP, &group, );
5. recvfrom();
++++++++++++++++++++++++++++++++++++
服务器sendto客户端 里面是255+端口号 或组播地址+端口号
客户端ip随意0000 但端口号必须与协商的一样 recv可以传NULL不需要知道服务器的属性信息
++++++++++++++++++++++++++++++++++++
分屏软件设计思想:
安装客户端) 调分辨率 压缩
1. 截屏模块,截取本地屏幕。得到图片。24帧。 几M。
2. 降帧12帧、8帧。保证数据发送效率。
3. server端选用适当的算法进行压缩。几十KB
4. client端接收到数据包之后安装相同算法的逆方式,解压缩。
5. 进一步提高效率,只将有变化、改动、坐标区域,截取,压缩,发送。
6. 各个应用模块的优化。
本地套接字domain:
由网络socket衍生到本地socket进程间通信
本质就是一个伪文件的缓冲区
bind建立各自的sock缓冲区(bind各自的套接字文件) 客户端connect服务器 服务器accept客户端
两个套接字用于通信连接
struct sockaddr_un {
sun_family = AF_UNIX; //本地套接字 AF_LOCAL也可以
sun_path = socket文件名; }
socket(); 参1: AF_UNIX:
参2(TCP UDP都可以):SOCK_STREAM: 流式协议, 参3: 0;---TCP
SOCK_DGRAM: 报式协议,参3: 0;---UDP
bind(); 创建 sun_path 所指定的文件。
bind()调用之前,应该确保对应的目录下,没有sun_path所指定的文件。
比较 网络socket 和 本地socket:
网络socket 本地socket
server:lfd = Socket(AF_INET, SOCK_STREAM, 0); lfd = Socket(AF_UNIX, SOCK_STREAM, 0);
bzero()--- struct sockaddr_in bzero()--- struct sockaddr_un
sin_family = AF_INET; sun_family = AF_UNIX;
sin_port / sin_addr.s_addr (服务器IP+Port)----- sun_path (本地套接字文件名)
bind(,sizeof(struct sockaddr_in)) bind(,offsetof(struct sockaddr_un, sun_path)+strlen())创新文件
listen(); listen();
Accpet(); 传出client的IP+Port Accpet(); 传出 client 套接字文件名
client:
cfd = Socket(AF_INET, SOCK_STREAM, 0); cfd = Socket(AF_UNIX, SOCK_STREAM, 0);
隐式绑定 socket IP+Port(客户端的) ---------- Bind(, offsetof()+ strlen())客户端套接字文件名,创新文件
bzero()--- struct sockaddr_in bzero()--- struct sockaddr_un
sin_family = AF_INET; sun_family = AF_UNIX;
sin_port / sin_addr.s_addr (服务器IP+Port)----- sun_path (服务器套接字文件名)
Connect(cfd, (struct sockaddr *)&servaddr, len); Connect(cfd, (struct sockaddr *)&servaddr, len);
++++++++++++++++++++++++++++++++++++++
bind建立各自的sock缓冲区(bind各自的套接字文件) 客户端connect服务器 服务器accept客户端
++++++++++++++++++++++++++++++++++++++
----------------linux系统、网络完结------------------------------------------------------------------
看总结 看重点
多进程并发服务器:智能家居
setsockopt:复用 分节 组播 广播 UDP缓冲区拓展大小 看那本书
开源库 libevent libv
开发快,不用上面的。封装
跨平台、精简、开源
wc -l | *.c
main.c --- a.out -L库路径 -l库名 -I库对应头文件位置
./configure --prefix /usr即把库安装到/usr目录下
编译 安装最后在/usr/lib目录下看那些.so文件
源码好复杂: install库出现libs目录 里面有.so
再看example 从example入手 读画
3天可以看函数接口(画图) 2个月可以看具体内部实现
libv线程支持比libevent好一些
压力测试:
wareshark:抓包
webbench 十大小清新网 7000多客户端
redis也是开源的
开源