C常用函数

108 阅读34分钟

常用函数

1. linux系统IO函数

1.1 open/close函数

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

// open函数只有一个, 是变参函数
// 作用: 打开或者的创建一个文件
// 打开文件
int open(const char *pathname, int flags);
	参数:
		- pathname: 要打开的文件的路径(相对/绝对)
        - flag: 打开的文件权限
        	- 三选一(必须):
                - O_RDONLY: 只读
                - O_WRONLY: 只写
                - O_RDWR: 读写
            - 附加属性:
        		- O_APPEND: 写文件的时候追加
        		- O_CREAT: 创建新文件
                - O_EXCL: 检测文件是否存在
                 	- 必须和O_CREAT一起使用
                 		- O_EXCL|O_CREAT
                 	- 如果文件存在, open调用失败, 返回-1
                    - 如果文件不存在, 创建新文件
     返回值:
		- 成功: 文件描述符
		- 失败: -1
// 创建文件
int open(const char *pathname, int flags, mode_t mode);
	参数:
		- mode: 八进制的整形数, 指定的新文件的权限
			- 文件所有者/所属组/其他人对文件的访问权限
			- 最终权限需要计算: (mode & ~umask)
				777				111111111
                775				111111101
               &
                775				111111101

#include <unistd.h>
// 关闭文件
// 参数: fd: open函数的返回值, 也就是要关闭的文件对应的文件描述符
int close(int fd);

1.2 read/write函数

#include <unistd.h>
// 读文件
ssize_t read(int fd, void *buf, size_t count);
	参数:
		- fd: open函数的返回值, 也就是要读的文件对应的文件描述符
		- buf: 指向一块有效的内存地址, 存储读到的数据
		- count: 描述了buf参数对应的内存大小
	返回值:
		- 失败: -1
        - 成功: 
			>0: 实际读出的字节数
			=0: 文件读完了
	errno:
		EAGAIN 通常指非阻塞模式下文件读完

#include <unistd.h>
// 写文件
ssize_t write(int fd, const void *buf, size_t count);
	参数:
		- fd: open函数的返回值, 也就是要写的文件对应的文件描述符
		- buf: 存储了要写入磁盘的数据
		- count: 参数buf中存储的数据的长度 == 要往文件里写入的数据长度
	返回值:
		- 成功:
			>=0: 实际写入文件的字节数
		- 失败: -1
// 1. 拷贝文件(比较大)
int fd = open("bigfile", O_RDONLY);
int fd1 = open("copyfile", O_WRONLY|O_CREATE, 0664);

