【四】应用编程(应用开发)与网络编程

应用编程(应用开发)与网络编程

Makefile

直接在linux中执行gcc -o test a.c b.c,编译器会分别对a.c和b.c进行编译(包括预处理、编译a.s和汇编a.o)并链接a.i。

a.c->a.s->a.o->a.i(可执行文件)

但是,如果修改了a.c或b.c,再进行执行该语句去编译两个文件。那么,编译器会从头开始进行编译和链接,这是非常浪费时间的。

所以,我们应该对文件分别编译再手动链接。这就是Makefile的思想。
Makefile会自动的查看文件是否被修改(看文件修改时间),自动的选择编译并链接相关文件。
makefie最基本的语法是规则,规则:

目标文件 : 依赖文件1 依赖文件2
[TAB]命令

当某个依赖文件被修改,执行下面语句

test :a.o b.o //test是目标文件,它依赖于a.o b.o文件,一旦a.o或者b.o比test新的时候,
就需要执行下面的命令,重新生成test可执行程序。
gcc -o test a.o b.o
a.o : a.c //a.o依赖于a.c,当a.c更加新的话,执行下面的命令来生成a.o
gcc -c -o a.o a.c
b.o : b.c //b.o依赖于b.c,当b.c更加新的话,执行下面的命令,来生成b.o
gcc -c -o b.o b.c

一个命令能被执行的条件:
(1)目标文件不存在
(2)依赖文件比目标文件新

文件IO

文件IO指的就是操作系统提供支持的API接口,以对文件的输入和输出。

文件IO是由操作系统提供的API接口,不同操作系统之间不通用。由于文件IO直接由内核提供,所以效率比较高,但是频繁的调用会极大的浪费CPU资源。

linux下可查看man手册:
1 可执行程序或 shell 命令
2 系统调用(内核提供的函数)
3 库调用(程序库中的函数)
4 特殊文件(通常位于 /dev)
5 文件格式和规范,如 /etc/passwd
6 游戏
7 杂项(包括宏包和规范,如 man(7),groff(7))
8 系统管理命令(通常只针对 root 用户)
9 内核例程 [非标准]

linux下C语言对于文件的操作,我们会经常用到fopen(),fclose(),fwrite(),fread(),fgets()等一系列库函数,基本和是和windows下学习C语言一样的,其实这些库函数就是在linuxx下对系统调用函数的封装,因此这里只介绍系统函数下的文件操作函数。

异步IO和同步IO

区别:如果是同步IO,当一个IO操作执行时,应用程序必须等待,直到此IO执行完。相反,异步IO操作在后台运行,IO操作和应用程序可以同时运行,提高系统性能,提高IO流量。

在同步文件IO中,线程启动一个IO操作然后就立即进入等待状态,直到IO操作完成后才醒来继续执行。而异步文件IO中,线程发送一个IO请求到内核,然后继续处理其他事情,内核完成IO请求后,将会通知线程IO操作完成了。

同步IO 不一起
异步IO 一起

.open()打开文件

#include <sys/types.h>
#include <sys/stat.h>
#include
int open(const char *pathname, int flags);

参数1:pathname,文件所在路径
参数2:flags,文件权限,相对于程序进程
常见宏为:O_WRONLY,O_RDONLY,O_RDWR,O_EXCL,O_APPEND,O_DUMP
参数3:mode,当创建文件时候使用,一般为umask的值。
返回值: 成功返回文件描述符,否则返回-1.

close,关闭一个文件。参数为文件描述符

#include
int close(int fd);

write,向文件中写数据

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

fd: 文件描述符
buf:存储将要写的数据
count: 写入的长度,以字节为单位
返回值:写入成功时,返回写入的字符长度,否则返回-1。

read,读文件中数据

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

fd: 文件描述符
buf:存储将要读入的数据
count: 读出的长度,以字节为单位
返回值:读成功时,返回读出的字符长度,否则返回-1。

lseek,修改文件偏移量

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

