Linux编程函数参考

1. 进程

$ ps aux  //a: 查看所有终端的信息 u: 查看用户相关的信息 x: 显示和终端无关的进程信息
$ kill -l  //执行shell命令查看信号
$ kill -SIGKILL 进程ID  //无条件杀死进程

1.1 进程控制

/*头函数*/
#include <sys/types.h>  //Linux系统基本数据类型,包含了size_t,time_t,pid_t等。
#include <unistd.h>   //包含了fork、pipe系统调用 以及各种 I/O 原语(read、write、close、getpid等等)
#include <stdlib.h>  //包含了malloc、free等函数
#include <sys/wait.h>  //包含等待函数wait()
#include <sys/mman.h>  //包含内存映射区
#include <sys/ipc.h>
#include <sys/shm.h>  //包含共享内存
#include <signal.h>  //定义了程序执行时如何处理不同的信号
#include <sys/time.h>  //Linux系统的日期时间头文件
#include <pthread.h>  //多线程库
#include <semaphore.h>  //信号量头函数


/*函数*/
pid_t getpid(void)  //获取当前进程的进程 ID(PID)

pid_t getppid(void)  //获取当前进程的父进程 ID(PPID)

pid_t fork(void)  //创建一个新的进程

//执行一个可执行程序,执行成功,没有返回值,如果执行失败,返回 -1  
//file: 可执行程序的名字,arg:启动的进程的名字,...:要执行的命令需要的参数,,最后以NULL结尾,
//例子:execl("/bin/ps", "name", "20", NULL)
int execl(const char *path, const char *arg, ...)

//阻塞函数wait, 阻塞等待检测子进程退出,回收子进程,成功返回被回收的子进程的进程失败返回-1 
//status:判断回收的进程是怎么退出的,如果不需要该信息可以指定为NULL
pid_t wait(int *status)

//可以选择非阻塞,可以回收某个或者某一类或者是全部子进程资源
//如果函数是非阻塞的,并且子进程还在运行,返回 0  成功:得到子进程的进程ID  失败: -1
//pid: -1:回收所有的子进程资源  大于0:回收子进程的进程ID  0:回收当前进程组的所有子进程ID  小于 -1:pid 的绝对值代表进程组ID
//options: 0: 阻塞  WNOHANG: 非阻塞
//例子pid_t ret = waitpid(-1, NULL, WNOHANG)
pid_t waitpid(pid_t pid, int *status, int options)

1.2 进程通信

1.2.1 匿名管道
//匿名管道:通过内核中的一段内存进行IO操作来实现通信,半双工,只能实现有血缘关系的进程间通信
// 读管道
ssize_t read(int fd, void *buf, size_t count)
// 写管道的函数
ssize_t write(int fd, const void *buf, size_t count)

//创建一个匿名的管道,成功返回 0,失败返回 -1  pipefd[0]:管道读端的文件描述符  pipefd[1]:管道写端的文件描述符 
int pipe(int pipefd[2])

close(pipefd[0]);
char* s = "hello";
int n = write(pipefd[1], s, strlen(s));
close(pipefd[1]);

close(pipefd[1]);
char buff[128] = {0};
int n = read(pipefd[0], buff, 127);
close(pipefd[0]);
1.2.1 有名管道
//半双工,通过磁盘上的管道文件标识进行数据交互

//创建一个管道文件,创建成功返回 0,失败返回 -1   pathname:名字 mode:操作权限
int mkfifo(const char *pathname, mode_t mode)

int fd = open("./testfifo", O_WRONLY);
write(fd, "hello", strlen("hello"));
close(fd);

int fd = open("./testfifo", O_RDONLY);
char buff[128] = {0};
int n = read(pipefd[0], buff, 127);
close(fd);
1.2.2 内存映射区
//存映射区对应的内存空间在进程的用户区(加载动态库的区),在每个进程内部都有一块,将各自的内存映射区和同一个磁盘文件进行映射

//成功返回一个内存映射区的起始地址,失败: MAP_FAILED (that is, (void *) -1)
//addr: 从动态库加载区的什么位置开始创建内存映射区,一般指定为 NULL
//length: 创建的内存映射区的大小(单位:字节)
//prot:  PROT_READ: 读内存映射区  PROT_WRITE: 写内存映射区,读写权限: PROT_READ | PROT_WRITE
//flags: MAP_SHARED: 多个进程可以共享数据,进行映射区数据同步  MAP_PRIVATE: 映射区数据是私有的,不能同步给其他进程
//offset: 磁盘文件的偏移量,偏移量必须是 4k 的整数倍,写 0 代表不偏移
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)  //创建内存映射区

//addr: mmap () 的返回值,创建的内存映射区的起始地址
//length: 和 mmap () 第二个参数相同
int munmap(void *addr, size_t length)  //释放内存映射区,函数调用成功返回 0,失败返回 -1