// 2. 
int len = 0;
char buf[1024];
while((len=read(fd, buf, sizeof(buf)) != 0)
{
	write(fd1, buf, len);          
}
close(fd);
close(fd1);

1.3 lseek函数

#include <sys/types.h>
#include <unistd.h>
// 移动文件指针
off_t lseek(int fd, off_t offset, int whence);
	参数:
		- fd: open函数的返回值, 也就是要操作文件对应的文件描述符
		- offset: 偏移量, 整形数, 代表的意思要结合第三个参数看
		- whence: 
			- SEEK_SET: 设置文件指针的位置 : 从文件的开头位置开始偏移
			- SEEK_CUR: 当前文件指针的位置 + 第二个参数的偏移量:从文件的当前位置开始偏移
			- SEEK_END: 文件的大小 + 第二个参数的偏移量:从文件的末尾位置开始偏移
	返回值:
		- 成功: 文件指针最后的位置
		- 失败: -1
// 三个操作
// 1. 移动到文件头  
lseek(fd, 0, SEEK_SET);         
// 2. 获取文件指针当前的位置
lseek(fd, 0, SEEK_CUR); 
// 3. 获取文件长度  
lseek(fd, 0, SEEK_END); 
// 4. 拓展文件大小
lseek(fd, 100, SEEK_END); 
// 如果想要文件拓展成功, 必须要再进行一次写操作
write(fd, "a", 1);

1.4 truncate拓展或者截断

#include <unistd.h>
#include <sys/types.h>
// 文件拓展或截断
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
	参数:
		- path: 磁盘文件路径
		- fd: 通过open函数打开了磁盘文件得到了文件描述符
		- length: 文件的最终长度
			- 假设源文件长度 > length, 文件变小了
				- 文件尾部数据被删除
			- 假设源文件长度 < length, 文件变大了
				- 文件被拓展, 增加的部分被填充了0

1.5 stat/lstat查看文件属性

// 函数原型 -> 查看文件的属性信息
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

struct stat {
    dev_t          st_dev;        	//文件的设备编号
    ino_t           st_ino;        	//节点
    mode_t      st_mode;      		//文件的类型和存取的权限, 16位整形数
    nlink_t        st_nlink;     	//连到该文件的硬连接数目,刚建立的文件值为1
    uid_t           st_uid;       	//用户ID
    gid_t           st_gid;       	//组ID
    dev_t          st_rdev;      	//(设备类型)若此文件为设备文件,则为其设备编号
    off_t            st_size;      	//文件字节数(文件大小)
    blksize_t     st_blksize;   	//块大小(文件系统的I/O 缓冲区大小)
    blkcnt_t      st_blocks;    	//块数
    time_t         st_atime;     	//最后一次访问时间
    time_t         st_mtime;     	//最后一次修改时间(文件内容)
    time_t         st_ctime;     	//最后一次改变时间(指属性)
};

int stat(const char *pathname, struct stat *buf);
	参数:
		- pathname: 要操作的文件的路径和名字 /home/robin/a.txt   ./a.txt
		- buf: 结构体指针, 传出参数, stat函数会将得到的属性信息写入该指针对应的内存中
	
int lstat(const char *path, struct stat *buf); 
	可以判断是否是软连接文件
  • 获取文件大小
int main()
{
    struct stat st;

    stat("abc.txt", &st);
    printf("abc.txt size: %d\n", (int)st.st_size);

    return 0;
}
  • 获取文件类型

    S_ISREG(m)  is it a regular file?
    S_ISDIR(m)  directory?
    S_ISCHR(m)  character device?
    S_ISBLK(m)  block device?
    S_ISFIFO(m) FIFO (named pipe)?
    S_ISLNK(m)  symbolic link?  (Not in POSIX.1-1996.)
    S_ISSOCK(m) socket?  (Not in POSIX.1-1996.)
        
    int main()
    {
        struct stat st;
    
        stat("./hello", &st);
        // 判断文件类型
        if(S_ISREG(st.st_mode))
        {
            printf("该文件是一个普通文件\n");
        }
        else if(S_ISDIR(st.st_mode))
        {
            printf("这是一个目录\n");
        }
        printf("abc.txt size: %d\n", (int)st.st_size);
    
        return 0;
    }
    
  • 获取对文件的操作权限

      S_ISREG(m)  is it a regular file?
      S_ISDIR(m)  directory?
      S_ISCHR(m)  character device?
      S_ISBLK(m)  block device?
      S_ISFIFO(m) FIFO (named pipe)?
      S_ISLNK(m)  symbolic link?  (Not in POSIX.1-1996.)
      S_ISSOCK(m) socket?  (Not in POSIX.1-1996.)
          
      int main()
      {
          struct stat st;
      
          stat("./hello", &st);
          // 判断文件类型
          if(S_ISREG(st.st_mode))
          {
              printf("该文件是一个普通文件\n");
          }
          else if(S_ISDIR(st.st_mode))
          {
              printf("这是一个目录\n");
          }
          printf("abc.txt size: %d\n", (int)st.st_size);
      
          return 0;
      }
    

1.6 opendir 打开目录函数

#include <dirent.h>
// 打开目录
DIR *opendir(const char *name);
	参数: name: 要打开的目录的名字
	返回值: 
		- 成功: 目录指针
		- 失败: NULL

1.7 readdir 读目录函数

#include <dirent.h>
struct dirent {
	ino_t          d_ino;       /* 通过这个变量找到文件在磁盘位置 */
    off_t          d_off;       /* 偏移量, 用不到 */
    unsigned short d_reclen;    /* 变量d_name中存储的实际数据长度 */
    unsigned char  d_type;      /* 当前文件的类型 */
    char           d_name[256]; /* 当前文件的文件名 */
};

文件类型 d_type :
	   DT_BLK      This is a block device.
       DT_CHR      This is a character device.
       DT_DIR      This is a directory.
       DT_FIFO     This is a named pipe (FIFO).
       DT_LNK      This is a symbolic link.
       DT_REG      This is a regular file.
       DT_SOCK     This is a UNIX domain socket.
       DT_UNKNOWN  The file type is unknown.

// 因为在读一个目录的时候, 目录中文件个数一般>1
// 读这个目录的时候,可以通过readdir不停的读该目录, 当读完之后返回NULL
struct dirent *readdir(DIR *dirp);
	参数:
		- dirp: 打开的目录指针, opendir 的返回值
	返回值:
		- 结构体指针, 这个结构体中包含了读到的该文件的属性
		- 没有在该目录中读到文件, 返回值为NULL

1.8 closedir目录关闭

#include <sys/types.h>
#include <dirent.h>
// 关闭目录
int closedir(DIR *dirp);
	参数是 opendir 的返回值
  • 读指定目录下普通文件的个数

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <dirent.h>
    
    int getRegFiles(const char* path)
    {
        // 1. 打开目录
        DIR* dir = opendir(path);
        if(dir == NULL)
        {
            perror("opendir");
            return 0;
        }
        // 2. 遍历当前目录
        struct dirent* ptr = NULL;
        int count = 0;
        while((ptr = readdir(dir)) != NULL)
        {
            // 如果是目录 . ..
            if(strcmp(ptr->d_name, ".")==0 ||
               strcmp(ptr->d_name, "..") == 0)
            {
                continue;
            }
            // 假设读到的当前文件是目录
            if(ptr->d_type == DT_DIR)
            {
                // 目录
                char newPath[1024];
                sprintf(newPath, "%s/%s", path, ptr->d_name);
                // 读当前目录的子目录
                count += getRegFiles(newPath);
            }
            else if(ptr->d_type == DT_REG)
            {
                // 普通文件
                count ++;
            }
        }
    
        closedir(dir);
        return count;
    }
    
    int main(int argc, char* argv[])
    {
        // ./a.out path
        if(argc < 2)
        {
            printf("./a.out path\n");
            return 0;
        }
    
        int num = getRegFiles(argv[1]);
        printf("%s 目录中普通文件个数: %d\n", argv[1], num);
    
        return 0;
    }
    

1.9 dup、dup2、fcntl

#include <unistd.h>
// 复制文件描述符
// 返回值和参数指向的是同一个磁盘文件
int dup(int oldfd);
	 参数: 要被复制的文件描述符
 	返回值: 被复制出的文件描述符
#include <unistd.h>
/*
	场景1:
		oldfd -> a.txt
		newfd -> b.txt
		函数调用成功:
			newfd不在指向b.txt, newfd指向了a.txt
	场景2:
		oldfd -> a.txt
		newfd -> 不指向任何文件
		函数调用成功:
			newfd指向了a.txt
	场景2:
		oldfd -> a.txt
		newfd -> a.txt
		函数调用成功:
			什么事儿也不干
*/
// 复制文件描述符/重定向文件描述符
int dup2(int oldfd, int newfd);
	参数oldfd: 要被拷贝的文件描述符
	参数newfd: 要将oldfd中的信息拷贝到newfd中
	返回值返回 newfd 的值
// 函数原型
int fcntl(int fd, int cmd, ... /* arg */ );

// 1. 复制文件描述符
int fcntl(int fd, int cmd, ... /* arg */ );
	参数fd: 要复制的文件描述符
	参数cmd: 控制这个函数到底做什么, 现在要复制文件描述符使用: F_DUPFD
	返回值: 得到的复制完成之后的新的文件描述符
	
// 举例
	int fd  = open("a.txt", O_RDWR);
	int newfd = fcntl(fd, F_DUPFD);

// 2. 修改已经打开的文件的flag属性
	int open(const char *pathname, int flags);
int fcntl(int fd, int cmd, ... /* arg */ );
	参数fd: 打开的文件的文件描述符
	参数cmd: 
		- 获取文件设置的flags属性: F_GETFL
		- 设置文件的flags属性:    F_SETFL
			- 可以被设置的属性: O_APPEND, O_NONBLOCK
			- 不能被设置的属性:O_RDONLY,  O_WRONLY,O_RDWR,O_CREAT, O_EXCL,O_TRUNC

2. 标准C库文件函数

2.1 fopen/fclose 函数

FILE * fopen(const char * filename, const char * mode);
功能:打开文件
参数:
	filename:需要打开的文件名,根据需要加上路径
	mode:打开文件的权限设置
返回值:
成功:文件指针
	失败:NULL

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qf2xnKaf-1629332614411)(C:\Users\notbu\Desktop\assets\1.png)]

