Linux -TCP网络编程基础

2023-10-27

一.套接字编程基础知识

1.套接字地址结构

套接字编程需要指定套接字的地址作为形参,不同的协议族有不同的地址结构定义方式,这些地址结构通常以sockaddr_开头,每一个协议族有一个唯一的后缀,例如以太网,其结构名称为sockaddr_in

1.套接字数据结构

通用套接字地址类型如下,可以在不同协议族之间进行强制转换:

struct sockaddr{ //套接字地址结构
	sa_family_t sa_family; //协议族
	char  sa_data[14]; //协议族数据
	}

注:sa_family_t 类型为unsigned short类型,长度为16字节。

typedef unsigned short sa_faily_t;

2.实际使用的套接字数据结构

网络程序设计中所使用的函数中几乎所有的套接字函数都用这个结构作为参数,例如bind()函数:

int bind(int sockfd,//套接字文件描述符
const struct sockaddr *my_addr,//套接字地址结构
socklen_t addrlen);//套接字地址结构的长度

使用struct sockaddr不方便进行设置,以太网中使用struct sockaddr_in进行设置,如下:

struct sockaddr_in{//以太网套接字地址结构
	u8	sin_len;//结构struct sockaddr_in长度,16
	u8	sin_family;//通常为AF_INET
	u16	sin_port;//16位的端口号,网络字节序
	struct in_addr	sin_addr;//IP地址为32位
	char	sin_zero[8];//未用
	};

结构struct sockaddr_in的成员变量in_addr用于表示IP地址,这个结构定义如下:

struct in_addr{//IP地址结构
	u32 s_addr;//32位IP地址,网络字节序
	};

3.结构sockaddr和结构sockaddr_in关系

结构struct sockaddr和结构struct sockaddr_in是一个同样大小的结构,对应关系如下:
在这里插入图片描述

struct sockaddr_in的成员含义如下:

  1. sin_len: 无符号字符类型,表示结构struct sockaddr _in的长度,为16。

  2. sin_family: 无符号字符类型,通常设置为与socket()函数的domain一致,例如 AF_INET。

  3. sin_port: 无符号short类型,表示端口号,网络字节序。

  4. sin_addr: struct in_addr类型,其成员s_addr为无符号32位数,每8位表示IP地址的一 个段,网络字节序。

  5. sin_zero[8]: char类型,保留。

进行地址结构设置时,通常的方法是利用结构struct sockadd _in进行设置,然后强制转换为结构struct sockaddr类型。因为这两个结构大小是完全一 致的,所以进行这样的转换不会有副作用。

2.用户层和内核层交互过程

  • 套接字参数中有部分参数是需要用户传入的,这些参数用来与Linux内核进行通信,例如指向地址结构的指针。通常是采用内存复制的方法进行。

    • 例如bind()函数需要传入地址结构struct sockaddr *my_ addrmy_addr指向参数的长度。

1.向内核传入数据的交换过程

  1. 向内核传入数据的函数有send()、bind()等,从内核得到数据的函数有accept()、recv()等。

  2. 传入的过程如下图所示,bind()函数向内核中传入的参数有套接字地址结构结构的长度两个与地址结构有关的参数。

  3. 参数addlen表示地址结构的长度,参数my_addr是指向地址结构的指针。

  4. 调用函数bind()的时候,地址结构通过内存复制的方式将其中的内容复制到内核,地址结构的长度通过传值的方式传入内核,内核按照用户传入的地址结构长度来复制套接字地址结构的内容。

在这里插入图片描述

2.内核传出数据的交换过程

  1. 从内核向用户空间传递参数的过程则相反,传出的过程如下图所示。

  2. 通过地址结构的长度套接字地址结构指针来进行地址结构参数的传出操作。

  3. 通常是两个参数完成传出作的功能,一个是表示地址结构长度的参数,另一个是表示套接字地址结构地址的指针

  4. 传出与传入中的参数有所不同,表示地址结构长度的参数在传入过程中是传值,而在传出过程中是通过传址完成的。

  5. 内核按照用户传入的地址结构长度进行套接字地址结构数据复制,将内核中的地址结构数据复制到用户传入的地址结构指针中。

在这里插入图片描述

二.TCP网络编程流程

1.TCP网络编程架构

TCP网络编程有两种模式:

  1. 服务器模式:服务器模式创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进行处理;

  2. 客户端模式:客户端模式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。

1.服务器端的程序设计模式

流程主要分为:

  1. 套接字初始化(socket()函数)
  2. 套接字与端口的绑定(bind()函数)
  3. 设置服务器的侦听连接(listen()函数)
  4. 接受客户端连接(accept()函数)
  5. 接收和发送数据(read()函数、write()函数)
  6. 数据处理及处理完毕的套接字关闭(close()函数)

下图为TCP连接的服务器模式的程序设计:

在这里插入图片描述

  1. 套接字初始化根据用户对套接字的需求来确定套接字的选项。这个过程中的函数为socket(),它按照用户定义的网络类型、协议类型和具体的协议标号等参数来定义。系统根据用户的需求生成一个套接字文件描述符供用户使用。

  2. 套接字与端口的绑定将套接字与一个地址结构进行绑定。绑定之后,在进行网络程序设计的时候,套接字所代表的IP地址和端口地址,以及协议类型等参数按照绑定值进行操作。

  3. 服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理有限个数的客户端连接请求,所以服务器需要设置服务端排队队列的长度。服务器侦听连接会设置这个参数,限制客户端中等待服务器处理连接请求的队列长度。

  4. 在客户端发送连接请求之后,服务器需要接收客户端的连接,然后才能进行其他的处理。

  5. 在服务器接收客户端请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果发送给客户端。

  6. 当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接。

2.客户端的程序设计模式

主要分为:

  1. 套接字初始化(socket()函数)
  2. 套接字初始化(socket()函数)
  3. 连接服务器(connect()函数)
  4. 读写网络数据(read()函数、write()函数)
  5. 数据处理和最后的套接字关闭(close()函数)

如下图所示为TCP客户端模式:

在这里插入图片描述

  • 客户端程序设计模式流程与服务器端的处理模式流程类似
  • 不同之处是客户端在套接字初始化之后可以不进行地址绑定,而是直接连接服务器端。

    • 客户端连接服务器的处理过程中,客户端根据用户设置的服务器地址、端口等参数与特定的服务器程序进行通信。

在这里插入图片描述

3.客户端与服务器的交互过程

客户端与服务器在连接、读写数据、关闭过程中有交互过程:

  1. 客户端的连接过程,对服务器端是接收过程,在这个过程中客户端与服务器进行三次握手,建立TCP连接。建立TCP连接之后,客户端与服务器之间可以进行数据的交互。

  2. 客户端与服务器之间的数据交互相对的过程,客户端的读数据过程对应了服务器端的写数据过程,客户端的写数据过程对应服务器的读数据过程。

  3. 在服务器和客户端之间的数据交互完毕之后,关闭套接字连接。

2.创建网络插口函数socket()

socket()函数原型如下:调用成功返回表示这个套接字的文件描述符,失败返回-1。

#incluede<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);//协议族domain;协议类型type;协议编号protocol
  • 参数domain

用于设置网络通信的域,函数根据此参数选择通信协议的族。通信协议族在文件sys/socket.h中定义,包含下表所有值,以太网中应该使用PF_INET这个域,现有代码使用AF_INET这个值,在头文件中两个值是相同的。
在这里插入图片描述

  • 参数type

参数type用于设置套接字通信的类型,如下表所示type格式定义及含义。主要有SOCK_STREAM(流式套接字)SOCK_DGRAM(数据包套接字)等。

在这里插入图片描述
不是所有协议族都实现了这些协议,例:AF_INET协议族就没有实现SOCK_SEQPACKET协议类型。

  • 参数protocol