int fd = open("./english.txt", O_RDWR);
void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
const char* pt = "hello";
memcpy(ptr, pt, strlen(pt)+1);
printf("%s", (char*)ptr);
munmap(ptr, 4000);
1.2.3 共享内存
//共享内存不同于内存映射区,使用之前需要让进程和共享内存进行关联,在所有进程间通信的方式中共享内存的效率是最高的
//ipcs -m  查看系统中共享内存的详细信息   ipcrm -m shmid 删除某块共享内存

//创建打开共享内存,成功返回标识共享内存的唯一的 ID,失败返回 - 1
//key: 类型 key_t 是个整形数,参数的值大于0 
//size: 创建共享内存的时候,指定共享内存的大小,如果已存在,则没有意义
//shmflg:IPC_CREAT: 创建新的共享内存,如果创建共享内存,需要指定对共享内存的操作权限,如:IPC_CREAT | 0664  
//shmflg:IPC_EXCL: 检测共享内存是否已经存在了,必须和 IPC_CREAT 一起使用
int shmget(key_t key, size_t size, int shmflg)

// 将两个参数作为种子, 生成一个 key_t 类型的数值
//proj_id: 传参的时候要将其作为 char 进行操作,取值范围: 1-255
key_t ftok(const char *pathname, int proj_id)

//关联共享内存,关联成功,返回值共享内存的起始地址,关联失败返回 (void *) -1
//shmid: 要操作的共享内存的 ID, 是 shmget () 函数的返回值
//shmaddr: 共享内存的起始地址,内核指定,写 NULL
//shmflg: SHM_RDONLY: 读权限  0: 读写权限
void *shmat(int shmid, const void *shmaddr, int shmflg)

//解除进程和共享内存关联,成功返回 0,失败返回 - 1
//shmaddr: shmat () 函数的返回值,共享内存的起始地址
int shmdt(const void *shmaddr)

//将共享内存标记为删除状态,所有的进程全部和共享内存解除关联,共享内存才会被删除,成功返回值大于等于 0,调用失败返回 - 1
//shmid: 要操作的共享内存的 ID, 是 shmget () 函数的返回值
//cmd: IPC_STAT: 得到当前共享内存的状态 IPC_SET: 设置共享内存的状态  IPC_RMID: 标记共享内存要被删除了
//buf:cmd==IPC_STAT, 作为传出参数,会得到共享内存的相关属性信息 IPC_SET, 将用户的自定义属性设置到共享内存中 IPC_RMID, buf 指定为NULL
int shmctl(int shmid, int cmd, struct shmid_ds *buf)

int shmid = shmget(1000, 4096, IPC_CREAT|0664);
void* ptr = shmat(shmid, NULL, 0);
const char* p = "hello";
memcpy(ptr, p, strlen(p)+1);
int shmid = shmget(1000, 0, 0);
void* ptr = shmat(shmid, NULL, 0);
shmdt(ptr);
shmctl(shmid, IPC_RMID, NULL);
1.2.3.1 shm 和 mmap区别
1. 实现进程间通信的方式
mmap:位于每个进程的虚拟地址空间中,并且需要关联同一个磁盘文件才能实现进程间数据通信
shm: 多个进程只需要一块共享内存就够了,共享内存不属于进程,需要和进程关联才能使用
2. 生命周期
mmap:进程退出,内存映射区也就没有了
shm:进程退出对共享内存没有影响,调用相关函数 / 命令 / 关机才能删除共享内存
3. 数据的完整性
mmap:可以完整的保存数据,内存映射区数据会同步到磁盘文件
shm:数据存储在物理内存中,断电之后系统关闭,内存数据也就丢失了
1.2.4 信号
//信号也可以实现进程间通信,但是信号能传递的数据量很少,优先级很高,处理动作是回调完成的,会打乱程序原有的处理流程
//Linux 中的信号有三种状态,分别为:产生,未决,递达。

//执行shell命令查看信号
$ kill -l

int kill(pid_t pid, int sig)   //给某一个进程发送一个信号, pid: 进程 ID  sig: 要发送的信号
int raise(int sig)  //给自己发送某一个信号
void abort(void)  //中断函数, 杀死当前进程
unsigned int alarm(unsigned int seconds)  //倒计时seconds秒, 中断当前进程,返回值大于 0 表示倒计时还剩多少秒,0表示倒计时完成

struct itimerval {
	struct timeval it_interval; /* 时间间隔 */
	struct timeval it_value;    /* 第一次触发定时器的时长 */
};
struct timeval {
	time_t      tv_sec;         /* 秒 */
	suseconds_t tv_usec;        /* 微妙 */
};

//实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号
//which: ITIMER_REAL: 自然计时法  ITIMER_VIRTUAL: 只计算程序在用户区运行使用的时间  ITIMER_PROF: 只计算内核运行使用的时间
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value)