int fclose(FILE * stream);
功能:关闭先前fopen()打开的文件。此动作让缓冲区的数据写入文件中,并释放系统所提供的文件资源。
参数:
stream:文件指针
返回值:
	成功:0
	失败:-1
  

2.2 字符读写fgetc/fputc/feof

int fputc(int ch, FILE * stream);
功能:将ch转换为unsigned char后写入stream指定的文件中
参数:
	ch:需要写入文件的字符
	stream:文件指针
返回值:
	成功:成功写入文件的字符
	失败:返回-1
   
int fgetc(FILE * stream);
功能:从stream指定的文件中读取一个字符
参数:
	stream:文件指针
返回值:
	成功:返回读取到的字符
	失败:-1

int feof(FILE * stream);
功能:检测是否读取到了文件结尾
参数:
	stream:文件指针
返回值:
	非0值:已经到文件结尾
	0:没有到文件结尾

2.3 行读写fgets/fputs

int fputs(const char * str, FILE * stream);
功能:将str所指定的字符串写入到stream指定的文件中, 字符串结束符 '\0'  不写入文件。 
参数:
	str:字符串
	stream:文件指针
返回值:
	成功:0
	失败:-1

char * fgets(char * str, int size, FILE * stream);
功能:从stream指定的文件内读入字符,保存到str所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,最后会自动加上字符 '\0' 作为字符串结束。
参数:
	str:字符串
	size:指定最大读取字符串的长度(size - 1)
	stream:文件指针
返回值:
	成功:成功读取的字符串
	读到文件尾或出错: NULL

###2.4 块读写fwrite/fread

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式给文件写入内容
参数:
	ptr:准备写入文件数据的地址
	size: size_tunsigned int类型,此参数指定写入文件内容的块数据大小
	nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb
	stream:已经打开的文件指针
返回值:
	成功:实际成功写入文件数据的块数,此值和nmemb相等
	失败:0

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式从文件中读取内容
参数:
	ptr:存放读取出来数据的内存空间
	size: size_tunsigned int类型,此参数指定读取文件内容的块数据大小
	nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb
	stream:已经打开的文件指针
返回值:
	成功:实际成功读取到内容的块数,如果此值比nmemb小,但大于0,说明读到文件的结尾。
	失败:0

2.4 格式化读写fscanf/fprintf


int fprintf(FILE * stream, const char * format, ...);
功能:根据参数format字符串来转换并格式化数据,然后将结果输出到stream指定的文件中,指定出现字符串结束符 '\0'  为止。
参数:
	stream:已经打开的文件
	format:字符串格式,用法和printf()一样
返回值:
	成功:实际写入文件的字符个数
	失败:-1

int fscanf(FILE * stream, const char * format, ...);
功能:从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据。
参数:
	stream:已经打开的文件
	format:字符串格式,用法和scanf()一样
返回值:
	成功:实际从文件中读取的字符个数
	失败: - 1
    
注意:fscanf遇到空格和换行时结束。

2.5 随机读写fseek/ftell

int fseek(FILE *stream, long offset, int whence);
功能:移动文件流(文件光标)的读写位置。
参数:
	stream:已经打开的文件指针
	offset:根据whence来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了 文件末尾,再次写入时将增大文件尺寸。
	whence:其取值如下:
		SEEK_SET:从文件开头移动offset个字节
		SEEK_CUR:从当前位置移动offset个字节
		SEEK_END:从文件末尾移动offset个字节
返回值:
	成功:0
	失败:-1

long ftell(FILE *stream);
功能:获取文件流(文件光标)的读写位置。
参数:
	stream:已经打开的文件指针
返回值:
	成功:当前文件流(文件光标)的读写位置
	失败:-1

void rewind(FILE *stream);
功能:把文件流(文件光标)的读写位置移动到文件开头。
参数:
	stream:已经打开的文件指针
返回值:
	无返回值

3.标准C库数学函数

3.1 abs/fabs

4.linux 进程函数

4.1 fork 进程创建

#include <unistd.h>
// 创建一个新的进程
// 函数调用成功之后, 进程的个数在原基础上添加一个
pid_t fork(void);
返回值:
	- 失败: -1, 子进程不会被创建
	- 成功: 子进程被创建, 有两个返回值
		- 子进程: 返回0
		- 父进程: 返回子进程的进程ID

4.2 execl和execlp函数

