汇编、Linux API面试题

汇编语言

1、寻址方式

①立即数寻址

②寄存器寻址

③存储器寻址:直接寻址,寄存器间接寻址,基址变址寻址,基址变址寻址且相对寻址,寄存器相对寻址

④转移寻址

2、寄存器

AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW

Linux API

open、read、write、close、lseek、opendir、readdir、fcntl、closedir、rand、srand、time、localtime

进程创建函数:fork、子进程中返回值为0,父进程中返回值为子进程的PID。程序员可以根据返回值的不同让父进程和子进程执行不同的代码。

wait、exec、single、select、poll、epoll

1、线程创建与回收

函数名作用
pthread_create主线程用来创造子线程的
pthread_join主线程用来等待(阻塞)回收子线程
pthread_detach主线程用来分离子线程,分离后主线程不必再去回收子线程

2、线程取消

函数名作用
pthread_cancel一般都是主线程调用该函数去取消(让它赶紧死)子线程
pthread_setcancelstate子线程设置自己是否允许被取消
pthread_setcanceltype

3、线程函数退出相关

函数名作用
pthread_exit退出
return退出
pthread_cleanup_push这两个函数貌似和堆栈有关
pthread_cleanup_pop

4、fork底层原理

fork后父子进程共享一个文件描述符,父进程、子进程对文件操作都会影响对方。
函数功能:
以当前进程作为父进程创建出一个新的子进程,并且将父进程的所有资源拷贝给子进程,这样子进程作为父进程的一个副本存在。父子进程几乎时完全相同的,但也有不同的如父子进程ID不同。
需要注意的是:
当fork系统调用成功时,它会返回两个值:一个是0,另一个是所创建的新的子进程的ID(>0)。当fork成功调用后此时有两个数据相同的父子进程,我们可以通过fork的返回值来判断接下来程序是在执行父进程还是子进程。

id==0:执行子进程
id>0:在父进程中执行
id<0:fork函数调用失败

fork()系统调用通过复制一个现有进程来创建一个全新的进程。进程被存放在一个叫做任务队列的双向循环链表当中,链表当中的每一项都是类型为task_struct称为进程描述符的结构,也就是我们写过的进程PCB.

Tips:内核通过一个位置的进程标识值或PID来标识每一个进程。//最大值默认为32768,short int短整型的最大值.,他就是系统中允许同时存在的进程最大的数目。
可以到目录 /proc/sys/kernel中查看pid_max:

当进程调用fork后,当控制转移到内核中的fork代码后,内核会做4件事情:
1、分配新的内存块和内核数据结构给子进程

2、将父进程部分数据结构内容拷贝至子进程

3、添加子进程到系统进程列表当中

4、fork返回,开始调度器调度
在这里有一个疑问,那么fork函数在底层到底做了什么呢?
Linux平台通过clone()系统调用实现fork()。 fork(),vfork()和clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork(), 再然后do_fork()完成了创建中的大部分工作,该函数调用copy_process().做最后的那部分工作。

那么fork函数为什么是一次调用,却返回了两次呢?

当程序执行到下面的语句: pid=fork();
由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。
而且我们还需要注意的是:子进程的代码是从fork处执行的,fork底层实现采用了COW(copy_on_write)技术—-写入时拷贝
写时拷贝思想:父进程和子进程共享页帧而不是复制页帧。然而,只要页帧被共享,它们就不能被修改,即页帧被保护。无论父进程还是子进程何时试图写一个共享的页帧,就产生一个异常,这时内核就把这个页复制到一个新的页帧中并标记为可写。原来的页帧仍然是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。

5、fork和vfork的区别

(1)fork:子进程拷贝父进程的代码段和数据段
vfork:子进程和父进程共享代码段和数据段
(2)fork中父子进程的先后运行次序不定
vfork:保证子进程先运行,子进程exit后父进程才开始被调度运行
(3) vfork ()保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在 调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
(4)就算fork实现了写时拷贝,但其效率仍然没有vfork高,但是vfork在一般平台上都存在问题,所以一般不推荐使用

