深入探究Linux套接字操作函数实现原理 (linux 套接字操作函数 实现原理)

Linux是一个开源操作系统,拥有强大的操作系统内核和众多的特性。其中,套接字是一个非常重要的特性之一。套接字是一种用于网络通信的编程接口,它是Linux系统中实现网络通信的核心之一。在Linux系统中,通过调用套接字操作函数来进行网络通信。本文将深入探究Linux套接字操作函数的实现原理。

一、套接字

套接字,也被称为网络套接字或通信套接字,是一种用于网络通信的编程接口。套接字是一组描述符,表示两个应用程序之间的通信链路。在一对套接字描述符中,其中一个表示客户端,另一个表示服务器端,它们通过网络互相通信实现数据交换。

在Linux系统中,套接字可以使用C语言的标准库函数来实现。标准库提供了一组函数,例如socket()、bind()、listen()、accept()、connect()、send()和recv(),我们可以使用这些函数实现与网络的通信。

二、套接字操作函数实现原理

套接字操作函数实现原理可以分为以下几个方面:

1. 创建套接字

调用socket()函数创建套接字。该函数返回套接字描述符(socket descriptor),用于后续的通信操作。在Linux系统中,socket()函数的实现是基于内核系统调用的。

2. 绑定到本地地址和端口号

调用bind()函数将套接字绑定到本地地址和端口号。传入bind()函数的参数包含了IP地址、端口号以及协议族等信息。bind()函数的实现会根据传入的IP地址、端口号以及协议族信息,在内核中创建对应的网络数据结构。

3. 监听连接请求

调用listen()函数,将套接字转化为被动套接字,以等待客户端的连接请求。listen()函数将套接字置于监听状态,并在内核中创建对应的队列,用于处理客户端的连接请求。

4. 接受连接请求

调用accept()函数,在服务器端接受客户端的连接请求。accept()函数会阻塞等待客户端发送连接请求。当客户端发送连接请求时,accept()函数会返回一个新的套接字描述符,表示已连接到客户端的套接字。

5. 发送数据

调用send()函数,向连接的另一端发送数据。send()函数将数据从应用程序缓冲区复制到内核缓冲区,再由内核将数据发送至对端应用程序。

6. 接收数据

调用recv()函数,从连接的另一端接收数据。recv()函数将数据从内核缓冲区复制到应用程序缓冲区,供应用程序进行处理。

三、

套接字是Linux系统中实现网络通信的核心之一。套接字操作函数是使用套接字进行网络通信的基础,它们提供了一组函数,包括socket()、bind()、listen()、accept()、connect()、send()和recv()等,用于实现与网络的交互。了解套接字操作函数的实现原理,可以帮助开发人员更好地使用它们实现各种网络应用。

相关问题拓展阅读:

「图文结合」Linux 进程、线程、文件描述符的底层原理

开发十年经验总结,阿里架构师的手写Spring boot原理实践文档

阿里架构师的这份:Redis核心原理与应用实践,带你手撕Redis

Tomcat结构原理详解

说到进程,恐怕面试中最常见的问题就是线程和进程的关系了,那么先说一下答案:

在 Linux 系统中启瞎,进程和线程几乎没有区别

Linux 中的进程其实就是一个数据结构,顺带可以理解文件描述符、重定向、管道命令的底层工作原理,最后我们从操作系统的角度看看为什么说线程和进程基本没有区别。

首先,抽象地来说,我们的计算机就是这个东西:

这个大的矩形表示计算机的

内存空间

,其中的小矩形代表

进程

,左下角的圆形表示

磁盘

,右下角的图形表示一些

输入输出设备

,比如鼠标键盘显示器等等。另外,注意到内存空间被划分为了两块,上半部分表示

用户空间

,下半部分表示

内核空间

用户空间装着用户进程需要使用的资源,比如你在程序代码里开一个数迅盯组,这个数组肯定存在用户空间;内核空间存放内核进程需要加载的系统资源,这一些资源一般是不允许用户访问的。但是注意有的用户进程会共享一些内核空间的资源,比如一些动态链接库等等。