// 函数原型
#include <unistd.h>
extern char **environ;
// l --> list
// 一般使用execl启动磁盘上的自己编译出的可执行程序
// 启动系统自带的可执行程序也是可以的, 但是必须要指定路径
int execl(const char *path, const char *arg, .../* (char  *) NULL */);
	参数:
		- path: 要启动的磁盘上的可执行程序(必须带路径)
            - 绝对路径: 建议
            - 相对路径: 程序如果改变存储位置, path有可能就找不到了(不建议)
        - arg: 这是一个字符串描述(启动的进程名字), 出现下 ps aux 的 COMMOND 列
        - ...: 启动的可执行程序的参数(>=0), 如果参数结束了, 末尾使用NULL(哨兵)
            如果有多个参数: 
					"a", "u", "x", NULL
// p --> path
// 启动系统自带的可执行程序 -> 不需要指定路径, 直接写文件名即可
// 启动磁盘上的自己编译出的可执行程序 -> 指定路径
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
	参数: 
		- file: 要启动的磁盘上的可执行程序的名字(只写名字就可以)
            - 指定的这个名字在执行的时候, 会搜索环境变量 -> PATH (对应的值是n个路径)
            	- 搜索这些路径中有没有叫file的可执行程序
        - arg: 这是一个字符串描述(启动的进程名字), 出现下 ps aux 的 COMMOND 列
        - ...: 启动的可执行程序的参数(>=0), 如果参数结束了, 末尾使用NULL(哨兵)
            如果有多个参数: 
					"a", "u", "x", NULL
// 以上两个函数共同的特点:
	- 函数执行成功之后不会返回
	- 函数执行失败了返回 -1

4.3 exit结束进程

// 结束进程的函数
#include <stdlib.h>
// 参数是进程退出的状态码,此函数结束时会释放进程PCB资源
void exit(int status);

 #include <unistd.h>
void _exit(int status);


void funcA()
{
    exit(0);	// 进程退出
    return ;	// 返回到调用者的位置
}

int main()
{
    funcA();
    return 0;	// 退出进程
}

4.4 wait和waitpid进程回收

#include <sys/types.h>
#include <sys/wait.h>
// 回收子进程资源
// 函数调用一次回收一个子进程
// 这是一个阻塞函数: 子进程不死, 该函数一直阻塞, 等待子进程退出, 解除阻塞回收子进程资源
// 			没有子进程了就不阻塞了, 函数返回-1
pid_t wait(int *status);
	参数:
		- status: 传出参数, 返回进程退出时候的一些状态信息, 一般时候用不着, NULL
	返回值:
		成功: 回收的子进程的进程ID
		失败: -1
#include <sys/types.h>
#include <sys/wait.h>

// 该函数可以设置为阻塞或非阻塞
// 可以指定回收哪一个子进程的资源
// 不管阻塞还是非阻塞, 没有子进程返回-1
pid_t waitpid(pid_t pid, int *status, int options);
	参数:
		- pid: 
			<-1: 取绝对值, 作为某个进程的组ID, 回收的是这个组中的所有子进程资源
			-1: 回收任何子进程资源    ->>>>> 经常会用
			0: 回收当前进程所在的进程组的子进程资源
			>0: 某个进程的进程ID      ->>>>>> 经常用的
		- status: 传出参数, 返回进程退出时候的一些状态信息, 一般时候用不着, NULL
		- options: 控制当前函数阻塞或者非阻塞
			- WNOHANG: 非阻塞
			- 0: 阻塞的
	返回值:
		失败: -1
        成功: 回收的子进程的进程ID
        没有做任何操作 -> 非阻塞情况下: 
			- 子进程还在运行, 返回0
// wait / waitpid 的第二个参数
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

参数: status
	- 判断进程是不是正常退出, 并得到退出时候的状态码
		main函数中的return或者exit函数
			- WIFEXITED(status): 是不是正常退出
		得到退出时候的状态码:
			- WEXITSTATUS(status)
                
	- 判断进程是不是被信号杀死了, 并且得到信息是被哪个信号杀死了
		- 是不是被信号杀死了:
			- WIFSIGNALED(status)
        - 得到信息是被哪个信号杀死了:
			- WTERMSIG(status)

4.5 getpgrp/getpgid/setpgid

// 得到当前进程所属的进程组的组ID
pid_t getpgrp(void);
// 得到指定进程所属的组的组ID
// 返回值: pid进程所属的组的组ID
pid_t getpgid(pid_t pid);
// 将某个进程转移到另一个进程中 / 创建新的进程组
int setpgid(pid_t pid, pid_t pgid);
	参数:
		- pid: 进程ID, 要操作的进程
		- pgid: 进程组ID
			- pgid这ID是存在的, pid对应的进程被移动到这个进程组中
				- pid != pgid
			- pgid这ID是不存在的, 这个进程组就被创建了
				- pid == pgid

4.6 会话getsid/setsid

// 获取进程所属的会话ID
#include <unistd.h>
pid_t getsid(pid_t pid);   

// 进程调用这个函数就可以变成会话
pid_t setsid(void);

4.7 守护进程

创建守护进程的步骤
/*
	1. 创建一个新的子进程 - fork();
	2. 杀死父进程 -> exit(0);
	3. 将新的子进程提升为会话 -> setsid();
	4. 修改进程的工作目录 (可选)
		- 如何看进程的工作目录?
			- 在哪个目录下执行进程, 工作目录就是哪个进程
		- 为什么要修改呢?
			- 如果进的工作目录在一个U盘里边, U盘被拔掉了 ==> 工作目录就没有了 ==> 进程不能正常工作
		- 如何修改?
			- int chdir(const char *path);
	5. 修改掩码 (可选)
		- 什么是掩码?
			- 修改创建的新文件的权限, 新文件的权限 = 0777 - 掩码的值
		- 修改的目的?
			- 在进程中修改掩码, 创建新文件, 这个文件的权限会受掩码的影响
				- 设置掩码为 0111
				- 创建新文件: 肯定没有执行权限  
		- 如何设置掩码?
			mode_t umask(mode_t mask);
	6. 关闭/重定向文件描述符
		- 指定的是那个文件描述符?
			- 每个进程文件描述符表中的0, 1, 2默认是打开的, 对应当前终端
			- 虽然进程脱离了终端, 但是还是绑定这
		- 如何关闭:
			close(0);
			close(1);
			close(2);
		- 重定向:
			- 在linux的/dev目录中有一个空文件: /dev/null (设备文件)
			int fd = open("/dev/null", O_RDWR);
			dup2(fd, 0);
			dup2(fd, 1);
			dup2(fd, 2);
	7. 添加子进程的业务逻辑 (在后台不停的做某件事)
*/

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