//在信号产生之前捕捉信号, signum: 需要捕捉的信号, handler: 信号捕捉到之后的处理动作,这是一个函数指针
sighandler_t signal(int signum, sighandler_t handler)

void doing(int arg){
	printf("当前捕捉到的信号是: %d\n", arg);
}
signal(SIGALRM, doing);

1.3 守护进程

守护进程是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了
1.3.1 进程组
//多个进程的集合就是进程组,这个组中必须有一个组长,组长就是进程组中的第一个进程
//每个进程组都有一个唯一的组 ID,进程组的 ID 和组长的 PID 是一样的。

//得到当前进程所在的进程组的组 ID
pid_t getpgrp(void)

//获取指定的进程所在的进程组的组 ID
pid_t getpgid(pid_t pid)

//将某个进程移动到其他进程组中或者创建新的进程组,成功返回 0,失败返回 - 1
int setpgid(pid_t pid, pid_t pgid)
1.3.2 会话
//会话是一个或多个进程组的集合,当用户登录系统时,登录进程会为这个用户创建一个新的会话(session)
//shell进程(如bash)作为会话的第一个进程,称为会话进程(session leader)
//会话的PID(SID):等于会话首进程的PID
//会话会分配给用户一个控制终端(只能有一个),用于处理用户的输入输出
//一个会话包括了该登录用户的所有活动
//会话中的进程由一个前台进程组和N个后台进程组构成

//获取某个进程所属的会话ID
pid_t getsid(pid_t pid)

//将某个进程变成会话 =>> 得到一个守护进程
//使用哪个进程调用这个函数, 这个进程就会变成一个会话,函数的进程不能是组长进程,如果是该函数调用失败
pid_t setsid(void)
1.3.3 创建守护进程
1. 创建子进程,让父进程退出
	因为父进程有可能是组长进程,不符合条件,也没有什么利用价值,退出即可
	子进程没有任何职务,目的是让子进程最终变成一个会话,最终就会得到守护进程
2. 通过子进程创建新的会话,调用函数 setsid (),脱离控制终端,变成守护进程

2 线程

线程是轻量级的进程,在 Linux 环境下线程的本质仍是进程。
进程有自己独立的地址空间,多个线程共用同一个地址空间。
线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位。
CPU 的调度和切换:线程的上下文切换比进程要快的多。
线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小。
gcc pthread_create.c  -o main -lpthread

2.1 多线程

//返回当前线程的线程ID,每一个线程都有一个唯一的线程ID, ID类型为pthread_t
pthread_t pthread_self(void)

//在一个进程中调用线程创建函数,就可得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。
//thread: 传出参数,是无符号长整形数,线程创建成功,会将线程 ID 写入到这个指针指向的内存中
//attr: 线程的属性,一般情况下使用默认属性即可,写 NULL
//start_routine: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。
//arg: 作为实参传递到 start_routine 指针指向的函数内部
//返回值:线程创建成功返回 0,创建失败返回对应的错误号
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)

//线程退出,retval为线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为NULL
void pthread_exit(void *retval)

//线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,这个函数是一个阻塞函数,子线程退出函数解除阻塞进行资源的回收
//函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收,类似wait(),成功返回 0,回收失败返回错误号
//thread: 要被回收的子线程的线程ID
//retval: 二这个地址中存储了pthread_exit()传递出的数据,如果不需要这个参数,可以指定为 NULL
int pthread_join(pthread_t thread, void **retval)

// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了,防止主线程阻塞
int pthread_detach(pthread_t thread);

//取消线程,成功返回 0,失败返回非0错误号
int pthread_cancel(pthread_t thread);

struct Person
{
    int age;
};
struct Persion p;
void* working(void* arg)
{
	p.age = 12;
	pthread_exit(&p);
	return NULL;
}
pthread_t tid;
pthread_create(&tid, NULL, working, NULL)
pthread_detach(tid);
void* ptr = NULL;
pthread_join(tid, &ptr);
pthread_exit(NULL);

2.2 C++多线程

#include <thread>

 //默认构造函,构造一个线程对象,在这个线程中不执行任何处理动作
thread() noexcept

//移动构造函数,将 other 的线程所有权转移给新的 thread 对象。之后 other 不再表示执行线程。
thread( thread&& other ) noexcept

//创建线程对象,并在该线程中执行函数f(普通函数,类成员函数,匿名函数,仿函数)中的业务逻辑,args 是要传递给函数 f 的参数
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args )  //例如thread t(func, 520, "i love you")

//使用=delete显示删除拷贝构造,不允许线程对象之间的拷贝
thread( const thread& ) = delete

//获取线程ID,例如thread t1;  t1.get_id()
std::thread::id get_id() const noexcept