6、信号处理

信号由谁处理、如何处理
(1)忽略信号
(2)捕获信号(信号绑定了一个函数)
(3)默认处理(当前进程没有明显的管这个信号,默认:忽略或终止进程)

信号数字功能
SIGINT2Ctrl+C时OS送给前台进程组中每个进程
SIGPOLL SIGIO8指示一个异步IO事件,在高级IO中提及
SIGKILL9杀死进程的终极办法
SIGUSR110用户自定义信号,作用和意义由应用自己定义

signal函数介绍
这是一个API

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

用signal函数处理SIGINT信号
(1)默认处理
(2)忽略处理
(3)捕获处理
细节:

(1)signal函数绑定一个捕获函数后信号发生后会自动执行绑定的捕获函数,并且把信号编号作为传参传给捕获函数

(2)signal的返回值在出错时为SIG_ERR(SIG_ERR其实就是sighandler_t类型的-1),绑定成功时返回旧的捕获函数
SIG_DFL:sighandler_t类型的0,是默认处理的意思
SIG_IGN:sighandler_t类型的1,忽略处理,这几个宏用来给signal函数传的第二个参数,第二个参数也可以是自己实现的函数,用来捕获到信号后执行。
signal函数的优点和缺点

(1)优点:简单好用,捕获信号常用
(2)缺点:无法简单直接得知之前设置的对信号的处理方法
sigaction函数介绍
(1)2个都是API,但是sigaction比signal更具有可移植性
(2)用法关键是2个sigaction指针
sigaction比signal好的一点:sigaction可以一次得到设置新捕获函数和获取旧的捕获函数(其实还可以单独设置新的捕获或者单独只获取旧的捕获函数),而signal函数不能单独获取旧的捕获函数而必须在设置新的捕获函数的同时才获取旧的捕获函数。

#include <stdio.h> 
#include <signal.h> 
#include <stdlib.h> 

typedef void (*sighandler_t)(int);


void func(int sig)//捕获函数
{
    if (SIGINT != sig)
        return;
    
    printf("func for signal: %d.\n", sig);
}


int main(void)
{
    sighandler_t ret = (sighandler_t)-2;//给ret赋初值为-2。
    //signal(SIGINT, func);         //就是用来捕获信号的,信号发生时执行func函数
    //signal(SIGINT, SIG_DFL);    // 指定信号SIGINT为默认处理     
    ret = signal(SIGINT, SIG_IGN);        // 指定信号SIGINT为忽略处理     
    if (SIG_ERR == ret)
    {
        perror("signal:");
        exit(-1);
    }
    
    printf("before while(1)\n");
    while(1);
    printf("after while(1)\n");
    
    return 0;
}

alarm和pause函数

alarm函数

(1)内核以API形式提供的闹钟
pause函数
(1)内核挂起
pause函数的作用就是让当前进程暂停运行,交出CPU给其他进程去执行。当当前进程进入pause状态后当前进程会表现为“卡住、阻塞住”,要退出pause状态当前进程需要被信号唤醒。

7、高级IO相关

7、5种IO模型

阻塞IO、非阻塞IO、信号驱动IO、IO多路复用、异步IO

8、请你说说select、poll、epoll的区别,原理,性能,限制都说一说

1、IO多路复用
IO多路复用就是我们说的select,poll,epoll。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
I/O多路复用和阻塞I/O其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

2、select

select:是最初解决IO阻塞问题的方法。时间复杂度O(n)
存在的问题:
①内置数组的形式使得select的最大文件数受限与FD_SIZE;
②每次调用select前都要重新初始化描述符集,将fd从用户态拷贝到内核态,每次调用select后,都需要将fd从内核态拷贝到用户态;
③轮寻排查当文件描述符个数很多时,效率很低;

3、poll
时间复杂度O(n)
poll:解决了select文件描述符受限的问题。他的结构体保存描述符的信息,每增加一个文件描述符就向数组中加入一个结构体,结构体只需要拷贝一次到内核态。poll解决了select重复初始化的问题。轮寻排查的问题未解决。