int main()
{
    // 1. 创建子进程
    pid_t pid = fork();
    if(pid > 0)
    {
        // 父进程
        exit(0);
    }
    // 2. 子进程 -> 会长
    setsid();
    while(1)
    {
        sleep(100);
    }
    return 0;
}

5.linux进程通信

5.1 匿名管道通信pipe

#include <unistd.h>
// 创建匿名管道
// pipefd是传出参数, 对应的是管道的两端
// pipefd[0] -> 读端, pipefd[1] -> 写端
int pipe(int pipefd[2]);
返回值: 
	成功: 0
    失败: -1
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

// cmd 取值: F_GETFL / F_SETFL
// 非阻塞的属性: O_NONBLOCK

// 获取当前fd对应的flags属性
// 设置读端非阻塞
int flag = fcntl(fd[0], F_GETFL);
// 将 O_NONBLOCK 设置 到flag属性中
flag = flag | O_NONBLOCK;	// flag |= O_NONBLOCK;
// 将flag属性设置给 fd[0]
fcntl(fd[0], F_SETFL, flag);

5.2有名管道通信mkfifo

// 通过函数
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
	参数:
		- pathname: 要创建的管道的名字
		- mode: 创建的管道文件的权限, 指定一个八进制的数
			文件的最终权限: (mode & ~umask)

5.3内存映射mmap

#include <sys/mman.h>
// 创建内存映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
	参数:
		- addr: 在动态库加载区的什么位置创建一块映射内存, NULL -> 委托内核指定
		- length: 创建的内存映射区大小, 指定我们想要的大小即可
			- 实际分配的大小是4k整数倍(>0)
            - 这个参数长度不能为0
        - prot: 进程对内存映射区的操作权限, 读权限是必须要指定的
        	- PROT_READ: 读权限
        	- PROT_WRITE: 写权限
        	- PROT_READ | PROT_WRITE: 读写权限
        - flags: 实现进程间通信,必须指定 MAP_SHARED
			- MAP_SHARED: 数据共享, 当映射区数据被修改, 数据才会同步到磁盘文件
			- MAP_PRIVATE: 数私有, 当映射区数据被修改, 数据不会同步到磁盘文件
		- fd: 磁盘文件对应的文件描述符
			- 内存映射区和这个fd对应的磁盘文件进行映射
			- 得到fd需要open(), 在open的时候需要指定对文件的操作权限
				- 这个权限和prot参数的权限对应即可
		- offset: 在映射的时候对磁盘文件进行偏移
			- 这个参数必须是4k的整数倍
			- 不偏移指定为0
	返回值:
		- 成功: 内存映射区的起始地址
		- 失败: MAP_FAILED (that is, (void *) -1) )
// 释放内存映射区
int munmap(void *addr, size_t length);
	参数:
		- addr: 内存映射区的首地址
		- length: 和 mmap的 第二个参数值相同即可
	返回值:
		成功: 0
        失败: -1

6.linux信号函数

6.1 kill/raise/abort函数

// 发信号给对应的进程
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
	参数:
		- pid: 将信号发送给那个进程, 进程的 进程ID
			- >0: pid对应是指定的进程     ====> 用的比较多
			- =0: 发送给当前进程所在进程组中的所有进程
			- -1: 发送给所有的进程(有权的), init进程是绝对不允许的
			- <-1: pid取绝对值, 这个数表示一个进程组, 这个信号发送给这个进程组中所有进程
		- sig: 要发送的信号
	返回值:
		成功: 0
        失败: -1
#include <signal.h>
// 发送信号给当前进程
// 参数就是要发送的信号
int raise(int sig);

// 使用kill实现raise的功能
kill(getpid(), sig);
#include <stdlib.h>
// 给当前进程发送SIGABRT信号
void abort(void);

6.2 alarm/setitimer定时器函数

#include <unistd.h>
// 设置倒计时时长, 当时间倒计时 == 0的时候, 该函数会给进程发信号
// SIGALRM, 进程收到这个信号就会终止
unsigned int alarm(unsigned int seconds);
	参数: seconds: 计时时长
	返回值: 倒计时还剩多长时间

// 倒计时的时候终止计时
alarm(5);
.....
alarm(0);

// 算一下电脑1s可以数多少个数?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{
    int num = 0;
    alarm(1);
    while(1)
    {
        printf("%d\n", num++);
    }
    return 0;
}

# 重定向到文件
$ time ./a.out > a.txt
Alarm clock

real    0m1.001s		`总时长`
user    0m0.184s		`用户区代码执行的时间`
sys     0m0.740s		`内核区代码执行的时间`

real = user + sys + 消耗的时长(用户区到内核区切换)
#include <sys/time.h>
// 定时周期对应的结构体
/*
	luffy晚上12点睡觉, 明天早晨8点起床, 因此要定个闹钟, 但是luffy'赖床, 需要每隔5分钟再响一次
		- 倒计时8个小时: it_value
		- 每隔5分钟再响一次: it_interval
*/ 
struct itimerval {
	struct timeval it_interval; /* 从第二次开始发信号的时间周期 */
	struct timeval it_value;    /* 定时器函数第一次发信号的倒计时时长 */
};