//命名空间 this_thread
this_thread::sleep_for(chrono::seconds(1))  //线程从运行态变成阻塞态,参数为duration类型
this_thread::sleep_until(chrono::system_clock::now() + sec)  //线程阻塞到某一个指定的时间点,参数为time_point类型
this_thread::yield()//主动让出抢到的CPU时间片,变为就绪态

//线程阻塞,直到子线程任务完成,例如t.join()
void join()

//分离主线程和创建出的子线程,任务完成后,子线程自动释放系统资源,例如t.detach()
void detach()

//判断主线程和子线程是否处于连接状态,例如t.joinable()
bool joinable() const noexcept

//获取当前计算机的 CPU 核心数,例如thread::hardware_concurrency()
static unsigned hardware_concurrency() noexcept

2.3 线程同步

常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量。
2.3.1 互斥锁
//每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关
pthread_mutex_t  mutex

//初始化互斥锁,restrict是一个关键字,用来修饰指针访问指向的内存地址
//mutex: 互斥锁变量的地址  attr: 默认为NULL 例子:pthread_mutex_init(&mutex, NULL)
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr)

//释放互斥锁资源            
int pthread_mutex_destroy(pthread_mutex_t *mutex)

//对互斥锁加锁
int pthread_mutex_lock(pthread_mutex_t *mutex)

//对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex)

//尝试加锁,如果锁变量被锁,调用这个函数加锁的线程,不会被阻塞,而是会直接返回错误号
int pthread_mutex_trylock(pthread_mutex_t *mutex)
2.3.2 读写锁
读写锁是互斥锁的升级版,如果所有的线程都是做读操作, 那么读是并行的,但是使用互斥锁,读操作也是串行的
使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的
使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的
使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问,写锁运行,读锁阻塞,因为写锁比读锁的优先级高
//创建读写锁
pthread_rwlock_t rwlock

// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr)

//释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)

//在程序中对读写锁加读锁, 锁定的是读操作
//如果已经锁定读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)

//这个函数可以有效的避免死锁
//如果加读锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)

// 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)

// 这个函数可以有效的避免死锁
// 如果加写锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)

// 解锁, 不管锁定了读还是写都可用解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
2.3.3 条件变量
条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源
pthread_cond_t cond
//初始化  attr: 指定为NULL
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr)

//销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond)

//线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
//函数在阻塞线程的时候,需要一个互斥锁参数来进行线程同步
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)

//表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
//将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime)

time_t mytim = time(NULL);	// 1970.1.1 0:0:0 到当前的总秒数
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = time(NULL) + 100;	// 线程阻塞100s

//唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond)
//唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond)
2.3.4 信号量
信号量主要阻塞线程,如果要保证线程安全,需要信号量和互斥锁一起使用
sem_t sem

// 初始化信号量  pshared:0为线程同步 非0为进程同步   value:初始化当前信号量拥有的资源数
int sem_init(sem_t *sem, int pshared, unsigned int value)

int sem_destroy(sem_t *sem)

//参数 sem 就是 sem_init() 的第一个参数  
//函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem)

int sem_trywait(sem_t *sem)

//调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout)

//调用该函数给sem中的资源数+1
int sem_post(sem_t *sem)

//查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中
int sem_getvalue(sem_t *sem, int *sval)

//这两行代码的调用顺序是不能颠倒的,如果颠倒过来就有可能会造成死锁
sem_wait(&csem);
pthread_mutex_lock(&mutex);

3 套接字

//头函数
//包括从主机字节序到网络字节序的转换函数:htons、htonl
//从网络字节序到主机字节序的转换函数:ntohs、ntohl
//主机字节序(小端) 低位字节存储到内存的低地址位(更方便计算机处理)
//网络字节序(大端) 低位字节存储到内存的高地址位(更符合人的思维)
#include <arpa/inet.h>  //包含了<sys/socket.h>
#include <sys/socket.h> //包含了端口复用函数
#include <sys/select.h>  //包含了select复用相关函数
#include <poll.h>  //包含了poll复用相关函数
#include <sys/epoll.h>  //包含了epoll复用相关函数

// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int
//将一个短整形从主机字节序 -> 网络字节序
uint16_t htons(uint16_t hostshort);
//将一个短整形从网络字节序 -> 主机字节序
uint16_t ntohs(uint16_t netshort)
//将一个整形从主机字节序 -> 网络字节序
uint32_t htonl(uint32_t hostlong)
//将一个整形从网络字节序 -> 主机字节序
uint32_t ntohl(uint32_t netlong)

3.1 socket编程

// 主机字节序的IP地址转换为网络字节序,成功返回1,失败返回0或者-1
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
//af:地址族  AF_INET:ipv4格式的ip地址  AF_INET6:ipv6格式的ip地址
//src:传入参数,对应要转换的点分十进制的 如:192.168.1.100
//dst: 传出参数,转换得到的大端整形IP被写入到这块内存中
int inet_pton(int af, const char *src, void *dst);
//将大端的整形数, 转换为小端的点分十进制的IP地址,成功返回转换得到的IP字符串,失败返回NULL
//size:修饰dst,标记dst指向的内存中最多可以存储的字节   
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size)