用于指定某个协议类型,即type类型中某个类型。通常某个协议中只有一种特定类型,这样protocol参数仅能设置为0。但有些协议有很多种特定的类型,就需要设置这个参数来选择特定的类型。

  1. SOCK_STREAM的套接字表示一个双向的字节流,与管道类似。流式的套接字在进行数据收发之前必须已经连接,连接使用connect()函数进行,连接成功使用read()或者write()函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接收,当数据在一 段时间内仍然没有接收完毕,可以认为这个连接已经死掉。

  2. SOCK_DGRAMSOCK_RAW这两种套接字可以使用函数sendto()来发送数据,使用recvfrom()函数接收数据,recvfrom()接收来自指定IP地址的发送方的数据。

  3. SOCK _PACKET是一种专用的数据包,它直接从设备驱动接收数据。

  4. 函数socket()执行过程可能会出现错误,可以通过ermo获得,具体值和含义在如下表。

  5. 通常情况下造成函数socket()失败的原因是输入的参数错误造成的,例如某个协议不存在等,这时需要详细检查函数的输入参数。

  6. 由于函数的调用不一 定成功,在进行程序设计的时候,一定要检查返回值。

在这里插入图片描述

使用sockt()函数需要设置上述3个参数,如将socket()函数的第一个参数domain设置为AF_INET,第二个参数设置为SOCK_STREAM,第三个参数设置为0,建立一个流式套接字。

int sock = socket(AF_INET,SOCK_STREAM,0);

2.应用层函数socket()和内核函数之间关系

用户设置套接字参数后,函数能够起作用,需要与内核空间的相系统交互,应用层的socket()函数是和内核层的系统调用相对应的,如下图所示:

在这里插入图片描述
函数sock=socket(AF_INET,SOCK_STREAM,0),此函数调用系统函数sys_socket(AF_INET,SOCK_STREAM,0),(在文件net/socket.c中)。系统调用函数分为两部分,一部分生成内核socket结构(z注意于应用层的socket()函数是不同的),另一部分与文件描述符绑定,将绑定的文件描述符值传递给应用层。内核sock结构如下(在文件linux/net.h):

struct socket{
		socket		state;//socket状态(例如SS_CONNECTED等)
		unsigned long  flags;//socket标志(SOCK_ASYNC_NOSPACE等)
		const struct proto_ops *ops;//协议特定的socket操作
		struct fasync_struct *fasync_list;//异步唤醒列表
		struct file *file;//文件指针
		struct sock *sk;//内部网络协议结构
		wait_queue_head_t  wait;//多用户时的等待队列
		short type;//socket类型(SOCK_STREAM等);
};
  1. 内核函数sock_create()根据用户的domain指定协议族,创建一个内核socket结构绑定到当前的进程上,其中type与用户空间用户的设置值是相同的。

  2. sock_map_fd()函数将socket结构与文件描述符列表中的某个文件描述符绑定,之后的操作可以查找文件描述符列表来对内核socket结构。

3.绑定一个地址端口对bind()

建立套接字文件描述符成功后,需对套接字进行地址和端口的绑定,才能进行数据的接收和发送操作。

1.bind()函数介绍

bind()函数将长度为addlenstruct sockadd类型的参数my_addrsockfd绑定在一起,将socked绑定到某个端口上,如果使用connect()函数则没有绑定的必要。绑定的函数原型如下:

#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct socket *my_addr,socklen_t addrlen);
  1. sockfd:函数创建的文件描述符。

  2. my_addr:指向一个结构为sockaddr参数的指针,sockaddr包含地址、端口、IP地址信息。绑定时需将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置后才能进行绑定,绑定后才能将套接字文件描述符与地址等结合在一起。

  3. addrlen:表示my_addr结构的长度,可以设置成sizeof(struct sockaddr)。一般使用AF_INET设置套接字的类型和其他对应的结构,但不同类型的套接字有不同的地址描述符,强制指定地址长度,可能造成不可预料的后果。

bind()函数的返回值为0时表示绑定成功,-1表示绑定失败,erron的错误值如下:
在这里插入图片描述
下面代码初始化一个AF_UNIX族中的SOCK_STREAM类型的套接字。先使用结构struct sockaddr_un初始化my_addr,然后绑定,结构stuct sockaddr_un定义如下:

struct sockaddr_un{
	sa_family_t sun_family;//协议族,应该设置为AF_UNIX
	char sun_path[UNIX_PATH_MAX];//路径名。UNIX_PATH_MAX的值为108
};

2.bind()函数的例子

使用bind()函数进行程序设计的一个实例,先建立一个UNIX族的流类型套接字,然后将套接字地址套接字文件描述符进行绑定:

#define MY_SOCK_PATH "/somepath"
int main(int argc,char *argv[])
{
        int sfd;
        struct sockaddr_un addr;//AF_UNIX对应的结构
        sfd = socket(AF_UNIX,SOCK_STREAM,0);//初始化一个AF_UNIX族的流类型socket;将协议族参数设置为AF_UNIX建立为UNIX族套接字,使用函数socket()进行建立。

        if(sfd == -1)//检查是否正常初始化socket
        {
                perror("socket");
                exit(EXIT_FAILURE);
        }
        memset(&addr,0,sizeof(struct sockaddr_un));//将变量addr置0;初始化地址结构,将UNIX地址结构设置为0,这是进行程序设计时常用的初始化方式
        addr.sun_family = AF_UBIX;//协议族为AF_UNIX
        strncpy(addr.sun_path,MY_SOCK_PATH,//复制路径到地址结构
        sizeof(addr.sun_path - 1);
        if(bind(sfd, (struct sockaddr *) &addr,//绑定
        sizeof(struct sockaddr_un))==-1)//绑定并判断是否成功
        {
        perror("bind");
        exit(EXIT_FAILURE);
        }
        ...//数据接收发送及处理过程
        close(sfd);//关闭套接字文件描述符
   }

注:Linux 的GCC编译器有一个特点,一个结构的最后一个成员为数组时,这个结构可以通过最后一个成员进行扩展,可以在程序运行时笫一次调用此变量的时候动态生成结构的大小。例如上面的代码,并不会因为 struct sockaddr_unstruct sockaddr大而溢出。

另一个使用结构struct sockaddr_in绑定一个AF_INET族到流协议,先将结构struct sockaddr_in的sin_family设置为AF_INET,然后设置端口,接着设置一个IP地址,最后进行绑定:

#define MYPORT 3490 //端口地址
int main(int arg,char *argv[])
{       
        int sockfd;//套接字文件描述符变量
        struct sockaddr_in my_addr;//以太网套接字地址结构
        
        sockfd = socket(AF_INET,SOCK_STREAM,0);//初始化socket
        if(sockfd == -1){ //检查是否正常初始化socket
                perror("socket");
                exit(EXIT_FAILURE);
        }       
        my_addr.sin_family = AF_INET;//地址结构的协议族
        my_addr.sin_port = htons(MYPORT);//地址结构的端口地址,网络字节序,使用htins()进行字节序转换。
        my_addr.sin_addr.s_addr = inet_addr("192.168.1.150");//IP,将字符串的IP地址转化为网络字节序
        
        bzero(&(my_addr.sin_zero),8);//将my_addr.sin_zero置为0
        if(bind(sockfd,(struct sockaddr *)&myadd,
        sizeof(struct sockaddr)) == -1){ //判断是否绑定成功
                perror("bind");
                exit(EXIT FAILURE);
        }       
        ...//接收和发送数据,进行数据处理
                close(sockfd);//关闭套接字文件描述符
}       

3.应用层bind()函数和内核函数之间关系

bind()是应用层函数,要使用函数生效,就要将相关的参数传递给内核并进行处理,应用层的bind()函数与内核之间的函数过程如下所示,图中是一个AF_INET族函数进行绑定的调用过程:

在这里插入图片描述

  1. 应用层的函数bind(sockfd,(struct sockaddr*)&my_ addr, sizeof(struct sockaddr))调用系统函数过程sys_bind(sockf d,(struct sockaddr*)&my_ addr, sizeof(struct sockaddr))

  2. sys_bind()函数首先调用函数sockfd_lookup _light()来获得文件描述符sockfd对应的内核struct sock结构变量,然后调用函数move_addr_to_kemel()将应用层的参数my_addr复制进内核,放到address变量中。

  3. 内核的sock结构是在调用socket()函数时根据协议生成的,它绑定了不同协议族的bind()函数的实现方法,在AF_INET族中的实现函数为inet_bind(), 即会调用AF_INET族的bind()函数进行绑定处理。

4.监听本地端口listen

函数listen()用来初始化服务器可连接队列,多个客户端连接请求同时到来时,服务器不会同时处理,而是将不能处理的客户端连接请求放到等待队列中,队列长度由listen()函数定义。

1.listen()函数介绍

listen()函数原型如下,其中的backlong表示等待队列的长度:

#include<sys/socket.h>
int listen(int sockfd,int backlog);

运行成功时,返回0,失败返回-1,并且设置erron值,错误代码含义如下:
在这里插入图片描述

  1. 接受连接之前,需要用 listen()函数来侦听端口,listen()函数中参数 backlog 的参数表示在 accept()函数处理之前在等待队列中的客户端的长度,如果超过这个长度,客户端会返回一 个ECONNREFUSED 错误。
  2. listen() 函数仅对类型为SOCK_STR EAM 或者SOCK _SEQPACKET 的协议有效,例如,如果对一 个 SOCK_DGRAM 的协议使用函数 listen(), 将会出现错误 errno应该为值EOPNOTSUPP, 表示此 socket不支持函数 listen()操作。大多数系统的设置为 20, 可以将其设置修改为 5或者10, 根据系统可承受负载或者应用程序的需求来确定。

2.listen()函数的例子

在成功进行socket()函数初始化和bind()函数1端口之后,设置listen()函数队列的长度为5。

#define MYPORT 3490 // 端口地址
int main(int argc,char *argv[])
{       
        int sockfd;//套接字文件描述符变量
        struct sockaddr_in my_addr;
        
        sockfd = socket(AF_INET,SOCK_STREAM,0);//初始化socket
        if(sockfd == -1){//检查是否正常初始化socket
                perror("socket");
                exit(EXIT_FAILURE);
        }       
        my_addr.sin_family = AF_INET;//地址结构的协议族
        my_addr.sin_port = htons(MYPORT);//地址结构的端口地址,网络字节序
        my_addr.sin_addr.s_addr = inet_addr("192.168.1.150");//IP,将字符串的IP地址转化为网络字节序
        bzero(&(my_addr.sin_zero),8);//将my_addr.sin_zero置0
        if(bind(sockfd,(struct sockaddr *)&my_addr,
        sizeof(struct sockaddr)) == -1){//判断是否绑定成功
                        perror("bind");//打印错误信息
                        exit(EXIT_FAILURE);//退出程序
                }       
        if(listen(sockfd,5) == -1){//进行侦听队列长度绑定、判断是否listen成功
                perror("listen");
                exit(EXIT_FAILURE);
        }
        ...//接收数据、发送数据和数据的处理过程
                close(sockfd);//关闭套接字
}
        

3.应用层listen()函数和内核函数之间关系

  1. 应用层listen()函数和内核层listen()函数的关系如下图所示,应用层的listen()函数对应于系统调用sys _listen()函数。

  2. sys _listen()函数首先调用sockfd_ lookup _light()函数获得sockfd对应的内核结构struct socket, 查看用户的backlog设置值是否过大,如果过大则设置为系统默认最大设置。

  3. 然后调用抽象的listen()函数,这里指的是 AF_INET 的listen()函数和inet_listen()函数。

  4. inet_listen()函数首先判断是否合法的协议族和协议类型,再更新socket的状态值为TCP_LISTEN,然后为客户端的等待队列申请空间并设定侦听端口。
    在这里插入图片描述

5.接受一个网络请求accept()

客户端的连接请求到达服务器主机侦听的端口时,此时客户端的连接会在队列中等待,直到使用服务器处理接受请求。

函数accept()成功执行后,会返回一个新的套接字文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新描述符来获得。因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符表示正在监听的socket新产生的文件描述符表示客户端的连接,函数send()和recv()通过新的文件描述符进行数据发送。

1.accept()函数介绍

函数原型:

#include <sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
  1. accept()函数可以得到成功连接客户端的 IP 地址、端口和协议族等信息,这个信息是通过参数 addr获得的。

  2. 当accept()函数返回的时候,会将客户端的信息存储在参数 addr中,参数 addrlen表示第2个参数(addr)所指内容的长度,可以使用 sizeof(structsockaddr _in) 来获得。

  3. 需要注意在 accept中addrlen 参数是一 个指针而不是结构,accept()函数将这个指针传给 TCP/IP 协议栈。

  4. accpet()函数的返回值是新连接的客户端套接字文件描述符,与客户端之间的通信是通过accept()函数返回的新套接字文件描述符来进行的,而不是通过建立套接字时的文件描述符,这是在程序设计时候需要注意的地方。

如果accept()函数发生错误,accept()函数会返回-1。通过errno可以得到错误值,含义如下:

在这里插入图片描述

2.accept()函数的例子

首先建立一个流式套接字然后对套接字进行地址绑定,当绑定成功后,初始化侦听队列的长度,然后等待客户端的连接请求。

int main(int argc,char *argv[])
{
        int sockfd,clinet_fd;//sockfd为侦听的socket,clinet_fd为连接方的socket值

        struct sockaddr_in my_addr;//本地地址信息
        struct sockaddr_in client_addr;//客户端连接的地址信息
        int addr_length;//int类型变量,用于保存网络地址长度量
        socket = socket(AF_INET,SOCK_STREAM,0);//初始化一个IPv4族的流式连接

        if(sockfd == -1){//检查是否正常初始化socket
                perror("socket");
                exit(EXIT_FAILURE);
        }
        my_addr.sin_family = AF_INET;//协议族为IPv4,主机字节序
        my_addr.sin_port = htons(MYPORT);//端口,短整型,网络字节序
        my_addr.sin_addr.s_addr = INADDR_ANY;//自动IP地址获得
        bzero(&(my_addr.sin_zero),8);//将sin_zero置0
        if(bind(sockfd,(struct sockaddr *)&my_addr,//绑定端口地址
                sizeof(struct sockaddr)) == -1){//判断是否绑定成功
                perror("bind");
                exit(EXIT_FAILURE);
        }
        if(listen(sockfd,BACKLOG) == -1){//设置侦听队列长度BACKLIG=10并判断是否listen成功
                perror("listen");
                exit(EXIT_FAILURE);
        }
        addr_lenth = sizeof(struct sockaddr_in);//地址长度
        client_fd = accept(sockfd,&client_addr,&addr_length);//等待客户端连接,地址在client_addr中
        
        if(client_fd == -1){//accept出错
                perr("accept");
                exit(EXIT_FAILURE);
        }       
        ...//处理客户端连接过程
                close(client_fd);//关闭客户端连接
        ...//其他过程
                close(sockfd);//关闭服务器端连接
}       

3.应用层accept()函数和内核函数之间关系

  • 应用层的accept()函数和内核层的accept()函数的关系如下图,应用层的 accept()函数对应内核层的sys_accept()函数系统调用函数。
    • 函数sys_accept()查找文件描述符对应的内核socket结构、申请一个用于保存客户端连接的新的内核socket结构 、获得客户端的地址信息、将连接的客户端地址信息复制到应用层的用户、返回连接客户端socket对应的文件描述符。
      在这里插入图片描述
  1. 函数sys_accept()调用函数sockfd_lookup_light()查找到文件描述符对应的内核socket结构后,然后会申请一块内存用于保存连接成功的客户端的状态。

  2. socket结构的一些参数,如类型type、操作方式ops等会继承服务器原来的值,假如原来服务器的类型为AF_INET,则其操作模式仍然是af_inet.c文件中的各个函数。

  3. 然后会查找文件描述符表,获得一个新结构对应的文件描述符。

  4. accept()函数的实际调用根据协议族的不同而不同,即函数指针sock->ops->accept要由socket()函数初始化时的协议族而确定。当为AF_INET时,此函数指针对应于af_inet.c文件中的inet_accept()函数。

  5. 当客户端连接成功后,内核准备连接的客户端的相关信息,包含客户端的IP地址、客户端的端口等信息,协议族的值继承原服务器的值。

  6. 在成功获得信息之后会调用move_addr_to_user()函数将信息复制到应用层空间,具体的地址由用户传入的参数来确定。

6.连接目标网络服务器connect()

客户端建立套接字后,不需要进行地址绑定可以直接连接服务器。连接服务器的函数为connect(),此函数连接指定参数的服务器,例如IP地址、端口等。

1.connect()函数介绍

#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd,struct sockaddr*,int addrlen);
  1. 参数sockfd是建立套接字时返回的套接字文件描述符,由系统调用socket()函数返回。
  2. 参数serv_addr表示一个指向数据结构sockaddr的指针,包含客户端需要连接服务器的目的端口和IP地址,以及协议类型。
  3. 参数addrlen表示第二个参数内容大小,是个整型的变量而不是指针,可使用sizeof(struct sockaddr)获得,与bind()函数不同。

connect()函数返回值成功时为0,错误返回-1,可查看errno获得错误原因,如:
在这里插入图片描述

2.connect()函数例子

先建立一个套接字文件描述符,成功建立描述符后,将需要连接的服务器IP地址和端口填充到一个地址结构中,connect()函数连接到地址结构所指定的服务器上。

#define DEST_IP "132.241.5.10" // 服务器的IP地址
#define DEST_PORT 23 //服务器端口
int main(int argc.char *argv[])
{       
        int ret=0;
        int socket;//sockfd为连接的socket
        struct sockaddr_in  server;//服务器地址信息
        sockfd = socket(AF_INET,SOCK_STREAM,0);//初始化一个IPv4族的流式连接
        
        if(socket == -1){
                perror("socket");
                exit(EXIT_FAILURE);
        }       
        server.sin_family = AF_INET;//协议族为IPv4,主机字节序
        server.sin_port = htons(DEST_PORT);//端口、短整型
        server.sin_addr.s_addr.s_addr = htonl(DEST_IP);//服务器的IP地址
        bzero(&(server.sin_zero),8);//保留字段置 0
        
        ret=connect(sockfd,(struct sockaddr *)&server,sizeof(struct sockaddr));//连接服务器
        ...//接收或者发送数据
        close(sockfd);
}       

3.应用层connect()函数和内核之间的关系

connect()函数主要进行不同的协议映射的时候要根据协议的类类型进行选择,例如流式的回调函数为inet_stream_connetc(),数据的回调函数为inet_dgram_connect()。如下图:

在这里插入图片描述

7.写入数据函数write()

服务器端接收到一个客户端的连接后,可以通过套接字描述符进行数据的写入操作。对套接字进行写入的形式与普通文件操作形式一样,内核根据文件描述符的值来查找所对应的属性,当为套接字的时候,会调用相应的内核函数。
例:向套接字文件描述符中写入数据的例子,将缓冲区的data的数据全部写入套接字文件描述符s中,返回成功写入数据长度。

int size;
char data[1024];
size = write(s,data,1024);


//字节流套接字上的read和write函数所表现的行为不同于通常的文件IO。
//字节流套接字上调用read和write输入或输出的字节数可能比请求的数量少,
//因为内核中用于套接字的缓冲区是有限制的,需要调用者多次调用read或write函数。

//像描述符filedes中写入nbytes个字节,从buff位置开始写
ssize_t writen(int filedes,const void *buff,size_t nbytes);

ssize_t	  writen(int fd, const void *vptr, size_t n)
{
	size_t		nleft;
	ssize_t		nwritten;
	const char	*ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
			if (nwritten < 0 && errno == EINTR)
				nwritten = 0;		/* and call write() again */
			else
				return(-1);			/* error */
		}

		nleft -= nwritten;
		ptr   += nwritten;
	}
	return(n);
}
void Writen(int fd, void *ptr, size_t nbytes)
{
	if (writen(fd, ptr, nbytes) != nbytes)
		err_sys("writen error");
}
8.读取数据函数read()