fd: 文件描述符
offset:将要偏移 的字节数。
whence:从那开始偏移,宏定义如下:
SEEK_END 文件末尾
SEEK_CUR 当前偏移量位置
SEEK_SET 文件开头位置
注意:当偏移量大于文件长度时,产生空洞,空洞是由所设置的偏移量超过文件尾端,并写了一些数据造成了,其从原文件尾端到新偏移的位置之间即是产生的空洞。空洞不占用磁盘空间,可以使用:

du filename #查看文件所占实际磁盘空间
ls filename #实际文件的大小

access,dup或dup2,sync与fsync和fcntl函数

Linux缓冲问题

linux中有两个级别的缓冲:IO缓冲与内核缓冲

(1)IO缓冲:对于标准IO操作,都会有一个缓冲区,当用户想要写数据时,首先将数据写入缓冲区,待缓冲区满之后才能调用系统函数写入内核缓冲区。当用户想读取数据时,首先向内核读取一定的数据放入IO缓冲区,读操作从缓冲区中读数据,当读完IO缓冲区的数据时,才能再读取数据到IO缓冲区。

目的:减少对磁盘的读写次数,提高工作效率。

(2)内核缓冲区:操作系统内核部分也有缓冲,其与IO缓冲区是不同的,其主要区别用一张图表示:
image

标准IO

标准I/O是C库函数,该库通过系统调用完成自己的目标。

文件IO的头文件是unistd.h

标准IO的为 stdio.h

文件IO 标准IO
open(char *, flag, mode) FILE * fopen(const char * path,const char * mode)
close(fd) int fcolse(fd)
lseek(int fd, off_t offset,int whence) fseek,rewind

系统调用

系统:操作系统

调用:调用函数

系统调用:操作系统提供给应用程序的接口(函数)

缓冲区问题

缓冲机制:减少系统调用的次数
1.全缓存:当缓冲区满,程序运行结束,强制刷新缓冲区时会刷新缓冲区
2.行缓存:当缓冲区满,程序运行结束,强制刷新缓冲区遇到换行符时会刷新缓冲区
3.不缓存:当程序运行起来后,有三个文件默认已经打开:标准输入,标准输出,标准出错,对应的流指针为:stdin,stdout,stderr

一般而言,printf是带有行缓冲的函数,printf把打印的消息先输出到行缓冲区,在以下几种情况下:1.程序结束时调用exit(0)/return;2.遇到回车\n(因此,这种缓存也叫行缓存),3.调用fflush函数(fclose()会调用该函数);4.缓冲区满。会自动刷新缓冲区,缓冲区的内容显示到标准输出上。

比如在LINUX系统下,执行如下程序:

#include <stdio.h>
int main(void)
{
     printf("hello");
     while(1);
     return 0;
}

使用GCC编译后执行,发现shell中并没有输出hello,这是因为LINUX系统下,printf把“hello”输出到缓冲区,而此时没有发生缓冲区刷新的4种情况,因此shell中并不会看到hello。但是如果使用printf("hello\n");或者在printf后使用fflush(stdout);那么执行时在shell中就会看到hello输出。
缓冲区的概念:
image

三个缓存的概念(数组):

我们的程序中的缓存,就是你想从内核读写的缓存(数组)----用户空间的缓存

每打开一个文件,内核在内核空间中也会开辟一块缓存,这个叫内核空间的缓存

文件IO中的写即是将用户空间中的缓存写到内核空间的缓存中。

文件IO中的读即是将内核空间的缓存写到用户空间中的缓存中。

标准IO的库函数中也有一个缓存,这个缓存称为----库缓存

库缓存写满时,会调用系统调用函数,将lib_buffer 内容写到kernel_buffer中去。库缓存的测试大小时1024byte。

FILE * fopen()

FILE *fopen (const char *path, const char *mode);

int fclose(FILE * stream)

fseek

fflush(FILE *fp)

把库函数中的缓存的内容强制写到内核中。

参考资料:
https://www.cnblogs.com/lsxkugou/p/14162816.html https://blog.csdn.net/bx1091182836/article/details/128906386

程序、进程、线程和任务