//只能处理ipv4的地址
//点分十进制IP -> 大端整形
in_addr_t inet_addr (const char *cp)
//大端整形 -> 点分十进制IP
char* inet_ntoa(struct in_addr in)


typedef unsigned short  uint16_t
typedef unsigned int    uint32_t
typedef uint16_t in_port_t
typedef uint32_t in_addr_t
typedef unsigned short int sa_family_t

//将端口和ip地址写到了一起,不好用
struct sockaddr {
	sa_family_t  sa_family;       // 地址族协议, ipv4
	char         sa_data[14];     // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}

struct in_addr
{
    in_addr_t s_addr;
}

struct sockaddr_in
{
    sa_family_t sin_family;		/* 地址族协议: AF_INET */
    in_port_t sin_port;         /* 端口, 2字节-> 大端  */
    struct in_addr sin_addr;    /* IP地址, 4字节 -> 大端  */
    char sin_zero[8];			/*不使用*/
}

//创建套接字,成功返回套接字通信的文件描述符,失败返回-1
//domain: 地址族  AF_INET或AF_INET6
//type: SOCK_STREAM: 流式传输  SOCK_DGRAM: 报文传输
//protocol: 写0即可 流式传输默认tcp 报文传输默认udp
int socket(int domain, int type, int protocol)

//将文件描述符和本地的IP与端口进行绑定
//sockfd: 监听的文件描述符
//addr: 传入参数
//addrlen: 参数addr指向的内存大小,sizeof(struct sockaddr)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 

//给监听的套接字设置监听
//backlog: 同时能处理的最大连接要求,最大值为 128
int listen(int sockfd, int backlog)

// 等待并接受客户端的连接请求, 建立新的连接, 得到一个新的文件描述符(通信文件)
//成功得到一个文件描述符,用于和建立连接的这个客户端通信,调用失败返回-1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

//接收数据,大于0为实际接受的字节数,等于0为对方断开了连接,-1为接受数据失败
//sockfd: 用于通信的文件描述符,accept()的返回值
//buf: 指向一块有效内存,用于存储接收是数据
//flags: 一般不使用,指定为0
ssize_t read(int sockfd, void *buf, size_t size)
ssize_t recv(int sockfd, void *buf, size_t size, int flags)

//发送数据的函数,大于0为实际发送的字节数(和参数len相等),-1表示发送数据失败
//fd: 通信的文件描述符,accept()的返回值
//buf: 传入参数,要发送的字符串
//len: 要发送的字符串的长度
ssize_t write(int fd, const void *buf, size_t len)
ssize_t send(int fd, const void *buf, size_t len, int flags)

//成功连接服务器之后, 客户端会自动随机绑定一个端口
//服务器端调用accept(), 第二个参数存储客户端的IP和端口信息
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

3.2 TCP通信流程

3.2.1 服务器端
int lfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
ret = listen(lfd, 128);

while(1)
{
	struct sockaddr_in cliaddr;
	int clilen = sizeof(cliaddr);
	int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
	while(1)
	{
		char buf[1024]={0};
		int len = read(cfd, buf, sizeof(buf)); //没有数据会阻塞
		if(len <= 0)
		{
			break;
		}
		printf("%d : %s\n",cfd,buf);
		len = write(cfd,"OK",3);  //返回客户端数据
	}
	close(cfd);//关闭客户端
}
close(lfd);//关闭服务端
3.2.2 客户端
int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000); 
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
while(1)
{
	char buf[1024]={0};
	fgets(buf,127,stdin);
	if(strncmp(buff,"end",3)==0)
	{
		break;
	}
	write(fd, buf, strlen(buf)+1);
	memset(buf, 0, sizeof(buf));
	len = read(fd, buf, sizeof(buf));
	printf("recv data:%s\n",buf);
}
close(fd);

3.3 IO复用

通过这种方式可以同时监测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪(可以读数据或者可以写数据)
程序的阻塞就会被解除,之后就可以基于这些就绪的文件描述符进行通信了,常见的 IO 多路转接方式有:select、poll、epoll。

一、多线程/多进程并发
	1. 主线程 / 父进程:调用 accept() 监测客户端连接请求
		●如果没有新的客户端的连接请求,当前线程 / 进程会阻塞
		●如果有新的客户端连接请求解除阻塞,建立连接
	2. 子线程 / 子进程:和建立连接的客户端通信
		●调用 read()/recv() 接收客户端发送的通信数据,如果没有通信数据,当前线程/进程会阻塞,数据到达之后阻塞自动解除
		●调用 write()/send() 给客户端发送数据,如果写缓冲区已满,当前线程/进程会阻塞,否则将待发送数据写入写缓冲区中
		