4、epoll
时间复杂度O(1)底层为红黑树
epoll:轮寻排查所有文件描述符的效率不高,使服务器并发能力受限。因此,epoll采用只返回状态发生变化的文件描述符,便解决了轮寻的瓶颈。epoll实际上是事件驱动的。他的最大连接限制很大,他通过mmap(),文件映射内存加速与内核空间的消息传递,减少复制开销。
epoll有两种触发方式:LT(level trigger)水平触发和ET(edge trigger)边缘触发。LT模式是默认模式
①LT模式
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
②ET模式
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

③LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

9、线程池

满足并发的需求。
现在的系统中一班都实现了线程池。线程池就是在进程开始时创建一定数量的线程,并加到池中等待工作,当服务器收到请求,会唤醒一个线程,并将需要的服务传递给他,一旦线程完成服务,在返回到池中等待。

9、信号量、互斥锁、条件变量

信号量的介绍和使用
信号量和互斥锁都是系统层面实现的,其本质是因为硬件实现了原子操作,不可打断,操作系统进行了封装,方面我们使用,才有了信号量和互斥锁

在我们使用上面的函数创建进程之后,进程之间不能够实现同步的功能,所以这里我们引入信号量。使用的是Linux中的库函数。信号量相关函数如下:

信号量初始化函数
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数中的pshared和value一般为0,sem需要自己定义。
激活信号量
#include <semaphore.h>
int sem_post(sem_t *sem);
用于阻塞,成功就会返回0
#include <semaphore.h>
int sem_wait(sem_t *sem);
销毁信号量
#include <semaphore.h>
int sem_destroy(sem_t *sem);

需要声明的是,在终端中编译Linux线程相关的代码时,要在gcc后面加上-lpthread ,这是因为:

pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程,以及调用 pthread_atfork()函数建立fork处理程序时,需要链接该库。 在编译中要加 -lpthread参数     gcc thread.c -o thread -lpthread     thread.c为你些的源文件,不要忘了加上头文件#include<pthread.h>

#include <stdio.h> 
#include <string.h> 
#include <stdlib.h> 
#include <pthread.h> 
#include <semaphore.h> 

char buf[200] = {0};
sem_t sem;//需要作为全局变量,子线程也能用
unsigned int flag = 0;


// 子线程程序,作用是统计buf中的字符个数并打印 
void *func(void *arg)
{        
    // 阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符     
    // 长度,然后打印;完成后再次被阻塞     
    sem_wait(&sem);    
    while (flag == 0)
    {    
        printf("本次输入了%d个字符\n", strlen(buf));
        memset(buf, 0, sizeof(buf));
        sem_wait(&sem);
    }
    pthread_exit(NULL);
}


int main(void)
{
    int ret = -1;
    pthread_t th = -1;
    
    sem_init(&sem, 0, 0);
    ret = pthread_create(&th, NULL, func, NULL);
    if (ret != 0)
    {
        printf("pthread_create error.\n");
        exit(-1);
    }
    
    printf("输入一个字符串,以回车结束\n");
    while (scanf("%s", buf))
    {
        // 去比较用户输入的是不是end,如果是则退出,如果不是则继续            
        if (!strncmp(buf, "end", 3))
        {
            printf("程序结束\n");
            flag = 1;
            sem_post(&sem);            
            break;
        }
        
        // 主线程在收到用户收入的字符串,并且确认不是end后         
        // 就去发信号激活子线程来计数。         
        // 子线程被阻塞,主线程可以激活,这就是线程的同步问题。         
        // 信号量就可以用来实现这个线程同步         
        sem_post(&sem);    
    }

    
    // 回收子线程     
    printf("等待回收子线程\n");
    ret = pthread_join(th, NULL);
    if (ret != 0)
    {
        printf("pthread_join error.\n");
        exit(-1);
    }
    printf("子线程回收成功\n");
    
    sem_destroy(&sem);
    
    return 0;
}

互斥锁