我们用 C 语言写一个 hello 程序,编译后得到一个可执行文件,在命令行运行就可以打印出一句 hello world,然后程序退出。在操作系统层面,就是新建了一个进程,这个进程将我们编译出来的可执行文件读入内存空间,然后执行,最后退出。

你编译好的那个可执行程序只是一个文件,不是进程,可执行文件必须要载入内存,包装成一个进程才能真正跑起来。进程是要依靠操作系统创建的,每个进程都有它的固有属性,比如进程号(PID)、进程状态、打开的文件等等,进程创建好之后,读入你的程序,你的程序才被系统执行。

那么,操作系统是如何创建进程的呢?

对于操作系统,进程就是一个数据结构

,我们直接来看 Linux 的源码:

task_struct 就是 Linux 内核对于一个进程的描述,也可以称为「进程描述符」。源码比较复杂,我这里就截取了一小部分比较常见的。

我们主要聊聊 mm 指针和 files 指针。 mm 指向的是进程的虚拟内存,也就是载入资源和可执行文件的地方; files 指针指向一个数组,这个数组里装着所有该进程打开的文件的指针。

先说 files ,它是一个文件指针数组。一般来说,一个进程会从 files 读取输入,将输出写入 files ,将错误信息写入 files 。

举个例子,以我们的角度 C 语言的 printf 函数是向命令行打印字符,但是从进程的角度来看,就是向 files 写入数据;同理, scanf 函数就是进程试图从 files 这个文件中读取数据。

每个进程被创建时, files 的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。我们常悄昌空说的「文件描述符」就是指这个文件指针数组的索引 ,所以程序的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。

我们可以重新画一幅图:

对于一般的计算机,输入流是键盘,输出流是显示器,错误流也是显示器,所以现在这个进程和内核连了三根线。因为硬件都是由内核管理的,我们的进程需要通过「系统调用」让内核进程访问硬件资源。

PS:不要忘了,Linux 中一切都被抽象成文件,设备也是文件,可以进行读和写。

如果我们写的程序需要其他资源,比如打开一个文件进行读写,这也很简单,进行系统调用,让内核把文件打开,这个文件就会被放到 files 的第 4 个位置,对应文件描述符 3:

明白了这个原理,

输入重定向

就很好理解了,程序想读取数据的时候就会去 files 读取,所以我们只要把 files 指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘:

同理,

输出重定向

就是把 files 指向一个文件,那么程序的输出就不会写入到显示器,而是写入到这个文件中:

错误重定向也是一样的,就不再赘述。

管道符其实也是异曲同工,把一个进程的输出流和另一个进程的输入流接起一条「管道」,数据就在其中传递,不得不说这种设计思想真的很巧妙:

到这里,你可能也看出「Linux 中一切皆文件」设计思路的高明了,不管是设备、另一个进程、socket 套接字还是真正的文件,全部都可以读写,统一装进一个简单的 files 数组,进程通过简单的文件描述符访问相应资源,具体细节交于操作系统,有效解耦,优美高效。

首先要明确的是,多进程和多线程都是并发,都可以提高处理器的利用效率,所以现在的关键是,多线程和多进程有啥区别。

为什么说 Linux 中线程和进程基本没有区别呢,因为从 Linux 内核的角度来看,并没有把线程和进程区别对待。

我们知道系统调用 fork() 可以新建一个子进程,函数 pthread() 可以新建一个线程。 但无论线程还是进程,都是用 task_struct 结构表示的,唯一的区别就是共享的数据区域不同 。

换句话说,线程看起来跟进程没有区别,只是线程的某些数据区域和其父进程是共享的,而子进程是拷贝副本,而不是共享。就比如说, mm 结构和 files 结构在线程中都是共享的,我画两张图你就明白了:

所以说,我们的多线程程序要利用锁机制,避免多个线程同时往同一区域写入数据,否则可能造成数据错乱。