二、IO多路转接并发
	使用 IO 多路转接函数委托内核检测服务器端所有的文件描述符(通信和监听两类),这个检测过程会导致进程 / 线程的阻塞
	如果检测到已就绪的文件描述符阻塞解除,并将这些已就绪的文件描述符传出,根据传出的所有已就绪文件描述符类型,做出不同的处理
		1. 监听的文件描述符:和客户端建立连接
			●此时调用 accept() 是不会导致程序阻塞的,因为监听的文件描述符是已就绪的(有新请求)
		2. 通信的文件描述符:调用通信函数和已建立连接的客户端通信
			●调用 read()/recv() 不会阻塞程序,因为通信的文件描述符是就绪的,读缓冲区内已有数据
			●调用 write() / send() 不会阻塞程序,因为通信的文件描述符是就绪的,写缓冲区不满,可以往里面写数据
		3. 对这些文件描述符继续进行下一轮的检测

结论:与多进程/线程技术相比,I/O 多路复用技术系统开销小,系统不必创建和维护这些进程/线程
3.3.1 select
通过调用select函数检测若干个文件描述符的状态,其实就是检测这些文件描述符对应的读写缓冲区的状态
	●读缓冲区:检测里边有没有数据,如果有数据该缓冲区对应的文件描述符就绪
	●写缓冲区:检测写缓冲区是否可以写 (有没有容量),如果有,缓冲区对应的文件描述符就绪
	●读写异常:检测读写缓冲区是否有异常,如果没有,该缓冲区对应的文件描述符就绪
检测的文件描述符被遍历检测完后,已就绪的文件描述符会通过 select() 的参数分 3 个集合传出

局限性
	●待检测集合(第 2、3、4 个参数)需要频繁的在用户区和内核区之间进行数据的拷贝,效率低
	●内核对于 select 传递进来的待检测集合的检测方式是线性的(文件描述符多了后会效率低下)
	●使用select能够检测的最大文件描述符个数有上限,默认是1024
struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
}

//检测若干个文件描述符的状态,成功返回已就绪的文件描述符的总个数,超时没有检测到就绪的文件描述符返回0,失败返回-1
//nfds:委托内核检测的这三个集合中最大的文件描述符+1,这个值是循环结束的条件,Window中指定为-1
//readfds:传入传出参数,检测这个集合中文件描述符对应的读缓冲区
//writefds:传入传出参数,检测这个集合中文件描述符对应的写缓冲,不需可以指定为NULL
//exceptfds:检测集合中文件描述符是否有异常状态,不需可以指定为NULL
//timeout:超时时长  NULL:检测不到就绪的文件描述符会一直阻塞  等待固定时长(秒):在指定时长之后强制解除阻塞   0: 函数不会阻塞
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval * timeout)

//将文件描述符fd从set集合中删除(将fd对应的标志位设置为0)        
void FD_CLR(int fd, fd_set *set)

//判断文件描述符fd是否在set集合中(读一下fd对应的标志位到底是0还是1)
int  FD_ISSET(int fd, fd_set *set)

//将文件描述符fd添加到set集合中(将fd对应的标志位设置为1)
void FD_SET(int fd, fd_set *set)

//将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set)


//1.创建监听的fd
//2.绑定
//3.设置监听
int maxfd = lfd;  //将监听的fd的状态检测委托给内核检测
fd_set rdset;  //初始化检测的读集合
fd_set rdtemp;
FD_ZERO(&rdset);  //清零
FD_SET(lfd, &rdset);  //将监听的lfd设置到检测的读集合中
while(1)
    {
        rdtemp = rdset;  //rdset中是委托内核检测的所有的文件描述符
        int num = select(maxfd+1, &rdtemp, NULL, NULL, NULL);  
        if(FD_ISSET(lfd, &rdtemp))  //判断有没有新连接
        {
            struct sockaddr_in cliaddr;
            int cliLen = sizeof(cliaddr);
            int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &cliLen);
            FD_SET(cfd, &rdset);  //通信的文件描述符添加到读集合
            maxfd = cfd > maxfd ? cfd : maxfd;  //重置最大的文件描述符
        }
        for(int i=0; i<maxfd+1; ++i)  //没有新连接, 通信
        {
            if(i != lfd && FD_ISSET(i, &rdtemp)) //判断从监听的文件描述符之后到maxfd这个范围内的文件描述符是否读缓冲区有数据
            {
                char buf[10] = {0};//一次只能接收10个字节,一次是接收不完,下一轮select检测再读一次,直到缓冲区数据被读完
                int len = read(i, buf, sizeof(buf));
                if(len == 0)
                {
                    FD_CLR(i, &rdset);  //将检测的文件描述符从读集合中删除
                    close(i);
                }else if(len > 0)
                {
                    write(i, buf, strlen(buf)+1);
                }else
                {
                    perror("read");
                }
            }
        }
    }