(1)互斥锁又叫互斥量(mutex)

(2)相关函数:

pthread_mutex_init //初始化互斥锁
pthread_mutex_destroy //销毁互斥锁
pthread_mutex_lock //上锁
pthread_mutex_unlock //解锁

(3)互斥锁和信号量的关系:可以认为互斥锁是一种特殊的信号量

(4)互斥锁主要用来实现关键段保护,保护这段代码不被别人访问。互斥锁上锁的代码,就不可以访问了,从而实现保护,是阻塞的。

用互斥锁来实现的代码

#include <stdio.h> 
#include <string.h> 
#include <stdlib.h> 
#include <pthread.h> 



char buf[200] = {0};
pthread_mutex_t mutex;
unsigned int flag = 0;


// 子线程程序,作用是统计buf中的字符个数并打印 
void *func(void *arg)
{
    // 子线程首先应该有个循环     
    // 循环中阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符     
    // 长度,然后打印;完成后再次被阻塞     
    
    //while (strncmp(buf, "end", 3) != 0)     
    sleep(1);
    while (flag == 0)
    {    
        pthread_mutex_lock(&mutex);
        printf("本次输入了%d个字符\n", strlen(buf));
        memset(buf, 0, sizeof(buf));
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    
    
    pthread_exit(NULL);
}


int main(void)
{
    int ret = -1;
    pthread_t th = -1;
    
    
    
    pthread_mutex_init(&mutex, NULL);
    
    ret = pthread_create(&th, NULL, func, NULL);
    if (ret != 0)
    {
        printf("pthread_create error.\n");
        exit(-1);
    }
    
    printf("输入一个字符串,以回车结束\n");
    while (1)
    {
        pthread_mutex_lock(&mutex);
        scanf("%s", buf);
        pthread_mutex_unlock(&mutex);
        // 去比较用户输入的是不是end,如果是则退出,如果不是则继续            
        if (!strncmp(buf, "end", 3))
        {
            printf("程序结束\n");
            flag = 1;
            
            //exit(0);             
                        break;
        }
        sleep(1);
        // 主线程在收到用户收入的字符串,并且确认不是end后         
        // 就去发信号激活子线程来计数。         
        // 子线程被阻塞,主线程可以激活,这就是线程的同步问题。         
        // 信号量就可以用来实现这个线程同步         
    }

    
    // 回收子线程     
    printf("等待回收子线程\n");
    ret = pthread_join(th, NULL);
    if (ret != 0)
    {
        printf("pthread_join error.\n");
        exit(-1);
    }
    printf("子线程回收成功\n");
    
    pthread_mutex_destroy(&mutex);
    
    return 0;
}

线程同步之条件变量

什么是条件变量

相关函数

​ pthread_cond_init pthread_cond_destroy

​ pthread_cond_wait pthread_cond_signal/pthread_cond_broadcast

使用条件变量来实现代码

条件变量和互斥锁之间有一定的关联。

#include <stdio.h> 
#include <string.h> 
#include <stdlib.h> 
#include <pthread.h> 


char buf[200] = {0};
pthread_mutex_t mutex;
pthread_cond_t cond;
unsigned int flag = 0;


// 子线程程序,作用是统计buf中的字符个数并打印 
void *func(void *arg)
{
    // 子线程首先应该有个循环     
    // 循环中阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符     
    // 长度,然后打印;完成后再次被阻塞     
    //while (strncmp(buf, "end", 3) != 0)     
    //sleep(1);     
    while (flag == 0)
    {    
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);
        printf("本次输入了%d个字符\n", strlen(buf));
        memset(buf, 0, sizeof(buf));
        pthread_mutex_unlock(&mutex);
        //sleep(1);     }
    
    
    pthread_exit(NULL);
}


int main(void)
{
    int ret = -1;
    pthread_t th = -1;
    

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    
    ret = pthread_create(&th, NULL, func, NULL);
    if (ret != 0)
    {
        printf("pthread_create error.\n");
        exit(-1);
    }
    
    printf("输入一个字符串,以回车结束\n");
    while (1)
    {
        //pthread_mutex_lock(&mutex);         
        scanf("%s", buf);
        pthread_cond_signal(&cond);
        //pthread_mutex_unlock(&mutex);         
        // 去比较用户输入的是不是end,如果是则退出,如果不是则继续            
        if (!strncmp(buf, "end", 3))
        {
            printf("程序结束\n");
            flag = 1;
            
            //exit(0);             break;
        }
        
        //sleep(1);         
        // 主线程在收到用户收入的字符串,并且确认不是end后         
        // 就去发信号激活子线程来计数。         
        // 子线程被阻塞,主线程可以激活,这就是线程的同步问题。         
        // 信号量就可以用来实现这个线程同步         
    }

    
    // 回收子线程     
    printf("等待回收子线程\n");
    ret = pthread_join(th, NULL);
    if (ret != 0)
    {
        printf("pthread_join error.\n");
        exit(-1);
    }
    printf("子线程回收成功\n");
    
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    
    return 0;
}

10、socket编程相关API

函数名称函数简单描述附加说明
socket创造某种类型的套接字
bind将一个 socket绑定一个ip与端口的二元组上
listen将一个 socket 变为侦听状态
connect试图建立一个 TCP 连接一般用于客户端
accept尝试接收一个连接一般用于客户端
send通过一个socket发送数据
recv通过一个socket收取数据
select判断一组socket上的读事件
gethostbyname通过域名获取机器地址
close关闭一个套接字,回收该 socket 对应的资源Windows 系统中对应的是 closesocket
shutdown关闭 socket 收或发通道
setsockopt置一个套接字选项
getsockopt获取一个套接字选项

socket函数——创建套接字

#include <sys/types.h>       
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数domain

函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义。

名称含义名称含义
PF_UNIX、PF_UNIX本地通信PF_X25ITU-T X25 / ISO-8208协议
AF_INET、PF_INETIPv4 Internet协议PF_AX25Amateur radio AX.25
PF_INET6IPv6 Internet协议PF_ATMPVC原始ATM PVC访问
PF_IPXIPX-Novell协议PF_APPLETALKAppletalk
PF_NETLINK内核用户界面设备PF_PACKET底层包访问

参数type
type 函数socket()的参数type用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字)、SOCK——DGRAM(数据包套接字)等。