那么你可能问, 既然进程和线程差不多,而且多进程数据不共享,即不存在数据错乱的问题,为什么多线程的使用比多进程普遍得多呢 ?

因为现实中数据共享的并发更普遍呀,比如十个人同时从一个账户取十元,我们希望的是这个共享账户的余额正确减少一百元,而不是希望每人获得一个账户的拷贝,每个拷贝账户减少十元。

当然,必须要说明的是,

只有 Linux 系统将线程看做共享数据的进程

,不对其做特殊看待

,其他的很多操作系统是对线程和进程区别对待的,线程有其特有的数据结构,我个人认为不如 Linux 的这种设计简洁,增加了系统的复杂度。

在 Linux 中新建线程和进程的效率都是很高的,对于新建进程时内存区域拷贝的问题,Linux 采用了 copy-on-write 的策略优化,也就是并不真正复制父进程的内存空间,而是等到需要写操作时才去复制。

所以 Linux 中新建进程和新建线程都是很迅速的

linux驱动程序结构框架及工作原理分别是什么?

一、Linux device driver 的概念\x0d\x0a\x0d\x0a  系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接枣圆口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能:\x0d\x0a\x0d\x0a  1、对设早岩裤备初始化和释放;\x0d\x0a\x0d\x0a  2、把数据从内核传送到硬件和从硬件读取数据;\x0d\x0a\x0d\x0a  3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据;\x0d\x0a\x0d\x0a  4、检测和处理设备出现的错误。\x0d\x0a\x0d\x0a  在Linux操作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来陆简等待。\x0d\x0a\x0d\x0a  已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,之一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。\x0d\x0a\x0d\x0a  最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。\x0d\x0a\x0d\x0a  二、实例剖析\x0d\x0a\x0d\x0a  我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。\x0d\x0a\x0d\x0a  由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如 open,read,write,close?, 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:\x0d\x0a\x0d\x0a  STruct file_operatiONs {\x0d\x0a\x0d\x0a  int (*seek) (struct inode * ,struct file *, off_t ,int);\x0d\x0a\x0d\x0a  int (*read) (struct inode * ,struct file *, char ,int);\x0d\x0a\x0d\x0a  int (*write) (struct inode * ,struct file *, off_t ,int);\x0d\x0a\x0d\x0a  int (*readdir) (struct inode * ,struct file *, struct dirent * ,int);\x0d\x0a\x0d\x0a  int (*select) (struct inode * ,struct file *, int ,select_table *);\x0d\x0a\x0d\x0a  int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long);\x0d\x0a\x0d\x0a  int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *);\x0d\x0a\x0d\x0a  int (*open) (struct inode * ,struct file *);\x0d\x0a\x0d\x0a  int (*release) (struct inode * ,struct file *);\x0d\x0a\x0d\x0a  int (*fsync) (struct inode * ,struct file *);\x0d\x0a\x0d\x0a  int (*fasync) (struct inode * ,struct file *,int);\x0d\x0a\x0d\x0a  int (*check_media_change) (struct inode * ,struct file *);\x0d\x0a\x0d\x0a  int (*revalidate) (dev_t dev);\x0d\x0a\x0d\x0a  }\x0d\x0a\x0d\x0a  这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。\x0d\x0a\x0d\x0a  下面就开始写子程序。\x0d\x0a\x0d\x0a  #include

基本的类型定义\x0d\x0a\x0d\x0a  #include

文件系统使用相关的头文件\x0d\x0a\x0d\x0a  #include

\x0d\x0a\x0d\x0a  #include

\x0d\x0a\x0d\x0a  #include

\x0d\x0a\x0d\x0a  unsigned int test_major = 0;\x0d\x0a\x0d\x0a  static int read_test(struct inode *inode,struct file *file,char *buf,int count)\x0d\x0a\x0d\x0a  {\x0d\x0a\x0d\x0a  int left; 用户空间和内核空间\x0d\x0a\x0d\x0a  if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )\x0d\x0a\x0d\x0a  return -EFAULT;\x0d\x0a\x0d\x0a  for(left = count ; left > 0 ; left–)\x0d\x0a\x0d\x0a  {\x0d\x0a\x0d\x0a  __put_user(1,buf,1);\x0d\x0a\x0d\x0a  buf++;\x0d\x0a\x0d\x0a  }\x0d\x0a\x0d\x0a  return count;\x0d\x0a\x0d\x0a  }\x0d\x0a\x0d\x0a  这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1。buf 是read调用的一个参数。它是用户进程空间的一个地址。但是在read_test被调用时,系统进入核心态。所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考,在向用户空间拷贝数据之前,必须验证buf是否可用。这就用到函数verify_area。为了验证BUF是否可以用。\x0d\x0a\x0d\x0a  static int write_test(struct inode *inode,struct file *file,const char *buf,int count)\x0d\x0a\x0d\x0a  {\x0d\x0a\x0d\x0a  return count;\x0d\x0a\x0d\x0a  }\x0d\x0a\x0d\x0a  static int open_test(struct inode *inode,struct file *file )\x0d\x0a\x0d\x0a  {\x0d\x0a\x0d\x0a  MOD_INC_USE_COUNT; 模块计数加以,表示当前内核有个设备加载内核当中去\x0d\x0a\x0d\x0a  return 0;\x0d\x0a\x0d\x0a  }\x0d\x0a\x0d\x0a  static void release_test(struct inode *inode,struct file *file )\x0d\x0a\x0d\x0a  {\x0d\x0a\x0d\x0a  MOD_DEC_USE_COUNT;\x0d\x0a\x0d\x0a  }\x0d\x0a\x0d\x0a  这几个函数都是空操作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。\x0d\x0a\x0d\x0a  struct file_operations test_fops = {?\x0d\x0a\x0d\x0a  read_test,\x0d\x0a\x0d\x0a  write_test,\x0d\x0a\x0d\x0a  open_test,\x0d\x0a\x0d\x0a  release_test,\x0d\x0a\x0d\x0a  };\x0d\x0a\x0d\x0a  设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。\x0d\x0a\x0d\x0a  int init_module(void)\x0d\x0a\x0d\x0a  {\x0d\x0a\x0d\x0a  int result;\x0d\x0a\x0d\x0a  result = register_chrdev(0, “test”, &test_fops); 对设备操作的整个接口\x0d\x0a\x0d\x0a  if (result

\x0d\x0a\x0d\x0a  #include

\x0d\x0a\x0d\x0a  #include

\x0d\x0a\x0d\x0a  #include

\x0d\x0a\x0d\x0a  main()\x0d\x0a\x0d\x0a  {\x0d\x0a\x0d\x0a  int testdev;\x0d\x0a\x0d\x0a  int i;\x0d\x0a\x0d\x0a  char buf;\x0d\x0a\x0d\x0a  testdev = open(“/dev/test”,O_RDWR);\x0d\x0a\x0d\x0a  if ( testdev == -1 )\x0d\x0a\x0d\x0a  {\x0d\x0a\x0d\x0a  printf(“Cann’t open file \n”);\x0d\x0a\x0d\x0a  exit(0);\x0d\x0a\x0d\x0a  }\x0d\x0a\x0d\x0a  read(testdev,buf,10);\x0d\x0a\x0d\x0a  for (i = 0; i );\x0d\x0a\x0d\x0a  close(testdev);\x0d\x0a\x0d\x0a  }\x0d\x0a\x0d\x0a  编译运行,看看是不是打印出全1 \x0d\x0a\x0d\x0a  以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O port等问题。这些才是真正的难点。上述给出了一个简单的字符设备驱动编写的框架和原理,更为复杂的编写需要去认真研究LINUX内核的运行机制和具体的设备运行的机制等等。希望大家好好掌握LINUX设备驱动程序编写的方法。关于linux 套接字操作函数 实现原理的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。


数据运维技术 » 深入探究Linux套接字操作函数实现原理 (linux 套接字操作函数 实现原理)