3.3.2 poll
与select类似,但有两个区别
	●poll没有最大文件描述符数量的限制
	●select可以跨平台使用,poll只能在Linux平台使用
struct pollfd {
    int   fd;         /* 委托内核检测的文件描述符 */
    short events;     /*  POLLIN: 读事件  POLLOUT: 写事件  POLLERR: 错误事件*/
    short revents;    /* 文件描述符实际发生的事件 -> 传出 可以默认不填写 */
}

struct pollfd myfd[100];

//nfds: 这是第一个参数数组中最后一个有效元素的下标 + 1
//timeout: 指定poll函数的阻塞时长,-1:一直阻塞直到有就绪的文件描述符  0:不阻塞,大于  0:阻塞指定的毫秒
//失败返回-1    成功返回检测的集合中已就绪的文件描述符的总个数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

//1.创建监听的fd
//2.绑定
//3.设置监听
struct pollfd fds[1024];
for(int i=0; i<1024; ++i)
   {
       fds[i].fd = -1;
       fds[i].events = POLLIN;
   }
fds[0].fd = lfd;
int maxfd = 0;
while(1)
{
	int ret = poll(fds, maxfd+1, -1);
	if(fds[0].revents & POLLIN)
	{
		struct sockaddr_in sockcli;
		int len = sizeof(sockcli);
		int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len);
		int i;
		for(i=0; i<1024; ++i)
		{
			if(fds[i].fd == -1)
			{
				fds[i].fd = connfd;
				break;
			}
		}
		maxfd = i > maxfd ? i : maxfd;
	}
	for(int i=1; i<=maxfd; ++i)
	{
		if(fds[i].revents & POLLIN)
		{
			char buf[128];
			int ret = read(fds[i].fd, buf, sizeof(buf));
			if(ret == -1)
			{
				perror("read");
				exit(0);
			}else if(ret == 0)
			{
				close(fds[i].fd);
				fds[i].fd = -1;
			}else
			{
				printf("客户端say: %s\n", buf);
				write(fds[i].fd, buf, strlen(buf)+1);
			}
		}
	}
}
close(lfd);
3.3.3 epoll
epoll相对于select和poll,改进了这些
	●对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合的
	●epoll使用的是回调机制,效率高,处理效率不会随着检测集合的变大而下降
	●select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以直接得到已就绪的文件描述符集合,无需再次检测
	●使用 epoll 没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制
	●适合服用的文件数量庞大,IO流量频繁的情况 
//设置端口复用,成功返回0,失败返回SOCKET_ERROR错误
//level: 设置SOL_SOCKET使用套接字
//optname: SO_REUSEADDR是复用功能
//optval:不为0时,打开端口复用,为0时关闭端口复用
//optlen:optval缓冲区长度
int setsockopt( int socket, int level, int optname, const void *optval, socklen_t optlen)

//创建epoll实例,通过一棵红黑树管理待检测集合,失败返回-1  成功返回一个有效的文件描述符
//size: 随意指定一个大于0的数值
int epoll_create(int size)

typedef union epoll_data {  //联合体, 多个变量共用同一块内存
 	void        *ptr;
	int          fd;	// 通常情况下只使用这个成员, 和epoll_ctl的第三个参数相同即可
	uint32_t     u32;
	uint64_t     u64;
} epoll_data_t;

struct epoll_event {
	uint32_t     events;      /* epoll事件  EPOLLIN:读事件  EPOLLOUT:写事件  EPOLLERR:异常事件 */
	epoll_data_t data;
};


//管理红黑树上的文件描述符(添加、修改、删除),成功返回0,失败返回-1
//epfd:epoll_create()函数的返回值,通过这个参数找到epoll实例
//op:EPOLL_CTL_ADD:添加新的节点  EPOLL_CTL_MOD:修改已经存在的节点  EPOLL_CTL_DEL:删除指定的节点
//fd:文件描述符,即要添加/修改/删除的文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

//检测epoll树中是否有就绪的文件描述符,返回0表示没有检测到满足条件的,大于零表示检测到已就绪的文件总个数,失败返回-1
//epfd:epoll_create()函数的返回值
//events:传出参数,这是一个结构体数组的地址,里边存储了已就绪的文件描述符的信息
//maxevents:修饰第二个参数,结构体数组的容量
//timeout:0: 函数不阻塞  大于0: 阻塞的时间  -1:函数一直阻塞,直到有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
LT(水平)模式
LT是缺省的工作方式,并且同时支持block和no-block socket。
在这种做法中,内核通知使用者哪些文件描述符已经就绪,之后就可以对这些已就绪的文件描述符进行 IO 操作了。
如果我们不作任何操作,内核还是会继续通知使用者。