程序就是存在于硬盘上静态的源代码或指令。

进程就是正在运行的、动态的、正在使用系统资源的程序、实体,需要统一管理、创建和回收。进程之间相互独立。

线程就是进程的一部分,一个进程可以有多个线程。线程之间共享进程的资源。

任务就是具体要做的事情。

进程及进程间通信

https://blog.csdn.net/weixin_53447537/article/details/129372775

地址空间模型

image

空间 含义
text segment 存储代码的区域
data segment 存储初始化不为0的全局变量和静态变量、const型常量。
bss segment 存储未初始化的、初始化为0的全局变量和静态变量。
heap(堆) 用于动态开辟内存空间。
memory mapping space(内存映射区) mmap系统调用使用的空间,通常用于文件映射到内存或匿名映射(开辟大块空间),当malloc大于128k时(此处依赖于glibc的配置),也使用该区域。在进程创建时,会将程序用到的平台、动态链接库加载到该区域。
stack(栈) 存储函数参数、局部变量
kernel space 存储内核代码

子父进程

子父进程之间拥有相互独立的内存空间。但子进程继承父进程的绝大部分资源,包括堆栈、内存、用户号和组号、打开的文件描述符、当前工作目录、根目录。

子进程与父进程的区别在于:
• 1、父进程设置的锁,子进程不继承(因为如果是排它锁,被继承
的话,矛盾了)
• 2、各自的进程ID和父进程ID不同
• 3、子进程的未决告警被清除;
• 4、子进程的未决信号集设置为空集

进程ID

pid_t getpid(void);//获取当前进程的id
pid_t getppid(void);//获取当前进程的父进程的id

进程的系统调用

进程的创建和终止

//创建
#include <sys/types.h>
#include <unistd.h>
       pid_t fork(void);//通过拷贝调用fork的进程(父进程)创建一个子进程,返回子进程的id号和成功标志(0为成功,-1为失败)
//例子:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(void)
{
        pid_t pid1, pid2;
        pid1 = fork();
        pid2 = fork();

        printf("pid1 = %d, pid2 = %d\n", pid1, pid2);
        return 0;
}
/*		->A
	->A
A		->C
		->B
	->B
		->D
所以最后打印出来的四个printf,ACBD
*/


另外,fork()会复制父进程,运行速度较慢,可采用vfork()函数。

vfork()函数创建一个子进程,但是并不拷贝父进程。它通过允许父子进程可访问相同物理内存从而伪装成对父进程地址空间的真实拷贝,当子进程需要改变内存中数据时才拷贝父进程。(写操作拷贝

//终止
#include <unistd.h>
#include <stdlib.h>
void _exit(int status);//立刻结束进程将缓存释放掉
void exit(int status);//先查看当前进程有没有文件缓存区。如果有,会先处理缓存区的数据,然后释放内存

exit(0);//表示运行正常结束进程;
exit(1);//表示异常退出,返回值1是给操作系统的

exec函数族

exec函数族提供一个在进程中启动另一个程序执行的方法。

  • 它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。
  • 在执行完之后,原调用进程的内容除了进程号外,其他全部都被替换了。
  • 可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

exec函数族-何时使用?

  • 当进程认为自己不能再为系统和用户做出任何贡献了时就可以调用exec函数,让自己执行新的程序
  • 如果某个进程想同时执行另一个程序,它就可以调用fork函数创建子进程,然后在子进程中调用任何一个exec函数。这样看起来就好像通过执行应用程序而产生了一个新进程一样

execl,execlp,execvp

wait

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);
/*
函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出。
如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;
如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

返回值:正常情况下,wait的返回值为子进程的PID。
如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD
*/
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int * status, int options)
//功能和wait函数类似。可以指定等待某个子进程结束以及等待的方式(阻塞或非阻塞)

守护进程

守护进程,也就是通常所说的 Daemon 进程,是 Linux 中的后台服务进程。周期
性的执行某种任务或等待处理某些发生的事件。