// 这个结构体表示一个时间段
// tv_sec + tv_usec
struct timeval {
	time_t      tv_sec;         /* seconds */
	suseconds_t tv_usec;        /* microseconds */
};

// 设置一个定时器, 而且能够实现周期性定时
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
	参数:
		- which: 使用什么样的计时法
			- ITIMER_REAL: 自然计时法, 真正使用的时长, 使用的的 SIGALRM 信号   ==> 常用
			- ITIMER_VIRTUAL: 用户区使用的总时长, 使用 SIGVTALRM 信号
			- ITIMER_PROF: 使用内核使用的总时长计时, 使用信号 ITIMER_VIRTUAL
		- new_value: 指定的定时规则
		- old_value: 获取上一次定时的一些信息, 如果不需要, 指定为NULL

6.3 signal/sigaction信号捕捉

#include <signal.h>

typedef void (*sighandler_t)(int);
	- int参数: 代表捕捉到的信号

// 注册捕捉哪个信号
// 在信号产生之前调用该函数
sighandler_t signal(int signum, sighandler_t handler);
	参数:
		- signum: 指定要捕捉的信号
		- handler: 回调函数, 信号被捕捉到之后的处理动作

// 什么是回调?
	- 1. 作为程序猿只需要实现这个函数, 不需要进行调用
	- 2. 需要在程序的适当位置对函数进行注册
		- 注册过程一般就是一个函数调用, 这个注册函数有一个参数是函数指针
		- 将函数被调用的时机告诉框架/操作系统/内核
	- 3. 框架/操作系统/内核 会检测时机是不是成熟了
			- 成熟了 框架/操作系统/内核 会调用我们指定的函数
#include <signal.h>

struct sigaction {
	void     (*sa_handler)(int);
	void     (*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t   sa_mask;
	int        sa_flags;
	void     (*sa_restorer)(void);
};
	- sa_handler: 函数指针, 指向信号捕捉函数, 一般时候用这个
	- sa_flags:
		== 0: 使用函数指针 -> sa_handler   ==> 通常指定为0
		== SA_SIGINFO: 使用是函数指针 -> sa_sigaction  ===> 不常用
	- sa_restorer: 这是一个被废弃的变量
	- sa_mask: 
		- 当信号的回调函数被执行, 在该函数执行期间可以设置临时屏蔽某些信号
			- 我们要屏蔽哪些信号只需要将这些信号设置到该集合中
		- 如果不想阻塞任何信号, 需要将该变量初始化为空
			- int sigemptyset(sigset_t *set);
		- 在当前捕捉到的的这个信号的回调函数执行期间, 默认屏蔽当前信号
			- 比如: 捕捉了SIGINT, SIGINT的处理函数被执行
				- 这个处理函数执行期间, 又产生了一个SIGINT信号, 这个信号被临时阻塞
				- 当处理函数处理完毕, SIGINT信号的处理函数会被再次调用
            
// 注册信号捕捉
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
	参数:
		- signum: 要捕捉的信号
		- act: 传入参数, 设置当前捕捉的信号对应的处理动作
		- oldact: 传出参数, 获得上一次设置的信号捕捉的处理动作

6.4 信号集相关函数

#include <signal.h>
// 将sigset_t中所有的标志位初始化为0
int sigemptyset(sigset_t *set);
// 将sigset_t中所有的标志位初始化为1
int sigfillset(sigset_t *set);
// 将信号 signum 添加到 set 信号集中, 也就是这个信号对应的标志位被设置为1
int sigaddset(sigset_t *set, int signum);
// 将信号 signum 从 set 信号集中删除, 也就是这个信号对应的标志位被设置为0
int sigdelset(sigset_t *set, int signum);
// 判断 signum 信号有没有被设置到信号集 set中
// 返回值: 如果信号在信号集中返回 1, 没有在返回0
int sigismember(const sigset_t *set, int signum);
// 将用户区的初始化的信号集设置到内核中, 设置或者解除阻塞
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
	参数:
		- how: 
			- SIG_BLOCK: 将参数 set 信号集中阻塞的信号追加到内核的阻塞信号集
			- SIG_UNBLOCK: 将参数 set 信号集中的信号从内核的阻塞信号集中删除
			- SIG_SETMASK: 使用参数 set 信号集中的数据覆盖内核的阻塞信号集
		- set: 用户初始化的信号集, 传入参数
		- oldset: 在做这次设置之前是设置, 得到了设置之前的内核中的阻塞信号集
// 查看当前内核中的未决信号集
#include <signal.h>
int sigpending(sigset_t *set);
	参数:
		- set: 函数调用成功, 未决信号集的状态被写入到该变量中

7.linux线程函数

7.1pthread_create创建线程

#include <pthread.h>

// pthread_t: 线程ID的类型, 无符号长整形
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
	参数:
		- thread: 传出, 通过这个参数得到了创建出的新的线程的ID
		- attr: 线程属性, 一般使用默认值, 该参数指定为NULL
		- start_routine: 函数指针, 指向的函数就是子线程的处理动作
		- arg: 对应第三参数函数指针的参数, 回调函数被调用, 这个参数作为实参传递到start_routine中
	返回值: 函数的状态
		- 0: 成功了
		- 失败了: 返回错误号 >0
// Compile and link with -pthread.
gcc *.c -l pthread
// 因为返回的是错误号, 因此不能使用perror直接打印错误信息 == 不能使用perror
// 使用strerror
#include <string.h>
// 参数是得到的错误号, 返回值是错误号对应的错误信息
char *strerror(int errnum);

7.2 pthread_self 获取线程ID

#include <pthread.h>
// 获取当前线程的线程ID
pthread_t pthread_self(void);

7.3 pthread_exit 线程退出

/*
	有多个线程:
		- 1个主线程
		- 若干个子线程
	线程退出的时候:
		- 如果其中子线程先退出, 对主线程和其他子线程没有影响
		- 主线程先退出, 运行的子线程强制被退出
			- 这些线程共用虚拟地址空间, 主线程退出的时候会销毁虚拟地址空间
*/
#include <pthread.h>
// 让某一个线程退出, 退出的时候对其他线程没有任何影响
void pthread_exit(void *retval);
	- 参数: 
		- retval: 线程退出的时候可以携带以下信息, 在这个指针指向的内存中
					- 在线程回收的时候, 可以将这些信息读出来

7.4 pthread_join 回收子线程

// 在子线程退出的时候, 主线程回收子线程资源
#include <pthread.h>
// 这是一个阻塞函数, 当有子线程还活着的时候, 子线程死了解除阻塞, 回收其资源
void* pt;
pthread_join(tid, &pt);
int pthread_join(pthread_t thread, void **retval);
	参数:
		- thread: 要回收的线程的线程ID
		- retval: 指向一级指针的地址, 传出参数, 里边的数据
			- pthread_exit(void *retval), 参数指针指向的数据
			- 如果对这个值不感兴趣, 直接写NULL
	返回值: 函数的状态
		- 0: 成功了
		- 失败了: 返回错误号 >0

7.5 pthread_detach线程分离

// 在子线程被创建出之后, 设置线程分离, 主线程就不需要负责子线程的资源回收
#include <pthread.h>
// 参数: thread子线程的线程ID
int pthread_detach(pthread_t thread);

7.6 pthread_cancel 杀死线程

// 在合适的时机杀死子线程
// 在该函数被调用之后, 子线程不会马上就终止
// 合适的时机 == 在子线程的处理函数中要有一个系统调用, 并且执行到该系统调用的位置的时候
#include <pthread.h>
// 参数: 子线程的线程ID
int pthread_cancel(pthread_t thread);

// 子线程的处理函数
void* callback(void* arg)
{
    int a;
    .....;				======> 子线程执行到该位置, 主线程调用了 pthread_cancel
    .....;
    .....;
    int fd = open();
}

7.7 pthread_equal 线程比较

比较线程ID是否相同
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
	返回值:
		相同: !=0
        不同: =0

7.8 线程属性函数

// 在创建新线程的时候, 给其设置属性
// 一般时候使用默认属性, 因此该属性值指定为NULL
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
// 线程的属性类型: pthread_attr_t
// 属性的使用: 
	- 创建属性变量: pthread_attr_t attr;
	- 对属性初始化: pthread_attr_init
	- 设置具体的属性(设置线程分离): pthread_attr_setdetachstate
	- 在创建新线程的时候使用这个属性: pthread_create
	- 销毁属性: pthread_attr_destroy

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

// 设置线程属性(线程分离)
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
	参数: 
		- attr: 属性变量
		- detachstate: 分离状态
			- PTHREAD_CREATE_DETACHED: 设置线程分离
			- PTHREAD_CREATE_JOINABLE: 设置线程不分离

7.9 互斥锁操作函数

// 互斥锁的类型: pthread_mutex_t
// 初始化互斥锁
// 关键字: restrict, 修饰指针, mutex指针指向的内存只能通过mutex指针变量访问
// pthread_mutex_t *ptr = mutex;	// error
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
	参数:
		- mutex: 初始化互斥锁变量, 并传出
		- attr: 互斥锁属性, 一般使用默认的, 指定为NULL
// 销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 互斥锁加锁, 这是阻塞函数
// 阻塞的条件:
// 	- 这个mutex锁已经被锁上了, 其他线程调用这个函数就阻塞
//  - 这个mutex锁是打开的, 线程调用这个函数可以加锁成功, 不阻塞
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 尝试加锁, 不是阻塞函数
// 	- 这个mutex锁已经被锁上了, 其他线程调用这个函数就不阻塞, 函数直接返回, 加锁失败
//  - 这个mutex锁是打开的, 线程调用这个函数可以加锁成功, 不阻塞, 加锁成功
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

7.10 读写锁操作函数

#include <pthread.h>
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);
	参数:
		- rwlock: 要初始化的读写锁
		- attr:读写锁的属性, 默认属性指定=NULL
// 释放读写锁资源
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);

以上所有的返回值:
	成功: 0
    失败: != 0, 错误号
通过函数: char* strerror(errno);

7.11 条件变量

#include <pthread.h>
// 初始化条件变量
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);
	参数: 
		- cond: 初始化的条件变量
		- 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);
	参数: 
		- cond: 初始化的条件变量
		- mutex: 互斥锁, 用户数据同步
		- abstime: 阻塞的时间
			- 阻塞100s之后解除阻塞
			- struct timespec tsp;
			  tsp.tv_sec = time(NULL) + 100;
// 通知阻塞在参数cond上的线程解除阻塞
// 至少唤醒一个阻塞的线程
int pthread_cond_signal(pthread_cond_t *cond);
// 通知阻塞在参数cond上的线程解除阻塞
// 唤醒全部阻塞的线程
int pthread_cond_broadcast(pthread_cond_t *cond);

7.12 信号量函数

#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
	参数:
		- sem: 被初始化的信号量变量, 这个参数看做一个整形数即可
		- pshared: 
			- 0: 线程
			- 1: 进程
		- value: 给第一个参数sem赋值, 通过这个变量中的值控制线程是阻塞还是不阻塞
// 销毁信号量
int sem_destroy(sem_t *sem);
// 有可能阻塞线程
// 这个函数被调用一次 sem 中的整形变量 -1
// 当sem 中的整形变量 == 0, 线程被阻塞
int sem_wait(sem_t *sem);
// 这个函数被调用一次 sem 中的整形变量 -1
// 当sem 中的整形变量 == 0, 线程不阻塞
int sem_trywait(sem_t *sem);

// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
// 这个函数被调用一次 sem 中的整形变量 -1
// 当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);

8.SOCKET通信函数

8.1 大小端转换函数

#include <arpa/inet.h>
// h -> host -> 主机字节序
// to -> 转换
// n -> net -> 网络字节序
// s -> short -> 16位整形数
// l -> long -> 32位整形数
// 参数: 要转换的数据
// 返回值: 转换完成之后得到的结果

// 一般端口转换的时候使用这个函数
// 主机字节序 -> 网络字节序
uint16_t htons(uint16_t hostshort);
// 网络字节序 -> 主机字节序
uint16_t ntohs(uint16_t netshort)

// 一般对整形IP进行转换
// 主机字节序 -> 网络字节序
uint32_t htonl(uint32_t hostlong);
// 网络字节序 -> 主机字节序
uint32_t ntohl(uint32_t netlong);

#include <arpa/inet.h>
// p -> 主机字节序的点分十进制IP地址(字符串): 192.168.1.100
// n -> 网络字节序的整形IP地址
// 将本地IP字符串 -> 整形大端IP地址
int inet_pton(int af, const char *src, void *dst);
	参数: 
		- af: 地址族协议, 使用的ipv4还是ipv6的ip地址
			- ipv4: AF_INET
			- ipv6: AF_INET6
		- src: 要被转换的点分十进制的IP地址: 192.168.1.100这种格式
		- dst: 转换得到的大端IP存储到这个指针指向的内存中
	返回值:
		转换成功: 0
           失败: -1
               
// 整形大端IP地址  -> 将本地IP字符串         
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
	参数: 
		- af: 地址族协议, 使用的ipv4还是ipv6的ip地址
			- ipv4: AF_INET
			- ipv6: AF_INET6
		- src: 这个指针指向的内存中存储了大端的整形IP地址
		- dst: 存储得到的IP地址字符串
		- size: dst参数指向的内存的总大小(容量)
    返回值:
		成功: 返回第三个参数dst的地址
		失败: NULL
		
// -> 将本地IP字符串  
struct sockaddr_in {
       sa_family_t    sin_family; /* address family: AF_INET */
       in_port_t      sin_port;   /* port in network byte order */
       struct in_addr sin_addr;   /* internet address */
 };

/* Internet address. */
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};
 char *inet_ntoa(struct in_addr in);

8.2 套接字API

#include <arpa/inet.h>
// 创建套接字
int socket(int domain, int type, int protocol);
	参数:
		- domain: 指定地址族协议
			- AF_UNIX, AF_LOCAL: 用于本地进程间通信(本地套接字)
            - AF_INET: 使用IPv4
            - AF_INET6: 使用IPv6
        - type: 使用什么传输协议
        	- SOCK_STREAM: 流式传输协议
        	- SOCK_DGRAM: 报式传输协议
        - protocol: 指定具体的协议, 这个参数一般指定为0
        	- 使用流式协议中的tcp协议
        	- 使用报式协议中的udp协议
	返回值:
		成功: 返回用于socket的文件描述符
		失败: -1

// 将监听的套接字和本地的IP和端口绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	参数:
		- sockfd: 用于监听的套接字
		- addr: 在这个变量中初始化要绑定的IP和端口信息
		- addrlen: 第二个参数addr对应的内存大小
	返回值:
		成功: 0
        失败: -1

// 设置监听->给监听的套接字
int listen(int sockfd, int backlog);
	参数:
		- sockfd: 绑定成功的套接字
		- backlog: 可以最大同时监听多少个客户端连接
			- 这个值最大是128, 如果参数值超过128, 还是按128处理
	返回值:
		成功: 0
        失败: -1
            
// 等待并接受客户端连接, 阻塞函数
// 开始监听之后, 没有客户端连接服务器, 这时候程序就阻塞在了accept函数上
// 当检测到了客户端连接, 这个函数解除阻塞, 和客户端建立连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
	参数:
		- sockfd: 用户监听的套接字
		- addr: 传出参数, 得到连接的客户端的IP和端口信息
		- addrlen: 传入传出参数, 记录了参数addr指针对应的内存大小
	返回值:
		成功: 通信的文件描述符
		失败: -1

// 接收数据
ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
	参数:
		- 前三个参数和read一样
		- flags: 套接字是一些属性, 默认使用0
// 发送数据
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
	参数:
		- 前三个参数和write一样
		- flags: 套接字是一些属性, 默认使用0
		
// 连接服务器, 这个函数也是阻塞函数
// 连接完成之后, 解除阻塞
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	参数:
		- sockfd: 客户端用于通信的套接字
		- addr: 初始化服务器的IP和端口, 通过这个地址连接服务器
		- addrlen: 参数 addr 指针指向的内存大小, sizeof(addr)
   返回值:
		成功: 0
        失败: -1
            
 #include <sys/socket.h>
// 设置套接字半关闭
int shutdown(int sockfd, int how);
	参数:
		sockfd: 要操作 的文件描述符
		how: 
			- SHUT_RD: 关闭读
			- SHUT_WR: 关闭写
			- SHUT_RDWR: 关闭读写
	
 //目标结构体清空,置为0
#include <strings.h>
 void bzero(void *s, size_t n);

8.3 端口复用函数

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
	参数:
		- sockfd: 要被设置属性的文件描述符
		- level: SOL_SOCKET
		- optname: 设置端口复用
			- SO_REUSEPORT
			- SO_REUSEADDR
		- optval: 要设置的属性的值
			- 根据查手册 设置端口复用 该指针指向的一个int数据
			- 值: 1 -> 设置端口复用
			- 值: 0 -> 不设置端口复用, 默认端口不能复用
		- optlen: 参数 optval 指针对应的内存大小
	返回值: 
		设置成功: 0
        失败失败: -1