水平模式的特点:
	读事件:如果文件描述符对应的读缓冲区还有数据,读事件就会被触发,epoll_wait () 解除阻塞
		●当读事件被触发,epoll_wait () 解除阻塞,之后就可以接收数据了
		●如果接收数据的 buf 很小,不能全部将缓冲区数据读出,那么读事件会继续被触发,直到数据被全部读出
		●因为读数据是被动的,必须要通过读事件才能知道有数据到达了,因此对于读事件的检测是必须的
	写事件:如果文件描述符对应的写缓冲区可写,写事件就会被触发,epoll_wait () 解除阻塞
		●当写事件被触发,epoll_wait () 解除阻塞,之后就可以将数据写入到写缓冲区了
		●写事件的触发发生在写数据之前而不是之后,被写入到写缓冲区中的数据是由内核自动发送出去的
		●如果写缓冲区没有被写满,写事件会一直被触发
		●因为写数据是主动的,并且写缓冲区一般情况下都是可写的(缓冲区不满),因此对于写事件的检测不是必须的
//1.创建监听的fd
//2.初始化结构体sockaddr_in
//3.设置端口复用
int opt = 1
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))
//4.绑定端口
//5.设置监听
int epfd = epoll_create(100);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(struct epoll_event);
while(1)
{
	int num = epoll_wait(epfd, evs, size, -1);
	for(int i=0; i<num; ++i)
	{
		int curfd = evs[i].data.fd;
		if(curfd == lfd)
		{
			int cfd = accept(curfd, NULL, NULL);
			ev.events = EPOLLIN;    // 读缓冲区是否有数据
			ev.data.fd = cfd;
			ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
		}
		else
		{
			char buf[1024];
			memset(buf, 0, sizeof(buf));
			int len = recv(curfd, buf, sizeof(buf), 0);
			if(len == 0)
			{
				printf("客户端已经断开了连接\n");
				epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
				close(curfd);
			}
			else if(len > 0)
			{
				printf("客户端say: %s\n", buf);
				send(curfd, buf, len, 0);
			}
		}
	}
}
ET(边沿)模式
ET是高速工作方式,只支持no-block socket.
当文件描述符就绪时,内核会通过epoll通知一次使用者,不会再为那个文件描述符发送更多的就绪通知
如果对文件描述符做IO操作导致再次变成未就绪,当这个未就绪的文件描述符再次变成就绪状态,内核会再次进行通知,并且还是只通知一次
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高

边沿模式的特点:
	读事件:当读缓冲区有新的数据进入,读事件被触发一次,没有新数据不会触发该事件
		●如果有新数据进入到读缓冲区,读事件被触发,epoll_wait () 解除阻塞
		●读事件被触发,可以通过调用 read ()/recv () 函数将缓冲区数据读出
			◆如果数据没有被全部读走,并且没有新数据进入,读事件不会再次触发,只通知一次
			◆如果数据被全部读走或者只读走一部分,此时有新数据进入,读事件被触发,并且只通知一次
	写事件:当写缓冲区状态可写,写事件只会触发一次
		●如果写缓冲区被检测到可写,写事件被触发,epoll_wait () 解除阻塞
		●写事件被触发,就可以通过调用 write ()/send () 函数,将数据写入到写缓冲区中
			◆写缓冲区从不满到被写满,期间写事件只会被触发一次
			◆写缓冲区从满到不满,状态变为可写,写事件只会被触发一次

epoll的边沿模式下epoll_wait ()检测到文件描述符有新事件才会通知,通知的次数比水平模式少,效率比水平模式要高。
//1.创建监听的fd
//2.初始化结构体sockaddr_in
//3.设置端口复用
int opt = 1
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))
//4.绑定端口
//5.设置监听
int epfd = epoll_create(100);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(struct epoll_event);
while(1)
{
	int num = epoll_wait(epfd, evs, size, -1);
	for(int i=0; i<num; ++i)
	{
		int curfd = evs[i].data.fd;
		if(curfd == lfd)
		{
			int cfd = accept(curfd, NULL, NULL);
			int flag = fcntl(cfd, F_GETFL);
			flag |= O_NONBLOCK;
			fcntl(cfd, F_SETFL, flag);
			ev.events = EPOLLIN|EPOOLET;    //设置ET模式
			ev.data.fd = cfd;
			ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
		}
		else
		{
			char buf[10];
			memset(buf, 0, sizeof(buf));
			while(1)
			{
				int len = recv(curfd, buf, sizeof(buf), 0);
				if(len == 0)
				{
					printf("客户端已经断开了连接\n");
					epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
					close(curfd);
					break;
				}
				else if(len > 0)
				{
					printf("客户端say: %s\n", buf);
					send(curfd, buf, len, 0);
				}
			}
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值