Linux 系统有很多守护进程,大多数服务都是用守护进程实现的。比如:像我们
的 tftp,samba,nfs 等相关服务。
UNIX 的守护进程一般都命名为* d 的形式,如 httpd,telnetd 等等。

守护进程会长时间运行,常常在系统启动时就开始运行,直到系统关闭时才终止。不依赖于终端。

守护进程创建流程如下:

  1. 创建子进程,父进程退出
  2. 在子进程中创建新会话
  3. 改变当前目录为根目录
  4. 重设文件权限掩码
  5. 关闭文件描述符

僵尸进程

僵尸进程:当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程.

如何防止僵尸进程过多:
可用top命令查看僵尸进程的数量(zombie),也可用ps -aux | grep Z(僵尸进程的状态显示为Z)来查看僵尸进程的PID等信息。
每当子进程退出,父进程都会收到SIGCHLD信号,故可在父进程中设置SIGCHLD信号的捕获函数,在捕获函数中回收子进程。

个人理解:

    1. 父进程 Process A 创建子进程 Process B,当子进程退出时会给父进程发送信号 SIGCHLD;
    1. 如果父进程没有调用 wait 等待子进程结束,退出状态丢失,转换成僵死状态,子进程会变一个僵尸进程。

孤儿进程

(1)父进程先于子进程结束,此时子进程成为一个孤儿进程。

(2)Linux系统规定:所有孤儿进程都成为一个特殊进程(进程1,也就是init进程)的子进程。

个人理解:
如果父进程退出,并且没有调用wait函数,它的子进程就变成孤儿进程,会被一个特殊进程继承,这就是init进程,init进程会自动清理所有它继承的僵尸进程。

五种状态

image
(1)就绪态:所有运行条件已就绪,只要得到了CPU时间就可运行。

(2)运行态:得到CPU时间正在运行。

(3)僵尸态:进程已经结束了但父进程还没来得及回收。

(4)等待态:包括浅度睡眠跟深度睡眠。进程在等待某种条件,条件成熟后即进入就绪态。浅度睡眠时进程可以被信号唤醒,但深度睡眠时必须等到条件成熟后才能结束睡眠状态。

(5)暂停态:暂时停止参与CPU调度(即使条件成熟),可以恢复。

进程上下文、中断上下文

(1)进程上文:是指进程由用户态切换到内核态时需要保存用户态时CPU寄存器中的值,进程状态以及堆栈上的内容。即保存当前进程的状态,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

(2)进程下文:是指切换到内核态后执行的程序,即进程运行在内核空间的部分。

(3)中断上文:硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上文可以看作硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境)。

(4)中断下文:执行在内核空间的中断服务程序。

进程间通信

常用的进程间通信方式:
• 传统的进程间通信方式
无名管道(pipe)、有名管道(fifo)和信号(signal)
• System V IPC对象
共享内存(share memory)、消息队列(message queue)和信号灯(信号量)
(semaphore)
• BSD
套接字(socket)

管道(无名管道、有名管道)、信号量、消息队列、共享内存、信号、socket套接字

管道(Pipe):管道是一种半双工的通信方式,它只能用于父子进程或者兄弟进程之间的通信。管道有两种类型:无名管道和命名管道。无名管道只存在于相关进程的内存中,而命名管道则存在于文件系统中,允许不相关的进程进行通信。

无名管道(unNamed Pipe):无名管道只存在于相关进程的内存中

int pipe(int pipefd[2]);//pipefd[0]是读端,pipefd[1]是写端。
无名管道容量:65536

命名管道(Named Pipe):命名管道允许不相关的进程进行通信。它在文件系统中创建一个特殊文件,任何知道文件名的进程都可以通过文件访问方式进行通信。

//man 3 mkfifo查看操作手册
mkfifo()

管道阻塞

信号量(semaphore):一个计数器,通常作为一种同步机制,用于进程和线程间的同步。分为命名信号量和未命名信号量。主要用于用于任务之间同步

//信号量需要放在共享内存中
int sem_init(sem_t *sem, int pshared, unsigned int value);//创建信号量
int sem_wait(sem_t *sem);//检查信号量
int sem_post(sem_t *sem);//发布信号量

//命名信号量
sem_open();//创建命名信号量

消息队列(Message Queue):消息队列是一种存放在内核中的消息链表,用于不相关的进程间的通信。进程可以向队列中发送消息,其他进程可以从队列中接收消息。

int msgget(key_t key, int msgflg);

共享内存(Shared Memory):共享内存允许多个进程访问同一块物理内存。进程可以将数据写入共享内存区域,其他进程可以直接从该内存区域读取数据。

int shmget(key_t key, size_t size, int shmflg);//创建共享内存函数

shmat();//映射共享内存
shmdt();//解除映射共享内存

信号(Signal):信号是一种异步通信机制,用于通知进程发生了某些事件。一个进程可以向另一个进程发送信号,另一个进程可以选择接受或忽略信号。传递控制命令。

signal();

套接字(Socket):套接字是一种通用的进程间通信方式,可用于不同主机之间的通信。套接字提供了一组接口,进程可以使用这些接口来建立网络连接并进行通信。

线程

线程定义

线程可以看作是轻量级的进程,Linux线程的本质还是进程。
Linux先有进程后有线程,当创建了一个进程时,系统给它分配一段4G的虚拟内存,并生成进程的PCB,当进程使用相关函数创建一个线程时,会为新的线程也生成一个PCB存放在当前的4G虚拟内存中,不需要开辟额外的地址空间,原来的进程也沦为了一个线程(主线程),所以进程和线程的区别就是是否共享地址空间。
进程总是独享4G的虚拟内存,而多个线程共享一段4G的空间。

一个进程中的多个线程共享以下资源:
• 可执行的指令
• 静态数据
• 进程中打开的文件描述符
• 信号处理函数
• 当前工作目录
• 用户ID
• 用户组ID

线程的优缺点:
优点:① 提高程序并发性② 开销小③ 数据通信、共享数据方便(不同线程可以使用全局变量)
缺点:① 线程使用第三方库函数,不稳定② 代码不好调试,没法用gdb调试③ 对信号的支持不好

线程不能独立运行,但一个线程崩溃不一定导致整个进程崩溃。
因为:
(1)线程属于进程,线程的运行需要依赖进程的地址空间和系统资源。
(2)线程崩溃的本质就是内存出错,若出错的内存没有被其他线程访问,则不会导致其他线程出错,也就不会导致进程崩溃。

线程的系统调用

#include <pthread.h>

//创建一个线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg);

//主动结束当前线程
void pthread_exit(void *retval);

在不终止整个进程的情况下,单
个线程可以有三种方式停止其工
作流并退出:

  • 线程从其工作函数中返回,返
  • 回值是线程的退出码
  • 线程可以被同一进程中的其他
  • 线程取消
  • 线程自己调用pthread_exit
//线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止。
//当函数返回时,被等待线程的资源被收回,可以理解为进程当中的wait收尸
int pthread_join(pthread_t thread, void **retval);

线程间同步和互斥机制

线程间同步方式和互斥机制:
信号、信号量、条件变量、线程锁(互斥锁、自旋锁、读写锁)。

信号量

信号量(Semaphore):信号量是一种计数器,用于控制多个线程对共享资源的访问。当计数器大于零时,线程可以访问资源,并将计数器减一;当计数器等于零时,线程就必须等待。信号量可以用来实现读写锁和生产者-消费者模型等并发编程模型。

条件变量

条件变量(Condition Variable):条件变量用于在某个条件满足时通知等待线程。线程可以在条件变量上等待,直到满足条件后才被唤醒。条件变量通常与互斥锁一起使用,以确保在修改共享资源时不会出现竞态条件

#include <pthread.h>

//初始化和创建条件变量
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond)

//条件变量等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)

//条件变量唤醒
int pthread_cond_signal(pthread_cond_t *cptr);//唤醒一个等待线程
int pthread_cond_broadcast(pthread_cond_t *cptr);//唤醒所有等待线程


互斥锁