名称含义
SOCK_STREAMTcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输
SOCK_DGRAM支持UDP连接(无连接状态的消息)
SOCK_SEQPACKET序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出
SOCK_RAWRAW类型,提供原始网络协议访问
SOCK_RDM提供可靠的数据报文,不过可能数据会有乱序

并不是所有的协议族都实现了这些协议类型,例如,AF_INET协议族就没有实现SOCK_SEQPACKET协议类型。

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

errno
函数socket()并不总是执行成功,有可能会出现错误,错误的产生有多种原因,可以通过errno获得:

含义
EACCES没有权限建立制定的domain的type的socket
EAFNOSUPPORT不支持所给的地址类型
EINVAL不支持此协议或者协议不可用
EMFILE进程文件表溢出
ENFILE已经达到系统允许打开的文件数量,打开文件过多
ENOBUFS/ENOMEM内存不足。socket只有到资源足够或者有进程释放内存
EPROTONOSUPPORT制定的协议type在domain中不存在

比如我们建立一个流式套接字可以这样:

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

bind

#include <sys/types.h>         
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

在套接口中,一个套接字只是用户程序与内核交互信息的枢纽,它自身没有太多的信息,也没有网络协议地址和 端口号等信息,在进行网络通信的时候,必须把一个套接字与一个地址相关联,这个过程就是地址绑定的过程。许多时候内核会我们自动绑定一个地址,然而有时用 户可能需要自己来完成这个绑定的过程,以满足实际应用的需要,最典型的情况是一个服务器进程需要绑定一个众所周知的地址或端口以等待客户来连接。这个事由 bind的函数完成。

sockfd

就是我们调用socket函数后创建的socket 句柄或者称文件描述符号。

