# LinuxEnvironmentProgramming **Repository Path**: giteeliuyou/LinuxEnvironmentProgramming ## Basic Information - **Project Name**: LinuxEnvironmentProgramming - **Description**: No description available - **Primary Language**: Unknown - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2021-05-03 - **Last Updated**: 2026-03-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # IO ## 标准IO IO: 是一切实现的基础 stdio标准IO sysio系统IO(文件IO) 标准IO有两大优点: 移植性好、合并系统调用(buffer缓冲区, 提高吞吐量, 优化效率等)。 --- ### fopen() stdio: FILE类型贯穿始终。 errno: errno 是 error number 的缩写,意味系统调用错误码。 如果系统调用返回成功,errno 有可能但不一定会置0;而系统调用出错时,errno 必定会被设为对应的错误编号。因此,强迫症患者可以在调用系统调用之前,手动将 errno 置0。不过,如果系统调用返回成功,谁会闲着没事去看 errno 呢? 原文链接:https://blog.csdn.net/tissar/article/details/87996113 把errno转化为error msg的两个函数: void perror(const char *s); perror函数在形参的字符串后面加上`: error msg`再加上一个换行符。 char *strerror(int errnum); ```c++ // 把errno转化为error msg的两个函数 std::perror("fopen()"); std::fprintf(stderr, "fopen() failure: %s\n", std::strerror(errno)); ``` fopen函数返回的结构体指针指向的结构体放在了内存的动态存储区的堆上。 在fopen函数内部动态分配FILE结构体的内存, 然后在fclose函数中释放掉fopen函数分配的内存。 ##### 0666 & ~umask umask值默认为0002。 注意: 这些都是8进制数。 umask的值越大, 生成的文件的权限就越小。 --- ### fclose() 是资源就有上限。 --- ### fgetc() getchar() / putchar() getc() / putc() fgetc() / fputc() int getchar(void); int getc(FILE *stream); int fgetc(FILE *stream); getchar() is equivalent to getc(stdin). getc和fgetc其实用法没有什么区别, 只是一个是宏函数一个是普通的函数。 ### fputc() int fputc(int c, FILE *stream); int putc(int c, FILE *stream); int putchar(int c); putchar(c) is equivalent to putc(c, stdout). 和fgetc()等用法是一样一样的。 ### fgets() man手册指名道姓不要使用gets(), 而是使用fgets()。 char *fgets(char *s, int size, FILE *stream); ### fputs() int puts(const char *s); int fputs(const char *s, FILE *stream); ### fread() ```text size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 参数size(一个对象的大小)和参数nmemb(对象的个数)相乘就是读到的总的字节个数。 fread和fwrite在实际中使用很多。 使用有两种情况: 假设buf是10个字节的char数组。 // 1个对象1个字节, 读10个对象。即读10个字节。 fread(buf, 1, 10, fp); // 1个对象10个字节, 读1个对象。即也是读10个字节。但是这样有缺陷, 如果没有10个字节了。那就失败了。 fread(buf, 10, 1, fp); 所以, 推荐只使用一个对象1个字节, 然后读多个对象!!!!其实就是当成fgetc和fputc来使用。 ``` size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); ### fwrite() ### printf() ```c int printf(const char *format, ...); // fprintf这个函数比printf更加好用, 因为fprintf函数可以指定向哪个流上进行输出。我也倾向于使用它而不是printf。 // 比如可以将错误定向的输出到某一个日志文件中, 把输出定向到某一个文件中。之后可以查看这些文件来揣摩程序的执行过程。 int fprintf(FILE *stream, const char *format, ...); int dprintf(int fd, const char *format, ...); // 这个函数也经常使用, 将格式化的C字符串输出到(放入到)str所指向的字符数组空间中。 // 还有另外一个函数 atoi , 将一个C字符串转换为一个整型数。 int sprintf(char *str, const char *format, ...); // 这个函数是对上面这个函数的改进, 有了参数size防止越界。size是str指针指向的字符数组的大小。 // 但是它们容纳的最大字节数是size-1个, 因为最后一个是'\0', 它要预留一个字节来存储'\0'。 int snprintf(char *str, size_t size, const char *format, ...); // 其实 snprintf依然没有解决问题和fgets一眼都没有真正的解决问题。 ``` ### scanf() ```c // 和printf函数族是一样的。 int scanf(const char *format, ...); int fscanf(FILE *stream, const char *format, ...); int sscanf(const char *str, const char *format, ...); ``` ### fseek() ```c int fseek(FILE *stream, long offset, int whence); // 只能定位2G大小的文件。但是现在随着硬件的发展, 文件超过2G是非常常见的事情。例如: 服务器上的日志文件超过2G是非常非常常见的事。 long ftell(FILE *stream); // 所以有以下俩个函数对上面俩个函数的改进。但是这俩个函数并不支持C89和C99。是一个"方言"函数。但是上面的俩个函数是支持C89和C99, // 因为上面俩个函数太古老了, 移植性好, 但是文件超过2G就不能使用上面的方法了需要另寻它法了。 int fseeko(FILE *stream, off_t offset, int whence); off_t ftello(FILE *stream); ``` ```c // 文件位置指针, 读只能从当前位置这个字符开始读, 写只能从当前位置覆盖这个字符开始写。 // 所以这三个函数是重新定位一个流, 使操作文件更加灵活。 int fseek(FILE *stream, long offset, int whence); // 告诉你当前文件的位置指针在哪 long ftell(FILE *stream); void rewind(FILE *stream); ``` ### ftell() ### rewind() ```text The rewind() function sets the file position indicator for the stream pointed to by stream to the beginning of the file. It is equivalent to: (void) fseek(stream, 0L, SEEK_SET) ``` 空洞文件: 文件全部是ASCII码的空字符'\0'的文件为空洞文件。 ### fflush() int fflush(FILE *stream); If the stream argument is NULL, fflush() flushes all open output streams. #### buffer(缓冲区) 缓冲区的作用: 大多数情况下是好事, 合并系统调用。 * 行缓冲 换行的时候刷新(`\n`); 满了的时候也刷新; 也可以强制刷新(使用fflush函数); * 全缓冲 满了的时候刷新; 也可以强制刷新(使用fflush函数); * 无缓冲 如: stderr, 需要立即输出的内容 缓冲区可不可以改, 可以改的通过 setvbuf函数。但是不要修改缓冲区。使用默认就好。 ### getline ```c ssize_t getline(char **lineptr, size_t *n, FILE *stream); // getline函数内部是先malloc一块内存, 不够了再realloc内存。 // 具体可参考 StandardIOTest08.cpp文件。 // 但是getline函数内部只进行了分配内存并没有释放内存, 而且标准中也没有设计一个函数是针对getline函数的内存释放。 // 所以, 这里老师也不推荐使用free(linePtr); 因为我们不知道getline内部调用的是malloc还是new或者其它去分配的内存。 // 在实际开发工作中都会有自己设计的getline函数, 这里是跑不掉的。 ``` getline并不是C/C++的标准库中的内容, 它遵循的是GNU标准, 是方言函数。 ### 临时文件(temporary file) 有一个服务器, 用户会有一些请求等。一个服务器在工作过程中会受到用户发过来的很多数据, 服务器需要向用户反馈的话, 服务器需要暂时保留用户的请求或者对用户的 请求做一个统计等。总而言之, 对用户提交上来的数据, 服务器有必要把这些数据暂时存储起来, 其中有一些数据可能会永久保存起来或者保留一个月的时间等; 还有一部分数据 可能接下来马上就要用, 然后进行分析加工二次计算然后反馈用户一个结果。然后这个结果就不用了, **没有保存的必要**, 这种情况就可以将这些数据存储在临时文件中。 临时文件的问题: 1. 如何不冲突的创建临时文件 在unix和linux环境下, 我们经常会在tmp/目录下做一些小例子。而linux系统中跑着许多的进程, 这些进程都可能在tmp/目录下创建临时文件。如果创建了相同的临时文件名。 (名字冲突)其实就是冲突了。 2. 没有想到及时销毁临时文件 其实没有想到及时销毁临时文件会加大名字冲突, 如果不同的进程都是默认在tmp/目录下产生临时文件。所以服务器上的很多进程(也不一定是服务器才有)产生的临时文件都是有严格的命名要求的。 临时文件的创建 ```c // create a name for a temporary file // 注意: tmpnam并不是原子操作! char *tmpnam(char *s); ``` ```c // tmpfile - create a temporary file // 注意: 创建的临时文件没有文件名, 是一个匿名文件。ls命令也无法看到它。 // 其实文件名我们程序员是不关心的, 只需要一个FILE就可以了。所以这样就不会产生命名冲突了。 FILE *tmpfile(void); ``` ### 总结标准IO IO是一切实现的基础, 因为程序没有IO数据就没有办法保存, 想分析或者进行二次封装什么都别谈! ## 可以将文件描述符FD(包括socket)封装成一个流。 ## 文件IO/系统调用IO file descriptor(fd/FD)是在文件IO中贯穿始终的类型。而FILE是标准IO中贯穿始终的类型。 ### fd概念 #### FILE FILE中一定有一个文件位置指针(pos), 因为读完一个字节就读下一个字节, 写完当前位置就写下一个位置。 因为fopen依赖于open支持, 所以FILE中一定也有一个fd。 一个进程能打开的文件个数是1024个。 #### fd 使用open打开文件, 系统也会创建一个结构体来记录这个文件(inode)的信息, 并创建一个结构体指针数组, 将新创建的这个记录文件信息的结构体用一个结构体指针指向它, 然后将这个指针存放到这个结构体指针数组中, 然后返回这个指针所在的结构体指针数组的下标, 其实也就是一个整型数。系统这么做其实是为了隐藏这个记录文件信息的结构体。这样使用fd其实就是在使用一个 指针, 其实就是在使用指针指向的记录文件信息的结构体, 其实就是通过这个记录文件信息的结构体来处理文件。 这个数组中下标 0 1 2 已经关联了3个设备, stdin stdout stderr。 * fd -> 0 1 2 fd(用户自定义产生的fd) ··· * stream -> stdin stdout stderr fp(用户自定义产生的文件流) ··· 这个记录文件信息的结构体中一定也有一个文件位置指针(pos)。 fd: 整型数, 数下标, fd使用策略: 优先使用当前可用范围内(数组下标)最小的。 这个数组是存储在一个进程空间中的, 每一个进程都会有一个这样的数组。 如果两个进程同时打开了一个文件, 这两个进程是都会有一个数组, 而且都会有一个结构体(记录信息的结构体)来操作这个文件的, 但是这两个结构体(记录信息的结构体) 是不同的互不影响的, 两个数组也是互不影响的。当然也是互相影响的, 因为两个进程打开了同一个文件, 然后这两个进程没有任何协议, 然后这两个进程就开始七手八脚的 读写这个文件, 这个时候造成的是竞争是冲突。 当然, 如果这两个进程之间有协议, 那么就可以协同去操作同一个文件。 ### fd的IO操作 open, close, read, write, lseek。标准IO的实现是依赖于系统IO的支持的。 #### open ```c /** * flags: 这个参数是一个位图, 表示当前的使用这个文件的权限问题。 */ int open(const char *pathname, int flags); // cache 和 buffer // buffer可以理解为写缓冲区 // cache可以理解为读缓冲区 ``` ```text file creation flags and file status flags(文件创建标志和文件状态标志) |O_TRUNC|O_CREATE : 有则清空无则创建 r -> O_RDONLY r+ -> O_RDWR w -> O_WRONLY|O_TRUNC|O_CREATE w+ -> O_RDWR|O_TRUNC|O_CREATE ``` ```c // 包含头文件 //#include //#include //#include // 如果flags当中有O_CREATE, 那么一定要用3参的形式; // 如果flags当中没有O_CREATE, 那么要用2参的形式。 // 这里不是使用重载实现的, 而是使用变参实现的。 int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); ``` #### close ```text #include int close(int fd); ``` #### read ```c // 这里 buf没有使用const的原因: 这个函数是从fd中读取内容然后写到buf中的, 这个函数需要改变 // buf的权限。 ssize_t read(int fd, void *buf, size_t count); ``` #### write ```c // 这里 buf加const修饰符的原因: 这个函数只是从这个buf中读的权限。 // 从buf中读取数据然后写入fd中。 ssize_t write(int fd, const void *buf, size_t count); ``` #### lseek ```c off_t lseek(int fd, off_t offset, int whence); ``` #### MyCopy示例 参考`Test/FileIO/`目录下的示例文件。 ### 文件IO和标准IO的区别 * FILE * fd/FD/file descriptor 举例: 传达室老大爷跑邮局 区别: 响应速度(文件IO响应速度快)和吞吐量(标准IO吞吐量大) 面试: 如何使一个程序变快? (一分为二作答: 如果是提高响应速度..., 如果是提高吞吐量...) 标准IO和文件IO不可混用! 补充两个函数: ```c // 将文件流转换为fd, 将标准IO操作转换为文件IO操作。 // 这个函数的返回值就是stream这个文件流中的文件描述符fd。 int fileno(FILE *stream); ``` ```c // 把一个fd封装为一个文件流进行使用, 将文件IO转换成标准IO来使用。 FILE *fdopen(int fd, const char *mode); ``` ### IO的效率问题 将mycpy.c的程序进行更改, 将BUFSIZE的值放大, 并观察进程消耗的时间。注意: 性能的最佳拐点出现时的BUFSIZE的值, 以及何时程序会出现问题。 ### 文件的共享 面试: 写程序删除一个文件的第10行。 补充函数: ```c // 把一个未打开的文件截短到length int truncate(const char *path, off_t length); // 把一个已经打开的文件截短到length int ftruncate(int fd, off_t length); ``` ### 原子操作 原子: 不可分割的最小单位。 原子操作: 不可分割的操作, 如果一些操作可以分割, 那么就会造成竞争。 原子操作的作用: 解决竞争和冲突。 如: 之前提到的tmpnam函数, 它的操作不原子。 ### 程序中的重定向 dup, dup2 ```c // dup, dup2, dup3 - duplicate a file descriptor(复制一个文件描述符) // dup函数使用当前范围内没有使用的最小的文件描述符作为新的文件描述符 // 而且这俩个新旧fd都是指向同一个结构体(记录文件信息的结构体)。 // 但是dup这个函数不原子 int dup(int oldfd); // dup2这个函数原子。 int dup2(int oldfd, int newfd); ``` 不要当自己在写一个main函数, 永远当成自己在写一个小模块。执行了这个模块, 对工程的状态没有影响,不改变外界环境。 大的工程在执行这个模块之前的状态和执行完这个模块之后的状态, 能不给人家改变就不要改变。 ### 同步(sync) sync, fsync, fdatasync ### fcntl() & ioctl() fcntl: fd所变的魔术几乎都来自于这个函数, 管家级别函数。 ```c // fcntl - manipulate file descriptor(操作文件描述符) int fcntl(int fd, int cmd, ... /* arg */ ); ``` ioctl: 设备相关的内容 ### /dev/fd/目录: /dev/fd/目录: 这是一个虚目录, 显示的是当前进程的fd信息。 # 文件系统 设计一个类似ls的实现, myls。 ## 目录和文件 1. 获取文件属性信息 ```c // 这是一个系统调用函数。 // stat, fstat, lstat, fstatat - get file status(获取文件状态) int stat(const char *pathname, struct stat *statbuf); int fstat(int fd, struct stat *statbuf); int lstat(const char *pathname, struct stat *statbuf); ``` ```text struct stat { dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* Inode number */ mode_t st_mode; /* File type and mode */ nlink_t st_nlink; /* Number of hard links */ uid_t st_uid; /* User ID of owner */ gid_t st_gid; /* Group ID of owner */ dev_t st_rdev; /* Device ID (if special file) */ off_t st_size; /* Total size, in bytes */ blksize_t st_blksize; /* Block size for filesystem I/O */ blkcnt_t st_blocks; /* Number of 512B blocks allocated */ /* Since Linux 2.6, the kernel supports nanosecond precision for the following timestamp fields. For the details before Linux 2.6, see NOTES. */ struct timespec st_atim; /* Time of last access */ struct timespec st_mtim; /* Time of last modification */ struct timespec st_ctim; /* Time of last status change */ #define st_atime st_atim.tv_sec /* Backward compatibility */ #define st_mtime st_mtim.tv_sec #define st_ctime st_ctim.tv_sec }; ``` 2. 文件访问权限问题 文件的权限信息。使用命令 ll 可显示在终端上。 ```shell [lucas@VM-0-10-centos FileSystem]$ ll total 20 -rwxrwxr-x 1 lucas lucas 14456 May 9 15:18 a.out -rw-rw-r-- 1 lucas lucas 1000 May 9 16:01 FileSystemTest01.cpp # -rw-rw-r-- # - : 是文件类型 # rw- : user的权限, 也就是owner的权限 # rw- : group的权限, 也就是同组用户的权限 # r-- : other的权限 # 3 + 3 + 3 + 1 + 1 + 1 = 12 # 文件类型有7种, 使用3个二进制位就可以表示。 # 12 + 3 = 15 # 因为没有15位的整型数, 所以(mode_t st_mode;)一定是一个16位的整型数。 # st_mode当中有两部分组成, 文件类型 + 文件权限。 # 文件类型: 7种, dcb-lsp # directory 目录文件 # character 字符设备文件 # block 块设备文件 # - : regular file(常规文件) # link : 链接文件, 这里只是指符号链接文件。 # socket socket文件 # pipe pipe管道文件, 这里指的是匿名管道文件, 匿名管道在磁盘上你是看不到的。 ``` st_mode是一个16位的位图, 用于表示文件类型, 文件访问权限, 及特殊权限位。 3. umask 默认是: 0666 & ~umask umask命令作用: 防止产生权限过松的文件。 umask命令是通过umask函数支持的。 ```c mode_t umask(mode_t mask); ``` 4. 文件权限的更改和管理(chmod / fchmod) chmod命令: 改变文件的权限。 ```shell [lucas@VM-0-10-centos FileSystem]$ ll total 24 -rwxrwxr-x 1 lucas lucas 14392 May 9 16:34 a.out -rw-rw-r-- 1 lucas lucas 1000 May 9 16:01 FileSystemTest01.cpp -rw-rw-r-- 1 lucas lucas 1000 May 9 16:31 FileSystemTest02.cpp # 文件权限常用为 664 即, rw-rw-r-- 。 ``` ```c // 在进程中(在程序中)更改文件权限的函数。 int chmod(const char *pathname, mode_t mode); // 更改一个打开的文件的文件权限。 int fchmod(int fd, mode_t mode); ``` 5. 粘住位 t位 现在对t位的使用很少了, 最常见的就是对一个目录进行t位设计, 这个目录设计成t位后, 各个用户对这个目录的操作或者对目录下文件的操作就比较特殊化了。 6. 文件系统: FAT, UFS 文件系统: 文件或数据的存储格式问题。打个比方: 文件系统就是一个银行, 文件或数据就是金钱。不同的文件系统好比是不同国家的银行, 但是金钱资本是一样的, 只是对文件的存储实现的是不同的管理方式。 FAT文件系统是非常怕大文件的。 UFS是不怕大文件的。 目录项记录了一个inode对应的文件名称是什么。目录项是存放在目录文件中的。 7. 硬链接, 符号链接 硬链接, 是目录项的同义词, 和目录项的功能一样。 没有软链接这个概念。与硬链接对应的是符号链接。符号链接相当于windows平台下的快捷方式。 硬链接相当于两个指针指向了同一个inode。 ll命令下的 l 指的是 符号链接文件。 ```c int link(const char *oldpath, const char *newpath); // 可以用来创建一个临时文件。 int unlink(const char *pathname); // remove - remove a file or directory // 这个函数支撑 rm 命令 int remove(const char *pathname); // rename, renameat, renameat2 - change the name or location of a file // 这个函数支撑 mv 命令 int rename(const char *oldpath, const char *newpath); ``` 硬链接和目录项是同义词, 并且建立硬链接有限制, 不能给分区建立, 不能给目录建立。 符号链接的优点: 可以跨分区, 可以跨目录。 8. utime ```c // 这个函数不常用。 // 更改一个文件的时间, 更改一个文件atime和mtime(最后读的时间和最后写的时间) // 为什么要改, 一看就是要干坏事儿。 int utime(const char *filename, const struct utimbuf *times); ``` 9. 目录的创建和销毁 mkdir rmdir 这两个命令都有相应的系统调用。 ```c // 创建一个目录。 int mkdir(const char *pathname, mode_t mode); // 只能删除一个空目录。 int rmdir(const char *pathname); ``` 10. 切换目录(更改当前工作路径) cd命令 cd这个命令是通过chdir系统调用函数进行封装的。 ```c // chdir, fchdir - change working directory int chdir(const char *path); int fchdir(int fd); // 需要长时间跑的进程(如守护进程), 我们需要为它切换工作路径, 通常会找一个一直存在的路径, 或者不会被轻易卸载的路径, 比如说根目录root。 ``` ```c // 补充函数 // getcwd, getwd, get_current_dir_name - get current working directory(获取当前的工作路径) // 这个函数支撑了 pwd这个命令 char *getcwd(char *buf, size_t size); ``` 11. 分析目录/读取目录内容 分析目录有两种方法: ```c // 第一种: // glob函数, 进行目录的解析 // glob, globfree - find pathnames matching a pattern, free memory from glob() // glob函数作用: 帮我们分析一个pattern(模式、通配符)。 // 解析pattern的结果存放在参数pglob指向的内存。 // 使用glob解析一个pattern, 当然也可以使用glob解析一个目录。 // glob(): 解析模式/通配符 int glob(const char *pattern, int flags, int (*errfunc) (const char *epath, int eerrno), glob_t *pglob); void globfree(glob_t *pglob); ``` ```c // 第二种: // 这些函数将目录看成了文件, 目录流。 // 把当前的目录当做是一个目录流的操作来进行解析。这是不同于glob函数的另外一种解析目录的方法。 // opendir closedir readdir(在手册第3章) rewinddir seekdir telldir // 这些函数特别像文件IO操作那些函数。 ``` ### 设计 mydu 小程序 见 Test/FileSystem/FileSystemTest05.cpp文件。 ## 系统数据文件和信息 1. /etc/passwd ```c // 传名字或者userID, 这两个函数返回用户信息。 // getpwnam, getpwnam_r, getpwuid, getpwuid_r - get password file entry struct passwd *getpwnam(const char *name); struct passwd *getpwuid(uid_t uid); ``` ```c struct passwd { char *pw_name; /* username */ char *pw_passwd; /* user password */ uid_t pw_uid; /* user ID */ gid_t pw_gid; /* group ID */ char *pw_gecos; /* user information */ char *pw_dir; /* home directory */ char *pw_shell; /* shell program */ }; ``` 2. /etc/group ```c // getgrnam, getgrnam_r, getgrgid, getgrgid_r - get group file entry struct group *getgrnam(const char *name); struct group *getgrgid(gid_t gid); ``` ```c struct group { char *gr_name; /* group name */ char *gr_passwd; /* group password */ gid_t gr_gid; /* group ID */ char **gr_mem; /* NULL-terminated array of pointers to names of group members */ }; ``` 3. /etc/shadow 这个文件非常重要, 不要轻易进行修改!!!! ```c // get shadow password file entry struct spwd *getspnam(const char *name); // passphrase hashing char *crypt(const char *phrase, const char *setting); // get a password char *getpass(const char *prompt); ``` ```c struct spwd { char *sp_namp; /* Login name */ char *sp_pwdp; /* Encrypted password */ long sp_lstchg; /* Date of last change (measured in days since 1970-01-01 00:00:00 +0000 (UTC)) */ long sp_min; /* Min # of days between changes */ long sp_max; /* Max # of days between changes */ long sp_warn; /* # of days before password expires to warn user to change it */ long sp_inact; /* # of days after password expires until account is disabled */ long sp_expire; /* Date when account expires (measured in days since 1970-01-01 00:00:00 +0000 (UTC)) */ unsigned long sp_flag; /* Reserved */ }; ``` 4. 时间戳(Timestamp) atime / mtime / ctime 使用到的类型: time_t / struct tm / char* , 即字符串 ```c // time - get time in seconds // 从内核当中取时间戳。 time_t time(time_t *tloc); time_t mktime(struct tm *tm); ``` ```c // transform date and time to broken-down time or ASCII // 将日期和时间转换为细分时间或ASCII struct tm *gmtime(const time_t *timep); struct tm *localtime(const time_t *timep); struct tm { int tm_sec; /* Seconds (0-60) */ int tm_min; /* Minutes (0-59) */ int tm_hour; /* Hours (0-23) */ int tm_mday; /* Day of the month (1-31) */ int tm_mon; /* Month (0-11) */ int tm_year; /* Year - 1900 */ int tm_wday; /* Day of the week (0-6, Sunday = 0) */ int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */ int tm_isdst; /* Daylight saving time */ }; ``` ```c // strftime - format date and time size_t strftime(char *s, size_t max, const char *format, const struct tm *tm); ``` ## 进程环境 其实还是在讲单进程, 因为每个进程要搞清楚它里面是怎么样去组成的, 有哪些状态, 还有一些跟系统变量或者资源相关的内容, 为多进程的并发做铺垫。 1. main函数 ```c int main(int argc, char** argv) { return 0; } ``` 其实再早之前, main函数是有第三个参数的, 而那个第三个参数就是环境变量。因为它很好用, 后来 unix/linux 就把它做成了一个线程(现成)的机制。 2. 进程的终止 有正常终止, 也有异常终止 正常终止: 5 --- * 从main函数返回, 即 return 0; main函数是一个进程的入口。 return 0;是为当前进程的父进程看的。 如果在linux terminal上运行 ./a.out 的话, 它的父进程就是 shell, ./a.out的return 0;是给shell看的。 而且return 0;只是大家约定俗成的规律, 并不是说默认的情况。 shell中有一个命令是 ```shell # 打印上一条语句的执行状态 [lucas@VM-0-10-centos LinuxEnvironmentProgramming]$ echo $? 0 [lucas@VM-0-10-centos LinuxEnvironmentProgramming]$ ``` * 调用exit (例如: 在某一个位置, 当前进程出现问题, 开发者觉得认为没有必要让进程继续下去, 那就可以调用exit) ```c // exit - cause normal process termination // 使得进程正常终止 // exit能带回去的值只有256种: status & 0377(这是一个八进制数) -128~127。 // status & 0xFF void exit(int status); ``` ```c // atexit - register a function to be called at normal process termination // 注册一个在正常进程终止时被调用的函数 // 注册一个函数, 这个函数会在正常进程终止时被调用 // 钩子函数 // 在一个进程正常终止之前会调用钩子函数, 去释放所有该释放的内容。 // 它特别像C++当中的析构函数。 int atexit(void (*function)(void)); // atexit钩子函数的作用: // 执行exit之前会执行atexit钩子函数。 // 另外一个钩子函数on_exit和atexit一样也是钩子函数功能相同, 但是一般使用atexit钩子函数多。 ``` * 调用_exit或_Exit exit是库函数, _exit和_Exit是系统调用。exit是依赖于_exit和_Exit的。 调用_exit或者_Exit系统调用时不执行终止处理程序(即钩子函数)和标准IO清理程序的。而调用exit是要处理这些事情的。 但是_exit或_Exit系统调用也有使用的场合: 避免错误扩大。此时如果使用exit(1)的话, 执行终止处理程序和标准IO清理程序( 该清理的清理, 该同步的同步。如果刚好把错误的内容同步了, 那就错误扩大了。), 此时就什么都不敢做了, 那就调用_exit(1)。 如果不这么做的话, 还有一种方法: 调用信号 abort 中止。 * 最后一个线程从其启动例程返回 (进程可以当成一个容器来理解) * 最后一个线程调用了pthread_exit (这个函数相当于进程的exit, 指的是一个线程的终止) 异常终止: 3 --- * 调用abort * 接到一个信号并终止 (之前的死循环的程序, ctrl+c将它终止了, 这其实就是接受到了一个信号) * 最后一个线程对其取消请求做出响应 3. 命令行参数的分析 ```c // 几乎所有的shell命令行参数分析(命令行解析)都是这两个函数做的。 // 带参数的选项, 非选项的传参 // ls -l /home // 其中, -l 是选项, 而 /home是非选项的传参 int getopt(int argc, char * const argv[], const char *optstring); // getopt_long来分析长格式 // 例如: ls -a 对应的长格式是 ls -all , 这两个是一样的功能 int getopt_long(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex); ``` 4. 环境变量 环境变量的本质就是 KEY = VALUE , 其中VALUE是字符串(有的VALUE小, 有的VALUE是很长的字符串)。 如果查看环境变量, 使用 export ```shell [lucas@VM-0-10-centos LinuxEnvironmentProgramming]$ export ``` ```text 环境变量是什么? 举个例子: 将我们当前的操作系统想象成一个正在跑的程序的话, 那么可以认为环境变量就是这个程序所用到的全局变量。 ``` ```text linux的环境变量全部存储在environ这个字符指针数组中(和argv一样, char* []/char** )。 environ这个全局变量在头文件中, 截取的有关environ的代码片段: /* NULL-terminated array of "NAME=VALUE" environment variables. */ extern char **__environ; #ifdef __USE_GNU extern char **environ; #endif 其实这说明 environ的具体定义的地方还是没有在头文件中, 应该在其它地方, 具体在哪其实作为使用者我们是可以不关心的。 ``` 环境变量非常好用, 也非常常用。每一个C进程的虚拟空间里面都有。其实每个进程空间里面都有环境变量存在。 ```c // 关于environ环境变量的函数 // getenv, secure_getenv - get an environment variable char *getenv(const char *name); // setenv - change or add an environment variable // TODO 注意: 会将原来与argv存储在一起的环境变量拷贝到堆上, 在堆中存储添加或者修改的以及原来的环境变量。 int setenv(const char *name, const char *value, int overwrite); // 删除一个环境变量 int unsetenv(const char *name); // 这个putenv函数了解就可以了, 不好用 // putenv - change or add an environment variable // 因为参数string是char*, 而不是const char*。所以这个函数并不保证不会修改string的内容。所以尽量不要使用这个 // putenv来设置环境变量, 而是使用setenv来设置环境变量。 int putenv(char *string); ``` ```text 在写程序时有时会用到环境变量, 甚至不写程序, 在进行一个搭建的话, 有一些也跟环境变量有关: 比如, 我声明一个库所在的位置等等。 ``` 5. 一个C程序的存储空间布局/一个进程空间的分布情况 ```text 假设当前系统是32位环境, 那么它的虚拟空间是4G。64位的话是128G。 0~3G的用户态(user), 最高的1G是用来给内核态(kernel)来使用的。 3G: 0xc0000000 其实用户态用地址空间也不是从0开始用, 而是从0x08048000。而0~0x08048000是空出来的内存。 然后挨着0x08048000的内存空间是code段(代码段), 或者叫text段。 然后在code段的往上的内存段是已初始化的数据段, 再上面是未初始化的数据段(简称bss/BSS段)。然后再往上 的内存段是堆。然后再往上是一段空白区。 挨着3G的地址下面内存段有一个小薄层, 这个小薄层中放的是argv和environment。然后挨着这块小空间往下是栈。 然后堆和栈之间的内存块, 我们暂且叫它为阴影区。而且这块阴影区的内存段是最大的。阴影区是我们导入进来的一些 静态库或者动态库的内容。静态库导入进来占用阴影区的大小比较多, 所以我们一般使用的是动态库。 以上就是大概的一个进程空间的分布情况。 补充: shell命令 ps axf (查看当前的进程关系) pmap PID (查看一个进程的内存分布) ``` 6. 库 * 动态库 * 静态库 * 手工装载库(共享库) 内核当中的模块/各种服务都是以这种插件的这种形式表现出来的。(李慧琴老师) 其实就是插件。 ```c // 手工装载库(共享库)相关函数 // dlclose, dlopen, dlmopen - open and close a shared object void *dlopen(const char *filename, int flags); int dlclose(void *handle); char *dlerror(void); void *dlsym(void *handle, const char *symbol); ``` 7. 函数跳转 goto不能跨函数跳转。 补充函数 ```c // performing a nonlocal goto // 执行非本地goto // 这两个函数可以完成安全的跨函数跳转 // 设置跳转点 int setjmp(jmp_buf env); // 从某一个位置跳转到某个跳转点 void longjmp(jmp_buf env, int val); ``` 8. 资源的获取以及控制(ulimit) ```shell [lucas@VM-0-10-centos FileSystem]$ ulimit -a core file size (blocks, -c) unlimited data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 7192 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 100001 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 7192 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited [lucas@VM-0-10-centos FileSystem]$ ``` ```c // getrlimit, setrlimit, prlimit - get/set resource limits int getrlimit(int resource, struct rlimit *rlim); int setrlimit(int resource, const struct rlimit *rlim); ``` # IPC: inter process communication(进程间通信) ## 进程基础 ### PID(进程标识符) type: pid_t command: ps fd的使用策略: 优先使用当前可用范围内最小的那个。 PID是顺次向下使用。 ```c // getpid, getppid - get process identification // 获得当前的进程号 pid_t getpid(void); // 获得父进程的进程号 pid_t getppid(void); ``` ### 进程的产生 fork(); // vfork()这个函数已经弃用了。 vfork(); ```c // fork - create a child process // fork是典型的执行一次返回两次, fork的返回是在不同的两个进程中返回。 // 所以一般会在fork执行后写的是if语句。 // fork生成一个子进程, 这个子进程会和父进程一模一样, 而且连执行到的位置也是一样的。 // 父进程是通过复制自己的方式来产生一个子进程。连执行到的位置都是一样的。 // duplicate // // 但是也是有一些区别的! // 区别: fork之后父子进程的区别 // 1. fork的返回值不同 // 2. pid不同ppid也不同 // 3. 未决信号和文件锁不继承 // 4. 资源利用量归0 pid_t fork(void); ``` init进程是所有进程的祖先进程。init process PID: 1 fflush()的重要性。 ```c // 见工程 IPCTest01.cpp文件。 ``` 使用 ps axf 命令来查看父子进程的关系。 init进程有一个责任是负责接管孤儿进程, 为孤儿进程进行"收尸"。 vfork()是当时那个年代为了解决fork()成本的一种解决方案, 即与父进程公用一段内存。但是现在已经不用这个vfork函数了。 而是现在的fork()函数将CopyOnWrite技术设计到了fork()函数内, 其实就是将vfork()函数的优点设计到了fork()函数内。 如果创建出来的子进程没有std::exit(0)或者退出, 那么它也会创建子进程, 这样创建出来的子进程的数量是阶乘。所以, 子进程干完活马上让子进程正常退出。 ### 进程的消亡及释放资源 wait() waitpid() waitid() wait3() wait4() 其实子进程可以看成是父进程管理的资源。父进程有责任关照子进程。 ```c // wait, waitpid, waitid - wait for process to change state // wait是死等。wait一定是阻塞的。但是有一些情况下, 你就是要阻塞。 // wait相当于waitpid(-1, &wstatus, 0); pid_t wait(int *wstatus); // 这个函数就要比wait函数设计的好。 // 第一个参数指定了收谁的尸, 这是比wait函数进步的地方。 // 而且第三个参数: options可以将waitpid变为非阻塞的。 pid_t waitpid(pid_t pid, int *wstatus, int options); int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options); /* This is the glibc and POSIX interface; see NOTES for information on the raw system call. */ ``` #### 进程分配之交叉分配法实现 * 分块 这就造成了第一段的子进程负载比较大, 其它的2个子进程负载比较小。 * 交叉分配 在实际中如果能使用分块来进行并发, 或者交叉分配进行并发的话, 那么一般会使用交叉分配进行并发。 IPC目录下的`IPCTest05-prime-number.cpp`是使用了交叉分配。 * 池 比交叉分配更加随机。进程池, 线程池。子进程之间进行竞争, 更加具有随机性。 ### exec函数族 ```c // execl, execlp, execle, execv, execvp, execvpe - execute a file // 执行的是二进制的可执行程序。 // exec这一系列的函数 replace the current process image with a new process image. // 这个函数是给它一个可执行的二进制文件的路径。 // 注意参数arg是从类似argv[0]开始的, 所以第一个参数是可执行文件名。 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 wait exec组合使用 unix世界就是使用这3个函数搭建起来的。 * fork * wait * exec 例子: shell ```text 当我们从命令行 ./a.out 时, shell会fork一个shell, 然后这个子进程shell通过exec变成 ./a.out , 而父进程shell中再通过wait等待子进程的结束。 ``` ### 用户权限及组权限 普通用户可以调用`passwd`命令来更改自己的口令。 u+s、g+s是怎么做到的? uid和gid存的都不是一份。 * r: real * e: effective * s: safe 可以没有safe。 ##### 整个unix/linux世界就是使用`fork`、`exec`、`wait`这三个函数搭建起来的。 ```c // getuid, geteuid - get user identity // getuid() returns the real user ID of the calling process. uid_t getuid(void); // geteuid() returns the effective user ID of the calling process. uid_t geteuid(void); ``` ```c // getgid, getegid - get group identity // getgid() returns the real group ID of the calling process. gid_t getgid(void); // getegid() returns the effective group ID of the calling process. gid_t getegid(void); ``` ### 观摩课: 解释器文件 其实就是shell编程。脚本文件, 老师提到其实py会比shell更加的方便。 ### system(); ```c // system - execute a shell command // system函数相当于fork、exec、wait这三个函数的简单封装。 // system相当于调用shell来执行system参数的C风格字符串的shell命令。 int system(const char *command); ``` ### 进程会计 简单了解即可。 ### 进程时间 ```c // times - get process times // time命令是通过times()函数(系统调用函数)做出来的。 clock_t times(struct tms *buf); ``` ### 守护进程 Daemon 守护进程有时也叫精灵进程。守护进程一般是一个会话的leader, 还是一个group(group process/进程组)的leader。 ```text 会话(session) 终端(terminal), 现在真正意义上的终端几乎用不到。之后涉及到终端都是虚拟终端。 虚拟终端(virtual terminal): ctrl+alt+Fx拿到一个(虚拟)终端。 ``` #### session 一次成功的终端登录(一次成功的shell登录), 会产生一个会话session。一个shell就相当于一个session。这个会话中包含多个进程组, 一个进程组中包含多个进程。 一个进程中至少包含一个线程在跑。 前台进程组和后台进程组: * 最多有一个作为前台进程组, 可以没有前台进程组。可以将前台进程放到后台去运行。 * 前台进程组能够接受标准输入或者进行标准输出, 但是后台进程组不行。 * 守护进程脱离了的终端的控制(这句话是帮助理解) 会话的标识: sid。 ```c // setsid - creates a session and sets the process group ID // 创建一个会话并设置进程组ID // 可以通过setsid()函数来实现一个守护进程。 // setsid()往往不能是父进程来调用, 因为父进程往往是一个进程组的领导。 // 所以, setsid()函数往往是子进程调用。 // setsid() creates a new session if the calling process is not a process group leader. The calling process is the leader of the new ses‐ // sion (i.e., its session ID is made the same as its process ID). The calling process also becomes the process group leader of a new // process group in the session (i.e., its process group ID is made the same as its process ID). // // The calling process will be the only process in the new process group and in the new session. // // Initially, the new session has no controlling terminal. pid_t setsid(void); ``` 守护进程脱离控制终端, 所以 ```shell [lucas@VM-0-10-centos LinuxEnvironmentProgramming]$ ps axj PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 0 1 1 1 ? -1 Ss 0 2:10 /usr/lib/systemd/systemd --switched-root --system --deserialize 17 0 2 0 0 ? -1 S 0 0:00 [kthreadd] 2 3 0 0 ? -1 I< 0 0:00 [rcu_gp] 2 4 0 0 ? -1 I< 0 0:00 [rcu_par_gp] 2 6 0 0 ? -1 I< 0 0:00 [kworker/0:0H-kblockd] 2 8 0 0 ? -1 I< 0 0:00 [mm_percpu_wq] 2 9 0 0 ? -1 S 0 0:40 [ksoftirqd/0] 2 10 0 0 ? -1 R 0 0:32 [rcu_sched] 2 11 0 0 ? -1 S 0 0:00 [migration/0] 2 12 0 0 ? -1 S 0 0:00 [watchdog/0] 2 13 0 0 ? -1 S 0 0:00 [cpuhp/0] 2 15 0 0 ? -1 S 0 0:00 [kdevtmpfs] 2 16 0 0 ? -1 I< 0 0:00 [netns] 2 17 0 0 ? -1 S 0 0:02 [kauditd] 2 18 0 0 ? -1 S 0 0:00 [khungtaskd] 2 19 0 0 ? -1 S 0 0:00 [oom_reaper] 2 20 0 0 ? -1 I< 0 0:00 [writeback] 2 21 0 0 ? -1 S 0 0:00 [kcompactd0] 2 22 0 0 ? -1 SN 0 0:00 [ksmd] 2 23 0 0 ? -1 SN 0 0:02 [khugepaged] 2 24 0 0 ? -1 I< 0 0:00 [crypto] 2 25 0 0 ? -1 I< 0 0:00 [kintegrityd] 2 26 0 0 ? -1 I< 0 0:00 [kblockd] 2 27 0 0 ? -1 I< 0 0:00 [tpm_dev_wq] 2 28 0 0 ? -1 I< 0 0:00 [md] 2 29 0 0 ? -1 I< 0 0:00 [edac-poller] 2 30 0 0 ? -1 S 0 0:00 [watchdogd] 2 53 0 0 ? -1 S 0 0:00 [kswapd0] 2 146 0 0 ? -1 I< 0 0:00 [kthrotld] 2 147 0 0 ? -1 I< 0 0:00 [acpi_thermal_pm] 2 148 0 0 ? -1 S 0 0:00 [hwrng] 2 149 0 0 ? -1 I< 0 0:00 [kmpath_rdacd] 2 150 0 0 ? -1 I< 0 0:00 [kaluad] 2 151 0 0 ? -1 I< 0 0:00 [ipv6_addrconf] 2 152 0 0 ? -1 I< 0 0:00 [kstrp] 2 275 0 0 ? -1 I< 0 0:00 [iscsi_eh] 2 430 0 0 ? -1 I< 0 0:12 [kworker/0:1H-kblockd] 2 445 0 0 ? -1 I< 0 0:00 [ata_sff] 2 447 0 0 ? -1 S 0 0:00 [scsi_eh_0] 2 449 0 0 ? -1 I< 0 0:00 [scsi_tmf_0] 2 450 0 0 ? -1 S 0 0:00 [scsi_eh_1] 2 451 0 0 ? -1 I< 0 0:00 [scsi_tmf_1] 2 477 0 0 ? -1 S 0 0:22 [jbd2/vda1-8] 2 478 0 0 ? -1 I< 0 0:00 [ext4-rsv-conver] 1 579 579 579 ? -1 Ss 0 1:32 /usr/lib/systemd/systemd-journald 1 607 607 607 ? -1 Ss 0 0:01 /usr/lib/systemd/systemd-udevd 2 671 0 0 ? -1 I< 0 0:00 [nfit] 1 747 747 747 ? -1 S SysV key: ftok(); ```c // ftok - convert a pathname and a project identifier to a System V IPC key // 将路径名和项目标识符转换为 System V IPC 通信地点。 key_t ftok(const char *pathname, int proj_id); ``` XXXget、XXXop、XXXctl。 主动端: 先发包的一方。 被动端: 先收包的一方。(一定是被动端先运行起来) #### Message Queues(消息队列) msg #### Semaphore Arrays(信号量数组) sem #### Shared Memory Segments(共享内存段, 即共享内存) shm ### 网络套接字 socket 讨论: 跨主机的传输要注意的问题。 > 字节序问题: 大端存储, 小端存储 ```text 大端存储: 低地址处放高字节 小端存储: 低地址处放低字节 数据传输的时候永远是 低地址的数据先被传输, 高地址的数据后被传输。 主机字节序: host 网络字节序: network 解决字节序问题 -> _to__ : htons, htonl, ntohs, ntohl。 ``` > 对齐问题 ```text address % 按多少字节对齐(32bit machine一般是4个字节, 64bit machine一般是8个字节) struct StructName { int i; char ch; float f; }; sizeof(StructName) : 12 占12个字节而不是9个字节。 解决对齐问题 -> 不对齐(定义结构体的时候通过宏规定编译器不对齐) ``` > 类型长度问题 ```text int char 因为int, char, double等类型没有严格的规定是多少个字节。所以在不同的机器上所占 的字节大小很可能不一样。 解决类型长度问题 -> int32_t, uint32_t, int64_t, int8_t, uint8_t。 ``` #### socket ```c // socket - create an endpoint for communication int socket(int domain, int type, int protocol); ``` ##### 报式套接字(一对一, 点对点, 可以一对多) ```text 被动端(先运行, 一般为服务端) 1. 取得socket 2. 给socket取得地址 3. 收/发消息 4. 关闭socket 主动端(一般为用户(客户)端) 1. 取得socket 2. 给socket取得地址(可省略) 3. 发/收消息 4. 关闭socket ``` ```c // socket - create an endpoint for communication int socket(int domain, int type, int protocol); // bind - bind a name to a socket int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // send, sendto, sendmsg - send a message on a socket ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // recv, recvfrom, recvmsg - receive a message from a socket ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // inet_pton - convert IPv4 and IPv6 addresses from text to binary form int inet_pton(int af, const char *src, void *dst); // inet_ntop - convert IPv4 and IPv6 addresses from binary to text form const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); // getsockopt, setsockopt - get and set options on sockets int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen); int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); ``` ###### 多点通信: 广播(全网广播和子网广播), 多播/组播 ```text 为什么说多播使用起来比广播要灵活? 因为多播当中有一个特殊的地址 224.0.0.1。 这个地址表示所有支持多播的结点, 默认都存在在这个组当中并且无法离开。 所以, 如果你在sender方往224.0.0.1多播地址放消息, 就相当于往 255.255.255.255上发消息。 ``` ```text 什么是协议? 通信的双方来约定对话的格式。我发什么样的消息(以什么样的结构体发消息), 你用什么样的结构体来接受。 ``` ###### udp传输分析 udp中会有丢包现象发生。 ttl: time to live(生存时间), 一个包在网络上可以存活的时间(这个时间不是以秒为单位的, 而是以可以在网络中多少跳为单位的(能够跳转的路由的个数)。)。 在linux/unix中默认是128, 在windows中默认是64。 丢包现象不是因为ttl耗尽。因为不管是64还是128这么多跳完全足够将世界上的每个地方相连在一起。 丢包现象主要是由于阻塞造成的。那解决办法就是不阻塞, 那就要进行流控。之前学习的流控是开环流控。但是不适用于这里。这里使用的是停等式流控。 但是停等式流控并没有将丢包率降下来。但是它确保了双方通信一定接受到了数据报。 停等式流控 -> 窗口 -> 三次握手 -> 流式套接字。 半连接 -> 半连接池 -> 半连接洪水(半连接洪水攻击的就是半连接池)。 ##### 流式套接字(一对一, 点对点, 不可以一对多) ```text Server端(服务端)(被动端) 1. 获取socket 2. 给socket获取地址 3. 将socket置为监听模式 4. 接受连接 5. 收/发消息 6. 关闭 Client端(用户端)(主动端) 1. 获取socket 2. 给socket获取地址(可省略) 3. 发送连接 4. 发/收消息 5. 关闭 ``` ###### 静态进程池套接字实例 ###### 动态进程池套接字实例 # 流媒体广播项目 基于ipv4的流媒体广播项目。 一对多的广播, 比如: 学校内的广播, 商场内的广播, 出租车司机的听的广播等等。 Client端应该使用java/qt/android来写。这样的实现你可以拉一个界面出来。 Server端使用c/c++来写。 # 项目总结以及课程总结 ```text 项目并没有完整的做出来, 问题实在是太多了。所以我放弃了对 NetRadio 工程的后续完善。回望这几个月确实很难忘。 大致过完了一遍 APUE。确实有中说不出的感受。说起来也是非常的可笑, 突然一下子不知道我坚持的这个方向我是否真的 适合感兴趣。但是路却走到这里了。毫无疑问, 我更加倾向于c/c++的开发。将来我也会继续坚持下去的。确实没有一项 技术可以长期为整个计算机互联网人机交互愈发司空见惯的现在以及未来长盛不衰。也许某一天我不在进行c/c++开发了, 我希望 这些是我谈不上美好的回忆吧。 这段时间的学习其实也是断断续续, 因为中途进行了许多考试, 以及学习遇到问题后我实在不想学了。现在想想, 刚到信号时怎么也学不懂, 之后硬着头皮干脆先把慧芹老师的实例写了吧。很可惜我照着写也写不对。后来又遇到了一个大问题, 就是并发使用库的问题, pthread这个库 如何加入到工程中, 当时我也是在网上不断的找资料。因为是关于cmake的问题, 网上的有关内容不多。但是非常非常非常头疼。:( 后来终于可以 正确导入后又遇到一个问题, 就是CentOS配置CLion进行远程开发时, 配置yum时cmake以及gcc/g++/gdb等, CLion识别不了。当时不知道 是版本的问题。那叫一个难受, 后来直接安装的最新版的CentOS并且到阿里源配置yum。可以了。然后就想到问题出在哪了->CentOS版本与阿里源 版本不匹配问题。而且CLion也老是出一些奇奇怪怪问题。这些问题是我现在能回忆起来的。 最后也就是APUE到了尾声。这个收官项目很可惜我没有写出来。工程中完成了接近90%的内容。但是BUG太多了, 现阶段我一个人解决不掉, 我也确实累了, 这个工程放一放吧, 暂时不完善了。这个工程也有我自己挖的坑, 比如: 过早引入命名空间, 以及我使用的是c++实现而不是c, 所以很多强转在c++是被禁止的。 不过我也意识到了很多开发中的问题, 比如: extern对象/变量、宏。隐藏数据类型->c语言的工程上的编程风格, 以及c++可以在c构建程序时可以借鉴的地方, 头文件的导入问题, 不能乱导入因为有时是编译可以通过但是运行的时候出错。在工程的开发过程中我也试着使用google test编写测试单元。 也是看了一些jetbrains的公开课, 学到了关于测试单元等内容。 ``` 也许会有志同道合的朋友也在学习linux系统编程, 这里我来推荐视频: [https://www.bilibili.com/video/BV1yJ411S7r6?from=search&seid=11376942628282733805][linux系统编程(李慧芹老师)]。 以及笔记: [https://www.cnblogs.com/0xcafebabe/tag/APUE/][博客园一位作者整理] 也许是真的结束了APUE了。但是之后我还会再相遇的。谢谢李慧芹老师。感谢在c/c++开发道路上奉献的前辈们。 时间: 2021-8-11 晚 留攸于邑大