互斥锁(Mutex):互斥锁是一种二元锁,只有两个状态:锁定和未锁定。当一个线程持有互斥锁时,其他线程必须等待该线程释放锁之后才能继续执行。如果多个线程同时竞争锁,只有一个线程可以获得锁,其他线程将被阻塞,直到获得锁的线程释放锁。互斥锁提供了一种简单而有效的方法,用于确保多个线程之间对共享资源的互斥访问。

通俗来讲,只有获得锁的线程才可以访问临界资源,访问完资源后释放该锁。无法获得锁的线程,一直阻塞。

可用理解为是一种拥有两种状态,加锁和解锁的信号量,用于任务之间同步
线程互斥锁的相关函数:

/*
mutex:指向互斥锁的指针。
attr:指向互斥锁属性的指针。可以为 NULL,表示使用默认属性。
*/

//用于初始化一个互斥锁。可以使用默认属性或者自定义属性进行初始化
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

//用于获取一个互斥锁(上锁),如果该锁已经被其他线程获取,则当前线程将阻塞,直到该锁被释放为止
int pthread_mutex_lock(pthread_mutex_t *mutex);

//用于释放一个互斥锁(解锁),如果该锁当前没有被任何线程获取,则此函数将返回一个错误。
int pthread_mutex_unlock(pthread_mutex_t *mutex);

//用于销毁一个互斥锁,释放相关资源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);

在使用线程互斥锁的相关函数时,有一些需要注意的地方:
1.互斥锁的初始化和销毁:在使用互斥锁之前,需要先对其进行初始化,使用完毕后需要将其销毁。初始化可以使用 pthread_mutex_init() 函数,销毁可以使用 pthread_mutex_destroy() 函数。
2.加锁和解锁的配对:在使用 pthread_mutex_lock() 和 pthread_mutex_unlock() 函数时,需要保证加锁和解锁的配对性,即每次加锁后都必须要对应的解锁。
3.避免死锁:死锁是指两个或多个线程互相等待对方释放锁,导致程序无法继续执行。为了避免死锁,需要确保加锁的顺序一致,并且不要在加锁的状态下等待其他锁。
4.不要重复加锁:重复加锁会导致线程阻塞,无法进行下去,需要注意避免。
5.对共享资源的访问必须要在加锁状态下进行:为了确保多个线程访问共享资源的互斥性,需要在对共享资源进行访问之前,先获取对应的互斥锁,以避免多个线程同时访问共享资源。

其他方式

互斥锁是线程锁的一种,当然还有:

读写锁(Reader-Writer Lock):读写锁是一种更高效的锁,适用于读多写少的场景。读写锁允许多个线程同时读取共享资源,但是只有一个线程可以写入共享资源。当一个线程持有读锁时,其他线程也可以持有读锁,但是当一个线程持有写锁时,其他线程必须等待该线程释放写锁之后才能继续执行。读写锁的优点是在读取共享资源时可以允许多个线程同时进行,从而提高了程序的并发性和性能。

自旋锁(Spin Lock):自旋锁是一种非阻塞锁,线程在尝试获取锁时不会被挂起,而是一直循环尝试获取锁。当锁被其他线程持有时,线程会一直尝试获取锁,直到锁被释放。自旋锁适用于锁被持有时间很短的场景,因为在等待锁的过程中,线程会消耗大量的CPU资源。

进程和线程的联系和区别

(1)进程是系统中程序执行和资源分配的基本单位,线程是CPU调度的基本单位。
(2)一个进程个拥有多个线程,线程可以访问其所属进程地址空间和系统资源(数据段、已经打开的文件、I/O设备等),同时也拥有自己的堆栈。
(3)同一进程中的多个线程可以共享同一地址空间,因此它们之间的通信实现也比较简单,而且切换开销小、创建和消亡的开销也小。而进程间的通信则比较麻烦,而且进程切换开销、进程创建和消亡的开销也比较大。

image

多进程、多线程

多线程优点