addr

addr是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要弦将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等接合在一起。

由于历史原因,我们前后有两个地址结构: struct sockaddr 该结构定义如下:

struct sockaddr { 
    uint8_t sa_len;   
    unsigned short sa_family; /* 地址家族, AF_xxx */    
    char sa_data[14]; /14字节协议地址/ 
};

其实这个结构逐渐被舍弃,但是也还是因为历史原因,在很多的函数,比如connect、bind等还是用这个作为声明,实际上现在用的是第二个结构,我们需要把第二个结构强转成sockaddr。 struct sockaddr_in 其定义如下:

struct sockaddr_in { 
    uint8_t sa_len;   /* 结构体长度*/ 
    short int sin_family; /* 通信类型 */ 
    unsigned short int sin_port; /* 端口 */ 
    unsigned char sin_zero[8]; /* 未使用的*/ 
};

struct in_addr {   //sin_addr的结构体类型in_addr 原型
    unsigned long s_addr;     /存4字节的 IP 地址(使用网络字节顺序)。/
 };

在使用的时候我们必须指定通信类型,也必须把端口号和地址转换成网络序的字节序

addrlen
addr结构的长度,可以设置成sizeof(struct sockaddr)。使用sizeof(struct sockaddr)来设置套接字的类型和其对已ing的结构。

bind()函数的返回值为0时表示绑定成功,-1表示绑定失败,errno的错误值如表1所示。

含义备注
EADDRINUSE给定地址已经使用
EBADFsockfd已经绑定到其他地址
ENOTSOCKsockfd是一个文件描述符,不是socket描述符
EACCES地址被保护,用户的权限不足
EADDRNOTAVAIL接口不存在或者绑定地址不是本地UNIX协议族,AF_UNIX
EFAULTmy_addr指针超出用户空间UNIX协议族,AF_UNIX
EINVAL地址长度错误,或者socket不是AF_UNIX族UNIX协议族,AF_UNIX
ELOOP解析my_addr时符号链接过多UNIX协议族,AF_UNIX
ENAMETOOLONGmy_addr过长UNIX协议族,AF_UNIX
ENOENT文件不存在UNIX协议族,AF_UNIX
ENOMEN内存内核不足UNIX协议族,AF_UNIX
ENOTDIR不是目录UNIX协议族,AF_UNIX

比如这样:

struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) 
{
    perror("bind");
    exit(1);
}

listen

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

listen()函数将sockfd标记为被动打开的套接字,并作为accept的参数用来接收到达的连接请求。

sockfd

是一个套接字类型的文件描述符,具体类型为SOCK_STREAM或者SOCK_SEQPACKET。

backlog参数

用来描述sockfd的等待连接队列能够达到的最大值。当一个请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,或者如果底层协议支持重传(比如tcp协议),本次请求会被丢弃不作处理,在下次重试时期望能连接成功(下次重传的时候队列可能已经腾出空间)。
说起这个backlog就有一点儿历史了,等下文描述。

errno

含义
EADDRINUSE另一个套接字已经绑定在相同的端口上。
EBADF参数sockfd不是有效的文件描述符
ENOTSOCK参数sockfd不是套接字。
EOPNOTSUPP参数sockfd不是支持listen操作的套接字类型。

connect

#include <sys/types.h>         
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数说明如下

sockfd

系统调用 socket() 返回的套接字文件描述符。

serv_addr

保存着目的地端口和 IP 地址的数据结构 struct sockaddr_in。

addrlen

设置 为 sizeof(struct sockaddr_in)

errno

connect函数在调用失败的时候返回值-1,并会设置全局错误变量 errno。

含义
EBADF参数sockfd 非合法socket处理代码
EFAULT参数serv_addr指针指向无法存取的内存空间
ENOTSOCK参数sockfd为一文件描述词,非socket。
EISCONN参数sockfd的socket已是连线状态
ECONNREFUSED连线要求被server端拒绝。
ETIMEDOUT企图连线的操作超过限定时间仍未有响应。
ENETUNREACH无法传送数据包至指定的主机。
EAFNOSUPPORTsockaddr结构的sa_family不正确。