read()函数从套接字描述符中读取数据。(注:读取之前必须建立套接字并连接)。例:从套接字描述符s中读取1024个字节,放入缓冲区data中,size变量的值为成功读取的数据大小。

int size;
char data[1024];
size = read(s,data,1024);



//字节流套接字上的read和write函数所表现的行为不同于通常的文件IO。
//字节流套接字上调用read和write输入或输出的字节数可能比请求的数量少,因为内核中用于套接字的缓冲区是有限制的,
//需要调用者多次调用read或write函数。
//解决问题:
//从描述符filedes中读取nbyes个字节,存入buff指针的位置。
ssize_t readn(int filedes,void *buff,size_t nbytes);//例:
ssize_t	readn(int fd, void *vptr, size_t n)
{
	size_t	nleft;
	ssize_t	nread;
	char	*ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR)
				nread = 0;		
			else
				return(-1);
		} else if (nread == 0)
			break;				
		nleft -= nread;
		ptr   += nread;
	}
	return(n - nleft);		
}

ssize_t
Readn(int fd, void *ptr, size_t nbytes)
{
	ssize_t		n;

	if ( (n = readn(fd, ptr, nbytes)) < 0)
		err_sys("readn error");
	return(n);
}



//从描述符filedes中读一行文本,长度不超过maxlen,一次读1个字节。存放在buff位置
ssize_t readline(int filedes,void *buff,size_t maxlen);
//例


ssize_t Readline(int fd, void *ptr, size_t maxlen)
{
	ssize_t		n;

	if ( (n = readline(fd, ptr, maxlen)) < 0)
		err_sys("readline error");
	return(n);
}

ssize_t readline(int fd, void *vptr, size_t maxlen)
{
	ssize_t	n, rc;
	char	c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++) {
		if ( (rc = my_read(fd, &c)) == 1) {
			*ptr++ = c;
			if (c == '\n')
				break;	/* newline is stored, like fgets() */
		} else if (rc == 0) {
			*ptr = 0;
			return(n - 1);	/* EOF, n - 1 bytes were read */
		} else
			return(-1);		/* error, errno set by read() */
	}

	*ptr = 0;	/* null terminate like fgets() */
	return(n);
}





#include	"unp.h"

static int	read_cnt;
static char	*read_ptr;
static char	read_buf[MAXLINE];
//每次最多读MAXLINE个字符,然后每次返回一个字符
static ssize_t my_read(int fd, char *ptr)
{

	if (read_cnt <= 0) {
again:
		if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
			if (errno == EINTR)
				goto again;
			return(-1);
		} else if (read_cnt == 0)
			return(0);
		read_ptr = read_buf;
	}

	read_cnt--;
	*ptr = *read_ptr++;
	return(1);
}

//readline较快版本
ssize_t readline(int fd, void *vptr, size_t maxlen)//my_read取代read
{
	ssize_t	n, rc;
	char	c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++) {
		if ( (rc = my_read(fd, &c)) == 1) {
			*ptr++ = c;
			if (c == '\n')
				break;	/* newline is stored, like fgets() */
		} else if (rc == 0) {
			*ptr = 0;
			return(n - 1);	/* EOF, n - 1 bytes were read */
		} else
			return(-1);		/* error, errno set by read() */
	}

	*ptr = 0;	/* null terminate like fgets() */
	return(n);
}
//展露内部缓冲区的状态,便于调用者查看当前文本之后是否收到新数据
ssize_t readlinebuf(void **vptrptr)
{
	if (read_cnt)
		*vptrptr = read_ptr;
	return(read_cnt);
}
/* end readline */

ssize_t Readline(int fd, void *ptr, size_t maxlen)
{
	ssize_t		n;

	if ( (n = readline(fd, ptr, maxlen)) < 0)
		err_sys("readline error");
	return(n);
}

9.关闭套接字函数close()

close()用于关闭已经打开的socket连接,内核会释放相关资源,关闭之后不能使用这个套接字文件描述符进行读写操作。
函数shutdown()可以使用更多方式来关闭连接,允许单方向切断通信或者切断双方的通信。

#include<sys/socket.h>
int shutdown(int s,int how);//s指切断通信的套接字文件描述符,how表示切断方式

此函数用于关闭双向连接的一部分,具体的关闭行为方式通过参数how设置实现。可以如下:

  1. SHUT_RD:值为0,表示切断读,之后不能使用此文件描述符进行读操作。
  2. SHUT_WR,值为1,表示切断写,之后不能使用此文件描述符进行写操作。
  3. SHUT_RDWR:值为2,表示切断读写,之后不能使用此文件描述符进行读写操作与close()函数功能相同。

函数shutdown()调用成功返回0,失败返回-1.通过errno获得错误信息:

在这里插入图片描述

10.fork和exec函数
#include<unistd.h>
//调用一次,返回两次
//一次在父进程中返回子进程PID,一次在子进程中返回0。
//派生进程
pid_t fork(void);


//如果fork进程是unix程序员唯一可以创建进程的手段,那么linux性能会非常差,而且只能fork出同样的进程。

//exec函数就是解决这个问题,它把一个新的程序装载进进程的内存空间,来改变调用进程的执行代码,
//相当于产生一个新的进程,故通常exec使用方式是先fork一个子进程,然后exec执行进程。
//exec并没有产生一个新的进程,而是执行一个可执行文件,让该进程取代原有的进程


 	   #include <unistd.h>

       extern char **environ; //系统环境变量

       int execl(const char *path, const char *arg, ...
                       /* (char  *) NULL */);
       int execlp(const char *file, const char *arg, ...
                       /* (char  *) NULL */);
       int execle(const char *path, const char *arg, ...
                       /*, (char *) 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[]);

//区别:
//fork会产生一个完全相同的进程,是两个进程;exec则本进程会加载可执行文件成为一个新的进程,只有一个进程。
10.getsockname和getpeername函数
// 这两个函数的最后一个参数都是值-结果参数,(这两个函数都得装填由localaddr和peeraddr指针所指的套接字地址结构。)
#include<sys/socket.h>
//返回套接字关联的本地协议地址
int getsockname(int sockfd,struct sockaddr *localaddr,socklen_t *addrlen);
//返回套接字关联的外地协议地址
int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addrlen);
  1. 在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回有内核赋予该连接的本地IP地址和本地端口号。
  2. 在一端口号0调用bind(告知内核去选择本地端口号)后,getsockname用于返回内核赋予的端口号。
  3. getcockname可用于获取某个套接字地址的地址族:
#include	"unp.h"

int sockfd_to_family(int sockfd)
{
	struct sockaddr_storage ss;
	socklen_t	len;

	len = sizeof(ss);
	if (getsockname(sockfd, (SA *) &ss, &len) < 0)
		return(-1);
	return(ss.ss_family);
}
/* end sockfd_to_family */

int
Sockfd_to_family(int sockfd)
{
	int		rc;

	if ( (rc = sockfd_to_family(sockfd)) < 0)
		err_sys("sockfd_to_family error");

	return(rc);
}
  1. 在一个以通配字符地址调用bind的TCP服务器上,于某个客户的连接一旦建立(accept成功返回),getsockname就可以用于返回由内核赋予该连接的本地IP地址。在这样的调用中,套接字描述符参数必须是已连接套接字的描述符,而不是监听套接字的描述符。
  2. 当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途径是调用getpeername。inetd fork并exec某个TCP服务程序时就是如此情形,如下图所示。
    在这里插入图片描述

三.服务器/客户端的简单例子

1.例子功能描述

  1. 程序分为服务器端和客户端,客户端连接服务器后从标准输入读取输入的字符串,发送给服务器;
  2. 服务器接收到字符串后,发送接收到的总字符串个数给客户端;
  3. 客户端将接收到的服务器的信息打印到标准输出。

程序框架如下图所示:在这里插入图片描述

2.服务器网络程序

程序的代码如下,程序按照网络流程建立套接字、初始化绑定网络地址、将套接字与网络地址绑定、设置侦听队列长度、接收客户端连接、收发数据、关闭套接字进行编写。

#include<stdio.h>
#include<stdlib.h>
#include<strings.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#define PORT 8888//侦听端口地址
#define BACKLOG 2//侦听队列长度

int main(int argc,char *argv[])
{
        int ss,sc;//ss:服务器的socket描述符,sc:客户端的socket描述符
        struct sockaddr_in server_addr;//服务器地址结构
        struct sockaddr_in client_addr;//客户端地址结构
        int err;//返回值
        pid_t pid;//分叉进行ID
        ss = socket(AF_INET,SOCK_STREAM,0);//建立流式套接字
        if(ss<0){
                printf("socket error\n");
                return -1;
        }
        //设置服务器地址
        bzero(&server_addr,sizeof(server_addr));//清零
        server_addr.sin_family = AF_INET;//协议族
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//本地地址
        server_addr.sin_port = htons(PORT);//服务器端口
        //绑定地址结构到套接字描述符
        err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
        //设置侦听
        if(err<0){
                printf("bind error\n");
                return -1;
        }
        err = listen(ss,BACKLOG);
        if(err<0){
                printf("listen error\n");
                return -1;
        }



        for(;;){
                socklen_t addrlen = sizeof(struct sockaddr);
                sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);//接收客户端连接
                if(sc<0){
                        continue;
                }
                pid = fork();//分叉进程
                if(pid ==0)//子进程
                {
                        close(ss);//在子进程关闭服务器侦听;通常调用
                        process_conn_server(s);
                }else{
                        close(sc);//在父进程中关闭客户端的连接
                }
        }
}

一个进中的套接字文件描述符的关闭,不会造成套接字的真正关闭,因为任然有一个进程在使用这些套接字描述符,只有所有的进程都关闭了这些描述符,Linux才释放,子进程中,处理通过调用函数process_conn_server()来完成。

3.客户端的网络程序

建立一个流式套接字后,将服务器的地址和端口绑定到套接字描述符上,然后连接服务器,进程处理,最后关闭连接。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<arpa/inet.h>
#define PORT 8888//侦听端口地址

extern void process_conn_client(int s);

int main(int argc,char *argv[])
{
        int s;//s:socket描述符
        struct sockaddr_in server_addr;//服务器地址结构
        s=socket(AF_INET,SOCK_STREAM,0);//建立流式套接字
        if(s<0){
                printf("socket error\n");
                return -1;
        }
        //设置服务器地址
        bzero(&server_addr,sizeof(server_addr));//清零
        server_addr.sin_family = AF_INET;//协议族
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//本地地址
        server_addr.sin_port = htons(PORT);//服务器端口
        //将用户输入的字符串类型的IP地址转换整型
        inet_pton(AF_INET,argv[1],&server_addr.sin_addr);
        //连接服务器
        connect(s,(struct sockaddr*)&server_addr,sizeof(struct sockaddr));
        process_conn_client(s);//客户端处理过程
        close(s);//关闭连接
        return 0;
}
4.服务器/客户端 读取和显示字符串
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>

//先读取从客户端发来的数据,然后将接收到的数据发送给客户端
void process_conn_server(int s)//服务器对客户端的处理
{
        ssize_t size = 0;
        char buffer[1024];//数据的缓冲区

        for(;;){//循环处理
                size = read(s,buffer,1024);//从套接字中读取数据放到缓冲区buffer中
                if(size ==0 ){//没有数据
                        return;
                }
                //构建响应式字符,为接收到客户端字节的数量
                sprintf(buffer,"%ld bytes altogether\n",size);
                write(s,buffer,strlen(buffer)+1);//发给客户端
        }
}


//客户端从标准输入读取数据到缓冲区buffer中,发送服务器端,然后从服务器端读取服务器的响应,将数据发送到标准输出。
void process_conn_client(int s)
{
        ssize_t size = 0;
        char buffer[1024];//数据的缓冲区

        for(;;){//循环处理
                size = read(0,buffer,1024);
                if(size>0)//读到数据
                {
                        write(s,buffer,size);//发送给客户端
                        size = read(s,buffer,1024);//从服务器读取数据
                        write(1,buffer,size);//写到标准输出
                }
        }
}

注:使用read()和write()函数时,文件描述符0表示标准输入,1表示标准输出,可以直接对这些文件描述符进行操作,例如读和写。

6.编译运行程序

服务器:tcp_server.c,客户端:tcp_client.c,服务器/客户端 读取和显示字符串:tcp_process.c,建立Makefile文件:

all:client server

client:tcp_process.o tcp_client.o
        gcc -o client tcp_process.o tcp_client.o

server:tcp_process.o tcp_server.o
        gcc -o server tcp_process.o tcp_server.o

tcp_process.o:
        gcc -c tcp_process.c -o tcp_process.o

clean:
        rm -f client server *.o

在这里插入图片描述

运行服务器可执行程序server,侦听8888端口,等待客户端连接请求。
在这里插入图片描述
在另一个窗口运行客户端,输入hello和nihao字符串,服务器将客户端发送的数据进行计算并返回客户端:
在这里插入图片描述
使用netstat查看网络连接情况,8888服务器端口,55143的端口,服务器和客户端通过这两个端口建立连接。
在这里插入图片描述

四.截取信号的例子

  1. 在Linux操作系统中出现某些状况时,系统会向相关的进程发送信号

  2. 信号的处理方式是系统会先调用进程中注册的处理函数,然后调用系统默认的响应方式,包括终止进程。

  3. 因此在系统结束进程前,注册信号处理函数进行一些处理是一个完善程序的必备条件。

1.信号处理

信号是发生某件事情时的 一个通知,有时候也将称其为软中断。信号将事件发送给相关的进程,相关进程可以对信号进行捕捉并处理。信号的捕捉由系统自动完成,信号处理函数的注册通过函数signal()完成。函数signal()的原型为:

#include<signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);

向信号signum注册一个void(*sighandler_t)(int)类型的函数,函数的句柄handler。
进程捕捉到注册的信号时,会调用响应式函数句柄handler。信号处理函数在处理系统默认的函数之前会被调用。

2.信号SIGPIPE

  1. 如果正在写入套接字的时候,当读取端已经关闭时,可以得到一个 SIGPIPE 信号 。

  2. 信号SIGPIPE 会终止当前进程,因为信号系统在调用系统默认处理方式之前会先调用用户注册的函数,所以可以通过注册 SIGPIPE 信号的处理函数来获取这个信号,并进行相应的处理。

例如,当服务器端已经关闭,而客户端试图向套接字写入数据的时候会产生一个SIGPIPE 信号,此时将造成程序的非正常退出。可以使用signal()函数注册一个处理函数,释放资源,进行一 些善后工作。下面的例子将处理函数sig_pipe()挂接到信号SIGPIPE 上。

void sig_pipe(int sign)
{
printf("Catch a SIGPIPE signal\n");
/*释放资源*/
}
signal(SIGPIPE,sig_pipe);

将此代码加到上面客户端程序中,进行信号测试:在客户端连接后,退出服务器程序。当标准输入有数据的时候,客户端会通过套接字描述符发送数据到服务器端,而服务器已经关闭,因此客户端会收到一 个SIGPIPE 信号。其输出如下:

Catch a SIGPIPE signal

3.信号SIGINT

信号SIGINT通常由Ctrl+C终止造成的,与Ctrl+C一致,kill命令默认发送SIGINT信号,用于终止进程运行向当前活动的进程发送这个信号:

void sig_int(int sign)
{
	printf("Catch a SIGINT signal\n");
	/*释放资源*/
	}
	signal(SIGINT,sig_pipe);

4.僵尸进程

一个子进程子在父进程还没有调用wait()或waitpid()的情况下退出,这个进程就是僵尸进程。

两种解决方法:
1.捕捉SIGCHLD信号,并在信号处理函数里面调用wait函数
2.两次frok。

  • 设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。

    • 信息包括子进程的进程ID、终止状态以及资源利用信息(CPU时间、内存使用量等等)。

      • 如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程父进程ID将被重置为1(init进程)。

      • 继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。

    • 有些Unix系统在ps命令输出的COMMAND栏以<defunct>指明僵尸进程。
  1. 留存僵尸进程,将占用内核中的空间,导致耗尽进程资源。无论何时我们fork子进程都得wait它们,以防它们变成僵尸进程。
  2. 建立一个俘获SIGCHLD信号的信号处理函数,在函数体中我们调用wait.
#include	"unp.h"
void sig_chld(int signo)
{
	pid_t	pid;
	int		stat;

	while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
		printf("child %d terminated\n", pid);
	}
	return;
}

5.wait和waitid函数及waitid

僵尸进程中子进程结束后,需要由父进程回收子进程,父进程不能先于子进程结束的, 当子进程结束后会通知父进程(子进程会给父进程发送SIGCHILD信号),然后父进程接收到通知就会处理子进程了,这时就需要用到wait函数了。

//都返回两个值:1.已终止进程的进程ID号;2.通过status指针返回的子进程终止状态(整数)
//三个宏检查终止状态:
 1.WIFEXITED(status)    //返回非0表示进程正常结束
   WEXITSTATUS(status)  //如上面宏函数为真,则使用此宏函数获取进程退出状态 (exit的参数)

 2. WIFSIGNALED(status)     //返回非0表示进程异常终止
    WTERMSIG(status)        //如上面宏函数为真,使用此宏函数取得使进程终止的那个信号的编号

3.  WIFSTOPPED(status)        //返回非0表示进程处于暂停状态
    WSTOPSIG(status)      //如上面宏函数为真,使用此宏函数取得使进程暂停执行的那个信号的编号
    WIFCONTINUED(status)   //如上面宏函数为真,使用此宏函数让进程暂停后已经恢复运行
//

#include<sys/wait.h>
//wait()会暂时停止目前进程的执行, 直到有信号来到或子进程结束. 如果在调用wait()时子进程已经结束, 则wait()会立即返回子进程结束状态值. 子进程的结束状态值会由参数status 返回, 而子进程的进程识别码也会一起返回. 如果不在意结束状态值, 则参数 status 可以设成NULL. 。
pid_t wait(int *status);//status:用于保存子进程的退出状态等信息(传出参数)。

//父进程在调用wait函数回收子进程时,wait函数将会执行以下几个动作:
//阻塞等待子进程退出;回收子进程残留资源;获取子进程结束状态(退出原因)。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main(void) {
    pid_t pid, wpid;
    int status;
    pid = fork();

    if (pid == 0) {
        sleep(300);
        printf("child, pid = %d\n", getpid());
        return 19;

    } else if (pid > 0) {
        printf("parent, pid = %d\n", getpid());
        //获取子进程的状态
        //父进程一直在阻塞等待子进程结束
        wpid = wait(&status);
        printf("wpid ---- = %d\n", wpid);

        if (WIFEXITED(status)) {
            //获取进程退出状态
            printf("exit with %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            //获取使进程异常终止的信号编号
            printf("killed by %d\n", WTERMSIG(status)); 
        }
    }
}





//  waitpid函数功能和wait一样,都是用来获取子进程状态或改变,但是waitpid函数更加强大,但可指定pid进程回收,可以不阻塞。
pid_t waitpid(pid_t pid,int *status,int options);

/*参数pid:指定回收子进程pid
  pid > 0,回收指定pid的子进程
  pid = -1,回收任意子进程(相当于wait)
  pid = 0,回收和当前调用waitpid一个进程组内的所有子进程
  pid < -1,回收指定进程组内为|pid|的所有子进程

参数status:传出参数,用于保存清理子进程的状态(如果不关心子进程的退出状态可传NULL)

参数options:设置回收状态阻塞或非阻塞
  WUNTRACED:可获取子进程暂停状态,也就是可获取stopped状态

  WCONTINUED(linux2.6.10内核):可获取子进程恢复执行的状态,也就是可获取continued状态

  WNOHANG:设置非阻塞,如果参数pid指定的子进程运行正常未发生状态改变,则立即返回0,如果调用进程没有与pid匹配的子进程,waitpid则出错,设置errno为ECHILD。

  参数options是一个位掩码,可以使用|运算符操作组合以上几个标志,如果options = NULL(也就是这三个开关都不打开),调用waitpid函数会以阻塞方式回收子进程,这点需要注意。

  另外,对于waitpid的参数status值也可以使用WIFEXITED,WIFSIGNALED,WIFSTOPPED等宏函数来进一步判断进程终止的具体原因。*/
//注:使用waitpid函数的时候需要注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>

/*
主要思路:熟悉waitpid函数原型,参数作用和返回值
*/
int main(void) {
    pid_t pid, wpid;
    int flg = 0;
    pid = fork();
    //fork进程失败
    if(pid == -1){
        perror("fork error");
        exit(1);
    //子进程
    } else if(pid == 0){        
        printf("I'm process child, pid = %d\n", getpid());
        sleep(5);               
        exit(4);

    //父进程
    } else {
        do {
            //WNOHANG非阻塞回收指定子进程
            wpid = waitpid(pid, NULL, WNOHANG);
            //wpid = wait(NULL);
            printf("---wpid = %d--------%d\n", wpid, flg++);

            //如果wpid == 0说明参数3为WNOHANG非阻塞回收,且子进程正在运行中
            if(wpid == 0){
                printf("NO child exited\n");
                sleep(1);
            }
        //每次循环前,判断子进程是否可回收
        } while (wpid == 0);    
        //如果为真,可回收指定子进程
        if(wpid == pid){
            printf("I'm parent, I catched child process, pid = %d\n", wpid);
        //回收的任意子进程
        } else {
            printf("other...\n");
        }
    }
    return 0;
}

//与waitpid函数类似,waitid函数是用于获取一个子进程更加详细的状态或改变。waitid是源于System V下的系统调用,现在已加入到2.6.9 linux内核中。

#include <sys/wait.h>
int waitid( idtype_t idtype, id_t id, siginfo_t *infop, int options );
/*参数idtype和id指定需要获取那些子进程的状态:
  1.如果idtype = P_ALL,则获取任意子进程状态,并忽略参数id
  2.如果idtype = P_PID,获取参数id指定的子进程状态
  3.如果idtype = P_PGID,则获取进程组为id的所有子进程

waitid函数和waitpid函数最大的区别在于,waitid可以通过参数options更加精准详细的获取子进程的状态或改变:
  WEXITED:获取正常终止的子进程状态

  WSTOPPED:获取因信号而暂停的子进程

  WCONTINUED:获取由SIGCONT信号恢复执行的子进程

  WNOHANG:同waitpid函数中的选项意义相同

  WNOWAIT:从“可等待状态”的子进程处返回,后面的wait依然可以获取子进程状态
*/


// 调用waitid函数返回0会将子进程相关信息保存到参数infop中,infop是一个传出参数,它的数据类型是siginfo_t结构体,以下是siginfo结构体中的比较重要的成员信息:

siginfo_t {
int      si_signo;   //表示信号,如SIGCHILD信号
 int      si_code;    //一般有这几个值,CLD_EXITED表示子进程是调用_exit终止的,CLD_KILLED表示子进程因某个信号杀死的,CLD_STOPPED表示子进程因某个信号而终止,CLD_CONTINUED表示子进程收到SIGCONT信号恢复执行。 
 pid_t    si_pid;   //发送信号的进程id
 uid_t    si_uid;     //发送信号的进程实际用户
    int      si_status;   //进程退出的原因,比如正常退出对应的值,异常退出对应的信号。子进程具体的退出原因可以通过si_code来进一步判断。
. . . . . .
}



//若参数options指定了WNOHANG,waitid函数会在以下几种情况下返回0:子进程的状态已经改变,或者子进程的状态没有改变。为了确保区分这两种情况,最好是在调用waitid函数之前把siginfo_t结构体清空:

siginfo_t info;
memset(&info , 0 , sizeof(siginfo_t));
waitid(idtype , id , &info , WNOHANG);
if(info.si_pid == 0){
    /*没有任何子进程的状态发生改变*/
}else{
    /*有子进程的状态发生改变*/
}


#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