• 经济实惠
分配的资源少、线程切换比进程切换快、维护线程的开销小
• 资源共享
线程共享它们所属进程的存储器和资源。
• 提高了响应速度允许程序在它的一部分被阻塞或正在执行一个冗长 的操作时持续运行
• 提高了多处理机体系结构的利用率
在多CPU机器中,多线程提高了并发性

选择问题

(1)当程序的安全性、稳定性要求较高时用多进程。
(2)需要频繁通信/切换程序/创建跟销毁程序时用多线程。

网络通信

OSI层

image

1.七层划分为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。

2.五层划分为:应用层、传输层、网络层、数据链路层、物理层。

七层过于复杂,采用精简的四层TCP/IP协议:
image

3.四层划分为:应用层、传输层、网络层、网络接口层。(TCP/IP协议对应模型)
各层功能:
(1)应用层:在实现多个应用进程相互通信的同时,完成一系列业务处理所需的服务,比如电子邮件、文件传输、远程登录等。
(2)传输层:为通信双方的主机提供端到端的服务,有两个不同的传输协议TCP和UDP,TCP提供可靠交付,而UDP并不能保证可靠交付。
(3)网络层IP协议,处理分组在网络中的活动,例如分组的选路。
(4)网络接口层:处理与电缆(或其他任何传输媒介)的物理接口细节。

TCP/IP

稳定,可靠,重传,面向连接。

连接是三次握手,释放是四次挥手。(反复确认)
三次握手:

image

(1)第一次握手:客户端创建传输控制块,然后向服务器发出连接请求报文(将标志位SYN置1,随机产生一个序列号seq=x),接着进入SYN-SENT状态。

(2)第二次握手:服务器收到请求报文后由SYN=1得到客户端请求建立连接,回复一个确认报文(将标志位SYN和ACK都置1,ack=x+1,随机产生一个序列号seq=y),接着进入SYN-RCVD状态。此时操作系统为该TCP连接分配TCP缓存和变量。

(3)第三次握手:客户端收到确认报文后,检查ack是否为x+1,ACK是否为1,是则发送确认报文(将标志位ACK置1,ack=y+1,序列号seq=x+1),此时操作系统为该TCP连接分配TCP缓存和变量。服务器收到确认报文并检查无误后则连接建立成功,两者都进入ESTABLISHED状态,完成三次握手。

四次挥手:
image
(1)第一次挥手:客户端发出连接释放报文(FIN=1,seq=u),进入FIN-WAIT-1状态。

(2)第二次挥手:服务器收到连接释放报文后发出确认报文(ACK=1,ack=u+1,seq=v),进入CLOSE-WAIT状态。这时客户端向服务器方向的连接就释放了,这个连接处于半关闭状态,服务器还可继续发送数据。

(3)中间状态:客户端收到服务器的确认报文后,进入FIN-WAIT-2状态,等待服务器发送连接释放报文,此时仍要接收数据。

(4)第三次挥手:服务器最后的数据发送完,向客户端发送连接释放报文(FIN=1,ACK=1,ack=u+1,seq=w),进入LAST-ACK状态。

(5)第四次挥手:客户端收到服务器的连接释放报文后,必须发出确认报文(ACK=1,ack=w+1,seq=u+1),进入TIME-WAIT状态。注意此时连接还未释放,必须进过2*MSL(最长报文寿命)的时间,客户端撤销相应的TCB后,才进入CLOSED状态。服务器一旦收到确认报文,立即进入CLOSED状态。

端口:端口是一个软件结构,被客户进程或服务进程用来发送和接收信息。一个端口对应一个16比特的数。服务进程通常使用一个固定的端口。

ARP和RARP: 将IP地址转换成物理地址的协议是ARP(地址解析协议),反之则是RARP反地址解析协议)。

IP地址由两部分组成,网络号和主机号。不过是要和“子网掩码”按位与上之后才能区分哪些是网络位哪些是主机位。

服务端和客户端通信套接字函数:
image

UDP

不稳定,不可靠,不面向连接

一次

image

posted @ 2023-07-25 13:21  我好想睡觉啊  阅读(24)  评论(0)    收藏  举报