Linux编程笔记4【个人用】

目录

一、多进程

1.进程

[1.1 什么是程序](#1.1 什么是程序)

[1.2 什么是进程](#1.2 什么是进程)

2.管理进程

[2.1 进程表](#2.1 进程表)

[2.2 查看进程](#2.2 查看进程)

[2.3 列的含义说明](#2.3 列的含义说明)

3.创建进程

[3.1 system函数](#3.1 system函数)

[3.2 exec系列函数](#3.2 exec系列函数)

[3.3 fork](#3.3 fork)

fork和exec的区别

[3.4 wait](#3.4 wait)

3.进程间通信

[3.1 按照通信类型分为四⼤类](#3.1 按照通信类型分为四⼤类)

共享存储器系统

管道通信系统

消息传递系统

客户机服务器系统

[3.2 管道通信](#3.2 管道通信)

原理

[无名管道(pipe)](#无名管道(pipe))

[命名管道(named pipe)](#命名管道(named pipe))

[3.3 文件权限(扩展)](#3.3 文件权限(扩展))

[3.4 消息队列( messagequeue )](#3.4 消息队列( messagequeue ))

消息队列对比管道的优势

使用流程

[3.4.1 msgget](#3.4.1 msgget)

[3.4.2 msgctl](#3.4.2 msgctl)

[3.4.3 msgsnd](#3.4.3 msgsnd)

[3.4.4 msgrcv](#3.4.4 msgrcv)

[3.5 信号 (sinal)](#3.5 信号 (sinal))

[3.5.1 kill](#3.5.1 kill)

[3.5.2 signal](#3.5.2 signal)

[3.6 共享内存(shared memory)](#3.6 共享内存(shared memory))

[3.6.1 shmget](#3.6.1 shmget)

[3.6.2 shmat](#3.6.2 shmat)

[3.6.3 shmdt](#3.6.3 shmdt)

[3.6.4 shmctl](#3.6.4 shmctl)

[3.7 信号量(semophore)](#3.7 信号量(semophore))

原子操作

[3.7.1 semget](#3.7.1 semget)

[3.7.2 semop](#3.7.2 semop)

[3.7.3 semctl](#3.7.3 semctl)

二、多线程

1.程序与进程、线程的关系

[2. 线程与进程的区别](#2. 线程与进程的区别)

3.多线程函数

[3.1 pthread_create](#3.1 pthread_create)

[3.2 pthread_exit](#3.2 pthread_exit)

[3.3 pthread_join](#3.3 pthread_join)

[3.4 pthread_cancel](#3.4 pthread_cancel)

4.同步与互斥

[4.1 同步](#4.1 同步)

[4.2 互斥](#4.2 互斥)

[4.3 互斥锁(互斥量)](#4.3 互斥锁(互斥量))

[4.3.1 相关函数](#4.3.1 相关函数)

[4.4 信号量(同步与互斥)](#4.4 信号量(同步与互斥))

[4.5 无名信号量](#4.5 无名信号量)

[4.5.1 sem_init](#4.5.1 sem_init)

[4.5.2 sem_wait](#4.5.2 sem_wait)

[4.5.3 sem_post](#4.5.3 sem_post)

[4.5.4 sem_getvalue](#4.5.4 sem_getvalue)

[4.5.5 sem_destroy](#4.5.5 sem_destroy)

[4.6 有名信号量](#4.6 有名信号量)

[4.6.1 sem_open](#4.6.1 sem_open)

[4.6.2 sem_close](#4.6.2 sem_close)

[4.6.3 sem_unlink](#4.6.3 sem_unlink)

[4.7 死锁](#4.7 死锁)

产生死锁的必要条件

预防死锁的产生

三、网络编程

1.TCP/IP协议

[1.1 TCP/IP模型](#1.1 TCP/IP模型)

[1.2 网络接口层(链路层)](#1.2 网络接口层(链路层))

[1.3 网络层](#1.3 网络层)

[1.4 传输层(运输层)](#1.4 传输层(运输层))

2.TCP与UDP

[2.1 TCP协议](#2.1 TCP协议)

TCP连接过程(三次握手)

TCP断开连接过程(四次挥手)

[2.2 UDP协议](#2.2 UDP协议)

[2.3 应用层](#2.3 应用层)

[3 套接字(socket )](#3 套接字(socket ))

[3.1 服务端](#3.1 服务端)

[3.2 客户端](#3.2 客户端)

[3.3 socket](#3.3 socket)

[3.4 bind](#3.4 bind)

[3.5 listen](#3.5 listen)

[3.6 accept](#3.6 accept)

[4.7 connect](#4.7 connect)

[4.8 send](#4.8 send)

[4.9 recv](#4.9 recv)

[4.10 close](#4.10 close)


一、多进程

1.进程

1.1 什么是程序

计算机程序是由计算机执行某一特定任务的指令(最基础的计算机动作称为指令)的集合。

程序是用众多编程语言编写出来的文本,这些文本可以让计算机做指定的事干活;例如exe文件,这就是一个编译完成的程序。

1.2 什么是进程

一个程序,它被存放在硬盘上,是静态的;当启动一个程序之后,程序被操作系统装载到内存并分配一定的资源,此时就会在内存里产生一个进程,再次启动又会产生一个进程

进程相对于程序就是一个动态的概念。进程是操作系统进行资源分配的基本单位。

简单理解:一个程序运行对应着一个进程。

linux允许多个用户同时访问系统,每个用户可以同时运行多个程序,这里就需要用到多个进程。

2.管理进程

2.1 进程表

linux会提供一个"进程表",把当前加载在内存中的所有进程相关信息都保存在表中,包括PID、进程状态、命令字符串等等。

  • 操作系统通过进程的"进程标识符(进程id)"来管理他们。
  • 现在的Linux,同时运行的进程数量一般没有具体限制。

2.2 查看进程

bash 复制代码
ps  查看当前进程

ps -ef 查看当前所有进程的所有详细信息

ps -af 查看当前所有终端上的进程的所有详细信息

ps -ef | grep 进程名 

cat /proc/sys/kernel/pid_max 查看当前最大可同时运行的进程数量

2.3 列的含义说明

  • UID: 执行该进程的用户id
  • PID: 进程标识符(进程id)。
  • PPID: 该进程的父级进程id,如果一个程序的父级进程找不到,该程序的进程被称为僵尸进程
  • C: cpu的占用率,形式是百分数(%)
  • STIME: 进程的启动时间
  • TTY: 终端设备,发起该进程的设备识别符号,如果显示' ?'表示该进程并不是由终端发起
  • TIME: 进程的执行时间
  • CMD: 该进程的名称或对应的路径

3.创建进程

3.1 system函数

在代码中使用字符串的形式运行shell命令(在这个过程中,会使用shell去启动一个对应的进程,比如pwd、ps、ls等等)

cpp 复制代码
system("pwd");
  • system有局限性,程序需要等待由system函数启动的进程结束后才能继续,因此不能立刻执行其他任务。
  • 使用system函数并非启动其他进程的理想手段,因为它必须用一个shell来启动需要的程序。
  • 由于在启动程序之前需要先启动一个shell,而且对shell的安装情况及使用的环境的依赖也很大。

因为system需要等待进程结束才能运行,并且还要依赖shell,所以system有很大的局限性。因此我们可以使用exec系列函数(或者叫exec函数族)。

3.2 exec系列函数

exec函数族可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。在执行完后,原调用进程的内容除了进程号外,其它全部被新程序的内容替换了。

不同的exec函数参数以及启动方式都会有所不同,具体使用场景可以根据自己的需求来选择。

exec指定的新进程,可以在函数中以参数的形式传递,可以直接指定任意的可执行程序。

使用场景如下

  1. 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec函数族让自己重生;
  2. 如果一个进程想执行另外一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生;
cpp 复制代码
头文件
#include <unistd.h>

函数原型
int execl(const char *path, const char *arg, ..., NULL);

int execlp(const char *file, const char *arg, ..., NULL);

int execle(const char *path, const char *arg, ..., NULL, char * const envp[]);

int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

函数原型的区别和说明
execl、execlp和execle的参数个数是可变的,参数以一个空指针结束
execv和execvp的第二个参数是一个字符串数组。

e 参数必须带环境变量部分,环境变量部分参数会成为执行exec函数期间的环境变量, 比较少用
l 命令参数部分必须以"," 相隔, 最后1个命令参数必须是NULL
v 命令参数部分必须是1个以NULL结尾的字符串指针数组的头部指针. 例如char * pstr就是1个字符串的指针, char * pstr[] 就是数组了, 分别指向各个字符串.
p 执行文件部分可以不带路径, exec函数会在$PATH中找


函数说明
path参数:指向想要执行的程序路径,需要取执行命令的完整路径 比如"/bin/ls"
file参数:可以直接填写文件名,前提是要配置过环境变量,系统就会自动从环境变量"$PATH"所指出的路径中进行查找
arg命令参数:需要传递到程序可执行程序内的参数,要以NULL结尾

返回值
执行成功0,失败返回-1

3.3 fork

调用fork可以创建一个新进程,fork属于系统调用,会复制当前进程,在进程表中创建新表项,新进程除了有不同的数据空间、环境和文件描述符以外,其他的都于原进程一模一样。新的进程我们称为子进程

我们经常把fork与exec函数结合在一起使用。

cpp 复制代码
头文件
#include <sys/types.h>
#include <unistd.h>

函数原型
pid_t fork(void);

返回值
如果fork成功,则在父进程中返回子进程的PID,子进程会返回0
如果失败,在父进程中返回-1,没有创建子进程
fork和exec的区别
  1. fork用于创建一个新的进程,称为子进程,而exec则是用指定的程序替换当前进程的全部内容;
  2. exec的前后进程id没有改变,而fork重新创建了子进程后id改变了。
cpp 复制代码
perror("fork 失败") //上一个函数发生错误的原因输出到标准设备

exit(0)//正常运⾏程序并退出程序
exit(1)//⾮正常运⾏导致退出程序

getpid()//查看当前进程pid

3.4 wait

wait系统调用可以暂停父进程,直到它的子进程结束为止。

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/wait.h>

函数原型
pid_t wait(int *stat_loc);

参数说明
stat_loc 我们在使用wait的时候需要声明一个int值,并且把地址传给wait,wait会将当前结束的这个子进程的一些状态信息写入到stat_loc中,

会返回已经运行结束的子进程的pid。
cpp 复制代码
stat_loc具体说明

如果子进程调用exit退出,那么内核把exit的返回值存放到这个整数变量中;如果进程是被杀死的,那么内核将信号序号存放在这个变量中。
这个整数由3部分组成,8个bit记录子进程exit值,7个bit记录信号序号,另一个bit用来指明发生错误并产生了内核映像(core dump)。
如果有需要的话,我们可以使用如下方式,从stat_loc中获取这几个值。
high_8 = stat_loc >> 8; 
low_7  = stat_loc & 0x7F;
bit_7  = stat_loc & 0x80;
printf("high_8 is %d, low_7 is %d, bit_7 is %d\n", high_8, low_7, bit_7);

使用宏定义解释状态信息

|-----------------------|------------|
| WIFEXITED(stat_loc) | 子级正常退出 |
| WEXITSTATUS(stat_loc) | 子级退出时的返回代码 |
| WIFSIGNALED(stat_loc) | 子级异常退出 |
| WTERMSIG(stat_loc) | 给出终止信号的代码 |
| WIFSTOPPED(stat_loc) | 子级被暂停 |
| WSTOPSIG(stat_loc) | 给出暂停信号的代码 |

3.进程间通信

IPC(Inter-Process Communication,进程间通信),在不同进程间进行数据传输(信息交换)的过程,称为进程间通信。

根据传输数据的大小,我们可以将进程通信分为:

  • 低级通信机制(效率低,例如信号量机制)
  • ⾼级通信机制(OS封装了细节,直接⾼效使⽤原语,在进程之间要传递⼤量数据时,应当使⽤⾼级通信机制)

3.1 按照通信类型分为四⼤类

共享存储器系统
  • 基于共享数据结构的通信方式(使用于传递少量数据的场景,通信效率低,属于低级通信)
  • 基于共享存储区的通信方式
管道通信系统

管道是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件(pipe文件)。

管道机制需要提供一下几点的协调能力:

  1. 互斥,即当一个进程正在对pipe执行读/写操作时,其它进程必须等待
  2. 同步,当一个进程将一定数量的数据写入,然后就去睡眠等待,直到读进程将数据取走,再去唤醒。读进程与之类似
  3. 确定对方是否存在
消息传递系统
  • 直接通信方式:发送消息的进程利用系统提供的发送原语直接把消息发送给目标进程
  • 间接通信方式:发送和接收进程都通过共享实体(邮箱)的方式进行消息的发送和接收
客户机服务器系统
  • 套接字(socket):与其他通信机制不同的是,socket可以用于不同机器间的进程通信

3.2 管道通信

一般说的"管道通信",都是默认说无名管道。管道通信特点如下:

  • 具有固定的读端和写端,所以数据只能在一个方向上流动,我们称之为"半双工"。
  • 只能用于具有亲缘关系的进程之间的通信(父子进程、兄弟进程)。
  • 可以看成是一个特殊的文件,我们也可以对他进行read、write等函数操作。但它区别于普通文件,不属于任何文件系统,只不过是存在于内存之中。
原理

管道可以分为**pipe(无名管道)fifo(命名管道)**两种,除了使用时的建立、打开、删除的方式不同外,两种管道几乎是一样的。

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。

无名管道(pipe)

用于相关进程之间的通信,比如父子进程、兄弟进程。通过pipe()系统调用来完成创建并打开,当最后一个使用它的进程关闭对他的引用时,pipe会自动撤销。

cpp 复制代码
头文件
#include <unistd.h>

函数原型
int pipe(int pipefd[2]);

参数说明
pipefd[2] 用于返回两个指向管道末端的文件描述符。
          Pipefd[0]指的是管道的读端。Pipefd[1]指的是管道的写入端。
          写入管道的写入端数据由内核进行缓冲,直到从管道的读取端读取为止
          
返回值
该函数在数组中填上两个新的文件描述符后返回0,如果失败则返回-1并设置errno来表明失败的原因。 perror();
EMFILE:进程使用的文件描述符过多。
ENFILE:系统的文件表已满。
EFAULT:文件描述符无效。
命名管道(named pipe)

无名管道只能在相关的进程间通信,但如果想要在任意的进程间交换数据,无名管道不能够适用。我们可以通过命名管道来做这件事。

命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。

有名管道的文件仅仅是作为传输数据的通道,它并不存放传输的数据。

cpp 复制代码
使用命令:在当前目录下创建了一个名为name的命名管道
mkfifo name
mknod name p

使用函数创建
头文件
#include <sys/types.h>
#include <sys/stat.h>

函数原型
int mkfifo(const char *pathname, mode_t mode);
 
参数说明
pathname: 路径名,即创建后有名管道的路径名
mode: 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同

返回值:
    0:成功。
    -1:失败,文件已存在,或出错。

创建管道文件后,使用文件IO系统调用函数通过文件名打开已经创建的命名管道。

cpp 复制代码
函数原型
int open( const char * pathname, int flags);
int open( const char * pathname,int flags, mode_t mode);

参数flags 常用的标识:    
O_RDONLY 以只读方式打开文件 read only
O_WRONLY 以只写方式打开文件 write only
O_RDWR 以可读写方式打开文件。read write
上述三种标识是互斥的,也就是不可同时使用,但可与下列的标识利用OR(|)运算符组合。
O_CREAT 若欲打开的文件不存在则自动建立该文件。
O_TRUNC 若文件存在并且以可写的方式打开时,此标识会令文件长度清为0,而原来存于该文件的资料也会消失。
O_APPEND 当读写文件时会从文件尾开始移动,也就是所写入的数据会以附加的方式加入到文件后面。
O_NONBLOCK 以不可阻塞的方式打开文件,也就是无论有无数据读取或等待,都会立即返回进程之中。
O_EXCL: 如果同时指定 O_CREAT,而该文件又是存在的,报错;也可以测试一个文件是否存在,不存在则创建。

//open调用将被阻塞,除非有一个进程以写的方式打开同一个fifo
int open( const char * pathname, O_RDONLY);
//即使没有其他进程以写方式打开fifo,这个open调用也会成功并立刻返回(不会阻塞)
int open( const char * pathname, O_RDONLY | O_NONBLOCK);
//open调用将阻塞,直到有一个进程以读的方式打开同一个fifo
int open( const char * pathname, O_WRONLY);
//总是立刻返回,但如果没有进程以读的方式打开fifo,open调用将返回一个错误,并且fifo也不会被打开;
//如果有进程以读的方式打开fifo文件,我们就可以正常对fifo进行写操作
int open( const char * pathname, O_WRONLY | O_NONBLOCK);

O_NONBLOCK分别搭配O_RDONLY和O_WRONLY在效果上的不同,如果没有进程以读方式打开管道,非阻塞写方式的open调用将失败,但非阻塞读方式的open调用总是成功。

close调用的行为并不O_NONBLOCK标志的影响。

3.3 文件权限(扩展)

Linux 系统中采用八进制数表示权限,如0755, 0644

bash 复制代码
ls -l  //该命令可以查看文件的操作权限

开头第一位

  • ---->普通文件(regular)

d--------->目录文件(directory)

l --------->链接文件(link)

b--------->块设备文件(block)

c--------->字符设备文件(character)

s--------->套接字文件(socket)

p--------->管道文件(pipe)

rwx(r:读 w:写 x:执行)

每一个文件权限,都有三组rwx分别对应以下几种用户的rwx权限:

第一组:该文件拥有者的权限

第二组:该文件拥有者所在组的其他成员对该文件的操作权限

第三组:其他用户组的成员对该文件的操作权限

举个例子:

  • -rwxrwxrwx:一个普通文件,所有用户组都对它有读、写、执行文件
  • prw-rw-r--:一个管道文件,第一组与第二组可读写但不可执行,第三组只读

上面的表达方式可以换算成二进制,再转成八进制

先换成二进制,有权限就换算成1,没权限就用0表示;再由二进制换算成八进制。

举个例子(进制换算):

  • -rwxrwxrwx:第一位可以用0表示,剩余三个用户组都可读可写可执行,所以换算为二进制0,111,111,111,对应八进制为0777
  • prw-rw-r--: 第一位可以用0表示,第一组与第二组可读写但不可执行,第三组只读,换算为二进制:0,110,110,100,对应八进制为0664

3.4 消息队列( messagequeue )

消息队列,可以理解为是一个存放消息(数据)容器。将消息写入消息队列,然后再从消息队列中取消息,一般来说是先进先出的顺序。

  • 消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
  • 如果没有释放消息队列或者关闭操作系统,消息队列会一直存在,而匿名管道是随进程的创建而建立,随进程的结束而销毁。
  • 每个数据块存储节点都有一个最大长度的限制,系统中所有队列所包含的全部数据块的总长度也有一个上限
消息队列对比管道的优势
  • 消息队列适合频繁地交换数据
  • 消息队列可以指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
使用流程
  1. ftok函数生成键值
  2. msgget函数创建消息队列
  3. msgsnd函数往消息队列发送消息
  4. msgrcv函数从消息队列读取消息
  5. msgctl函数进行删除消息队列
3.4.1 msgget

创建或者打开一个消息队列

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

函数原型
int msgget(key_t key, int msgflg);

参数说明
key:0(IPC_PRIVATE):会建立新的消息队列 
    大于0的32位整数:视参数msgflg来确定操作。通常要求此值来源于ftok返回的IPC键值
msgflg:
    IPC_CREAT:当msgflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列,返回此消息队列的标识符
    IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列则报错

返回值
    成功:返回消息队列的标识符
    出错:-1,错误原因存于error中
        EACCES:指定的消息队列已存在,但调用进程没有权限访问它
        EEXIST:key指定的消息队列已存在,而msgflg中同时指定IPC_CREAT和IPC_EXCL标志
        ENOENT:key指定的消息队列不存在同时msgflg中没有指定IPC_CREAT标志
        ENOMEM:需要建立消息队列,但内存不足
        ENOSPC:需要建立消息队列,但已达到系统的限制
3.4.2 msgctl

获取和设置消息队列的属性

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

函数原型
int msgctl(int msqid, int cmd, struct msqid_ds *buf)

参数说明
msqid:消息队列标识符
cmd:IPC_STAT:获得msgid的消息队列头数据到buf中
     IPC_SET:设置消息队列的属性,要设置的属性需先存储在buf中,可设置的属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes
     IPC_RMID:删除消息队列 remove id
buf:消息队列管理结构体,请参见消息队列内核结构说明部分

返回值
    成功:返回消息队列的标识符
    出错:-1,错误原因存于error中
        EACCESS:参数cmd为IPC_STAT,无权限读取该消息队列
        EFAULT:参数buf指向无效的内存地址
        EIDRM:标识符为msqid的消息队列已被删除
        EINVAL:无效的参数cmd或msqid
        EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行
3.4.3 msgsnd

将消息写入到消息队列

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

函数原型:将msgp消息写入到标识符为msqid的消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)

参数说明
msqid:消息队列标识符
msgp:发送给队列的消息。msgp可以是任何类型的结构体,但第一个字段必须为long类型,即表明此发送消息的类型,msgrcv根据此接收消息。
    msgp定义的参照格式如下:
    struct s_msg{ /*msgp定义的参照格式*/
        long type; /* 必须大于0,消息类型 */
        char mtext[256]; /*消息正文,可以是其他任何类型*/
    } msgp;
msgsz:要发送消息的大小
msgflg:0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
        IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
        IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。

返回值
    成功:返回消息队列的标识符
    出错:-1,错误原因存于error中
        EAGAIN:参数msgflg设为IPC_NOWAIT,而消息队列已满
        EIDRM:标识符为msqid的消息队列已被删除
        EACCESS:无权限写入消息队列
        EFAULT:参数msgp指向无效的内存地址
        EINTR:队列已满而处于等待情况下被信号中断
        EINVAL:无效的参数msqid、msgsz或参数消息类型type小于0

msgsnd()为阻塞函数,当消息队列容量满或消息个数满会阻塞。

msgsnd()解除阻塞的条件有以下三个条件:

  1. 消息队列中有容纳该消息的空间。
  2. msqid代表的消息队列被删除。
  3. 调用msgsnd函数的进程被信号中断。
3.4.4 msgrcv

从消息队列读取消息

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

函数原型:从标识符为msqid的消息队列读取消息并存于msgp中,读取后把此消息从消息队列中删除
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数说明
msqid:消息队列标识符
msgp:存放消息的结构体,结构体类型要与msgsnd函数发送的类型相同
msgsz:要接收消息的大小
msgtyp: 0:接收第一个消息
        >0:接收类型等于msgtyp的第一个消息
        <0:接收类型等于或者小于msgtyp绝对值的第一个消息
msgflg:0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
        IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
        IPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息
        IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃

返回值
    成功:实际读取到的消息数据长度
    出错:-1,错误原因存于error中
        E2BIG:消息数据长度大于msgsz而msgflag没有设置IPC_NOERROR
        EIDRM:标识符为msqid的消息队列已被删除
        EACCESS:无权限读取该消息队列
        EFAULT:参数msgp指向无效的内存地址
        ENOMSG:参数msgflg设为IPC_NOWAIT,而消息队列中无消息可读
        EINTR:等待读取队列内的消息情况下被信号中断

msgrcv()解除阻塞的条件有以下三个:

  1. 消息队列中有了满足条件的消息。
  2. msqid代表的消息队列被删除。
  3. 调用msgrcv()的进程被信号中断。

3.5 信号 (sinal)

信号是UNIX和Linux系统响应某些条件而产生的一个事件。接收到该信号的进程会相应地采取一些行动。

可作为进程间传递消息的一种方式,信号可以被生成、捕获、响应或忽略。

上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程

信号跟信号量虽然名字相似度 66.66%,但两者用途完全不一样。

在linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:

3.5.1 kill

使用kill可以发送信号

cpp 复制代码
int kill(pid_t pid, int sig)
kill函数把sig给定的信号发送给参数pid所指定的进程。
3.5.2 signal

signal注册信号接收

cpp 复制代码
头文件
#include <signal.h>

函数原型
sighandler_t signal(int signum,sighandler_t handler);

参数说明
signum:要捕捉的信号
handler:捕捉到如何处理
         SIG_IGN:忽略
         SIG_DFL:使用信号默认行文
         回调函数:这个函数是内核调用 

3.6 共享内存(shared memory)

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。

采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,对于像管道和消息队列等通信方式,则需要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。

共享内存没有同步机制(第一个进程操作完毕前,无法阻止第二个进程访问共享内存)

3.6.1 shmget

创建共享内存

cpp 复制代码
头文件
#include <sys/ipc.h>
#include <sys/shm.h>

函数原型
int shmget(key_t key, size_t size, int shmflg);

参数说明
key:提供一个参数key(非0整数),它有效地为共享内存段命名
size:以字节为单位指定需要共享的内存容量
shmflg:权限标志

返回值:成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.
3.6.2 shmat

第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/shm.h>


函数原型
void *shmat(int shm_id, const void *shm_addr, int shmflg);  

参数说明
shm_id:由shmget函数返回的共享内存标识。
shm_addr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
shm_flg:一组标志位,通常为0。

返回值:这段共享内存空间的实际地址,如果出错-1
3.6.3 shmdt

该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。

cpp 复制代码
头文件
 #include <sys/types.h>
#include <sys/shm.h>


函数原型
int shmdt(const void *shmaddr);  

参数说明
shmaddr:shmat函数返回的地址指针

返回值:调用成功时返回0,失败时返回-1
3.6.4 shmctl

用来控制共享内存

cpp 复制代码
头文件
#include <sys/ipc.h>
#include <sys/shm.h>

函数原型
int shmctl(int shm_id, int command, struct shmid_ds *buf);

参数说明
shm_id:shmget函数返回的共享内存标识符
command:要采取的操作,可以取下面的三个值 :
        IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值
        IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
        IPC_RMID:删除共享内存段                
buf:一个结构指针,它指向共享内存模式和访问权限的结构

3.7 信号量(semophore)

信号量(semaphore)是操作系统用来解决并发中的互斥和同步问题的一种方法。和不同,信号量可以看成是一个计数器

临界区(共享资源):

  • 临界区指的是一个访问公共资源的程序片段,这些公共资源又无法同时被多个进程同时访问。当有进程进入临界区段时,其他线程或是进程必须等待,以确保这些公共资源是被互斥获得使用。
  • 每个进程中访问临界资源的那段代码称为临界区 (Critical Section),每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。

信号量是一个特殊的变量,程序对其访问都是原子操作(要么发生、要么不发生),且只允许对它进行"等待"和"发送"信息操作。

最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。

  • n>0:当前有可用资源,可用资源数量为n
  • n=0:资源都被占用,可用资源数量为0
  • n<0:资源都被占用,并且还有n个进程正在排队

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

  • P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1
原子操作

一个程序在执行的时候可能被优先级更高的线程中断。而有些操作是不能被中断的,不然会出现无法还原的后果,这时候,这些操作就需要原子操作。就是不能被中断的操作。

3.7.1 semget

获取一个已存在的、或创建一个新的信号量

cpp 复制代码
头文件 
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

函数原型
int semget(key_t key, int num_sems, int sem_flags);

参数说明
key 对应一个唯一的信号量,不同的进程可通过该键值和semget获取唯一的信号量(IPC_PRIVAT该信号量只允许创建者本身,可用于父子进程间通信)
num_sems 需要的信号量数目
sem_flags 
        IPC_CREAT, 如果该信号量未存在,则创建该信号量如果该信号量已存在,也不发送错误。
        IPC_EXCL只有信号量不存在的时候,新的信号量才建立,否则就产生错误。
        
返回值:  成功, 则返回一个信号量标识符(正数)
         失败, 返回-1
3.7.2 semop

改变信号量的值,即对信号量执行P操作、或V操作。

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

函数原型
int semop(int semid, struct sembuf *sops, size_t nsops);

参数说明
semid 信号量标识符, 也就是semget的返回值
sops
struct sembuf{  
    short sem_num;   //除非使用一组信号量,否则它为0  
    short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数,                                         
                    //一个是-1,即P(等待)操作,  
                    //一个是+1,即V(发送信号)操作。  
    short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量,  
                  //并在进程没有释放该信号量而终止时,操作系统释放信号量  
};  
nsops 第二个参数sops所表示的数组的大小、表示有几个struct sembuf
3.7.3 semctl

删除和初始化信号量

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

函数原型
int semctl(int sem_id, int sem_num, int command, ...);

参数说明
sem_id:信号量标识符
sem_num:信号量组中的编号,如果只有一个信号量,则取0
command:SETVAL:用来把信号量初始化为一个已知的值。
         IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

如有需要第四个参数一般设置为union semnu arg;定义如下
union semun{  
    int val;  //使用的值
    struct semid_ds *buf;  //IPC_STAT、IPC_SET 使用的缓存区
    unsigned short *arry;  //GETALL,、SETALL 使用的数组
    struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区 
};  

 返回值: 出错返回-1
         根据不同的场景,返回不同的值。

二、多线程

1.程序与进程、线程的关系

如今,几乎所有的电脑(操作系统)都支持同时执行多个任务,比如一边用迅雷下载资源,一边听歌,一边用 QQ 和好友聊天,这样的执行方式简称"并发"或者"并行"。

并发和并行指的都是计算机可以同时执行多个任务。

程序并行的常用实现方式有两种,分别叫做"多进程编程"和"多线程编程"。

载入内存执行的程序都是一个进程。操作系统以进程为单位分配资源,各个进程相互独立,执行过程互不干扰。

与此同时,操作系统可以运行多个进程,每个进程还可以同时执行多个任务(线程),例如微信可以与多个好友一起聊天,迅雷支持同时下载多个文件等等,因此,同一进程中,执行的每个任务都被视为一个线程

2. 线程与进程的区别

  • 可以理解为父子关系
  • 线程更加节省资源
  • 线程间通讯更容易

一个进程中可以包含多个线程,所有线程共享进程拥有的资源。并且,每个线程也可以拥有自己的私有资源

  • 所有线程都可共享一个程序内的代码
  • 全局变量、函数内的静态变量、堆空间等等区域的所有数据可共享
  • 所有线程可共享系统为当前进程分配的内存空间
  • 使用线程1打开的文件,可以用线程2、3来进行读写操作。各个线程打开的文件资源,也可以为所有线程所共享
  • 除此之外,不同线程还有自己的私有资源,包括寄存器中存储的数据、线程执行所需的局部变量(函数参数)等。

3.多线程函数

Linux 上编写多线程程序,可以借助 头文件提供的一些函数,所以,多线程遵循POSIX标准 ,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h

3.1 pthread_create

创建一个线程

cpp 复制代码
函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数说明
thread:接收一个 pthread_t 类型变量的地址,当线程创建成功,用来返回创建的线程ID,所以这个指针指向线程标识符
attr:指向线程的属性,NULL表示使用默认属性
start_routine:以函数指针的方式指明新建的线程需要执行哪个函数。
arg:向 start_routinue() 函数的形参传递数据。将 arg 置为 NULL,表示不传递任何数据。

返回值:成功返回0
       失败返回错误码:
        各个非零值都对应着不同的宏,指明创建失败的原因,常见的宏有以下几种:
        EAGAIN:系统资源不足,无法提供创建线程所需的资源。
        EINVAL:传递给 pthread_create() 函数的 attr 参数无效。
        EPERM:传递给 pthread_create() 函数的 attr 参数中,某些属性的设置为非法操作,程序没有相关的设置权限。
        以上这些宏都定义在 <errno.h> 头文件中,如果想使用这些宏,需提前引入此头文件。

3.2 pthread_exit

终止线程

cpp 复制代码
函数原型
void pthread_exit(void *retval);

参数说明 
retval 线程执行结束时的返回值,如果不需要返回任何数据,将其置为 NULL 即可

3.3 pthread_join

线程等待

join函数的功能主要有3个,分别是:

  1. 接收目标线程执行结束时的返回值;
  2. 释放目标线程占用的进程资源。(由于一个进程中的多个线程是共享数据段的,因此,通常在线程退出后,退出线程所占用的资源并不会随着线程的终止而得到释放)
  3. 阻塞当前的主线程,直到目标线程执行结束
cpp 复制代码
函数原型
int pthread_join(pthread_t thread, void ** retval);

参数说明
thread 用于指定目标线程;
retval 用于存储接收到的返回值。
有时调用 pthread_join() 函数可能仅是为了及时释放目标线程占用的资源,并不想接收它的返回值,这种情况下可以将 retval 置为 NULL。

pthread_join() 函数会一直阻塞当前线程,直至目标线程执行结束,阻塞状态才会消除。

返回值:
    成功等到了目标线程执行结束(成功获取到目标线程的返回值),pthread_join() 函数返回数字 0
    失败返回错误码

3.4 pthread_cancel

在多线程的程序中,一个线程可以使用cancel向另一个线程发起终止指令

cpp 复制代码
函数原型
int pthread_cancel(pthread_t thread);

参数说明
thread 参数用于指定接收信号的目标线程

返回值
    当成功发送"终止执行"的信号时,函数返回值为 0,
    失败返回非零数。 

4.同步与互斥

在现代操作系统中,经常会遇到多个任务都需要访问同一个资源,或者多个任务有相互依赖的关系。所以有时候就需要控制线程的先后运行顺序。

想要控制线程的先后顺序,可以使用同步与互斥机制处理此文件。

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现线程不安全的问题。

4.1 同步

线程运行某段代码的顺序,必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:

两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

4.2 互斥

当线程正在运行某段代码时,其它任务就不能运行这些程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:

一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

4.3 互斥锁(互斥量)

为了保证同步的效果,我们引入------互斥锁(mutex),互斥锁是一种简单的加锁方法,用来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。

  • 原子性:把一个互斥量锁定为一个原子操作;
  • 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;
  • 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

因此,互斥锁保证让每个线程对共享资源按顺序进行原子操作。

4.3.1 相关函数

pthread_mutex_init

初始化互斥锁

cpp 复制代码
头文件
#include <pthread.h>

函数原型
int pthread_mutex_init(pthread_mutex_t *mutex, 
const pthread_mutexattr_t *attr);

参数说明
mutex:互斥锁标识符
attr:  PTHREAD_MUTEX_INITIALIZER                 快速创建互斥锁
        PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP    创建递归互斥锁
        PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP   创建检错互斥锁
        
返回值:成功返回0,否则返回错误码

其他相关函数

cpp 复制代码
头文件
#include <pthread.h>

// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,直到互斥锁解锁后再上锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);

 

// 调用该函数时,若互斥锁未加锁,则上锁,若互斥锁已加锁,则函数直接返回失败

int pthread_mutex_trylock(pthread_mutex_t *mutex);

 
//当程序试图获取一个已加锁的互斥量时,pthread_mutex_timedlock互斥量原语允许绑定线程阻塞时间。
//pthread_mutex_timedlock函数与pthread_mutex_lock函数是基本等价的,但是在达到超时时间时,pthread_mutex_timedlock不会对互斥量进行加锁,而是返回错误码ETIMEOUT

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);

 
 // 对指定的互斥锁解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
 

// 销毁指定的一个互斥锁。互斥锁在使用完毕后,
必须要对互斥锁进行销毁,以释放资源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);

返回值:成功返回0 失败返回错误码

4.4 信号量(同步与互斥)

信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。

编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。

在 POSIX 标准中,信号量分两种,一种是无名信号量,一种是有名信号量。无名信号量一般用于线程间同步或互斥,而有名信号量一般用于进程间同步或互斥。它们的区别和管道及命名管道的区别类似,无名信号量则直接保存在内存中,而有名信号量要求创建一个文件。

4.5 无名信号量

#include 需要加上参数-lpthread

4.5.1 sem_init

初始化信号量

创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。

cpp 复制代码
头文件
#include <semaphore.h>

函数原型
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数说明
sem:信号量的地址

pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享
value:信号量的初始值

返回值:成功返回0,否则返回错误码 
4.5.2 sem_wait

信号量 P 操作(减 1)。

将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。

cpp 复制代码
头文件
#include <semaphore.h>

函数原型
int sem_wait(sem_t *sem);

参数说明
sem:信号量的地址

返回值:成功返回0,否则返回错误码 
cpp 复制代码
//以非阻塞的方式来对信号量进行减 1 操作。若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回
int sem_trywait(sem_t *sem);
4.5.3 sem_post

信号量 V 操作(加 1)

将信号量的值加 1 并发出信号唤醒等待线程(sem_wait())

cpp 复制代码
头文件
#include <semaphore.h>

函数原型
int sem_post(sem_t *sem);

参数说明
sem:信号量的地址

返回值:成功返回0,否则返回错误码 
4.5.4 sem_getvalue

获取 sem 标识的信号量的值,保存在 sval 中。

cpp 复制代码
头文件
#include <semaphore.h>

函数原型
int sem_getvalue(sem_t *sem, int *sval);

参数说明
sem:信号量地址
sval:保存信号量值的地址

返回值:成功返回0,否则返回错误码 
4.5.5 sem_destroy

销毁信号量

cpp 复制代码
头文件
#include <semaphore.h>

函数原型
int sem_destroy(sem_t *sem);

参数说明
sem:信号量地址

返回值:成功返回0,否则返回错误码 

4.6 有名信号量

4.6.1 sem_open

创建

cpp 复制代码
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);  
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);    

参数说明:
    第一种函数:当使用已存在的有名信号量时调用该函数,flag 参数设为 0    
    第二种函数,flag 参数应设为O_CREAT ,如果有名信号量不存在,则会创建一个新的,如果存在,则会被使用并且不会再初始化。

返回值:若成功,返回指向信号量的指针;若出错,返回SEM_FALLED
4.6.2 sem_close

关闭

cpp 复制代码
#include <semaphore.h>
 
int sem_close(sem_t *sem);
 
返回值:若成功,返回 0 ;若出错,返回-1

销毁

cpp 复制代码
#include <semaphore.h>
 
int sem_unlink(const char *name);
 
返回值:若成功,返回 0 ;若出错,返回-1

4.7 死锁

死锁是两个或者两个以上的进程由于竞争资源而造成的,相互竞争系统资源而造成的"永久"阻塞,如无外力作用,这些进程将永远不能向前推进。

陷入死锁状态的进程称为死锁进程,所占用的资源或者需要他们进行某种合作的进程就会相继陷入死锁,最终可能导致整个系统瘫痪。

产生死锁的必要条件
  • 互斥条件------进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占有。
  • 请求和保持条件------当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件------进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  • 环路等待条件------在发生死锁时必然存在一个"进程---资源"的环形链。
预防死锁的产生
  1. 破坏死锁的四个必要条件
  2. 加锁顺序一致
  3. 避免锁未释放的场景
  4. 资源一次性分配

三、网络编程

1.TCP/IP协议

从字面意义上讲,有人可能会认为 TCP/IP 是指 TCP 和 IP 两种协议。实际生活当中有时也确实就是指这两种协议。然而在很多情况下,它只是利用 IP 进行通信时所必须用到的协议群的统称。具体来说,IP 或 ICMP、TCP 或 UDP、TELNET 或 FTP、以及 HTTP 等等一些列都属于 TCP/IP 协议。TCP/IP 定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。

1.1 TCP/IP模型

TCP/IP是一个四层的体系结构,它包含应用层、运输层、网际层和网络接口层,每一层都呼叫它的下一层所提供的协议来完成自己的需求。通俗而言:TCP负责发现传输的问题,一有问题就发出信号,要求重新传输,直到所有数据安全正确地传输到目的地。而IP是给因特网的每一台联网设备规定一个地址。

|-------|-------------|---------------------|------------------------------------|
| OSI模型 | TCP/IP概念层模型 | 功能 | TCP/IP协议族 |
| 应用层 | 应用层 | 文件传输、电子邮件、文件服务、虚拟终端 | TFTP、HTTP、SNMP、FTP、SMTP、DNS、Telnet |
| H表示层 | 应用层 | 数据格式化、代码转换、数据加密 | - |
| 会话层 | 应用层 | 建立或接触与别的接点的联系 | - |
| 传输层 | 传输层 | 提供端对端的接口 | TCP、UDP |
| 网络层 | 网络层 | 为数据包选择路由 | IP、ICMP、RIP、OSPF、BGP、IGMP |
| 数据链路层 | 链路层(网络接口层) | 传输有地址的帧以及错误检测功能 | SLIP、CSLIP、PPP、ARP、RARP、MTU |
| 物理层 | 链路层(网络接口层) | 以二进制数据形式在物理媒体上传输数据 | ISO2110、IEEE802 |

1.2 网络接口层(链路层)

实现网卡接口的网络驱动,以处理数据在以太网线等物理媒介上的传输;网络驱动程序隐藏了不同物理网络的不同电气特性,为上层协议提供一个统一的接口。

ARP和RARP(Reverse Address Resolve Protocol)即逆地址解析协议,该协议实现了IP地址和物理地址(MAC地址)之间的转换

1.3 网络层

网络有分局域网(LAN, Local Area Network)和广域网(WAN, Wide Area Network)。对于后者通常需要使用众多分级的路由器来连接分散的主机或者LAN,即通讯的两台主机一般不是直接连接,而是通过多个中间节点(路由器)连接的,从而形成网络拓扑连接。

  • 网络层的任务之一就是选择这些中间节点,以确定两台主机间的通讯路径。
  • 其次网络层对上层协议隐藏了网络拓扑连接的细节,在使得传输层看来通讯双方是直接连接的。

**IP协议(Internet Protocol)**是网络层最核心的协议,它根据数据包的目的IP地址来决定如何投递该数据包。

1.4 传输层(运输层)

负责对报文进行分组和重组,并以TCP或UDP协议格式封装报文(为应用程序隐藏了数据包跳转的细节,负责数据包的收发、链路超时重连等)。

2.TCP与UDP

|--------|-----------------|----------------------|
| | TCP | UDP |
| 是否连接 | 面向连接 | 无连接 |
| 是否可靠 | 可靠传输 | 不可靠传输 |
| 连接对象个数 | 只能是一对一通信 | 支持一对一,一对多,多对一以及多对多通信 |
| 传输方式 | 面向字节流 | 面向报文 |
| 首部开销 | 首部最小20字节,最大60字节 | 首部开销小,才8字节 |
| 适用场景 | 适用于要求可靠传输的应用 | 音视频会议、直播等 |

2.1 TCP协议

TCP协议(Transmission Control Protocol, 传输控制协议)为应用程序提供可靠的、面向连接的、基于流的服务,具有超时重传、数据确认等方式来确保数据包被正确发送到目的端。

因此TCP服务是可靠的,使用TCP协议通讯的双方必须先建立起TCP连接,并在系统内核中为该连接维持一些必要的数据结构,比如连接的状态,读写缓冲区,多个定时器等。当通讯结束时双方必须关闭连接以释放这些内核数据。

基于流发送意思是数据是没有长度限制,它可源源不断地从通讯的一段流入另一端。

  • 面向连接

发送数据之前必须在两端建立连接。建立连接的方法是"三次握手",这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。

  • 仅支持单播传输

每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。

  • 面向字节流

TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。

  • 可靠传输

对于可靠传输,判断丢包,误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。

  • 提供拥塞控制

当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞

  • TCP提供全双工通信

TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)

TCP连接过程(三次握手)

TCP连接过程(三次握手)

三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的目的就是为了确认双方的接收能力和发送能力是否正常;并指定自己的初始化序列号,为后面的可靠性传送做准备。实质就是连接服务器指定端口,建立TCP连接。

  • 第一次握手

客户端发送syn标志位和seq num,向服务器申请建立连接,客户端状态由closed变为syn_send

  • 第二次握手

服务端返回 syn和ack标志位,ack num以及seq num,确认第一次握手的报文段,返回ack num=seq num(第一次握手发送的)+1,同意建立连接,服务器状态由listen变为syn_received

  • 第三次握手

发送确认报文段,返回ack以及ack num=seq num(第二次握手发送的)+1,客户端状态变为:established(完成连接),服务器收到确认报文段,服务器状态由syn_received变为established(完成连接)

为什么需要三次才能建立连接:为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。

三次挥手详解:点击跳转

TCP断开连接过程(四次挥手)

四次挥手详解:点击跳转

2.2 UDP协议

UDP协议(User Datagram Protocol, 用户数据报协议)与TCP协议相反,它为应用程序提供的是不可靠的、无连接的服务。

  • 面向无连接

首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作。

  • 有单播,多播,广播的功能

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

  • UDP是面向报文的

发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文

  • 不可靠性

首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。

再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

  • 头部开销小,传输数据报文时是很高效的。

2.3 应用层

在用户空间实现,负责向用户提供应用程序,负责处理众多业务逻辑,如文件传输、网络管理。

  • telne协议: 远程登录协议,它使我们能在本地完成远程任务
  • DNS协议: DNS协议(Domain Name Service, 域名服务)提供机器域名到IP地址的转换。

3 套接字(socket )

socket是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。

3.1 服务端

  1. 调用socket函数创建socket,这一步会创建一个文件描述符FD。
  2. 调用bind函数将socket(也就是FD)绑定到某个ip地址和端口的上。
  3. 调用listen函数开启侦听端口。
  4. 调用accept阻塞等待接受连接,当有客户端请求连接上来后,产生一个新的socket(客户端socket)。
  5. 基于新产生的socket调用send或recv函数开始与客户端进行数据通信。
  6. 通信结束后,调用close函数关闭客户端socket。

3.2 客户端

  1. 调用socket函数创建客户端socket。
  2. 调用connect函数尝试连接服务器。
  3. 连接成功以后调用send或recv函数开始与服务器进行数据通信。
  4. 通信结束后,调用close函数关闭socket。

3.3 socket

创建socket

cpp 复制代码
头文件
#include <sys/types.h>         
#include <sys/socket.h>

函数原型
int socket(int domain, int type, int protocol);

参数说明
domain:指定通信协议族。常用的协议族有AF_INET、AF_UNIX等,对于TCP协议,该字段应为AF_INET(ipv4)或AF_INET6(ipv6)。
type:指定socket类型。常用的socket类型有SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)等。
protocol:指定socket所使用的协议,一般我们平常都指定为0,使用type中的默认协议。严格意义上,IPPROTO_TCP(值为6)代表TCP协议。

返回值:成功为文件描述符FD,失败未-1。

另外type参数可以附加设置socket的类型,阻塞还是非阻塞,默认是阻塞,设置为非阻塞SOCK_NONBLOCK的方法如下:
socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);

3.4 bind

将socket(也就是FD)绑定到某个ip和端口的二元组上

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/socket.h>

 函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明
sockfd:一般为调用socket函数返回的文件描述符。
addr:地址的格式与协议有关,ipv4查看
    man 7 ip,ipv6查看
    man 7 ipv6。
addrlen:*addr的长度。

返回值:0为成功,-1为失败。

3.5 listen

开启侦听端口

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/socket.h>

函数原型
int listen(int sockfd, int backlog);

函数说明:
sockfd:一般为调用socket函数返回的文件描述符。
backlog:待连接的等待队列的最大值。

返回值:0为成功,-1为失败。 

3.6 accept

阻塞等待接受连接

cpp 复制代码
头文件
#include <sys/types.h>          
#include <sys/socket.h>

函数说明
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

#define _GNU_SOURCE      
#include <sys/socket.h>

int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

函数说明:
sockfd:一般为调用socket函数返回的文件描述符。
addr:存储客户端的信息(如ip和端口),传入一个空的结构体,便于函数内部为这个结构体赋值,这样外部就能访问了。
addrlen:*addr的长度。
flags:c语言不能像java中一样方法重载,所以只能用个新的方法accept4加个flags参数,这个参数也是用来设置非阻塞SOCK_NONBLOCK的。

返回值:成功则返回与客户端建立连接的socket,也就是一个文件描述符FD,失败则返回-1。

4.7 connect

尝试连接服务器

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/socket.h>

函数说明
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数说明:
sockfd:一般为调用socket函数返回的文件描述符。
addr:服务器端的地址,格式参考bind函数。
addrlen:*addr的长度。

返回值:0为成功,-1为失败

4.8 send

发送消息

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/socket.h>

函数原型
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

函数说明:
sockfd:文件描述符。
*buf:要发送的内容。
len:发送的内容的长度
flags:标志位,默认为0,具体参考

返回值:成功则返回发送的内容的长度,失败则返回-1。
cpp 复制代码
头文件
#include <unistd.h>

函数原型
ssize_t write(int fd, const void *buf, size_t count);

与send的区别就是少了一个flags

4.9 recv

接收消息

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/socket.h>

原型
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

函数说明:
sockfd:文件描述符。
*buf:用来存放接收到的数据。
len:*buf的长度。
flags:标志位,具体参考man 2 recv。

返回值:成功则返回收到的内容的长度,失败则返回-1。
cpp 复制代码
头文件
#include <unistd.h>

函数原型
ssize_t read(int fd, void *buf, size_t count);

与send的区别就是少了一个flags

4.10 close

关闭已打开的socket

cpp 复制代码
头文件
 #include <unistd.h>
 
 函数原型
 int close(int fd);
相关推荐
user-猴子2 小时前
让网页“活”过来 —— 用AI打造会自主学习的动态知识图谱
人工智能·学习·知识图谱
七牛云行业应用2 小时前
Ollama 本地部署 DeepSeek 完全指南:macOS / Windows / Linux 三端安装 + GPU 配置 + API 调用
linux·windows·macos
网络工程小王2 小时前
【HCIE-AI】10.pytorch模型迁移分析
人工智能·学习·华为·llama
顧棟3 小时前
Zookeeper热缩容解析
linux·服务器·zookeeper
xqqxqxxq3 小时前
技术笔记:上下文工程(Context Engineering)心得总结(李博杰《深入理解 AI Agent》2.1,2.2观后总结)
人工智能·笔记
三言老师3 小时前
CentOS7.9:Redis服务器部署结构化实战教程
linux·运维·服务器·数据库
魔城烟雨4 小时前
从零开始学习betaflight《3-硬件接口设计规范标准》
学习·设计规范
味悲4 小时前
Linux提权
linux·服务器·安全
无足鸟ICT4 小时前
【RHCA+】替换变量
linux