accept

#include <sys/types.h>        
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明

sockfd是由socket函数返回的套接字描述符,

参数addr和addrlen用来返回已连接的对端进程(客户端)的协议地址。如果我们对客户端的协议地址不感兴趣,可以把arrd和addrlen均置为空指针。

返回值

成功时,返回非负整数,该整数是接收到套接字的描述符;出错时,返回-1,相应地设定全局变量errno。

含义
EBADF非法的socket
EFAULT参数serv_addr指针指向无法存取的内存空间
ENOTSOCK参数sockfd为一文件描述词,非socket。
EOPNOTSUPP指定的socket并非SOCK_STREAM
EPERM防火墙拒绝此连线
ENOBUFS系统的缓冲内存不足
ENOMEM核心内存不足

特别需要说明下的是,这个accept是一个阻塞式的函数,对于一个阻塞的套套接字,一直阻塞,或者返回一个错误值,对于非阻塞套接字。accept有可能返回-1,但是如果errno的值为,EAGAIN或者EWOULDBLOCK,此时需要重新调用一次accept函数。

send和recv函数

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd :套接字
  • buf : 待发送或者接收的缓存
  • len : 如果是recv指期望接收的长度,如果是send指要发送的长度。
  • flags : 标志位,取值如下表:
flags说明recvsend
MSG_DONTROUTE绕过路由表查找
MSG_DONTWAIT仅本操作非阻塞
MSG_OOB发送或接收带外数据
MSG_PEEK窥看外来消息
MSG_WAITALL等待所有数据


errno

含义
EBADFsock不是有效的描述词
EFAULT内存空间访问出错
ENOTSOCKsock索引的不是套接字 当返回值是0时,为正常关闭连接;
EAGAIN套接字已标记为非阻塞,而接收操作被阻塞或者接收超时
ECONNREFUSED连线要求被server端拒绝。
EINTR操作被信号中断
EINVAL参数无效
ENOTCONN与面向连接关联的套接字尚未被连接上
ENOMEM内存不足

当返回值为-1时是不是一定就错误了,当返回值为0时该怎么做呢?

IP相关的数据结构和函数

数据结构

表示IP地址相关数据结构都定义在 netinet/in.h这里。
struct sockaddr注意这个IP地址是不区分IPv4和IPv6的,这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数
中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6强制类型转换而来。

struct sockaddr { 
    uint8_t sa_len;   
    unsigned short sa_family; /* 地址家族, AF_xxx */    
    char sa_data[14]; /14字节协议地址/ 
};
typedef uint32_t in_addr_t;        网络内部用来表示IP地址的类型
struct in_addr
{
   in_addr_t s_addr;
};
struct sockaddr_in { 
    uint8_t sa_len;   /* 结构体长度*/ 
    short int sin_family; /* 通信类型 */ 
    unsigned short int sin_port; /* 端口 */ 
    unsigned char sin_zero[8]; /* 未使用的*/ 
};

我们电脑是有大小端的,有的电脑是大端,有的电脑是小端,在网络中统一规定使用大端,所以就把大端成为网络字节序。

相关函数

下面的函数用来实现IP地址的点分十进制和十六进制之间的转化。

是Linux中的库函数,用man 3查询。
inet_addr、inet_ntoa、inet_aton
inet_pton、inet_ntop

第一行的只能是和IPV4,第二行可以兼容IPV4和IPV6。所以最好使用第二行的函数。这几个函数内部都可以实现转为网络字节序,不必我们来担心了。

#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr)  //点分十进制ipv4地址转换为网络ipv4地址,失败返回INADDR_NONE
int inet_aton(const char* cp, struct in_addr* inp) //点分十进制ipv4地址转换为网络ipv4地址,成功为1,失败为0
char* inet_aton(struct addr_in in);      //网络ip4地址转为点分十进制ipv4地址,用字符串表示
#include <arpa/inet.h>
int inet_ptoa(int af, const char* src, void* dst) //点分转为网络
const char* inet_atop(int af, const void* src, char* dst, socklen_t cnt)