int main(void)
{
        pid_t pid;
        id_t id;
        int status;
        int ret;
        pid = fork();

        if(pid == -1){
                perror("fork error");
                exit(1);
        }
        //子进程
        else if(pid == 0){        
                printf("I'm child, pid = %d\n", getpid());
                exit(8);    //子进程以_exit方式退出,退出状态的值为8

        }
        //父进程
        else{
                siginfo_t info;
                //清空siginfo结构体
                memset(&info , 0 , sizeof(siginfo_t));
                ret = waitid(P_ALL , id , &info , WEXITED);
                if(ret < 0){
                        perror("waitid error:");
                }
                //判断子进程是否以_exit方式退出
                if(info.si_code == CLD_EXITED)
                {
                        printf("si_code = CLD_EXITED\n");
                }
                //打印子进程退出状态的值
                printf("si_status = %d\n" , info.si_status);
        }
        return 0;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux -TCP网络编程基础 的相关文章

  • 如何在文件夹中的 xml 文件中 grep 一个单词

    我知道我可以使用 grep 在这样的文件夹中的所有文件中查找单词 grep rn core 但我当前的目录有很多子目录 我只想搜索当前目录及其所有子目录中存在的所有 xml 文件 我怎样才能做到这一点 我试过这个 grep rn core
  • 用于时间线数据的类似 gnuplot 的程序

    我正在寻找一个类似 gnuplot用于在时间轴中绘制数据图表的程序 类似 gnuplot 在 Linux 上运行 命令行功能 GUI 对我帮助不大 可编写脚本的语法 输出为 jpg png svg 或 gif 输出应该是这样的 set5 s
  • 为什么 call_usermodehelper 大多数时候都会失败?

    从内核模块中 我尝试使用 call usermodehelper 函数来执行可执行文件 sha1 该可执行文件将文件作为参数并将文件的 SHA1 哈希和写入另一个文件 名为输出 可执行文件完美运行 int result 1 name hom
  • aarch64 Linux 硬浮点或软浮点

    linux系统有arm64 有arm架构armv8 a 如何知道 Debian 运行的是硬浮动还是软浮动 符合 AAPCS64 GNU GCC for armv8仅提供硬浮动aarch64工具链 这与 armv7 a 的 GCC 不同 后者
  • Apache LOG:子进程 pid xxxx 退出信号分段错误 (11)

    Apache PHP Mysql Linux 注意 子进程 pid 23145 退出信号分段错误 11 tmp 中可能存在 coredump 但 tmp下没有找到任何东西 我怎样才能找到错误 PHP 代码中函数的无限循环导致了此错误
  • 如何回忆上一个 bash 命令的参数?

    Bash 有没有办法回忆上一个命令的参数 我通常这样做vi file c其次是gcc file c Bash 有没有办法回忆上一个命令的参数 您可以使用 or 调用上一个命令的最后一个参数 Also Alt can be used to r
  • 由于 abi::cxx11 符号导致的链接问题?

    我们最近收到一份报告 因为GCC 5 1 libstdc 和双 ABI http gcc gnu org onlinedocs libstdc manual using dual abi html 它似乎Clang 不知道 GCC 内联名称
  • 如何使用 VSCode 调试 Linux 核心转储?

    我故意从我使用 VSCode 编写的 C 应用程序生成核心转储 我不知道如何调试核心转储 有没有人愿意分享这方面的经验 更新 我相信我现在已经可以使用了 我为核心文件创建了第二个调试配置 我需要添加指向生成的转储文件的 coreDumpPa
  • 打印本周星期一的日期(在 bash 中)

    我想获取本周星期一的 YYYYMMdd 格式的日期 例如 今天是 20110627 从明天到周日 我仍然想打印周一 今天 的日期 然后下周重复这个过程 monday date dmonday Y m d last monday date d
  • 当在 python linux 中执行命令 os.system() 时,在 python 中给出响应 yes/no

    考虑一个像这样的命令 yum install boto 当我在终端中执行时 要继续 会询问我是 否 我可以像这样用 python 回应它吗 os system yum install boto Next Yes 将通过相同的 python
  • 无法连接到 Azure Ubuntu VM - 公钥被拒绝

    我们在 Azure 上使用 Ubuntu VM 一段时间了 很少遇到任何问题 然而 其中一台虚拟机最近出现了问题 出乎意料的是 Ubuntu VM 开始拒绝公钥 ssh i azure key email protected cdn cgi
  • 有没有办法提高linux管道的性能?

    我正在尝试使用 64 位将超高速数据从一个应用程序传输到另一个应用程序CentOS http en wikipedia org wiki CentOS6 我使用以下方法进行了基准测试dd发现阻碍我的是管道而不是程序中的算法 我的目标是达到
  • 如何将命令输出作为多个参数传递给另一个命令

    我想将命令的每个输出作为多个参数传递给第二个命令 例如 grep pattern input returns file1 file2 file3 我想复制这些输出 例如 cp file1 file1 bac cp file2 file2 b
  • BeagleBone Black 如何用作大容量存储设备?

    是否可以使用 BB 作为大容量存储设备 我希望将其连接到可以从 USB 连接 例如 USB 闪存驱动器 读取文件的音频播放器并充当包含一个特定文件夹的数据存储设备 及其子文件夹 从文件系统 如果可能 在连接到开发板的闪存驱动器上 正如设备规
  • 通过名称获取进程ID

    我想在 Linux 下获得一个给定其名称的进程 ID 有没有一种简单的方法可以做到这一点 我还没有在 C 上找到任何可以轻松使用的东西 如果追求 易于使用 char buf 512 FILE cmd pipe popen pidof s p
  • 使用 Python 将阿拉伯语或任何从右到左书写系统的字符串打印到 Linux 终端

    非常简单的例子是 city print city 我期望输出是 但实际上输出是相反的字符串 字母看起来有点不同 因为它们有开始 中间和结束形式 我无法将其粘贴到此处 因为复制粘贴会再次更正字符串的顺序 如何在 Linux 终端上正确打印阿拉
  • 如何使用 PyAudio 选择特定的输入设备

    通过 PyAudio 录制音频时 如何指定要使用的确切输入设备 我的电脑有两个麦克风 一个内置 一个通过 USB 我想使用 USB 麦克风进行录音 这流类 https people csail mit edu hubert pyaudio
  • Linux 上的“软/硬 nofile”是什么意思

    当我尝试在RedHat EL5上安装软件时 我得到了错误 软 硬nofile的期望值是4096 而默认值是1024 我设法增加了这个数字 但我不知道参数是什么 他们指的是软链接和硬链接吗 我改变的方法是 a 修改 etc security
  • 使用 Vala 和 GLib 的正则表达式

    有没有一个函数 比如http php net manual en function preg match all php http php net manual en function preg match all php 使用 GLibh
  • sudo pip install python-Levenshtein 失败,错误代码 1

    我正在尝试在 Linux 上安装 python Levenshtein 库 但每当我尝试通过以下方式安装它时 sudo pip install python Levenshtein 我收到此错误 命令 usr bin python c 导入

随机推荐

  • Java和Python一些处理sql方式总结

    将查询结果导入csv文件中 public static int executeUpdate String sql Object param 创建一个PreparedStatement对象用来操作数据库 PreparedStatement p
  • TensorFlow之CNN图像分类及模型保存与调用

    本文主要通过CNN进行花卉的分类 训练结束保存模型 最后通过调用模型 输入花卉的图片通过模型来进行类别的预测 测试平台 win 10 tensorflow 1 2 数据集 http download tensorflow org examp
  • Python学习笔记(六):数据类型之 List

    序列是 python 中最基本的数据类型 序列中的每个元素都分配一个索引 索引从 0 开始 python 有六个序列的内置类型 最常见的是列表和元祖 序列都可以进行的操作 包括索引 切片 加 乘 检查成员 此外 python 已经内置确定序
  • 【华为OD机试真题 Python语言】102、简单的解压缩算法

    文章目录 一 题目 题目描述 输入输出 样例1 二 思路参考 三 代码参考 作者 鲨鱼狼臧 个人博客首页 鲨鱼狼臧 专栏介绍 2023华为OD机试真题 使用Python进行解答 专栏每篇文章都包括真题 思路参考 代码分析 订阅有问题后续可与
  • 单片机是什么?是做什么用的?

    简单的说单片机就是一个智能控制芯片 它是将计算机微型化的CPU 所以只要你会它的语言 就可以让它按着我们自己的想法来自由工作 这是其它任何一种芯片都无法替代的 所以一块芯片由于具备了这样一种类似计算机的强大功能 所以被多数设备大量使用 最为
  • HTML5 移动页面自适应手机屏幕四类方法

    1 使用meta标签 viewport H5移动端页面自适应普遍使用的方法 理论上讲使用这个标签是可以适应所有尺寸的屏幕的 但是各设备对该标签的解释方式及支持程度不同造成了不能兼容所有浏览器或系统 viewport 是用户网页的可视区域 翻
  • Array王锐大神力作:osg与PhysX结合系列内容——第3节 地形碰撞体

    Array王锐大神力作 osg与PhysX结合系列内容 地形碰撞体 烘焙 物理碰撞体 HeightField与TriangleMesh 物理材质的概念与使用 直接读取高度图数据 与osg HeightField结合使用 Pvd调试环境 构建
  • 怎么让文件脱离 SVN 版本控制

    1 建一个记事本文件 然后吧这句话复制进去 for r a in do if exist a svn rd s q a svn 2 然后保存 3 再把记事本文件重命名 叫 删除SVN信息 bat 注意后缀名不是txt而是bat 了 4 然后
  • 最大熵模型的学习

    最新思考 最大熵模型 maximum entropy model 由最大熵原理推导实现 是一种判别模型 也是利用条件概率P Y X 来进行判断 要想知道最大熵模型 首先需要从最大熵定理来说起 香农爸爸真的是强 对于通信专业的我们从本科就沐浴
  • Android系统的整个源码目录结构分析

    Android系统的整个源码目录结构分析 本文介绍的Android系统源码并不是像某一个类 比如 Handler 的源码 而是支持整个Android系统能够运行的源码 这里只对Android系统源码目录进行分析 具体的源码一般都是根据需求来
  • CCP2.1协议基础知识

    1 前言 1 1 名词介绍 ASAP Arbeitskreis Standardisierung von Applicationssystem 应用系统标准化小组 CCP CAN Calibration Protocol CAN标定协议 C
  • 一款好用的软件成分分析及同源漏洞检测工具(COBOT SCA)

    北大库博软件成分分析与同源漏洞检测工具 CoBOT SCA 是基于代码大数据 相似哈希等技术 研发的新一代代码同源分析系统 面向组织的第三方库检测需求 在不改变组织现有开发 测试流程的前提下 与源代码管理系统 Git SVN等 缺陷管理系统
  • python 报错“UnicodeDecodeError: ‘utf-8‘ codec can‘t decode byte“的解决办法

    参考 https blog csdn net weixin 40769885 article details 82288553 UnicodeDecodeError utf 8 codec can t decode byte 0xa1 in
  • CLIP(对比语言-图像预训练)Contrastive Language-Image Pre-Training

    CLIP 对比语言 图像预训练 是一种在各种 图像 文本 对上训练的神经网络 可以用自然语言指示它在给定图像的情况下预测最相关的文本片段 而无需直接针对任务进行优化 类似于 GPT 2 和 3 的零样本功能 我们发现 CLIP 与原始 Re
  • 微信小程序生成分享二维码,自定义内容二维码

    生成自定义内容二维码 使用 uQRCode 插件生成 1 导入插件 2 使用插件 官方提供的API 分享页面二维码 1 取得access token 2 生成二维码 本文主要介绍使用uniapp 再微信小程序端生成分享或自定义内容二维码 并
  • rabbitmq怎样确认是否已经消费了消息_【RabbitMq 篇六】消息确认(发送确认与接收确认)...

    前言 消息确认是保证消息传递可靠性的重要步骤 上一节我们说到持久化 持久化只能保证消息不丢失 但是如果消息如果投递失败我们怎么进行补偿操作呢 解决办法就是实现回调函数进行操作 在消息的发送和消息的消费都可以进行补偿操作 下面我们就要讲解消息
  • h5上传文件本分ios版本选择不了rar格式的压缩包

    preface 最近 需要实现一个上传 压缩包的需求 支持 zip rar 文件上传 问题背景 xxxx业务设置 上传类型为压缩包的时候 预期前台 ios 端只能选中zip rar类型 但是accept设置仅支持这两种类型后 ios14 6
  • VS开发Qt程序,无法打印QDebug调试信息,VS进行Qt开发时Qt Designer无法使用“转到槽”选项

    VS开发Qt程序 无法打印QDebug调试信息 VS进行Qt开发时Qt Designer无法使用 转到槽 选项 VS开发Qt程序 无法打印QDebug调试信息 VS进行Qt开发时Qt Designer无法使用 转到槽 选项 VS开发Qt程序
  • EXTJS-textfield等组件宽度自适应

    外部组件属性 layout column 内部组件属性 layout form 并且加入defaults anchor 90 属性 var north new Ext FormPanel region north margins 0 0 2
  • Linux -TCP网络编程基础

    Linux TCP网络编程基础 一 套接字编程基础知识 1 套接字地址结构 2 用户层和内核层交互过程 二 TCP网络编程流程 1 TCP网络编程架构 2 创建网络插口函数socket 3 绑定一个地址端口对bind 4 监听本地端口lis