基础概述
实现功能
- 服务端与客户端进行连接
- 服务端与客户端之间传输数据
- 服务端与客户端之间断开连接
1 | while(true) |
服务端与客户端连接
创建
socket
将
socket
与指定的 IP 和端口绑定让
socket
在绑定的端口处监听请求 (等待客户端连接到服务端绑定的端口)
当成功创建 对应的 socket()
会返回对应的 套接字
然后利用 bind()
绑定对应的 ip 和端口
然后监听 listen()
把套接字转化为可接受链接的状态
利用 accept()
函数受理链接请求
文件操作基础
linux 下
在 linux 中 socket
操作 与 文件操作没有区别
打开文件:
int open(const char *path, int flag); –> (Linux上对应socket(…)函数)
关闭文件或套接字:
int close(int fd); –>(Windows上对应closesocket(SOCKET S)函数)
将数据写入文件或传递数据:
ssize_t write(int fd, const void *buf, size_t nbytes);
读取文件中数据或接收数据:
ssize_t read(int fd, void *buf, size_t nbytes);
Windows 下
大部分和 linux 相同
Windows上的I/O函数和套接字I/O函数是不同的
传输函数
int send(SOCKET s, const char *buf, int len, int flags);
接收函数
int recv(SOCKET s, const char *buf, int len, int flags);
windows下,windows 首先要对 Winsock
库 进行一个初始化,最后退出还要注销 Winsock
相关库。
初始化
1 | int main(int argc, char *argv[]) |
退出时
1 | int WSACleanup(void); //返回0成功,失败返回SOCKET_ERROR |
进程
所有子进程都会消耗内存和cpu 资源。
子进程执行完后会发送一个 exit
型号然后死掉,父进程会读取到这个信号。随后父进程调用 wait
命令来读取进程的退出状态,并把子进程从进程列表中移除。
如果父进程没有使用 wait
或者 waitpid
而且子进程结束后,没有显示忽略 SIGCHILD
信号。那么他就变成了一个 僵尸进程。
如果父进程先结束,就不会存在僵尸进程,父进程结束时,系统会利用 init
进程来接管子进程成为父进程。然后 init
会自动调用 wait
其子进程,从而自进程不会变成僵尸进程。
僵尸进程会占用系统资源
僵尸进程不能使用 Kill 进程终止
并行和并发
并行:多个处理器或者多核的处理器 同时处理多个不同的任务
并发:一个处理器同时处理多个任务
创建进程时内核的操作
在linux 下 进程 线程都属于 task
都拥有自己的 task_struct
正常情况下 一个进程会有 4G 虚拟内存,其中 3G 交给用户使用,1G用于储存内核相关的资料
进程的创建是通过 sys_fork
函数 出发的 int 0x80 中断实现的,主要利用 find_empty_process
和 copy_process
这两个函数
find_empty_process
为进程分配一个进程号。 通过 last_pid
全局变量存放的进程id 来指定新的进程号。
copy_process
为新进程创建 task_struct
并且复制 父进程的 task_struct
给新进程。然后为新进程创建一个 页表 (通过 get_free_page
函数) 也是要复制 父进程的页表。新进程会共享父进程的文件,并设置新进程的全局描述符表。
父子进程数据共享
父子进程中
不共享的
- 全局变量
- 局部变量
- 动态分配的变量
共享的
- 文件内容,共享了文件的偏移量(句柄
进程回收
利用 wait()
函数,waitpid()
函数
wait 函数
pid_t wait(int *status);
返回值为 本次回收的 子进程的pid子进程结束时,会向其父进程发送
SIGCHILD
信号父进程调用
wait
函数后阻塞父进程被
SIGCHILD
信号唤醒,然后去回收僵尸子进程父子进程之间是异步的,
SIGCHILD
信号机制就是为了解决 父子进程之间的 异步通信问题,让父进程可以及时的去回收僵尸子进程若父进程没有任何子进程则 wait 返回错误
测试wait 函数
1 |
|
Waitpid 函数
pid_t waitpid(pid_t pid, int *status, int options);
进程功能一样
Waitpid 可以回收指定的 PID 子进程
Waitpid 可以阻塞式或非阻塞式 两种工作状态
进程间通信
管道
信号
信息队列
共享内存
信号量
Socket
管道
管道这种通信方式效率低,不适合进程间频繁地交换数据
类似命令行的 |
在 shell 里面执行 A | B
命令的时候,A 进程和 B 进程都是 shell 创建出来的子进程,A 和 B 之间不存在父子关系,它俩的父进程都是 shell。
匿名管道 pipe
管道是 半双开,数据只流向一个方向,如果想双方通信,需要建立两个管道
只能用于 父子进程或者兄弟进程之间
管道对于管道两端的进程,就是一个文件。他不属于某种文件系统,只存在于内存中。
数据的读出读入:写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
也就是一个 内核缓冲区,先进先出的方式存取
有名管道 FIFO
提供了 一个路径名与之关联。
这样 即使与有名管道的创建进程不存在亲缘关系。只要可以访问该路径,就能彼此通过有名管道相互通信。
有名管道的名字存在于文件系统中,内存存放在内存中。
磁盘文件的方式存在。
有名管道的 开大需要确定对方的存在,即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写,不会阻塞。
信号 (唯一的异步通信机制)
对于 异常情况下的工作模式 就需要用「信号」的方式来通知进程。
可以在任何时候发送给任一进程,无需知道进程状态。
如果该进程当前未处于执行状态,信号保存在内核中,直到进程回复执行并传递给它为止。
如果进程阻塞,信号传递被延迟。
信号生命周期和处理流程
(1)信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统;
(2)操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。
(3)目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。
消息队列
消息队列是保存在内核中的消息链表,每个消息队列由消息队列标识符表示
发送的数据,会被分成一个一个独立的数据单元(数据块)
消息队列允许一个或多个进程向它写入与读取信息。
消息队列不适合比较大的数据的传输。
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销。
共享内存
是的多个进程可以直接写同一个内存,内核分配的内存区,可以由需要访问的进程将其映射到自己的私有地址空间。
依靠某种同步机制(如信号量)来达到进程间的同步及互斥。
信号量
计数器,多进程共享数据的访问。 用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
- 创建一个信号量
- 等待一个信号量
- 挂出一个信号量
Posix(可移植性操作系统接口)有名信号量(使用Posix IPC名字标识)、Posix基于内存的信号量(存放在共享内存区中)、System V信号量(在内核中维护)。
- 一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
- 另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
套接字 Socket
Socket 是支持 TCP/IP 的网络通信的基本操作单元。双向通信
域、端口号、协议类型
TCP 协议通信模型
- 服务端和客户端初始化
socket
,得到文件描述符;- 服务端调用
bind
,将绑定在 IP 地址和端口;- 服务端调用
listen
,进行监听;- 服务端调用
accept
,等待客户端连接;- 客户端调用
connect
,向服务器端的地址和端口发起连接请求;- 服务端
accept
返回用于传输的socket
的文件描述符;- 客户端调用
write
写入数据;服务端调用read
读取数据;- 客户端断开连接时,会调用
close
,那么服务端read
读取数据的时候,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
UDP 模型
没有链接的 需要 IP 和 端口号,利用bind