af: 协议族:AF_INET或AF_INET6

因为网络字节序是大端模式,

uint16_t htons(uint16_t hostshort) //主机序到网络序
uint32_t htonl(uint32_t hostlong)
uint16_t ntohs(uint16_t netshort)// 网络序到主机序
uint32_t ntohl(uint32_t netlong)

我们设置IP地址的时候要使用上面的前两个函数函数。
具体的使用方法,在后面的程序中可以看出来。

socket的一个例子,总结上述的问题
概念:端口号,实质就是一个数字编号,用来在我们一台主机中(主机的操作系统中)唯一的标识一个能上网的进程。端口号和IP地址一起会被打包到当前进程发出或者接收到的每一个数据包中。每一个数据包将来在网络上传递的时候,内部都包含了发送方和接收方的信息(就是IP地址和端口号),所以IP地址和端口号这两个往往是打包在一起不分家的。
端口号中前1024个数字已经被占用了,不可以随便使用,1024~65535之间的一般来说可以随便用

server.c服务器程序

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define SERPORT        9003
#define SERADDR        "192.168.1.141"        // ifconfig看到的服务器IP
#define BACKLOG        100


char recvbuf[100];


int main(void)
{
    // 第1步:先socket打开文件描述符
    int sockfd = -1, ret = -1, clifd = -1;
    socklen_t len = 0;
    struct sockaddr_in seraddr = {0};
    struct sockaddr_in cliaddr = {0};
    
    char ipbuf[30] = {0};
    
    
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("socket");
        return -1;
    }
    printf("socketfd = %d.\n", sockfd);
    
    // 第2步:bind绑定sockefd和当前电脑的ip地址&端口号
    seraddr.sin_family = AF_INET;        // 设置地址族为IPv4
    seraddr.sin_port = htons(SERPORT);    // 设置地址的端口号信息
    seraddr.sin_addr.s_addr = inet_addr(SERADDR);    // 设置IP地址
    ret = bind(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
    if (ret < 0)
    {
        perror("bind");
        return -1;
    }
    printf("bind success.\n");
    
    // 第三步:listen监听端口
    ret = listen(sockfd, BACKLOG);        // 阻塞等待客户端来连接服务器
    if (ret < 0)
    {
        perror("listen");
        return -1;
    }
    
    // 第四步:accept阻塞等待客户端接入
    clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
    printf("连接已经建立,client fd = %d.\n", clifd);
    
    // 服务器给客户端发
    strcpy(recvbuf, "hello world.");
    ret = send(clifd, recvbuf, strlen(recvbuf), 0);
    printf("发送了%d个字符\n", ret);
    
    return 0;
}

客户端程序

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define SERADDR        "192.168.1.141"        // 服务器开放给我们的IP地址和端口号
#define SERPORT        9003

char sendbuf[100];

int main(void)
{
    // 第1步:先socket打开文件描述符
    int sockfd = -1, ret = -1;
    struct sockaddr_in seraddr = {0};
    struct sockaddr_in cliaddr = {0};
    
    // 第1步:socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("socket");
        return -1;
    }
    printf("socketfd = %d.\n", sockfd);
    
    // 第2步:connect链接服务器
    seraddr.sin_family = AF_INET;        // 设置地址族为IPv4
    seraddr.sin_port = htons(SERPORT);    // 设置地址的端口号信息
    seraddr.sin_addr.s_addr = inet_addr(SERADDR);    // 设置IP地址
    ret = connect(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
    if (ret < 0)
    {
        perror("listen");
        return -1;
    }
    printf("成功建立连接\n");

    ret = recv(sockfd, sendbuf, sizeof(sendbuf), 0);
    printf("成功接收了%d个字节\n", ret);
    printf("client发送过来的内容是:%s\n", sendbuf);
    
    return 0;
}

多线程相关

Last modification:February 9th, 2020 at 11:37 am
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment