网站首页 编程语言 正文
一、UNIX基础知识
1.1 Linux 主要特性
Linux 是一个基于文件的操作系统
操作系统需要和硬件进行交互,对应 Linux 来说这些硬件都是文件,比如:操作系统会将 硬盘 , 鼠标 , 键盘 , 显示屏等抽象成一个设备文件来进行管理。
Linux 操作系统是一种自由软件,是免费的,并且公开源代码。
可以同时登陆多个用户,并且每个用户可以同时运行多个应用程序。
提供了友好的图形用户界面,操作简单, 易于快速上手。
支持多平台(这里指的是基于不同 CPU 架构的平台,比如国产 Linux 使用的龙芯等)
UNIX体系结构
内核:控制计算机运行资源,提供程序运行环境
系统调用:内核的接口
共用库函数:共用库函数构建在系统调用的接口上
shell:shell是一个特殊的应用程序,为运行其他的应用程序提供接口
1.2 Linux 内核
Linux 系统从应用角度来看,分为内核空间和用户空间两个部分。内核空间是 Linux 操作系统的主要部分,但是仅有内核的操作系统是不能完成用户任务的。丰富并且功能强大的应用程序包是一个操作系统成功的必要件。这个和武林秘籍一样,不仅得有招式还得有内功心法。
Linux 的内核主要由 5 个子系统组成:进程调度、内存管理、虚拟文件系统、网络接口、进程间通信。下面将依次讲解这 5 个子系统。
- 进程调度 SCHED
-
SCHED_OTHER:分时调度策略(默认),是用于针对普通进程的时间片轮转调度策略。
-
SCHED_FIFO:实时调度策略,是针对运行的实时性要求比较高、运行时间短的进程调度策略
-
SCHED_RR:实时调度策略,是针对实时性要求比较高、运行时间比较长的进程调度策略。
- 内存管理 MMU
-
内存管理是多个进程间的内存共享策略。在 Linux 中,内存管理主要说的是虚拟内存。
-
虚拟内存可以让进程拥有比实际物理内存更大的内存,可以是实际内存的很多倍。
-
每个进程的虚拟内存有不同的地址空间,多个进程的虚拟内存不会冲突。
- 虚拟文件系统 VFS
- 在 Linux 下支持多种文件系统,如 ext、ext2、minix、umsdos、msdos、vfat、ntfs、proc、smb、ncp、iso9660、sysv、hpfs、affs 等。目前 Linux 下最常用的文件格式是 ext2 和 ext3。
- 网络接口
- Linux 是在 Internet 飞速发展的时期成长起来的,所以 Linux 支持多种网络接口和协议。网络接口分为网络协议和驱动程序,网络协议是一种网络传输的通信标准,而网络驱动则是对硬件设备的驱动程序。Linux 支持的网络设备多种多样,几乎目前所有网络设备都有驱动程序。
- 进程间通信
- Linux 操作系统支持多进程,进程之间需要进行数据的交流才能完成控制、协同工作等功能,Linux 的进程间通信是从 UNIX 系统继承过来的。Linux 下的进程间的通信方式主要有管道、信号、消息队列、共享内存和套接字等方法。
1.3 Linux 目录结构
在 linux 中根目录的子目录结构相对是固定的 (名字固定), 不同的目录功能是也是固定的
bin: binary, 二进制文件目录,存储了可执行程序,今天要将的命令对应的可执行程序都在这个目录中
sbin: super binary, root 用户使用的一些二进制可执行程序
etc: 配置文件目录,系统的或者用户自己安装的应用程序的配置文件都存储在这个目录中
lib: library, 存储了一些动态库和静态库,给系统或者安装的软件使用
media: 挂载目录,挂载外部设备,比如:光驱,扫描仪
mnt: 临时挂载目录,比如我们可以将 U 盘临时挂载到这个目录下
proc: 内存使用的一个映射目录,给操作系统使用的
tmp: 临时目录,存放临时数据,重启电脑数据就被自动删除了
boot: 存储了开机相关的设置
home: 存储了普通用户的家目录,家目录名和用户名相同
root: root 用户的家目录
dev: device , 设备目录,Linux 中一切皆文件,所有的硬件会抽象成文件存储起来,比如:键盘, 鼠标
lost+found: 一般时候是空的,电脑异常关闭 / 崩溃时用来存储这些无家可归的文件,用于用户系统恢复
opt: 第三方软件的安装目录
var: 存储了系统使用的一些经常会发生变化的文件, 比如:日志文件
usr: unix system resource, 系统的资源目录
/usr/bin: 可执行的二进制应用程序
/usr/games: 游戏目录
/usr/include: 包含的标准头文件目录
/usr/local: 和 opt 目录作用相同,安装第三方软件
环境变量表:PATH=/bin:/usr/bin:/usr/local/bin:. PATH 变量包含了一张目录表(称为路径前缀),目录之间用冒号(:)分隔。
1.4 登录
1 登录名
登录名、加密口令、数字用户ID、数字组、注释字段、起始目录、shell程序
dyc:x:205:105:trdycdyc:/home/dyc:/bin/dsh
2.shell
分为两种: 1、和用户交互的交互式shell。 2、 文件类型的shell脚本
1.5 输入和输出
1. 文件描述符
文件描述符(fledescriptor)通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符。在读、写文件时,可以使用这个文件描述符。
2. 标准输入、标准输出、标准错误
每当运行一个新程序时,所有的shell都为其打开3个文件描述符,标准输入、输出、错误,默认情况下这三个文件描述符都会链接向终端,shell也提供了一种方法(重定向符”>“),是这三个文件描述符重新定向到某个文件,当重定向的文件不存在时,shell会创建它。
编写代码可实现文件的复制
./a.out < infile > outfile
3. 不带缓冲的IO
函数open、read、write、lseek以及close提供了不带缓冲的IO
4. 标准I/O
标准IO为那些不带缓冲的IO提供了一个带缓冲的接口,这样就无需选择最佳的缓冲区大小
1.6 程序和进程
1. 程序
程序是一个存储在磁盘上某个目录中的可执行文件,内核使用exec函数将程序读入内存,并执行
2. 进程和进程ID
程序的执行实例叫进程,进程ID唯一标识了每个进程
3. 进程控制
主要用到三个函数:fork、exec、waitpid
4. 线程和线程ID
一个进程内的所有线程共享同一地址空间、文件描述符、栈、以及与进程相关的属性
1.7 出错处理
在<errno.h>中定义了errno以及可以赋予它的各种常量,这些常量都是以字符E开头的
errno并不是简单的一个数据结构,也不是一个int类型的变量,而是一个宏,下面是其实现
extern int *__errno_location(void);
#define errno (*__errno_location())
c标准提供了两个函数,用于打印出错消息
- strerror函数将errnum(即errno)的值映射为一个出错的消息字符串,并返回该字符串的指针
#include<string.h>
char *strerror(int errnum);
perror函数基于errno当前值,在标准错误上产生一条出错消息字符串,并返回此字符串的指针
#include<stdio.h>
void perror(const char *msg)
**注意:**只有当一个库函数失败时,errno才会被设置。当函数成功运行时,errno的值不会被修改。这意味着我们不能通过测试errno的值来判断是否有错误存在。反之,只有当被调用的函数提示有错误发生时检查errno的值才有意义。
1.8 用户标识
用户ID、组ID、附属组ID
1.9 信号
信号用于通知进程发生了某种情况,常见的信号处理方式有以下三种
- 忽略信号
- 按系统默认方式处理
- 提供一个函数。即信号发生时,调用该函数,这个过程也叫做捕捉该信号
终端键盘提供了产生两种信号的方法:中断键(ctrl + c)和退出键(ctrl + \)
另一种时调用kill产生信号,例如在一个进程中调用此函数就可以向另一个进程发送信号
1.10 时间值
历史上,共有两种时间:
- 日历时间 :保存在time_t中
- 进程时间 :保存在clock_t中
度量一个进程的执行时间,用三个时间值:
- 时钟时间 进程运行的时间总量
- 用户CPU时间 执行用户指令的时间
- 系统CPU时间 执行系统调用的时间
1.11 系统调用和库函数
二、UNIX标准及实现
2.1 ISO C
#include<assert.h> //验证程序断言
#include<errno.h> //出错码
#include<stdio.h> //标准I/O库
#include<stdlib.h> //使用函数
#include<string.h> //字符串操作
#include<time.h> //时间和日期
#include<wchar.h> //宽字符类型
#include<dirent.h> //目录项
#include<fcntl.h> //文件控制
#include<netdb.h> //网络数据库操作
#include<pthread.h> //线程
#include<unistd.h> //符号常量
#include<arpa/inet.h> //因特网定义
#include<net/if.h> //套接字本地接口
#include<sys/select.h> //select函数
#include<sys/socket.h> //套接字接口
#include<sys/stat.h> //文件状态
#include<sys/types.h> //基本系统数据类型
#include<sys/un.h> //UNIX域套接字定义
2.2 函数sysconf、pathconf、和fpathconf
#include<unistd.h>
long sysconf(int name);
long pathconf(const char *pathname, int name);
long fpathconf(int fd, int name);
这些函数用来修改一些系统配置,例如进程最大打开文件数、进程最大信号量等
2.3 ISO C和IEEE POSIX
POSIX是一个最初由IEEE制定的标准族,指的是可以指操作系统接口(Portable Operating System Interface),该标准以UNIX为基础,但是并不限于UNIX类系统。
三、文件I/O
3.1 文件描述符
对于内核而言,所有打开的文件都用文件描述符引用。文件描述符是一个非负整数。
按照习惯,UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与进程的标输出关联,文件描述符2与进程的标准错误关联,在哟应用程序中,我们应该将他们替换为STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO以提高程序可读性。这些常量定义在<unistd.h>中,文件描述符,最多打开63个。
3.2 函数open和openat
#include<fcntl.h>
int open(const char *path, int oflag, ...)
int openat(int fd, const char *path, int oflag,...)
两个函数的返回值:若成功,返回文件描述符;若出错,返回-1
- path参数是要打开或创建文件的名字。
- oflag参数可用来说明此函数的多个选项,该参数包括:
- O_RDONLY 只读打开
- O_WRONLY 只写打开
- O_RDWR 读、写打开
- O_EXEC 只执行打开
- O_SEARCH 只搜索打开(应用于目录)
- O_APPEND 写时追加到文件的尾端
- O_CREAT 文件不存在时创建它,使用此选项还需要指定第三个参数mode,来指定其权限。
- O_TRUNC 如果此文件存在,而且为只写或读-写成功打开,则其长度截断为0。
- O_NOFOLLOW 若path引用的是一个符号链接,则出错
- 如果path指定绝对路径,则两个函数完全相同。
由open和openat函数返回的文件描述符一定是最小的未用的描述符,
3.3 函数creat
#include<fcntl.h>
int creat(const car *path, mode_t mode)
此函数可以被open函数完全替代,这里不作介绍。
3.4 函数close
#include<ubnnistd.h>
int close(int fd);
当一个进程终止时,内核自动关闭它所有打开的文件,关闭文件时还会释放该进程加上去的所有记录锁。
3.5 函数lseek
每打开文件,内核都会维护一个与其相关联的“当前文件偏移量”,通常读、写操作都是从当前文件偏移量处开始,并使偏移量增加所读写的字节数。可以调用lseek显示地为一个文件设置偏移量。
#include<unistd.h>
off_t lseek(int fd, off_t offset, int whence);
//返回值:若成功,返回新的文件偏移量,若出错,返回-1
参数whence:
- 若whence是SEEK_SET,则将该文件的偏移量设置为据文件开始处offset个字节。
- 若whence是SEEK_CUR,则将该文件的偏移量设置为当前值加offset个字节,offset可正可负。
- 若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset个字节。
通常文件的当前偏移量应该是一个非负整数,但是某些设备也允许福德偏移量,因此,在比较lseek的返回值时应该谨慎,不要测试它是否小于0,而要测试是否等于-1。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的,在文件中但没有被写过的字节都被读为0. 空洞并不要求在磁盘上占用存储区。
3.6 函数read/write
#include<unistd.h>
/*不带缓冲的IO*/
ssoze_t read(int fd, void *buf, size_t nbytes);
ssize_t write(int fd, const void *buf, size_t nbytes);
//返回值:读到的字节数,若以到文件尾,返回0;若出错,返回-1
3.7 I/O的效率
系统CPU时间的几个最小值差不多出现在BUFFSIZE为4096以后,继续增加缓冲区长度对长度几乎没有效应影响。
3.8 文件共享
内核使用三种数据结构表示打开的文件,
- 每个进程在进程表中都有一个记录项,记录项中包含一张打开的文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
a. 文件描述符标志(close_on_exec)
b.指向一个文件表项的指针。
- 内核为所有打开的文件维持一张文件表,每个文件表包含:
a. 文件状态标志(读、写、添加、同步和非阻塞等)
b.当前文件偏移量;
c.指向该文件v结点表项的指针
-
每个打开文件都有一个v节点结构。v节点包含了文件类型和对文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点。
-
对于多个打开的文件
-
在每次完成write操作后,当前表项的中当前文件的文件偏移量即增加的字节数,如果超出了当前文件长度,则i节点中的当前文件长度也会同步更新。
-
lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作
-
3.9 原子操作
当多个进程同时写一个文件时,逻辑操作“先定位到文件尾端,然后写”的逻辑操作会使后一个写的覆盖掉前一个写的内容,解决问题的方法就是将定位到文件尾端+写操作
对于其他进程来说为一个原子操作。在打开文件时设置O_APPEND标志,在每次写时,内核都会先将当前文件偏移量设置到该文件的尾端,这样就不用调用lseek函数了。
- 函数pread和pwrite
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
返回值:读到的字节数,若已到文件尾,返回0;若出错,返回-1
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
返回值:若成功,返回已写的字节数;若出错,返回-1
针对这两个函数,需要注意的是:
- 调用pread时,无法中断其定位和读操作
- 不更新当前文件偏移量。(read/write函数都会更改当前文件偏移量)
- pwrite也有类似的区别
3.10 函数dup和dup2
#include<unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
//两个函数的返回值:若成功,返回新的文件描述符;若出错,返回-1
dup和dup2函数都用来复制一个指定的文件描述符,dup2函数可以指定返回的文件描述符fd2,如果fd2已经打开,则要想将其关闭。复制文件描述符的另一种方法是使用fcntl函数,这个将在后面介绍。
3.11 函数sync、fsync和fdatasync
传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们像文件写入数据时,通常都先写入高速缓存(为了效率),然后再排队,晚些再写入磁盘。这种方式称为延迟写。
内核需要重用缓存区时,他会把所有延迟写 数据块写入磁盘。为了保证磁盘实际数据和缓存区的内容一致性,UNIX系统提供了sync、fsync和fdatasync三个函数
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
返回值:若成功,返回0;若失败,返回-1
void sync(void)
sync只是将所有修改过的块缓冲区排入写队列,然后就返回,并不等待实际写磁盘结束。
通常,称为update的守护进程周期性的调用sync函数,这就保证了可以定时冲洗内核的块缓冲区。
fsync函数只对文件描述符fd指定的一个文件起作用,并等待写磁盘结束才返回,比如数据库操作需要调用此函数。
3.12 函数fcntl
fcntl函数可以改变已经打开文件的属性
#include<fcntl.h>
int fcntl(int fd, int cmd, .../*int arg*/);
//返回值:若成功,则依赖于cmd;若出错,返回-1
fcntl的返回值与命令有关。如果出错,所有命令有返回-1,成功则返回值其他值。
3.13 函数ioctl
ioctl函数是I/O操作的杂物箱。
#include<unistd.h>
#include<sys/ioctl.h>
int ioctl(int fd, int request,...);
//返回值:若出错,返回-1;若成功,返回其他值
3.14 /dev/fd
系统都会提供/dev/fd的目录,打开文件dev/fd/n等效于复制文件描述符n
例如fd = open("dev/fd/0"
,等效于fd = dup(0)
;值得注意的是/dev/fd在linux中是指向底层物理文件的,操作不当可能会造成底层截断。
例如,命令filter file2 | cat file1 - file3 | lpr
与filter file2 | cat file1 /deb/fd/0 file3 | lpr
相比,缺少了文件名参数的一致性。
该命令的解释:cat读file1,接着读其标准输入(也就是filter file2命令的输出),然后读file3文件
四、文件和目录
4.1 函数stat、fstat、fstatat和lstat
#include<sys/stat.h>
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, const char *restrict pathname, struct sat *restrict buf, int flag);
4.2 文件类型
文件类型包括以下几种:
- 普通文件
- 目录文件
- 块特殊文件
- 字符特殊文件
- FIFO
- 套接字
- 符号链接
4.3 设置用户ID和设置组ID
4.4 新文件和目录的所有权
新文件的用户ID设置为进程的有效用户ID。关于组ID,用户可以选择:1. 新文件的组ID可以是进程的有效组ID.2.新文件的组ID可以是他所在的目录的组ID。
4.7 函数access和faccessat
#include<unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
//两个函数返回值:若成功,返回0;若出错,返回-1
4.8 函数umask
#include<sys/stat.h>
mode_t umask(mode_t cmask);
//返回值:之前的文件模式创建屏蔽字
参数cmask是由图4-6中列出的9个常量中的若干位按照“或”构成的
在进程创建一个新文件或新目录时,就一定会使用文件模式创建屏蔽字,
下面程序创建两个文件,创建第一个时,umask为0.创建第二个时,umask值禁止所有组合和其他用户的访问权限。
#include<unistd.h>
#include<stdio.h>
#include<fcntl.h>
#define RWRWRW (S_IRUSR|S_IWUSR|S_IWGRP|S_IRGRP|S_IROTH|S_IWOTH)
int main(void)
{
umask(0);
if(creat("foo",RWRWRW) < 0)
err_sys("creat error for foo");
uamsk(S_IWGRP|S_IRGRP|S_IROTH|S_IWOTH);
if(creat("bar",RWRWRW) < 0)
err_sys("creat error for bar");
exit(0);
}
4.9 函数chmod、fchmod和fchmodat
这三个函数是我们可以更改现有文件的访问权限
#include<sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
// 成功返回0;出错,返回-1
chmod函数在指定文件上进行操作,而fchmod函数则是对已经打开的文件进行操作。
4.10 函数chown、fchown、fchownat和lchown
下面这几个chown函数可用于更改文件的用户ID和组ID。
#include <unistd.h>
int chown (const char *pathname, uid_t owner, gid_t group) ;
int fchown(int fd, uid_t owner, gid_t group) ;
int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag) ;
int lchown (const char *pathname, uid_t owner, gid_t group) ;
//4个函数的返回值:成功,返回0;失败,返回-1
4.11 文件长度
stat结构成员st_size表示亿字节为单位的文件长度。此字段只对普通文件、目录文件和符号链接有意义。
- 对于普通文件,得到的是文件的实际长度
- 对于目录文件,得到的是一个数(16或者512)的整数倍.
- 对于符号链接,文件长度是文件名中的实际字节数
当文件中存在空洞时,实际长度可能和ls -l
所得到的不相同,这是正常现象,当复制一个文件时,所有的空洞都会按实际字节被填充为0
4.12 文件截断
有时我们需要在文件尾端出截取一些数据以缩短数据。将一个文件的长度截断为0是一个特例,再打开文件使用O_TRUNC标志可以做到这一点,为了截断文件可以调用函数truncate和ftruncate。
#include<unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
//两个函数返回值:若成功,返回0;若出错,返回-1
4.13 文件系统
磁盘、分区和文件系统
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uNmKp1dS-1668346492367)(D:\file\课程\md\image-20221106172800189.png)]
如果更仔细的观察一个柱面组的i节点和数据块部分,则可以看出如下图所示情况。
- 在图中有两个目录项指向同一个i节点。每个i节点都有一个链接计数,其值时指向该i节点的目录项数。只有当链接计数减至0时,才可以删除文件。这也是为什么删除一个目录项的函数称之为unlink的而不是delete的原因,该链接称为硬链接。
- 另外一种链接类型称为符号链接。任何一个叶目录的链接计数总是2,数值2来源于命名该目录的目录项
..
以及在该目录中的.
项。如果文件有一个子目录,则数值应该为3,另一个为其子目录中的..
。注意,每个子目录又会使父目录的引用计数增加1。
4.14 函数link、linkat、unlink、unlinkat和remove
#include<unistd.h>
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
//两个函数的返回值:若成功,返回0;若出错,返回-1
这两个函数都用来创建一个新目录想newpath,他引用现有文件existingpath。
为了删除一个现有的目录项,可以调用unlink函数
#include<unistd.h>
int link(const char *pathname);
int linkat(int efd, const char *pathname, int flag);
//两个函数的返回值:若成功,返回0;若出错,返回-1
unlink的这种特性经常被程序用来确保即使在程序崩溃时,她所创建的临时文件也不会遗留下来。进程用open或creat创建一个文件,然后立即调用unlink,引文该文件仍然是打开的,所以不会将其删除,只有当进程终止时(内核会关闭打开的所有文件描述符),该文件才会删除。
#include<stdio.h>
int remove(const char *pathname);
//返回值:若成功,返回0;出错,返回-1
4.15 函数rename和renameat
文件或者目录可以用rename函数或者renameat函数重命名
#include<stdio.h>
int rename(const char *pathname);
int renameat(int oldfd, const char *oldname, int newflag,const char *newname);
//两个函数的返回值:若成功,返回0;若出错,返回-1
4.16 符号链接
4.17 创建和读取符号链接
4.18 文件时间
4.19 函数futimens、utimensat和utimes
4.20 函数mkdir、mkdirat和rmdir
用mkdir和mkdirat函数创建目录,用rmdir函数删除目录
#include<sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
// 两个函数的返回值:若成功,返回0;若出错,返回-1
两个函数创建一个新的空目录。其中.
和 ..
自动创建。访问权限由mode指定,对于牡蛎,通常至少要设置一个执行权限位,以允许访问该目录中的文件。
#include<unistd.h>
int rmdir(const char *pathname);
//函数的返回值:若成功,返回0;若出错,返回-1
4.21 读目录
4.22 函数chdir、fchdir、getcwd
4.23 设备特殊文件
st_dev和st__rdev这两个字段经常引起混淆。
五、标准I/O库
5.1 流和FILE对象
在第三章,所有I/O函数都围绕文件描述符的,而对于标准I/O库,他们操作都是围绕流进行的,当标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。
我们称指向FILE对象的指针(FILE *)为文件指针。
这里有必要科普一下流和文件描述符的区别:
-
任何操作系统,在程序访问(读写)时,都需要建立程序与文件之间的通道,这一过程称之为打开文件。UNIX提供了两种机制,分别为:(1)文件描述符。(2)流。
-
两者的相同点:
- 都是作为程序与文件之间的通道。
- 都包含了一大类的I/O库函数
-
两者不同点:
- 文件描述符使用int类型的变量来表示打开的文件,而流使用FILE*文件指针来进行标识
- 如果需要对特定设备进行控制操作,必须使用文件描述符。
- 如果需要使用特殊的方式进行I/O操作(例如非阻塞等),必须使用文件描述符的方式。
- 在执行实际的输入输出时,流提供操作接口更加灵活、强大。
- 文件描述符只提供简答的穿过送字符块的函数,而流函数提供格式化I/O,字符I/O,面向行的I/O的大量函数。
- 流函数更利于程序的移植,任何基于ANSI C的系统都支持流。
-
两者的关系:
流为用户提供了一些更高一级的I/O接口,它处在文件描述符的上层,也就是说流是通过文件描述符来实现的。
5.2 标准输入、标准输出和标准错误
对一个进程预定义了3个流,并且这3个流可以自动被进程使用。这3个标准I/O流通过预定义文件指针stdin、stdout和stderr加以引用,这三个文件指针定义在头文件<stdio.h>中。
5.3 缓冲
标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数。他也对每个I/O流自动地进行缓冲管理,从而避免不必要的麻烦。遗憾的是,标准I/O库最令人迷惑的也是他的缓冲。
标准I/O提供了以下3种类型的缓冲。
- 全缓冲。在填满标准I/O缓冲区后才进行实际I/O操作。对于驻留在磁盘上的文件通常是由I/O库实施全缓冲的。术语冲洗说明标准I/O缓冲区的写操作,缓冲区也可由标准i/o例程子自动冲洗,或者可以调用函数fflush冲洗一个流。
- 行缓冲。这种情况下,当输入和输出中遇到换行符时,才会执行I/O操作。这允许我们一次输入一个字符(啊函数fputc),但只有在写了一行之后,才会进行实际的I/O操作。
- 不带缓冲。标准I/O库不对字符进行缓冲存储。
5.4 打开流
#include<stdio.h>
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);
FILE *fdopen(in//三个函数的返回值:若成功,则返回0;若出错,则返回-1
调用fclose函数关闭流
#include<stdio.h>
int fclose(FILE *fp);
//返回值:若成功,则返回0;若出错,则返回-1
再关闭文件之前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据则被丢弃。如果标准I/O库以为该流自动分配了一个缓冲区,则释放此缓冲区。当一个程序正常终止时,所有的流都会被正确关闭。
5.5 读和写流
- 输入函数
#include<stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
//若成功,返回c;若出错,返回EOF
- 输出函数
#include<stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
//若成功,返回c;若出错,返回EOF
5.6 每次一行I/O
#include<stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
//若成功,返回buf;若已到达文件尾端或者出错,返回NULL
gets函数从标准输入读,而fgets函数从指定的流读。gets有些不安全,是不建议使用的
#include<stdio.h>
int *fputs(char *restrict str, FILE *restrict fp);
int *puts(char *str);
//若成功,返回buf;若已到达文件尾端或者出错,返回NULL
5.7 二进制I/O
5.6和5.7节中的函数,因为各自的特点均不适合读写二进制I/O函数,因此特地提供以下两个函数进行二进制I/O
#include<stdio.h>
size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
//返回值:读写的对象数
此函数常见的两种用法。
- 读或者写一个二进制数组。下面例程将数组中2~5个元素写至一文件中。
float data[10];
if(fwrite(&data[2], sizeof(float), 4, fp) != 4)
perror("fwrite error");
- 读写一个结构。
struct {
short count;
long total;
char name[NAMESIZE];
}item;
if(fwrite(&item, sizeof(item), 1, fp) != 1)
perror("fwrite error");
使用二进制I/O的基本问题是,它只能用于都在同一系统上已写的数据。具体表现为,在一个系统上写的数据,在另一个系统上进行处理,此时会出问题,不能正常工作,其原因包括:
- 在一个结构中,同一个成员的偏移量可以随编译程序和系统的不同而不同(由于不同的对要求)
- 用来存储多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同。
5.8 定位流
5.9 格式化I/O
- 格式化输出
格式化输出由5个printf函数来处理的。
#include<stdio.h>
int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fd, const char *restrict format, ...);
int dprintf(int fd, const char *restrict format, ...);
//3个函数的返回值:成功,返回输出字符数,若输出错误,返回负值
int sprintf(char *restrict buf, const char *restrict format, ...);
//返回值:成功,返回存入数组的字符数,若编码错误,返回负值
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);
//返回值:若缓冲区足够大,返回将要存入数组的字符数,若编码错误,返回负值
printf函数将格式化数据写到标准输出,fprintf写至指定的流,dprintf写至指定的文件描述符,sprintf将格式化的数据写入数组,同时在该数组的尾端自动加一个null字节,但该字符不包括到返回值中。snprintf函数显式的提供了缓冲区的长度,以防止缓冲区溢出的隐患,但是此函数不会再数组最后加NULL字节。
格式说明控制其余参数如何编写,以后如何显示,转换说明是以%
开始的。一个转换说明有4个可选择的部分。具体说明和示例见来链接:https://blog.csdn.net/weixin_44567318/article/details/115441167。
- 格式化输入
#include<stdio.h>
int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(const char *restrict buf, const char *restrict format, ...);
5.10 实现细节
#include<stdio.h>
int fileno(FILE *fp);
//返回值:与文件相关联的文件描述符
5.11 临时文件
5.12 内存流
5.13 标准I/O的替代软件
六、系统数据文件和信息
6.1 口令文件
6.2 阴影文件
6.3 组文件
6.4 附属组ID
6.5 实现区别
6.6 其他数据文件
6.7 登陆账户记录
6.8 系统标识
6.9 时间和日期例程
有UNIX提供的基本时间服务是自1970年00:00:00这一特定时间以来提供的秒数。
time函数返回当前的时间和日期
#include<time.h>
time_t time(time_t *calptr);
//返回值:成功,返回时间数,若参数列表非空,返回值也会保存到参数列表的指针中;失败,返回-1
两个函数localtime和gmtime将日历时间转换为分解后的时间,并将其存放在一个tm的时间结构中。
struct tm{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year
/**还包括夏令时间**/
...
};
#include<time.h>
struct tm *gmtime(const time_t *calptr);
struct tm *localtime(const time_t *calptr);
七、进程环境
7.1 main函数
C程序总是从main函数开始的。main函数的原型是:
int main(int argc, char *argv[]);
argc 是命令行的参数,argv是指向参数的哥哥指针多构成的数组。
当内核执行一个C程序时(使用exec函数),调用main前先调用一个特殊的启动例程。可执行文件将此启动例程指定为程序的起始地址。启动例程从内核取得命令行参数和环境变量值,然后为按照上述方式调用main函数做好安排。
7.2 进程终止
有8种方式是进程终止,其中5种为正常终止,他们是:
- 从main返回、调用exit、调用 _exit 或 _Exit、最后一个线程从启动例程中返回、从最后一个线程调用pthread_exit。
异常终止方式包括三种:
- 调用abort、接到一个信号、最后一个线程对取消请求做出响应。
启动例程是这样编写的:使得从main函数返回后立即调用exit函数。该例程通常使用汇编语言编写的
-
退出函数
3个函数用于正常终止一个程序:_exit和 _Exit立即进入内核,exit则先执行一些清理处理,然后返回内核。
#include<stdlib.h> void exit(int status); void _Exit(int status); #include<uistd.h> void _exit(int status);
exit函数总是会先执行一个标准I/O库的清理关闭操作。
3个退出函数都会带一个整形参数,称为终止状态。大多数UNIX系统都提供了检查进程终止状态的方法。
-
函数atexit
按照ISO C一个程序最多可以登记32个函数,这些函数有exit自动调用,我们称这些函数为终止处理函数。并调用atexit来登记这些函数。
#include<stdlib.h>
int atexit(void (*func)(void));
//返回值:成功,返回0,若出错,返回-1
其中,atexit函数的参数是一个函数地址,exit函数调用这些函数的顺序预登记时顺序相反,同意函数如果登记多次,那么也会调用多次。
根据ISO C和POSIX.1,exit首先谁调用各种终止处理程序,然后关闭所有打开流,此外若程序调用exec函数族,则将清除所有已安装的终止处理函数。
注意,内核使程序执行的唯一方法是调用exec函数。进程终止的唯一方法是显式或隐式的调用_exit或 _Exi。进程也可以通过信号的方式非自愿终止。
7.3 命令行参数
当执行一个程序时,调用exec的进程可将命令行参数传递给新程序。
ISO C和POSIC.1 都要求argv[argc]是一个空指针。这就使得我们可以将参数处理循环写为:
for(i = 0; argv[i] != NULL; i++)
7.4 环境表
每个程序都接收到一张环境表,环境表也是一个字符指针数组,全局变量environ则包含了该指针数组的地址:extern char **environ;
每个指针包含了一个以null结尾的C字符串的地址。
通常用getenv和putenv来访问指定的环境变量。
7.5 C程序的存储空间布局
C程序一直由以下几部分组成:
- 正文段。这是由CPU执行的机器指令部分。通常正文段是可共享的,所以即使是频繁执行的程序(如文本编辑器、C编译器和shell等)在存储其中也只需要一个副本,另外正文段常常是只读。
- 初始化数据段。通常将此段称为数据段(data段),他包含了程序中需明确地赋初值的变量。
- 未初始化数据段。通常将此段称为bss段,在程序开始 执行前,内核将此段中的数据初始化为0或者空指针。
- 栈。自动变量以及每次函数调用时所保存的信息都存放在此段中。
- 堆。通常在堆中进行动态存储分配。
- 命令行参数和环境变量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3oRsQuIS-1668346492369)(D:\file\课程\md\3.png)]
size命令报告了正文段、数据段和bss段的长度(以字节为短)。
这里需要补充几点:
- 程序是由进程使用exec函数执行的,而进程号是由内核指定的。C程序在编译过后,就会将正文段(txt段)和初始化的数据data段的数据写入到可执行文件中,有进程加载磁盘来读取这些数据。
- bss和data段的区别:
- bss段存放全局变量和静态变量(声明但未初始化),具体表现为一个占位符;data段存放:全局变量、静态变量(声明且初始化)、常量。
- bss段不占用磁盘的存储空间,其内容由操作系统初始化(清零)。而data段则占用存储空间。
- bss段和data段在c语言程序内存分区模型中对应为全局/静态变量区,事实上常量也存放在data区。
- 进程中堆栈的大小:linux中栈区默认为10M,而在Windows中默认为1M,默认一个线程的栈大小为1M;linux中堆区默认大小为3G(假设内存共4G),而在windows中为2G,高位空间留给内核。
- 如果函数中存在未释放的内存,则可能会使得进程分配的堆区空间不断增大,最终导致过度得换页开销,造成性能下降甚至程序崩溃。
7.6 共享库
共享库使得可执行文件不再需要包含公共的库函数,而只需要在所有进程都可引用的存储区中保持这种库例程的一个副本。程序第一次执行或者调用某个库函数时,用动态链接方法将程序与共享 库函数相链接。这减少每个可执行文件的长度,但增加了一些运行时间开销。例如编译如下程序:
$ gcc -static hello1.c 阻止gcc使用共享库
a.out -> 979443
$ gcc hello1.c gcc使用共享库
a.out -> 8378
可以看出,使用共享库编译此程序,可执行文件的正文和数据段的长度都显著减小;
7.7 存储空间分配
ISO C说明了三个用于存储空间动态分配的函数
- malloc,分配指定字节数的存储区,此存储区中的初始值不缺定。
- calloc,为指定数量指定长度的对象分配存储空间,每一位bit都初始化为0.
- realloc,增加或者减少以前分配区的长度
#include<stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
//成功返回非空指针,若出错,返回NULL
void free(void *ptr);
因为这三个alloc函数都返回void*, 所以若果程序中包含了#include<stdlib.h>
,那么当我们i将这些返回赋予一些其他类型时,就不需要显式的强制类型转换了。
大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度、指向下一个分配块的指针等,这就意味着,如果超过一个已分配区的尾端或者起始位置首端,则会造成在灾难性问题。
其他可能产生致命性错误的是:释放一个已经释放的块;调用free时使用的指针不是alloc函数返回值等。弱国一个进程调用malloc函数,但是忘了调用free函数,那么该进程占用的存储空间就会连续增大,这被称为泄露。
7.8 环境变量
ISO C定义使用getenc函数来获取环境变量得值,注意,此函数返回一个指针,指向name=value
中得value,而且获取得是一个副本,而不是直接访问environ的。
#include<stdlib.h>
char *getenv(const char *name);
//返回值:指向name对应value的指针,若未找到,返回NULL
此外,还可以修改环境变量的值:
#include<stdlib.h>
int putenv(char *str);
int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);
7.9 函数setjmo和longjmp
在C中,goto语句不能跨越函数的,而执行这种跳转功能的函数时setjmp和longjmp。
#include<setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
7.10 函数getrlimit和setrlimit
每个进程都有一组资源限制,其中一些可用以下两个函数执行
#include<sys/resource.h>
int getgetrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit *rlptr);
八、进程控制
8.2 进程标识
原文链接:https://blog.csdn.net/qq_55537010/article/details/127837953
- 上一篇:mq消息积压怎么对应
- 下一篇:开发板NFS挂载方案
相关推荐
- 2022-01-31 element-ui upload组件 上传文件类型限制
- 2023-02-14 C#实现ComboBox变色的示例代码_C#教程
- 2022-05-21 Python内置数据类型中的集合详解_python
- 2023-07-16 oracle 创建存储过程
- 2022-11-28 基于Python实现DIT-FFT算法_python
- 2022-04-23 arguments获取当前所在函数
- 2023-02-09 C++存储持续性生命周期原理解析_C 语言
- 2022-07-03 TypeScript 变量声明 —— 类型断言
- 最近更新
-
- window11 系统安装 yarn
- 超详细win安装深度学习环境2025年最新版(
- Linux 中运行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存储小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基础操作-- 运算符,流程控制 Flo
- 1. Int 和Integer 的区别,Jav
- spring @retryable不生效的一种
- Spring Security之认证信息的处理
- Spring Security之认证过滤器
- Spring Security概述快速入门
- Spring Security之配置体系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置权
- redisson分布式锁中waittime的设
- maven:解决release错误:Artif
- restTemplate使用总结
- Spring Security之安全异常处理
- MybatisPlus优雅实现加密?
- Spring ioc容器与Bean的生命周期。
- 【探索SpringCloud】服务发现-Nac
- Spring Security之基于HttpR
- Redis 底层数据结构-简单动态字符串(SD
- arthas操作spring被代理目标对象命令
- Spring中的单例模式应用详解
- 聊聊消息队列,发送消息的4种方式
- bootspring第三方资源配置管理
- GIT同步修改后的远程分支