mxxhcm's blog

  • 首页

  • 标签

  • 分类

  • 归档

UNIX Process Control

发表于 2019-11-25 | 更新于 2020-02-20 | 分类于 UNIX

概述

这一节主要介绍UNIX系统的进程控制,包括进程创建,进程执行和进程控制。以及进程属性的各种的ID-real UID, real GID和effective UID, effective GID和save UID, set UID和set GID,以及它们如何收到进程控制原语的影响。

进程标识(pid)

每一个进程都有一个非负整数表示它的唯一进程ID。进程ID标识符总是唯一的,但是可以复用。
关于和进程ID相关的内容,可以查看。

fork

fork创建一个子进程。函数原型:

fork原型

1
2
3
#include <unistd.h>

pid_t fork(void);

fork性质

  1. 进程ID。fork调用一次,返回两次,分别是0和子进程ID,用以区别父进程和子进程。对于父进程,返回子进程ID,对于子进程,返回0。因为父进程可能有多个子进程,并且没有提供获得一个进程所有子进程ID的函数,而fork只有一个父进程,可以通过getppid获得它的父进程的ID。所以这样子进行区分。
  2. 子进程和父进程分别继续执行调用fork之后的指令。子进程是父进程的副本。子进程获得父本的数据段,堆和栈的完全副本。这是子进程的副本,和父进程不一样,它们并不共享数据的内存空间,但是它们共享text segment。
  3. 现代的操作系统实现,使用写时复制代替了父进程数据段,堆和栈的完全副本。这些区域是由父进程和子进程共享的,但是它们的访问权限是只读。如果父进程或者子进程想要对这些区域进行修改的话,内核会为修改区域的那块内存制作一个副本,用于进程修改。
  4. 父进程和子进程因为不共享数据,堆和栈,每个进程都有自己的变量,不会相互影响。
  5. 执行顺序。fork后父进程和子进程的执行顺序是不确定的,这跟内核的调度算法有关。如果要求父进程和子进程之间进行同步,需要它们之间进行某种形式的进程通信。
  6. 文件共享。对于父进程打开的文件,fork相当于将父进程的文件描述符都复制到了子进程中,相当于对父进程的每一个文件描述符,都调用了dup函数。父进程和子进程每个相同的打开文件描述符共享同一个文件表项。一般来说,在fork之后处理文件描述符有以下两种情况:
    • 父进程等待子进程完成。父进程不需要对它的文件描述符做任何处理。
    • 父进程和子进程分别执行不同的程序段。父进程和子进程各自关闭它们不需要的文件描述符。
  7. fork后子进程继承的信息。
    • real UID, real GID, effective UID, effective GID
    • set UID和 set GID
    • 附属组ID
    • 进程组ID
    • Session ID
    • 控制终端
    • cwd
    • root dir
    • umask
    • signal mask
    • 文件描述符标志
    • 环境
    • 共享的内存段
    • 内存映像
    • Resource limits
  8. 父进程和子进程的区别。
    • fork的返回值不同
    • pid不同
    • 它们有不同的ppid
    • 子进程的很多时间设置为0
    • 父进程设置的文件锁子进程不继承。
    • 子进程的未处理闹钟被清除
    • 子进程的未处理信号被设置为空集。
  9. fork的两种用法。
    • 父进程和子进程分别执行不同的代码。比如网络服务中,父进程负责等待客户端请求,子进程负责处理父进程接收到的请求。
    • 一个进程要执行不同的程序。对shell比较常见,通常执行完fork返回后立即调用exec。

vfork

创建一个子进程,并且阻塞父进程,函数原型如下:

vfork原型

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t vfork(void);

vfork属性

  1. vfork和fork都创建一个新进程,但是vfork并不会将父进程的地址空间完全复制到子进程中。因为子进程会立即调用exec或者exit,就不会引用该地址空间。但是如果在调用exec或者exit之前,它会在父进程的空间中运行,这种做法会提高效率。但是如果子进程修改除了vfork的返回值,或者在没有调用exit或者exec之前调用其他函数,这种行为是未定义的。
  2. vfork保证子进程先运行,在子进程没有调用exec或者exit时,内核会使父进程休眠,在子进程调用exec或者exit这两个中的任何一个后,父进程才会恢复运行。如果子进程需要父进程进一步操作的时候,就会产生死锁。

exit函数

关于exit函数的介绍,可以查看C/C++ exit and return。
总共有八种方式可以让进程终止,包括五种正常和三种异常,前五种是正常终止,后五种是异常终止:

  1. 从main返回,相当于调用exit。
  2. 调用exit,ISO C定义的,它的操作包括调用各个exit handler,处理所有标准I/O流。exit会冲洗标准I/O流,如果这是函数库所采取的唯一的动作,那么不会出现什么问题。而如果exit除了冲洗标准I/O流,还会关闭I/O流,那么在vfork时就会出问题了。当然,现在的exit实现都不会关闭流了,这个操作一般都交给内核实现。
  3. 调用_exit或者_Exit,ISO C定义了_Exit,而POSIX.1说明了_exit。它的目的是提供一种无需运行exit handler或者信号处理程序而终止的方法。是否对标准I/O流进行flush,取决于实现。在UNIX中,_Exit和_exit是同义的,并不冲洗I/O流。
  4. 最后一个线程从其启动例程返回
  5. 最后一个线程调用pthread_exit
  6. 调用abort
  7. 接到一个signal
  8. 最后一个线程对取消请求做出响应

不管进程以哪种方式终止,最后都会执行内核中的同一段代码,这段代码为相关进程关闭所有打开的文件描述符,释放它使用的内存。
为了让终止进程能够通知父进程它是如何终止的。对于3个终止函数,将它的exit status作为参数传递给函数。在异常终止的情况下,内核产生一个指示其异常终止原因的terminaiton status(终止状态)。在任意终止情况下,这个终止进程的父进程都能用wait或者waitpid函数获得它的终止状态。
如果父进程在子进程之前终止,所有终止进程的子进程的父进程都变成init进程,init进程负责获得终止状态。对于一个即将终止的进程,内核检查所有活动进程,判断其中是否有待终止进程的子进程,如果有的话,将这些进程的父进程的ID改为init进程的PID 1。
如果子进程在父进程之前终止。内核为每一个终止进程保留了一部分信息,当终止进程的父进程调用wait或者waitpid时,可以获取这些信息,这些信息包含终止进程PID,进程的终止状态,进程占用的CPU时间总量。内核可以释放这些进程的内存,关闭打开的文件。如果一个进程终止了,但是它的父进程没有等待它,它被称为一个zombie(僵尸)进程,这些信息不会被释放。如果一个长期运行的进程,fork了很多子进程,除非父进程调用wait得到子进程的终止状态,否则它们就会变成僵尸进程。
init的子进程,不会变成僵尸进程,因为init进程被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数获得其终止状态。

wait和waitpid

当一个进程终止的话(无论正常还是异常),内核就会向它的父进程发送SIGCHLD信号。而子进程终止是个异步事件,可以在父进程运行的任何时候发生。对于这种信号,父进程可以忽略它,或者调用一个信号处理函数。
调用了wait或者waitpid的进程,可能会处于以下几种状态之一:

  1. 所有子进程都还在运行,则阻塞。
  2. 一个子进程已经终止,正在等待父进程获取其终止状态,那么取得该子进程的终止状态立即返回。
  3. 如果它没有任何子进程,立即出错返回。

如果进程由于接收到SIGCHLD而调用wait,我们期望wait会立即返回。如果在随机的时间点调用wait,进程可能会阻塞。

wait和waitpid原型

1
2
3
4
#include <sys/wait.h>

pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);

wait和waitpid性质

  1. 在一个子进程终止前,wait使其调用者阻塞,直到任意一个子进程终止。wait返回终止子进程的进程ID。如果wait要等待一个特定的进程,将返回的pid和要等待的pid相比,如果不相等,将这个pid和termination status保存起来,再次调用wait,直到等到目标pid。下一次想要等待一个特定进程的时候,现场看已经终止的进程列表中是否已有它 ,没有的haunted继续调用wait。
  2. ISO中定义了waitpid中option的四个可能值:0,WNOHANG, WUNTRACED和WCONTINUED。POSXI扩展了许多其他选项。其中0表示阻塞调用,WNOHANG表示如果没有子进程结束就立刻退出,WUNTRACED表示如果一个子进程停止了也返回,WCONTINUED表示一个子进程恢复运行了也会返回。
  3. 指定option为0时,设置waitpid阻塞等待指定的进程pid。当pid为-1时,waitpid和wait一样。当pid大于或者小于0时,等待相应的pid(绝对值)。当pid等于0时,等待gid等于调用进程组id的任意一个子进程。
  4. 指定option为WNOHANG,设置waitpid不阻塞,表示如果没有子进程结束,就立刻返回。在Linux上,WNOHANG是1。
  5. 指定option为WUNTRACED和WCONTINUED,设置waitpid支持job control。
  6. 对于wait,只有当调用进程没有子进程时,才出错。对于waitpid,指定的进程或者进程组不存在,或者参数pid不是调用进程的子进程时,都会出错。
  7. wstatus是一个整形指针。如果它不为空指针,终止进程的终止状态就存放在它所指的单元内。
  8. wstatus指向的整形变量的意义是由实现定义的,其中的某一些位表示exit status,即正常退出。另外一些位表示signal number,表示不正常退出,一位表示是否产生core file,等等。POSIX.1指定了termination status可以用<sys/wait.h>中定义的宏查看。四个互斥宏可以用来取得进程终止的原因:
    • WIFEXITED(status),如果status是一个正常终止子进程返回的,为true。执行WEXITSTATUS(statue)获取子进程传递给exit或者_exit的参数的低八位。
    • WIFSIGNALED(status),如果status是一个异常终止子进程返回的,为true。执行WTERMSIG(status)获取使得子进程终止的signal。
    • WIFSTOPPED(status),如果status是一个当前暂停的子进程返回的,为true。执行WSTOPSIG(status)获取使得子进程暂停的signal。
    • WIFCONTINUED(status),。
  9. fork两次可以让原始进程不用自己调用wait,也可以避免产生僵尸进程。

waitid

waitid原型

1
2
3
#include <sys/wait.h>

int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

waitid性质

  1. waitid是SUS指定的,不是ISO C的部分。
  2. waitid和waitpid类似,但是它使用两个单独的参数表示要等到的子进程所属的类型,用idtype表明id的类型,用id表示pid或者进程组id。idtype的取值如下:
    • P_PID,等待特定进程,id指定要等待的子进程的pid
    • P_PGID,等待特定进程组中的任一子进程,id是包含要等待子进程的组ID
    • P_ALL,等待任意子进程,忽略id。
  3. options参数是以下标志的按位或运算。
    • WCONTINUED
    • WEXITED
    • WNOHANG
    • WNOWAIT
    • WSTOPPED
  4. siginfo_t结构体包含了子进程状态改变有关signal的详细信息。

wait3和wait4

大多数UNIX系统都支持wait3和wait4,它们是从BSD延续下来的。它们的功能比POSIX.1函数wait, waitpid和waitid要多一个。可以通过附加参数允许内核返回终止进程以及其所有子进程使用的资源概况。包含用户CPU时间总量,系统CPU时间总量,缺页次数,接收到的signal次数。
它们的原型如下:

wait3和wait4原型

1
2
3
4
5
6
7
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>

pid_t wait3(int *wstatus, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *wstatus, int options, struct rusage *rusage);

race condition

如果多个进程都企图对共享数据进行某种处理,而且最后的结果取决于进程运行的顺序时,我们认为发生了race condition。
如果一个进程希望等待子进程终止,可以调用wait函数中的一个。如果一个子进程想要等待父进程终止,可以使用下列形式的循环,称为轮询(polling)

1
2
3
4
while(getppid() != 1)
{
sleep();
}

这种方式浪费了很多CPU时间。为了避免这些问题,可以使用signal或者进程间通信解决这些问题。

exec

通常使用fork创建新的子进程之后,子进程往往会调用一种exec函数执行另一个程序。当进程调用exec函数时,该进程执行的程序完全替换为新程序,而新程序从其main函数开始执行。调用exec并不会创建新进程,所以前后的进程ID不变。exec使用磁盘上的一个新程序替换了当前进程的text segment, data segment, heap和stack。exec函数只有在出错的时候才返回-1,并且设置errno。
总共有七种不同的exec函数,它们被统称为exec函数。它们的原型如下:

exec函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execve(const char *filename, char *const argv[], char *const envp[]);

int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
int fexecve(int fd, char *const argv[], char *const envp[]);

exec函数属性

  1. 传入参数的区别。 path是路径名作为参数,file是文件名作为参数。如果path中包含'/',将它看成路径名。否则按照PATH环境变量,在它指定的目录中搜寻可执行文件。如果函数execlp和execvp在PATH指定的目录中找到的文件不是link editor产生的可执行文件,就会把它当做一个shell脚本,调用/bin/sh,把这个文件当做shell的输入。
  2. argmuent list的区别。 l表示的是列表,v表示的是向量。execl, execlp和execle要求将新程序的每个命令行参数都说明为一个单独的参数,这种参数表以空指针结尾。而execv,execvp, execve和fexecve,需要先构造一个指向各个参数的指针数组,然后将该数组的地址作为这四个函数的参数。
  3. environment list的区别。 以e结尾的函数,execve, execvpe,execle, fexecve等可以传递一个指向environment字符串指针数组的指针,这个是自己指定的环境。其他几个不带e的函数使用进程中的environ变量为新程序复制现有的环境。
  4. 调用exec后,进程ID没有改变,但是新程序从调用进程继承了以下属性:
    • pid和ppid
    • real UID, real GID,
    • 附属组ID
    • 进程组ID
    • Session ID
    • 控制终端
    • 闹钟余留时间
    • cwd
    • root dir
    • umask
    • 文件锁
    • 进程信号屏蔽
    • 未处理信号
    • 资源限制
    • nice值
    • 时间
  5. 在exec前后,real UID和real GID不变,effective ID取决于是否设置set UID和set GID。如果新程序的set UID已经设置,则effective ID变成程序文件所有者的ID,否则不变。
  6. 这几个函数中,只有execve是系统调用,其他几个都只是库函数,最终都要调用execve。

解释器文件

关于解释器文件,可以查看。

system

关于system的介绍,可以查看。

进程会计

大多数UNIX系统都提供了一个选项进行进程会计处理。启动该选项之后,每当进程结束时内核就会写一个会记记录。典型的会计记录是一个二进制数据,一般包括命令名,所有的CPU时间总量,UID和GID,启动时间等。所有的标准都没有定义进程会记,所以实现上就千差万别。
acct函数启用和关闭进程会计。
会记记录结构定义在头文件<sys/acct.h>的struct acct中,其中ac_flag标志记录了进程执行期间的某些事件:

  • AFORK,进程是fork产生的,但是未调用exec
  • ASU,进程使用superuser权限
  • ACORE,进程转储到core
  • AXSIG,进程由一个signal杀死
  • AEXPND,扩展的会计条目
  • ANVER,新纪录格式

在LINUX上,ac_flag是枚举类型,所以不能使用#ifdef判断是否支持ACCORE等flag,可以使用if !defied HAS_ACCORE进行判断。

会计记录所需的各个数据(各CPU时间,传输的字符数等)都由内核保存在process table中,并在一个新进程被创建时初始化,进程终止时写一个会计记录。这产生了两个后果:

  1. 对于那些不会终止的进程,比如init进程,我们无法获得它的会计记录。内核守护进程也不会终止,所以也不会产生会计记录。
  2. 在会计文件中记录的顺序对应于进程终止的顺序,而不是它们启动的顺序。

会记记录对应于进程而不是程序。在fork之后,内核为子进程初始化一个记录,而不是在一个新程序被执行初始化时。exec并不会创建一个新的记录,但是相应记录中的名字会改变,AFORK标志没了。

获得当前登录用户名

可以使用getlogin获得当前登录用户的用户名。

1
2
3
#include <unistd.h>

char *getlogin(void);

进程调度

调度策略和调度优先级是由内核确定的。

nice, getpriority, setpriority原型

1
2
3
4
5
6
7
8
#include <unistd.h>

int nice(int inc);

#include <sys/resource.h>

int getpriority(int which, id_t who);
int setpriority(int which, id_t who, int prio);

nice, getpriority, setpriority属性

  1. nice函数将输入的参数加到当前的nice值上。nice值越大,优先级越低,否则越高。
  2. 在单核的机器中,同时运行一个父进程和一个子进程,它们的nice值不同的话,CPU占用比也可能会不同,这取决于进程调度程序如何使用nice值。在多核的机器上可能看不到这样的结果。

进程时间

可以使用times获得某进程和它的子进程的CPU时间以及墙上时钟时间,times通过struct tms传递信息。它的内容如下:

1
2
3
4
5
6
struct tms {
clock_t tms_utime;
clock_t tms_stime;
clock_t tms_cutime;
clock_t tms_cstime;
};

它包含进程的用户CPU时间,系统CPU时间和子进程的用户CPU时间和系统CPU时间。但是不包含墙上时钟时间,墙上时钟时间是通过函数的返回值得到的,而且得到的时间是相对于过去某个时间点得到的,所以不能使用它的绝对值,要使用相对值。比如第一次调用times,记录返回值,等到下一次调用times时,用新的值减去刚才保存的值,得到墙上时间时间。
所有时间(结构体和返回值)的单位都是滴答数。
函数原型如下:

1
2
3
#include <sys/times.h>

clock_t times(struct tms *buf);

shell的time(1)可以使用times(2)实现,看程序。

参考文献

1.《APUE》第三版

UNIX Process Environment

发表于 2019-11-25 | 更新于 2019-12-02 | 分类于 UNIX

概述

这一部分介绍的是进程运行的环境。主要包括进程执行时,main函数是如何被调用的,命令行参数如何传递给进程的,进程的存储空间结构,如何分配存储空间,环境变量的使用,以及进程是怎么终止的。

main函数和argc, argv

C语言总是从main函数开始执行,C语言中main有两个原型:

1
2
int main(void);
int main(int argc, char *argv[]);

其中argc是命令行参数的个数,argv是一个指针数组,ISO C和POSIX.1都要求argv[argc]设置为NULL,所以可以用它作为参数处理的循环终止条件。关于更多指针的信息可以查看。
当内核执行C程序(使用一个exec函数)时,在调用main函数之前设置一个特殊的启动例程。可执行程序文件将这个启动例程指定为程序的起始地址,这是由link editor设置的,它会被C编译器调用。启动例程从内核命令获得命令行参数和环境变量值,然后为调用main函数做好准备。

关于更多C和C++中main的介绍,可以查看C/C++ main argc argv。

全局变量environ

每个C程序都会接收到一个environment list,和argv一样,它是一个指针数组。每个指针指向一个以null结束的C字符串的地址,这个指针数组的地址存放在全局变量environ中:

1
extern char **environ;

在历史上,UNIX大多支持三个参数的main函数,第三个参数就是environment list:

1
int main(int argc, char *argv[], char *envp[]);

但是ISO C规定main只能有两个参数,POSIX.1也就规定使用全局变量environ而不是第三个参数。如果要查看所有的环境变量时,使用environ,而访问某个特定的环境变量时,使用getenv和setenv。

进程终止

总共有八种方式可以让进程终止,包括五种正常和三种异常,前五种是正常终止,后五种是异常终止:

  1. 从main返回
  2. 调用exit
  3. 调用_exit或者_Exit
  4. 最后一个线程从其启动例程返回
  5. 最后一个线程调用pthread_exit
  6. 调用abort
  7. 接到一个signal
  8. 最后一个线程对取消请求做出响应

exit函数

函数原型

1
2
3
4
5
6
7
8
#include <stdlib.h>

void exit(int status);
void _Exit(int status);

#include <unistd.h>

void _exit(int status);

性质

  1. exit和_Exit是ISO C的内容,而_exit是POSIX.1的内容
  2. 它们都用于正常终止一个程序,_Exit和_exit立刻进入内核,而exit先执行一些清理操作,然后返回内核。exit函数总是执行一个标准I/O库的关闭操作,对于所有打开的流调用fclose函数,所有带有未写缓冲的标准I/O流被flush。
  3. 三个退出函数都需要一个整形的参数,被称为exit status。
  4. 如果满足以下条件:
    • 调用这三个函数不带终止状态
    • main执行了一个不带返回值的return语句
    • main没有声明返回类型为整形,进程的终止状态是未定义的。
      那么这个进程的终止状态是未定义的。
  5. main返回返回一个整型值和用该值调用exit是等价的。对于某些C编译器和UNIX lint(1)程序来说,会产生警告信息,因为这些编译器并不了解main中的return和exit的作用是相同的。避开这种警告信息的一种方法是在main中使用return而不是exit,这样做的结果是UNIX grep命令无法找出程序中所有的exit调用。另一个方法是将main声明为void而不是int,然后调用exit,但是这不并不是标准,ISO C和POSIX.1定义main的返回值应当是带符号整形。

关于更多exit函数的内容,可以查看。
关于exit和return的内容,更多可以查看C/C++ exit and return。

atexit

每个进程可以通过atexit register至多32个由exit自动调用的函数,这些函数被称为exit handler(终止处理程序)。

1
2
3
#include <stdlib.h>

int atexit(void (*function)(void));

  1. atexit的参数是一个函数地址,不会有返回值
  2. exit调用atexit register的程序的顺序和使用atexit进行register的顺序相反。
  3. ISO C和POSIX.1标准规定,exit首先调用各个exit handler,然后使用fclose关闭所有标准I/O流。
  4. POSIX.1对ISO C进行了扩展,如果程序调用了任何exec函数,清除exit handler。
  5. 内核执行一个程序的唯一方法是调用一个exec函数。进程自愿终止的唯一办法是显式或者隐式的(通过exit)调用_exit和_Exit。

C程序的存储空间布局

更多关于C程序存储空间布局可以查看C/C++ program memory layout。

共享库

共享库使得可执行文件中不再需要包含公用的库函数,只需要在所有进程都可引用的存储区保存这种库例程的一个副本。程序第一次执行或者第一次调用某个库函数时,使用动态链接的方法将程序和共享库函数链接,这减少了每个可执行文件的长度,但是增加了一些时间运行开销。这种时间开销发生在程序第一次被执行时,或者每个共享库函数第一次被调用时。共享库的另一个优点是可以使用库函数的新版本代替老版本而无需对使用该库的程序重新链接和编辑。

内存空间分配

ISO C说明了三个用于memory allocation的函数,malloc, calloc和realloc,它们的原型如下,更多关于C中malloc的内容可以查看C/C++ malloc(alloc) free new and delete。

malloc, calloc和realloc原型

1
2
3
4
5
6
7
#include <stdlib.h>

void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void *reallocarray(void *ptr, size_t nmemb, size_t size);

malloc, calloc和realloc属性

  1. malloc,分配指定字节的内存空间,初始值不定。
  2. calloc,为指定长度的固定数量的对象分配空间,每一个bit都被初始化为0。
  3. realloc,增加或者减少已经分配的内存空间的大小。当这个大小增加时,可能需要将之前分配的空间中的数据移到另一个足够大的区域以便于增加大小,新增加的区域内的值是不确定的。
  4. 这三个函数返回的指针一定是对齐的,保证它可以用于任何对象。比如double的要求最严格,需要从8的倍数的地址单元开始,这三个函数返回的地址一定满足这个要求。
  5. 它们的返回类型都是void*,需要使用强制类型转换。
  6. realloc函数可以增加或者减少之前分配的内存空间的大小。比如分配了一个固定大小的数组,后来发小它不够用了,可以使用realloc对它进行扩充,如果原有的存储后有足够的大小进行扩充,则可以在原存储区的位置上向高地址进行扩充,无需移动原有数组,返回和传入相同的指针。如果原来的内存空间后没有足够的空间,就重新分配一个足够大的内存空间,再将原有数据的内容复制过去,然后释放原来的内存空间,返回新的指针。
  7. realloc传入的参数是存储区的新长度。如果传入的ptr参数是NULL指针,那就退化成了malloc。
  8. free可以释放ptr指向的内存空间,释放的空间通常送入可用内存池,之后可以通过这三个函数重新分配。
  9. malloc和free底层通常使用sbrk系统调用实现,这个系统调用扩充或者减小进程的堆,虽然sbrk可以扩充或者缩小进程的堆,但是一般malloc和free的实现不会减少进程的内存空间,释放的内存空间保存在malloc池中,而不是交给内核。
  10. 大多数实现分配的空间要比请求的空间大一些,因为需要存储一些管理信息,如block的大小,指向下一个block的指针等等。因此,如果对超过一个分配区域的内存进行读写的话,会造成很严重的错误。
  11. free一个已经释放了的块,free的不是alloc函数的返回值,没有进行free等等,都有可能造成很严重的后果。

环境变量

环境变量的形式是:
name = value
UNIX内核并不使用环境变量,通常都是应用程序使用这些环境变量。比如shell使用了大量的环境变量。

标准定义

ISO C定义了getenv函数可以获取环境变量。但是ISO C没有定义任何环境变量,SUS环境变量包括POSIX.1和XSI环境变量。
除了获取环境变量,有时候我们也需要设置环境变量。ISO C没有定义获取环境变量的函数。SUS除了定义了ISO C,还定义了putenv, setenv和unsetenv对环境变量进行操作。

putenv,setenv和unsetenv原型

1
2
3
4
5
#include <stdlib.h>

int putenv(char *string);
int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);

putenv,setenv和unsetenv性质

  1. putenv,创建或者重置一个name。
  2. setenv,如果name不存在,创建name;如果name存在,rewrite不为零,重写,rewrite为零,不进行重写。
  3. unsetenv,移除name的定义。如果name不存在,不会出错。
  4. 环境变量的修改和增加可能会遇到一些问题。因为环境变量存放在进程地址空间的最上面的一个不可扩展的空间。
    如果修改一个存在的name:
    当新的value的长度小于等于原来的value长度时,直接覆盖就行;
    当新的value的长度大于原来的value长度时,只需要给新的name-value字符串分配空间就行了。使用malloc为新的字符串分配空间,然后将该字符串复制到新的空间,让environment list中name的指针指向新分配的存放字符串的空间即可。

如果增加一个新的name,不仅需要给新的name-value分配空间,指针数组的元素也增加了,还需要给指针数组分配新的空间。首先给新的name-value字符串分配空间,调用malloc先为字符串分配空间,然后将该字符串复制到这个空间:
如果这是第一次增加一个name,必须调用malloc为指针数组增加空间。将原来的指针数组复制到新分配的空间中,然后将新字符串的指针放在指针数组的尾部,然后存放一个空指针。最后让全局变量char **environ指向这个指针数组。如果原来的指针数组存放在栈顶之上,需要将它复制到堆中。需要注意的是,这个指针数组中的未修改的环境变量的指针还是指向栈顶的字符串上。
如果这不是第一个添加name,只需要使用realloc重新多分配一个指针数组的空间即可,然后将它指向新字符串的地址即可。

setjmp和longjmp

setjmp和longjmp原型

1
2
3
4
#include <setjmp.h>

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

setjmp和longjmp属性

  1. 自动变量存储在每个函数的栈帧中。
  2. setjmp和longjmp实在栈上跳过若干调用栈,返回到当前函数调用路径上的某个函数中。

getrlimit和setrlimit

每个进程能使用的资源都是有限的,可以使用getrlimt和setrlimit进行修改。它们都是XSI扩展,不是ISO C的定义。有些资源可以设置为RLIM_INFINITY,表示无限。

getrlimit和setrlimit性质

  1. 任何一个进程都可以将rlim_cur改为小于等于rlim_max。
  2. 任何一个进程都可以将rlim_max改小,但是不能小于rlim_cur,且这个更改是不可逆的。
  3. 只有root用户可以更改rlim_max。

它们的原型原型如下:

getrlimit和setrlimit原型

1
2
3
4
5
#include <sys/time.h>
#include <sys/resource.h>

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);

resource种类

  • RLIMIT_AS
  • RLIMIT_VMEM
  • RLIMIT_DATA
  • RLIMIT_SWAP
  • RLIMIT_STACK
  • RLIMIT_NPROC
  • RLIMIT_FSIZE
  • RLIMIT_NOFILE
  • RLIMIT_NICE
  • …

结构体

结构体struct rlimt的定义如下:

1
2
3
4
5
struct rlimit
{
rlim_t rlim_cur; // soft limit
rlim_t rlim_max; // hard limit
}

参考文献

  1. 《APUE》第三版

UNIX system data file

发表于 2019-11-24 | 更新于 2020-02-20 | 分类于 UNIX

系统文件

文件名 结构 查看结构内容 头文件 查询函数 其他
/etc/passwd passwd man 5 passwd pwd.h getpwnam, getpwuid 可以使用vipw直接修改。
/etc/shadow shadow man 5 shadow shadow.h getspnam
/etc/group group man 5 group grp.h getgrnam, getgrgid
/etc/hosts hostent man 5 hosts netdb.h getnameinfo, getaddrinfo
/etc/networks netent man 5 networks netdb.h getnetbyname, getnetbyaddr
/etc/protocols protoent man 5 protocols netdb.h getprotobyname, getprotobynumber
/etc/services servent man 5 services netdb.h getservbyname, getservbyprot
/var/run/utmp/,/var/log/wtmp/ utmp man 5 utmp utmp.h getutid, getutline

其他操作

  1. 查看整个口令文件:struct pwd *getpwdent(void);
    void setpwent(void);
    void endpwent(void);
  2. 查看整个shadow文件:struct pwd* getspent(void);
    void setspent(void);
    void endspent(void);
  3. 查看整个group文件:struct pwd* getgrent(void);
    void setgrent(void);
    void endgrent(void);

一般来说,对于第一节中列出的所有文件,都存在三个函数:get,set和end,它们的功能类型。
第一次调用get函数,返回第一项,接下来顺序的返回文件中的每一项。
set函数定位到数据库的开始位置。
end函数关闭相应的数据库。

附属组ID

使用newgrp(1)加GID在当前seeion内更改组ID,不加参数更改回原来的组(/etc/passwd)中的组。
附属组ID的引入可以使得一个用户至多拥有16个另外的组(常用值是16)。
有三个函数可以操作附属组ID:

1
2
3
4
5
6
// 获得当前用户的至多size个gid,如果size=0,返回总共的附属组ID的个数,如果size!=0,返回实际写入数组中的组ID的个数。
int getgroups(int size, gid_t list[]);
// 为调用进程设置附属组ID。
int setgroups(size_t size, const gid_t *list);
// 为user初始化GID。
int initgroups(const char *user, gid_t group);

登录账户记录

utmp文件记录当前登录到系统的各个用户。
wtmp文件记录各个登录和注销事件。
同样,可以使用struct utmp* getutent(void);
void setutent(void);
void endtuent(void);
这三个访问。
who(1)读取utmp文件。
last(1)读取wtmp文件。

系统标识

使用utsname(1)获得主机和操作系统相关的消息,通过定义在<sys/utsname.h>中的uname实现。
使用hostname(1)获取和设置主机名,通过定义在<unistd.h>中的gethostname和sethostname实现。

参考文献

1.《APUE》第三版

linux glibc

发表于 2019-11-24 | 更新于 2019-12-17 | 分类于 linux

查看当前linux系统的glibc版本

ldd --version

1
2
3
4
5
ldd (Ubuntu GLIBC 2.27-3ubuntu1) 2.27
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

我的笔记本上的glibc是2.27版本。

参考文献

1.https://www.linuxquestions.org/questions/linux-software-2/how-to-check-glibc-version-263103/

C (ISO C) Standard I/O library

发表于 2019-11-22 | 更新于 2019-12-18 | 分类于 UNIX

总结

  1. fgets, sprintf, snprintf会在缓冲区数组的结尾加上一个null字节,但是使用的时候不会包含这个字节。
  2. fgets和getline都会读入回车,并且将它存入缓冲区,getchar, getc和fgetc也会读入回车,并且将它存起来。
  3. 每个标准I/O流都有一个和它相关联的文件描述符,可以对一个流调用fileno获得它的文件描述符。fileno不是ISO C的部分,因为文件描述符不属于ISO C。
  4. 标准I/O库的一个不足是效率不高。这和它复制的数据量有关。每当使用一次fgets和fputs时,通常需要复制两次数据,一次是在用户程序的行缓冲区和标准I/O缓冲区之间,一次是在内核和标准I/O缓冲区之间。
    使用fgets需要用户指定fgets使用的缓冲区,或者使用getline,如果传入的指针指向NULL,getline会负责分配缓冲区大小。
    read和write需要设置缓冲区,这是系统调用级别的,大小可以任意指定,通常使用sturct stat.st_blksize的大小,用户如果直接调用read和write的话,需要自己设置缓冲区。而标准I/O库可以自己选择是否进行缓冲,如果缓冲的话,标准I/O库可以负责进行缓冲区大小选择和分配,也可以用户自己进行指定缓冲类型:行缓冲和全缓冲,用户也可以自己通过setbuf和setvbuf更改缓冲区大小和地址。而在setbuf中,如果buf是NULL的话,是关闭缓冲区,如果不为空的话,必须是BUFSIZ大小。在setvbuf中,通过mode指定缓冲区的类型,buf是NULL的话,库函数负责分配缓冲区。否则buf是多大就用多大的缓冲区。
    而在内核中,还存在buffer cache和page cache,用于“延迟写”,减少和磁盘的交互。

概述

特殊符号的ASCII

'\n’是10。
EOF是-1。

标准I/O和文件I/O

文件I/O是围绕文件描述符进行的,使用open打开一个文件时,返回一个文件描述符,然后使用文件描述符进行后续I/O操作。文件I/O是UNIX相关的实现,其他系统可能有不同的实现,是不跨平台的。
标准I/O是围绕stream进行I/O操作的。当标准I/O库打开或者创建一个文件时,一个流已经和文件相关联。标准I/O库处理很多细节,比如缓冲区分配,使用优化的长度块执行I/O等,使用户不用担心选择多大的block进行I/O会更快。标准I/O库是ISO C标准定义的,不仅仅UNIX系统有实现,凡是支持ISO C标准的操作系统都应该实现,是支持跨平台的。标准I/O在UNIX上需要使用文件I/O实现,在windows等其他系统上就需要其他的实现。

stream和FILE对象

12.1 Streams
For historical reasons, the type of the C data structure that represents a stream is called FILE rather than “stream”. Since most of the library functions deal with objects of type FILE *, sometimes the term file pointer is also used to mean “stream”. This leads to unfortunate confusion over terminology in many books on C.

标准I/O的操作是围绕stream进行的,当打开一个stream时,它返回一个指向FILE类型的指针(通常叫做文件指针)。FILE是一个结构体,包含了标准I/O管理这个stream需要的所有信息,包含用于实际I/O的文件描述符,指向这个流缓冲区的指针,缓冲区的长度,当前缓冲区中的字符等。为了引用一个stream,需要将FILE指针作为参数传递给每个标准I/O函数。

stream的定向

对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符用多个字节表示。标准I/O FILE stream可以用于单字节也可以用于多字节字符集。stream的orientation决定了读写的字符是单字节还是多字节,最开始创建stream时,它的orientation没有被确定,使用什么字符的I/O就会将stream的orientation定义为什么。
有两个函数可以改变stream的orientation,它们是freopen和fwide,原型如下:

freopen和fwide原型

1
2
3
4
5
#include <stdio.h>
#include <wchar.h>

int fwide(FILE *stream, int mode);
FILE *freopen(const char *pathname, const char *mode, FILE *stream);

freopen和fwide性质

  1. fwide用于设置stream的orientation。如果mode为负,是单字节定向的。如果mode为正,是多字节定向的。如果mode为0,fwide确定当前stream的oritentation并返回。
  2. fwide不能改变已经定向的stream的orientation。
  3. fwide没有出错返回

标准输入,标准输出和标准错误

通常对一个进程预定义了三个stream,它们可以自动的被进程使用。它们是标准输入,标准输出和标准错误,这些stream引用的文件和文件描述符STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO所引用的文件一样。
这三个stream定义在头文件<stdio.h>中,通过预定义文件指针stdin, stdou和stderr使用。

三种缓冲类型

标准I/O库提供缓冲的目的是尽可能减少read和write的调用次数,标准I/O库对每个流自动的进行缓冲管理,使得应用程序不用考虑缓冲区的管理。
标准I/O提供了三种类型的缓冲:

全缓冲

填满标准I/O的缓冲区之后,进行实际的I/O操作。对于存储在磁盘上的文件通常是由标准I/O实施全缓冲的。在一个流上第一次执行I/O操作时,相关的标准I/O函数调用malloc获得需要的缓冲区。

行缓冲

在行缓冲中,当输入和输出遇到换行符时,标准I/O库执行I/O操作。但是需要注意的是行缓冲区的长度是固定的,当行缓冲区满时即使没有遇到换行符也进行I/O操作。当涉及终端的I/O时,通常使用行缓冲。使用标准I/O的fputc允许我们一次输出一个字符,但是只有在写了一行之后才能进行实际I/O操作。
此外,任何时候只要通过标准I/O库要求从一个不带缓冲的流或者一个行缓冲的流中得到输入数据,那么就会flush所有行缓冲输出流。从行缓冲的流中得到输入数据的一个例子就是从终端按下回车,刚才输入的数据就会立刻从输出流中输出。

不带缓冲

标准I/O库不对字符进行缓冲存储。如果将字符传入不带缓冲的输出流中,字符会立即输出到输出流关联的文件或者设备。

fflush函数

标准I/O库使用flush将输出流缓冲区的内容写到和输出流相关联的文件,缓冲区可以使用标准I/O例程自动的flush,比如当缓冲区填满时,或者缓冲区不满时可以手动调用fflush函数进行flush。

1
int flussh(FILE *fp);

任何时候,都可以手动强制冲洗一个流,当fp是NULL时,冲洗所有的输出流。
注意fflush和fsync的区别,fflush是将位于主存中的缓冲区的内存冲洗到内核。而内核也有一个缓冲区,叫做buffer cache或者page cache,内核接收到数据会首先将它们写入buffer cache或者page cache中,然后排入队列,晚些时候再写,这种方式叫做延迟写。fsync是将buff cache中的内容立即写入磁盘而不等待。

ISO C缓冲标准和UNIX具体实现

ISO C要求:

  1. 当且仅当标准输入和标准输出不指向交互设备时,它们才是全缓冲的。
  2. 标准错误不会是全缓冲的。

UNIX具体实现:

  1. 标准错误不带缓冲
  2. 指向终端设备的流,都是行缓冲的,否则是全缓冲的。

修改默认缓冲

可以通过setbuf和setvbuf更改流的缓冲类型。

setbuf和setvfuf原型

1
2
3
4
#include <stdio.h>

void setbuf(FILE *stream, char *buf);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

setbuf和setvfuf性质

  1. 这些函数需要在流被打开后调用,因为他们需要文件指针作为参数,而且应该在对流执行任何操作之前调用。
  2. 可以使用setbuf函数打开和关闭缓冲机制。将buf设置为NULL,就是关闭缓冲。如果buf不为NULL,它必须指向一个长度为BUFSIZ的缓冲区,通常在这之后就是全缓冲的,如果和终端设备关联,可能会是行缓冲的。
  3. setvbuf可以通过mode指定缓冲的类型,_IOFBF是全缓冲,_IOLBF是行缓冲,_IONBF是不缓冲。指定不缓冲,忽略buf和size参数。如果指定全缓冲或者行缓冲,buf和size可以通过buf和size指定缓冲区的位置和大小。如果指定带缓冲,而buf是NULL,系统会自动分配BUFSIZE大小的缓冲区。
  4. 一般而言,应该由操作系统选择缓冲区的长度,并且自动分配缓冲区,这种情况下,关闭流,标准I/O库会自动释放缓冲区。

打开一个stream

可以使用fopen, freopen和fdopen函数打开一个standard I/O stream。它们的原型如下:

fopen, freopen和fdopen原型

1
2
3
4
5
#include <stdio.h>

FILE *fopen(const char *pathname, const char *mode);
FILE *fdopen(int fd, const char *mode);
FILE *freopen(const char *pathname, const char *mode, FILE *stream);

fopen, freopen和fdopen性质

  1. fopen打开路径名为pathname的一个文件
  2. fdopen使用一个已有的文件描述符,并将一个标准I/O stream和该文件描述符结合。这个函数通常用于由创建管道和网络通信通道函数返回的文件描述符,因为这些特殊文件不能使用标准I/O函数fopen打开,所以需要使用设备专用函数获得一个文件描述符,然后使用fdopen将文件描述符和一个I/O stream结合。
  3. freopen函数在一个指定的stream打开一个指定的文件,如果这个stream已经打开,先关闭这个stream;如果这个stream已经进行了定向,使用freopen清楚该定向。这个函数一般用于将一个指定的文件打开为一个预定义的stream:stdin, stdout和stderr。
  4. fopen和freopen是ISO C的部分,因为ISO C不包含文件描述符,所以只有POSIX.1有fdopen。
  5. mode有15种取值:r, w, a, rb, wb, ab,r+,r+b, rb+,w+,w+b,wb+, a+, a+b, ab+。对于标准I/O来说,使用b可以区分二进制和文本文件。但是对于UNIX来说,二进制和文本文件没有区别,有没有b无所谓。
  6. 当用追加写时,如果有多个进程用追加写方式打开同一个文件,每个进程的数据都会正确的写入文件中。
  7. fdopen不会截断也不会创建文件。对于fdopen来说,因为需要文件描述符,所以文件必须是打开的,当mode是w,wb时,并不会截断文件,a和ab也不能用于创建文件,因为文件描述符必须引用一个存在的文件。而如果使用a+, ab+, w+, wb+等,这个时候文件已经存在了,不会创建,也不会截断,需要写或者追加就行了,就不会有前半句说的问题了。
  8. 使用a和w相关的mode创建文件时,没有办法指定文件的权限位。而POSIX.1要求使用如下的权限创建文件:
    S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IOTH|S_IWOTH
    可以在使用fopen等函数之前,使用umask指定文件的权限位。
  9. 如果流引用终端设备,是行缓冲的,否则是全缓冲的。

fclose函数和性质

1
2
3
#include <stdio.h>

int fclose(FILE *fp);

在文件被关闭之前,flush输出数据。缓冲区中的输入数据被丢弃。如果标准I/O库为这个stream自动分配了缓冲区,释放该缓冲区。
当一个进程正常终止时,所有带未写缓冲数据的标准I/O都被flush,所有打开的标准I/O都被关闭。

读写stream

对于一个打开的stream,可以使用3种不同的类型的非格式化I/O以及格式化I/O,对其进行读写操作。
3种非格式化I/O包括:

  1. 单字符的I/O。如果流是带缓冲的,标准I/O会负责处理缓冲。
  2. 单行的I/O。这里需要注意一下,单行I/O指定的buffer和标准I/O的buffer不一样。
  3. 直接I/O(direct I/O)。

ferror和feof, clearerr函数和属性

不管是出错还是到达文件结束,getc,fgetc和ungetc等许多函数都返回同样的值EOF,EOF是-1,可以使用ferro和feof判断到底是出错还是到达文件尾端。大多数实现中是为每个流在FILE对象中维护了出错标志和文件结束标志,可以使用clearerr清除相应的标志。函数的原型如下:

1
2
3
4
5
#include <stdio.h>

void clearerr(FILE *stream);
int feof(FILE *stream);
int ferror(FILE *stream);

单字符I/O

getc, fgetc和getchar函数可以用于一个读一个字符。它们的原型如下:

getc, fgetc和getchar, ungetc原型

1
2
3
4
5
6
7
#include <stdio.h>

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

int ungetc(int c, FILE *stream);

getc, fgetc和getchar, ungetc性质

getc和fgetc功能一样,只不过getc可以被实现为宏,而fgetc不能被实现为宏。所以:

  1. getc的参数不应该是具有副作用的表达式,因为它可能会被计算多次。
  2. fgetc一定是函数,所以可以得到它的地址。可以当做参数传递给其他函数。
  3. fgetc的调用时间通常要比getc长,因为调用函数的时间通常比调用宏的时间长。

ungetchar函数和属性

  1. 从流中读取的数据可以送回流中。
  2. ISO C规定可以支持任何次数的回送,但是一次只能送一个字符。
  3. 回送的字符可以不是上次读到的字符。
  4. 回送的字符不能是EOF,但是读到文件尾端时,还可以回送一个字符,因为一次成功的ungetc调用会清除EOF标志。
  5. 用ungetc只能将字符写入到标准I/O库的流缓冲区中,并没有将它们写到底层设备或者文件中。

函数的原型如下:

1
2
3
#include <stdio.h>

int ungetc(int c, FILE* fp);

输出函数putc, fputc和putchar

它们的原型如下:

1
2
3
4
5
#include <stdio.h>

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c); //相当于putc(c, stdout);

单行I/O

fgets和gets,getline提供了单行输入的功能,单行I/O需要指定一个缓冲区,这个缓冲区是用户自己定义的,是应用程序级别的,它和标准I/O的buffer不一样,我们可以通过setbuf和setvbuf设置标准I/O的buffer,这是标准I/O即库函数层级的,而read和write等使用的buffer又是一类buffer,这是系统调用层级的,我们也可以自己指定。
它们的原型如下:

fgets和gets,getline原型

1
2
3
4
5
6
#include <stdio.h>

char *fgets(char *s, int size, FILE *stream);
char *gets(char *s);
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
ssize_t getdelim(char **lineptr, size_t *n, int delim, FILE *stream);

fgets和gets,getline性质

  1. gets从标准输入读,而fgets从指定的流中读
  2. gets不会读入'\n',而fgets, getline都会读入'\n';
  3. fgets需要指定缓冲的长度,遇到"\n"停止,但是不能超过n-1个字符,读入的字符送入缓冲区。缓冲区以NULL字节结尾,如果这一行包含最后一个换行符超过了n-1个字符,fgets只返回一个不完整的行,但是这一行还是以NULL结束,下一次调用继续从该行读。
  4. gets不推荐使用,因为没有指定缓冲区的长度,可能会造成缓冲区溢出,很危险。

fputs和puts原型

1
2
3
4
#include <stdio.h>

int fputs(const char *s, FILE *stream);
int puts(const char *s);

fputs和puts性质

  1. fputs将一个以NULL字节结束的字符串写到指定的流中,尾端的NULL不输出。这并不是每次输出一行,只有NULL前面的字节中包含'\n'时,才会输出一行。
  2. puts不会输出NULL字节,但是会自动将字符串后添加一个换行符。
  3. puts并不像gets那样不安全,但是因为自动加了换行符很难受。所以尽量使用fgets和fputs。

二进制直接I/O

除了可以以字符和行为单位进行读取,还可以使用二进制stream进行I/O。

fread和fwrite原型

1
2
3
4
#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

fread和fwrite性质

  1. fread和fwrite通常用来读写一个二进制数组或者一个结构体。ptr是要读写的首地址,size是每个对象的大小,nmemb是要写的对象的个数。
  2. fread和fwrite返回读写的对象数,读出错或者到达文件结尾,返回的数可以少于nmemb。可以调用ferror或者feof判断是结束还是出错。如果写返回的数值小于nmemb,那么就是出错。
  3. fread只能用于读在同一个系统上的数据,因为不同的系统上二进制文件的格式可能不同。
  4. fread和read的区别,read是系统调用,而fread是ISO C的函数。read的buf大小是字节,而fread的size是每个对象的大小,nmemb是对象的个数。

格式化I/O

除了三种非格式化的I/O,还有标准化I/O函数。标准化I/O函数需要指定格式说明。

输出格式说明

格式说明控制其余参数如何编写,以后如何限制。每个参数按照转换说明编写,转换说明以%号开始。除了转换说明外,格式化字符串中的其他字符都按照原样输出。
一个输出格式说明由四个可选部分构成:
%[flags][fldwidth][precision][lenmodifier] convtype

flags

  • ',将整数按千位分组字符
  • ‘-’,左对齐
  • +,显示带符号数的正负号
  • ,如果第一个字符不是正负号,在前面加上一个空格
  • #,指定另一种形式,比如0x指定十六进制
  • 0,添加前导0而不是空格进行填充

fldwitdth

最小宽度,多余字符用空格填充

precision

整形转换后最少输出数字位数
浮点数转换后小数点后的最少位数。
字符串转换后最大字节数

精度使用一个.,然后跟着一个可选的非负十进制整数或者x。

lenmodifier

l, ll , L分别表示long, long long以及long double。

convtype

  • d, i,有符号十进制
  • o,无符号八进制
  • u,无符号十进制
  • x, X,无符号十六禁止
  • f, F,双精度浮点数
  • e, E,指数形式双精度浮点苏
  • g, G
  • a, A,十六进制指数形式双精度浮点数
  • c,字符
  • s,字符串
  • p,指向void的指针
  • n,
  • %,一个%字符
  • C,宽字符,等于lc
  • S,宽字符串,等于ls

常见的格式化输出函数原型如下:

printf, frpintf, dprintf, snprintf和fpritnf原型

1
2
3
4
5
6
7
#include <stdio.h>

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

printf,frpintf, snprintf,dprintf和fpritnf性质

  1. printf将格式化数据输出到标准输出
  2. fprintf将格式化数据写到指定的流。
  3. dprintf将格式化数据写到指定的文件描述符。
  4. sprintf将格式化数据送入数组buf中,sprintf在数组的尾端自动加一个null字节,但是该字符不包含在返回值中。
  5. sprintf可能会造成buf指向的缓冲区溢出,调用者有责任保证该缓冲区足够大。
  6. snprintf是为了解决缓冲区溢出的问题而引入的,它需要显式指定缓冲区的长度,超过这个长度的话,输入数据都会被丢弃,同样ssprintf在数组的尾端自动加一个null字节,但是该字符不包含在返回值中。

输入格式说明

一个输入格式转换说明由三个可选部分:
%[*][fldwidth][m][lenmodifier] convtype

  1. fldwidth用于说明最大宽度
  2. lenmodifier说明要用转换结果赋值的参数大小,printf函数族支持的长度修饰符同样能够得到scanf函数族的支持。
  3. 而convtype符号和printf中类似,但是有一些区别。比如,输入中的带符号数可以复制给无符号类型。

scanf, fscnaf, sscanf原型

1
2
3
4
5
#include <stdio.h>

int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

scanf, fscnaf, sscanf性质

  1. scanf用于分析输入字符串,并将字符序列转换成指定类型的变量。格式后的各个参数给出了变量的地址,用转换结果对这些变量赋值。
  2. 格式说明控制如何转换参数,以便于对他们赋值,除了转换说明和空白字符外,格式字符串中的其他字符必须和输入匹配,如果有一个不匹配,就停止处理。

标准I/O效率

fgets, fgetc, getc, read这几个函数,哪个效率更高?
当他们同时读取一个300万行的98.5M的程序时,read效果最好。它们的系统CPU时间基本一样,但是用户CPU时间查了很多,以及等待I/O的时间也差了很多。为什么呢?

  1. 系统CPU时间相同,因为它们对内核提出的读写请求数基本相同。
  2. CPU时间差太多是因为,getc和fgetc需要进行上亿次的循环(上亿个字符),而fgets需要进行百万次的循环,而read只需要几万次(缓冲区大小设置为4096时)。
  3. fgetc和read缓冲区大小设置为1时,read要慢很多,因为read调用了两亿次系统调用,而fget调用了两亿次函数调用。系统调用的时间和各项开销要比函数调用大得多。

定位stream

有三种方法对I/O stream进行定位,分别是ftell和fseek,ftello和fseeko,fgetpos和fsetpos。它们的原型如下:

ftell, feek, ftello, fseeko和fgetpos, fsetpos原型

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);

int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);

int fseeko(FILE *stream, off_t offset, int whence);
off_t ftello(FILE *stream);

ftell, feek, ftello, fseeko和fgetpos, fsetpos性质

  1. ftell和fseek假设文件的位置可以存放在一个长整形中,而ftello和fseeko使用off_t代替了长整形。除此之外,它们完全相同。
  2. fgetpos和fsetpos是ISO C的标准,其他是SUS,所以跨平台时,使用fgetpos和fsetpos。
  3. 对于二进制文件,whence可以使用SEEK_SET, SEEK_CUR,这是跨平台的,而SEEK_END不是平台的。
  4. 对于文本文件,whence必须要用SEEK_SET且offset只能是0或者ftell返回的值。

实现细节

所有的standard I/O库都要使用到文件的I/O。每个I/O stream都有一个和其相关的文件描述符,可以使用fileno函数获得stream的文件描述符。

临时文件

ISO C提供了两个函数tmpnam和tmpfile帮助创建临时文件。它们的原型如下:

tmpnam和tmpfile原型

1
2
3
4
5
6
7
#include <stdio.h>

char *tmpnam(char *s);
FILE *tmpfile(void);

char *mkdtemp(char *template);
int mkstemp(char *template);

tmpnam和tmpfile性质

  1. tmpnam产生一个与现有文件名不同的一个有效路径名字符串。避免使用tmpnam。
  2. tmpfile创建一个临时二进制文件(wb+),在关闭文件或者程序结束时自动删除这个文件。注意UNIX对于二进制文件不做特殊区分。
  3. tmpfile函数经常使用的标准UNIX技术是先使用tmpnam产生一个唯一的路径名,然后使用它创建一个文件,并且立刻unlink它。注意,对一个文件unlink之后,如果链接计数等于0,并不立即删除它,因为可能有进程在使用这个文件,关闭文件时才删除文件。
  4. mkdtemp和mkstemp是XSI的扩展部分。
  5. mkstemp和mkdtemp都需要传入一个字符串,它的后六位设置为XXXXXX,函数通过将这些占位符替换成不同的字符构建一个唯一的路径名。如果只指定了名字,就创建在当前目录下。
  6. mkdtemp创建的目录的权限是S_IRUSR,S_IWUSR, S_IXUSR。mkstemp创建的文件的权限是S_IRUSR,S_IWUSR,可以使用umask进行修改。
  7. mkstemp创建的文件不会被自动删除。

内存stream

Standard I/O把数据缓存在内存中,因此字符和单行的I/O更有效一些,我们也可以使用setbuf和setvbuf让标准I/O库使用自己指定的缓冲区。
在SUS4之后添加了对memory streams的支持,这些standard I/O streams没有底层文件支持,但是仍然可以使用FILE指针访问,所有的I/O都是通过在缓冲区和主存中来回交换字节实现的。这些流虽然看起来像文件流,但是某些特征更像字符串操作。

有三个函数可以用于内存流的创建,它们分别是fmemopen,open_memstream和open_wmemstream。

fmemopen函数和属性

1
2
3
#include <stdio.h>

FILE *fmemopen(void *buf, size_t size, const char *mode);
  1. fmemopen函数open memory as stream
  2. fmemopen函数允许调用者提供缓冲区用于memory stream,size指定了缓冲区大小的字节数。如果buf为空,fmemopen会分配size字节数的缓冲区,流关闭时缓冲区会被释放。
  3. type和fopen的取值一样,总共有15种取值,

open_memstream和open_wmemstream函数和属性

1
2
3
4
5
6
7
#include <stdio.h>

FILE *open_memstream(char **ptr, size_t *sizeloc);

#include <wchar.h>

FILE *open_wmemstream(wchar_t **ptr, size_t *sizeloc);

它们一个面向字节,一个面向宽字节。它们和fmemopen之间的区别:

  1. 创建的流只能打开;
  2. 不能指定自己的缓冲区,但是可以访问缓冲区地址和大小。
  3. 关闭流后需要自己释放缓冲区
  4. 对流添加字节会增加缓冲区大小。
  5. 缓冲区地址和长度只有在调用fclose或者fflush后才有效。这些值只有在下一次流写入或者调用fclose前。

标准I/O的替代软件

标准I/O库的一个不足是效率不高。这和它复制的数据量有关。每当使用一次fgets和fputs时,通常需要复制两次数据,一次是在用户程序的行缓冲区和标准I/O缓冲区之间,一次是在内核和标准I/O缓冲区之间。
OK,这章我就认识到了这一个很重要的知识点。。
使用fgets需要用户指定fgets使用的缓冲区,或者使用getline,如果传入的指针指向NULL,getline会负责分配缓冲区大小。
标准I/O可以设置行缓冲和全缓冲,如果设置缓冲的话也需要一个缓冲区,通常是由系统指定的,当然也可以通过setbuf和setvbuf自己进行更改。而在setbuf中,如果buf是NULL的话,是关闭缓冲区,如果不为空的话,必须是BUFSIZ大小。在setvbuf中,通过mode指定缓冲区的类型,buf是NULL的话,库函数负责分配缓冲区。否则buf是多大就用多大的缓冲区。
直接使用系统调用read和write函数也需要设置缓冲区,这是系统调用级别的,大小可以任意指定,通常使用sturct stat.st_blksize的大小。标准I/O库用的缓冲区和read,write指的是一个(我自己的理解)。
内核中有buffer cache和page cache,调用write只是将数据复制到buffer cache和page cache,然后排入队列,实际的写磁盘操作可能在满足某个条件之后才实际写入磁盘。

1
2
3
4
5
6
7
8
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

# include <stdio.h>
void setbuf(FILE *stream, char *buf);
void setbuffer(FILE *stream, char *buf, size_t size);

char *fgets(char *s, int size, FILE *stream);

参考文献

1.《APUE》第三版
2.https://stackoverflow.com/questions/20937616/what-is-the-difference-between-a-stream-and-a-file

UNIX file and directory

发表于 2019-11-21 | 更新于 2019-12-17 | 分类于 UNIX

概述

这一节主要介绍文件系统和文件的一些属性,这些属性都存在stat函数给出的struct stat结构体中,struct stat的部分字段如下所示,接下来的小节会对这些字段进行详细的介绍:

struct stat

这个结构体包括:

  1. mode_t st_mode; 文件类型和文件权限控制
  2. ino_t st_ino; 文件存放的i-node号
  3. dev_t st_dev; 文件所在文件系统的设备号
  4. dev_t st_rdev; 特殊文件所在文件系统的设备号
  5. nlink_t st_nlink; 链接到文件指向的i-node节点的数量,即hardlink
  6. uid_t st_uid; 文件所有者的UID
  7. gid_t st_gid; 文件所有者的GID
  8. off_t st_size; 普通文件的字节数
  9. struct timespec st_atime; 最后一次access文件内容的时间
  10. struct timespec st_mtime; 最后一个modification文件内容的时间
  11. struct timespec st_ctime; 最后一次文件i-node 内容change的时间
  12. blksize_t st_blksize; 最合适的I/O block size
  13. blkcnt_t st_blocks; 分配了多少个disk blocks

文件系统

UNIX文件系统有多种实现,比如基于BSD的UNIX文件系统(UFS),linux的ext4文件系统,以及Mac OS X的HFS文件系统。本节拿UFS举例子。
一个磁盘可以有一个或者多个partition,每个partition可以包含文件系统也可以不包含。对于UFS文件系统,它由boot blocks,super block,cylinder groups组成。每一个cylinder group由super block copy, config info, i-node图,block bitmap以及i-nodes和data blocks组成。i-node节点是固定长度的记录项,它包含有关文件的大部分信息。

  1. i-node节点包含了文件的:文件类型,文件访问权限,文件长度,和指向文件数据的指针等,stat绝大部分的数据都取自i-node。
  2. data blocks中存放实际的数据,包含data blocks和directory blocks。data blocks存放文件的实际数据。目录也是文件,directory blocks也是data block,只不过它是存放目录文件中所有directory entry(目录项)的data blcok,一个directory entroy包括文件名和i-node编号。i-node节点编号的数据类型是ino_t。
  3. hard links:在每一个i-node中,都有一个链接计数,记录指向它的目录项个数,POSIX.1常量LINK_MAX指定了一个文件连接数的最大值。链接计数可以从st_nlink中得到,类型是nlink_t。hard link创建一个新的目录项指向链接目标的i-node节点,删除原来的目录项对新的目录项没有影响。只有当指向i-node节点的链接数等于0时且没有进程使用该文件时,可以删除该文件。
  4. symbolic links:symbolic links文件的实际内容,也就是存储在data blocks中的内容,是该symbolic link指向的文件的名字。symbolic link和hard link不一样,它有自己的i-node号,删除它指向的文件,这个symbolic link会失效,变成一个dead link。它的文件类型是S_IFLNK。
  5. 一个目录项不能指向另一个文件系统的i-node,所以hard link不能跨越文件系统。
  6. 在不更换文件系统的情况下为一个文件重命名的话。文件的实际内容没有变,只是构造了一个新的目录项,它的i-node就是待重命名的文件,然后删除老的目录项。链接的计数并不会改变,这就是mv命令的工作方式。
  7. i-node节点指向的数据块也可以存放目录项,在i-node的st_mode中可以确定它的类型是S_IFDIR。创建一个名为testdir的空目录,它的链接计数是2,一个是.目录,一个是testdir。testdir的上一级目录,它的链接计数是3(假设上一级目录只包含testdir),一个是这个目录的名字,一个是.,一个是testdir中的..。

关于linux文件系统,可以查看linux file system.

设备特殊文件

这一小节主要介绍st_dev和st_rdev字段。

  1. 每个文件系统所在的存储设备都由其主次设备号表示。设备号所用的数据类型是基本系统数据类型dev_t。主设备号标识设备驱动程序,次设备号标识特定的子设备。一个磁盘驱动器通常可以包含多个文件系统,同一个磁盘上的文件系统通常具有相同的主设备号,但是次设备号却不同。
  2. 每个文件名中的st_dev是存放该文件名和其对应i节点的文件系统的设备号。
  3. 可以使用两个宏major和minor访问主,次设备号,它们的参数都是st_dev。POSIX.1说明dev_t类型存在,但是没有定义它是什么,具体取值与实现相关。
  4. 只有字符特殊文件和块特殊文件才有st_rdev值,同样使用major和minor两个宏访问相应的主次设备号。
  5. 块特殊文件是包含随机访问文件系统的设备,比如硬盘驱动器,软盘驱动器和CD-ROM以及磁带等。

文件类型

文件类型信息包含在struct stat的st_mode字段中。UNIX系统的文件类型有以下几种:

  1. 普通文件,可以是二进制文件,也可以是文本文件。除了二进制可执行文件必须遵循标准化格式外,其他的文件对于UNIX内核来说基本上没有区别。使用S_ISREG宏进行判断。
  2. 目录文件,包含了其他文件的名字,以及指向这些文件有关信息的指针。对一个目录具有读权限的任意进程都可以读目录的内容,但是只有内核才可以直接写目录文件。使用S_ISDIR宏进行判断。
  3. block special file,提供对设备带缓冲的访问,每次访问以固定的长度进行。使用S_ISBLK宏进行判断。
  4. character special file,提供对设备不带缓冲的访问,每次访问长度可变。系统中的设备要么是block special file要是character special file。使用S_ISCHR宏进行判断。
  5. FIFO,用于进程间通信。使用S_ISFIFO宏进行判断。
  6. socket,用于进程间的网络通信。使用S_ISSOCK宏进行判断。
  7. symbolic link,指向另一个文件。使用S_ISLNK宏进行判断。

stat, fstat, fstatat, lstat

可以通过stat等函数获得文件(各类文件)的struct stat结构体,它们的原型如下:

函数原型

1
2
3
4
5
6
#include <sys/stat.h>

int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
int fstatat(int dirfd, const char *pathname, struct stat *statbuf, int flags);

性质

  1. stat将pathname指定的文件的有关信息存在放在statbuf中
  2. fstat将文件描述符指定的文件的有关信息存在放在statbuf中
  3. lstat和stat类似,但是当文件是一个symbolic link时,lstat返回symbolic link相关的信息,stat返回的是symbolic link链向的文件。
  4. fstatat相当于将前三个函数进行了整合:
    当dirfd设置为AT_FDCWD时或者pathname是绝对路径时,设置AT_SYSLINK_NOFOLLOW flags时,相当于lstat。
    当dirfd设置为AT_FDCWD时或者pathname是绝对路径时,AT_SYSLINK_NOFOLLOW flags不进行设置时,相当于stat。
    当dirfd既不是AT_FDCWD而且pathname不是绝对路径时,就是fstatat处理的情况了。

hard link和函数link, linkat, unlink, unlinkat和remove

任何一个文件可以有多个目录项指向它的i-node节点。可以使用link和linkat创建hard link,使用unlink和unlinkat, remove删除hard link。

hard link的特点

  1. 使用hard link链接的文件具有相同的inode和data block。
  2. 删除一个hard link不会影响其他具有相同inode号的文件。
  3. 只能对已经存在的文件创建hard link。
  4. 不能跨越文件系统创建hard link,因为每个文件系统的i-node节点都是单独的。
  5. 不能对目录创建hard link,可能会形成循环。

link和linkat的函数原型

1
2
3
4
#include <unistd.h>

int link(const char *oldpath, const char *newpath);
int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags);

link和linkat的函数性质

  1. link和linkat应该是原子操作。
  2. linkat和link类似。

unlink和unlinkat的函数原型

1
2
3
4
#include <unistd.h>

int unlink(const char *pathname);
int unlinkat(int dirfd, const char *pathname, int flags);

unlink和unlinkat的函数性质

  1. 如果pathname是hard link,这两个函数删除pathname的相应目录项,将pathname指向文件的i-node的链接数减一。
  2. 如果pathname是symbolic link,unlink删除的是symbolic link本身,而不follow它。
  3. 给出symbolic link时,没有任何函数能够删除symbolic link指向的文件。
  4. 如果链接数等于0,并且内核检查打开该文件的进程个数,如果它们都等于0。unlink会删除这个文件。如果链接数或者打开文件的进程个数大于0,不会对文件做修改。
  5. unlinkat和unlink类似。

remove的函数原型

1
2
3
#include <stdio.h>

int remove(const char *pathname);

remove的函数性质

  1. remove可以解除对文件或者目录的hard link。
  2. 作用于目录时,remove相当于rmdir。
  3. 作用于文件时,remove相当于unlink。

symbolic link和函数symlink,symbolinkat, readlink, readlinkat

hard links有一些限制:

  1. 链接和文件必须处于同一文件系统中,
  2. 只有root用户才能创建指向目录的链接。

symbolic link的特点

而symbolic link可以避开hard link的这些限制。

  1. symbolic可以看成文件的间接指针,它有自己的文件属性和权限,有自己的i-node号和data block,而所有指向一个i-node节点的hard link拥有相同的i-node。
  2. symbolic link对于它指向的对象没有任何文件系统的限制,任何用户都可以创建指向目录的symbolic link。
  3. 使用文件名字为参数调用文件函数时,需要知道这个函数是否处理symbolic link的。如果函数具有处理symbolic link的功能,那么它处理的是symbolic link指向的文件,否则它直接处理这个symbolic link本身。
    如下所示,是常见的函数是否处理symbolic link,fstat和fchmod因为处理的是文件描述符,这个文件描述符是否是symbolic link本身通常是由open决定的。
    access,chdir, chmod, chown,creat, exec, link, open, opendir, pathconf, stat, truncate跟随symbolic link。
    lchown,lstat, readlink, remove, rename, unlink不跟随symbolic link。
  4. 使用symbolic link可能在文件系统中引入循环。unlink不跟随symbolic link,所以使用unlink消除symbolic link。但是hard link的循环很难消除。这也是为什么link函数不允许构造指向目录的hard link的原因。
  5. symbolic link可以指向系统中并不存在的文件。所以creat的参数可以是symbolic link。

创建和读取symbolic link的函数原型如下:

symlink和symlinkat函数原型

1
2
3
4
#include <unistd.h>

int symlink(const char *target, const char *linkpath);
int symlinkat(const char *target, int newdirfd, const char *linkpath);

symlink和symlinkat函数性质

  1. linkpath和target不需要在同一个文件系统,而且target甚至可以不存在
  2. symlinkat和symlink类似。

readlink和readlinkat函数原型

1
2
3
4
#include <unistd.h>

ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
ssize_t readlinkat(int dirfd, const char *pathname, char *buf, size_t bufsiz);

readlink和readlinkat函数性质

  1. open函数跟随symbolic link,而readlink打开symbolic link本身,并且读取symbolic link中的名字
  2. readlink组合了open,close和read的所有操作。如果函数成功执行,返回读入buf的字节数。
  3. readlinkat和readlink类似。

和进程相关的UID和GID

每一个进程有6个或者更多和它相关的ID:

实际UID和GID

real UID和real GID,用来表示当前用户。

有效UID和GID

effective UID和effective UID,决定我们的文件访问权限。通常情况下,effective UID以及effective GID和real UID以及real GID一样。

Set-User-ID和Set-Group-ID

saved set-user-ID和saved set-group-ID,在执行一个程序时,包含了有效user ID和有效group ID的副本。
每一个文件都有一个UID和GID,它们的值在st_uid和st_gid中。
当执行一个程序文件时,进程的effective UID通常就是real UID,而effective GID通常就是real GID。但是可以在st_mode中设置一个特殊的flag,意思是当执行此文件时,将执行此文件的进程的effective UID设置为文件所有者的UID。同样,还有另一个特殊的flag,它将执行此文件的进程的effective GID设置为文件组所有者的UID。这两个标志位被记为set-user-ID bit和set-group-ID bit,它们都存放在st_mode中,可以使用S_ISUID和S_ISGID测试。
运行set UID程序的进程通常会获得额外的权限!!!所以要格外注意。

文件和目录的访问权限

st_mode中还包含了文件的访问权限。对于所有文件类型(不单单是文件和目录),都有三种访问权限:

r-读权限

读权限查询目录名内的数据。

w-写权限

  • 新建文件与目录
  • 删除文件或者目录
  • 重命名以及转移文件或者目录

x-可执行权限

  • 进入某目录
  • 切换到该目录(cd命令)
  • 对于只具有可执行权限的目录,可以使用cd进入该目录,也可以打开该目录的文件,或者进入下一级目录。但是需要我们知道它们的名字,不能使用ls命令查看,因为没有读目录的权限。。

!!!能不能进入某一目录只与该目录的x权限有关,如果不拥有某目录的x权限,即使拥有r权限,那么也无法执行该目录下的任何命令。
但是即使拥有了x权限,但是没有r权限,能进入该目录但是不能打开该目录,因为没有读取的权限,但是可以进入下一级目录或者打开当前目录的文件(因为不能读目录,所以需要知道下一级目录的名字或者当前目录下要打开的文件的名字),路径上的所有目录都必须有可执行权限。。

sticky bit

S_ISVTX较老版本的UNIX叫做表示stick bit,而新版UNIX叫做saved-text bit,这也是S_ISVTX名字的由来。这一位的作用是,当一个可执行文件的这一位被设置了,当该程序第一次被执行,在它终止时,程序正文部分的一个副本,即机器指令,仍然保存在交换区中,这使得下次执行该程序时能较快的载入内存。因为通常的UNIX文件系统中,文件的数据块都是随机存放的,相对来说,交换区被当做一个连续文件来处理。而现在的UNIX系统都使用了虚拟存储系统和快速文件系统,已经不需要这种技术了。
现在系统扩展了stick bit的使用,SUS允许对目录设置目录的stick bit,如果一个目录设置了stick bit,只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或者重命名该目录下的文件:

  1. 拥有该文件
  2. 拥有此目录
  3. 是root用户

比如/tmp目录,设置了stick bit,任何用户都可以在这个目录下创建文件。任意用户,组和其它对这两个目录的权限都是读写和执行。但是用户不能删除和重命名属于其他人的文件。

十二个访问权限位

将rwx和user, group以及other进行组合,总共有九个访问权限位,再加上S_ISUID,S_ISGID和S_ISVTX三个特殊位:

  • S_ISUID,set user id,
  • S_ISGID, set group id,
  • S_ISVTX, stick bit,
  • S_IRUSR, user read,
  • S_IWUSR, user write,
  • S_IXUSR, user exectu,
  • S_IRGRP, group read,
  • S_IWGRP, group write,
  • S_IXGRP, group exectuble,
  • S_IROTH, other read,
  • S_IWOTH, other write,
  • S_TXOTH, other exectuble,

可以对最后九项做一个简洁版的表示:
S_IRWXU = S_IRUSR|R_IWUSR|S_IXUSR
S_IRWXG = S_IRGRP|S_IWGRP|S_IXGRP
S_IRWXO = S_IROTH|S_IWOTH|S_IXOTH

文件和目录的操作规则

  1. 使用名字打开任意类型的文件时,对于文件名字中包含的每一个目录,包括当前工作目录,都应该具有执行权限。
  2. 对于一个文件的读权限决定了我们能够打开先有文件进行读操作。这与open函数的O_RDONLY和O_RDWR有关。
  3. 对于一个文件的写权限决定了我们能够打开先有文件进行写操作。这与open函数的O_WRONLY和O_RDWR有关。
  4. 如果要在open函数中指定O_TRUNC标志,必须对该文件拥有写权限。
  5. 为了在一个目录中创建新文件,必须对这个目录具有写权限和执行权限。
  6. 为了删除一个现有文件,必须对包含该文件的目录拥有写权限和执行权限,而不必对文件本身拥有读权限和写权限。
  7. 如果使用7个exec函数中的任何一个执行某个文件,都必须拥有该文件的写权限。

访问权限检测

进程每次打开,创建或者删除一个文件时,内核就会进行文件访问权限测试,这种测试可能涉及到文件的UID,文件的组GID,进程的effective UID和effective GID。文件的UID和文件的GID都是文件的属性,而effective UID和effective GID是进程的属性。
内核进行访问权限测试的步骤如下:

  1. 如果进程的effective UID是0,结束权限判断,允许各项访问。否则跳转第2步进行判断。
  2. 如果进程的effectiev UID等于文件UID,也就是st_uid,结束权限判断,根据访问权限位允许相应操作。否则跳转第3步。
  3. 如果进程的effective GID等于文件的GID,结束权限判断,根据访问权限的设置允许相应的操作,否则跳转第4步。
  4. 如果不满足前三条,就按照若其他用户的访问权限位判定操作是否合法。

总结一下,就是依次判断effective UID是不是等于root,effective UID是不是等于st_uid,或者effective GID是不是等于st_gid,如果都不满足,就按照其它权限判定当前进程对文件的操作是否被允许。按照顺序来判断,满足一个就不用判断后面的了。

函数umask

前面介绍了和文件相关的9个访问权限位,使用进程创建文件时,文件的权限可以由umask修改,相当于在原有的mode上减去umask指定的mode。函数umask的原型如下:

函数原型

1
2
3
#include <sys/stat.h>

mode_t umask(mode_t cmask);

特点

  1. 这个函数的作用是去掉cmask中指定的权限,返回之前的mode。
  2. 在程序中使用open和creat等创建新文件时,如果想要确保指定的访问限权被激活,必须在进程运行时修改umask的值。否则,umask可能会覆盖掉我们创建文件时指定的mode。
  3. shell中有内置的umask命令,SUS要求shell的umask除了支持八进制的拒绝权限外,还要支持符号格式的指定许可的权限。使用
    1
    umask -S

查看。

新文件和目录的UID和GID

创建文件时,新文件的UID被设置为进程的effective UID。关于新文件的GID,可以选择以下两种方式中的一个进行设置:

  1. 新文件的GID可以是进程的effective GID
  2. 新文件的GID可以是它所在目录的GID。

不同的UNIX实现有不同的设置,这里拿linux来说,Linux 3.2.0以后,新文件的GID取决于它所在目录的set-group ID bit是否被设置,如果被设置了,新文件的ID就是它所在目录的GID,否则就是进程的effective GID。
新目录的UID和GID和文件一样。

函数mkdir,mkdirat和rmdir

可以使用mkdir和mkdirat创建目录,使用rmdir`删除目录。它们的原型如下:

函数原型

1
2
3
4
5
#include <sys/stat.h>

int mkdir(const char *pathname, mode_t mode);
int mkdirat(int dirfd, const char *pathname, mode_t mode);
int rmdir(const char *pathname);

性质

  1. mkdir创建一个空目录,自动包含.和..项。文件的访问
  2. 目录的权限默认权限是进程的umask给出的。对于目录,我们至少需要指定execute位。
  3. 目录的UID和GID跟进程create文件时一样。
  4. mkdirat和mkdir类似。
  5. rmdir删除目录。

函数access和faccessat

使用open函数打开文件时,内核使用进程的effective UID和effective GID检测它对文件的访问权限。
acess使用进程的real UID和real GID进行权限访问测试。访问权限测试步骤和之前介绍的四步一样,只不过使用real UID和real GID代替了effective UID和effective GID。

函数原型

access和faccessat的原型如下:

1
2
3
4
#include <unistd.h>

int access(const char *pathname, int mode);
int faccessat(int dirfd, const char *pathname, int mode, int flags);

参数和区别

  1. mode可选参数有,F_OK,R_OK, W_OK,X_OK,其中F_OK表示测试这个文件是否存在。
  2. faccessat和access在两种情况下相同,dirfd设置为AT_FDCWD且pathname四相对路径和pathname是绝对路径。否则的话,faccessat就是测试相对于dirfd指向的打开目录下的pathname的权限。
  3. 如果flags设置为AT_EACCESS的话,权限访问检测使用的是effective UID和effective GID而不是real UID和real GID。

函数chmod, fchmod和fchmodat

文件的访问权限可以使用chmod, fchmod和fchmodat进行修改,它们的原型如下:

原型

1
2
3
4
5
#include <sys/stat.h>

int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int dirfd, const char *pathname, mode_t mode, int flags);

特点

  1. chmod在指定的文件上进行操作
  2. fchmod是对已经打开的文件文件描述符进行操作。
  3. fchmodat和chmod在两种情况下是相等的,当pathname是绝对路径时,以及dirfd设置为AT_FDCWD且pathname是相对路径的时候。否则,fchmodat操作相对于打开目录的pathname。
  4. 当flags设置了AT_SYMLINK_NOFLOLLW时,不会followsymbolic link。
  5. 在以下两种情况下,chmod函数自动清除两个权限位:
    • 新创建文件的GID可能不是调用进程的effective GID。新文件的GID可能是父目录的GID。如果新文件的GID不等于进程的effective GID,而且进程没有root权限,set-group-id位会被自动关闭。
    • stick bit的设置

函数chown, fchown,chownat和lchown

可以使用chown, fchown,chownat和lchown更改文件的UID和GID。它们的原型如下:

函数原型

1
2
3
4
5
6
#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 lchown(const char *pathname, uid_t owner, gid_t group);
int fchownat(int dirfd, const char *pathname, uid_t owner, gid_t group, int flags);

特点

  1. 当文件不是symbolic link时,所有的函数操作类似。
  2. 当文件是symbolic link时,lchown和设置了AT_SYMLINK_NOFOLLOW标志的fchownat都修改的是symbolic link本身,而不是它链接的对象。
  3. fchown操作的是fd指向的已经打开的文件,因为它在一个已经打开的文件上操作,所以它不能修改symbolic link本身。
  4. fchownat和fchown或者lchown在下面两种情况下是等价的:当pathname是决定路径或者dirfd设置为AT_FDCWD时且pathname是相对路径时。在这两种情况下,如果flags设置了AT_SYMLINK_NOFOLLOW标志,fchownat和lchown一样,否则fchownat和fchown一样。如果不是这两种情况,fchowat是操作相对于打开目录的pathname。
  5. 根据_POSIX_CHOWN_RESTRICTED常量的值,可以查询是否只有超级用户才能更改文件的所有者,如果这个常量对指定的文件有效,那么
    • 只有root进程才能更新该文件的UID,普通用户不能修改其他用户文件的UID
    • 如果进程拥有此文件(进程的effective UID等于文件的UID), 可以更改这些文件的GID,但是只能更改到进程的effective GID或者继承的附属组ID之一。
  6. 如果这些函数由非root进程调用,在成功返回时,set UID和set GID位都会被清楚。

函数rename和renameat

可以使用函数rename和rename对文件或者目录进行重命名。它们的原型如下:

函数原型

1
2
3
4
#include <stdio.h>

int rename(const char *oldpath, const char *newpath);
int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath);

性质

  1. ISO C对文件定义了reanme,但是ISO C没有对目录定义该函数。POSIX.1扩展了定义,使得它rename可以处理目录和symbolic link。
  2. 如果oldpath指向普通文件,那么为文件进行重命名。如果newname存在,它不能是一个目录。如果它不是目录,先将该目录项删除,然后将oldname重命名为newname。进程需要对oldpath和newpath的父目录都拥有写和执行权限,因为rename会修改这两个目录。
  3. 如果oldpath指向目录,为目录重命名。如果newname已经存在,那么它必须是一个目录,而且应该是空目录。如果newname目录存在且为空,先将它删除,然后将oldname重命名为newname。且newname不能包含oldname,比如不能将/usr/foo重命名为/usr/foo/testdir,因为不能删除/usr/foo目录。
  4. 如果oldpath指向symbolic link,rename不follow symbolic link,直接处理symbolic link本身。
  5. 不能对.和..进行重命名。或者说.和..不能出现在oldpath和newpath上。
  6. 当oldpath和newpath指向同一个文件,函数不做任何操作直接返回。
  7. renameat和rename类似。

文件长度

struct stat中的st_size以字节为单位表示文件的长度。这个字段只对普通文件,目录文件和symbolic link有意义。
对于普通文件,它的长度可以是0,在开始读这种文件时,得到EOF标志。
对于目录,文件长度通常是一个数的整数倍。
对于symbolic link,文件长度是链接指向的文件名的实际字节数,不包含null字节。
对于现在的大多数UNIX系统,提供了st_blksize和st_blocks字段。st_blksize表示对文件I/O适合的块长度,第二个是所分配的固定大小的block数量。

文件中的hole

  1. 普通文件可以有hole,hole是当前文件偏移量超过文件尾端,然后进行写入造成的。
  2. 对于同样长度的有空洞和没有空洞的文件来说,它们所占用的blocks块数是不同的。这里说的同样长度指的是字节数,使用ls -l列出来的长度,那些空洞不占用磁盘上的存储区。
  3. 使用cat进行复制时,会将空洞使用0字节填满。
  4. 使用du -s file.txt可以查看文件所占用的blocks数量,这些blocks中还有一些用来存放指向实际数据块的指针。

文件截断

可以使用truncate和ftruncate截断文件,函数原型是:

函数原型

1
2
3
4
5
#include <unistd.h>
#include <sys/types.h>

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

特点

  1. 当length长度小于原来文件的长度时,超过length的进行截断。
  2. 当length长度大于原来文件的长度时,原来的文件长度到length之间的数据被读作0。
  3. ftruncate操作的是文件描述符,truncate操作的是文件。

文件的时间

文件的三个时间

每一个文件守护三个时间:

  1. st_atim,记录文件数据的最后访问时间。使用ls -u查看。
  2. st_mtim,记录文件数据的最后修改时间。默认的ls显示的就是文件的最后修改时间。
  3. st_ctim,记录i-node状态的最后更改时间。使用ls -c查看。常见的许多操作都会影响i-node,主要就是i-node中存放的那些信息,更改文件权限,更改文件的st_uid和st_gid,文件的链接数等等。

这个让我想不明白的是为什么link,unlink,creat等会影响所使用文件的父目录的inode节点。

函数futimens, utimensat和utimes原型

1
2
3
4
5
6
7
8
#include <sys/stat.h>

int utimensat(int dirfd, const char *pathname, const struct timespec times[2], int flags);
int futimens(int fd, const struct timespec times[2]);

#include <sys/time.h>

int utimes(const char *filename, const struct timeval times[2]);

函数futimens, utimensat和utimes性质

  1. 如果times为空,访问时间和修改时间都设为当前时间

  2. 如果times指向两个timespec结构的数组,任一元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳设置为当前时间,忽略相应的tv_sec字段。

  3. 如果times指向两个timespec结构的数组,任一元素的tv_nsec字段的值为UTIME_OMIT,相应的时间戳保持不变,忽略相应的tv_sec字段。

  4. 如果times指向两个timespec结构的数组,tv_nsec字段的值既不是UTIME_NOW也不是UTIME_OMIT,相应的时间戳设置为相应的tv_sec和tv_nsec字段。

  5. futimens需要打开文件更改它的时间

  6. utimensat可以使用文件名更改时间。

  7. utimes对路径进行操作。

  8. struct timespec结构体是:

    1
    2
    3
    4
    struct {
    time_t tv_sec;
    long tv_nsec;
    };

  9. struct timeval 结构体是:

    1
    2
    3
    4
    struct timeval{
    time_t tv_sec; //秒
    long tv_usec; //微妙
    };

读目录

目录可以被任何具有读权限的用户读取,但是为了保护文件系统,只有kernel可以写目录。目录的w权限位和x权限位表示的是用户是否可以在目录中创建或者删除新文件,并不是说用户可以写目录本身。

给出opendir, fopendir,

函数原型

1
2
3
4
5
6
7
8
9
10
#include <dirent.h>

DIR *opendir(const char *name);
DIR *fdopendir(int fd);

struct dirent *readdir(DIR *dirp);
void rewinddir(DIR *dirp);
int closedir(DIR *dirp);
long telldir(DIR *dirp);
void seekdir(DIR *dirp, long loc);

函数性质

  1. opendir将文件转换成目录处理函数需要的DIR结构,而fdopendir将文件描述符转换成目录处理函数需要的DIR结构。这里的DIR到底是什么,没有告诉,so上有一个回答说它是不透明数据类型,只需要使用它,不需要知道它的定义[3]。
  2. telldir和seekdir不是POSIX.1的组成部分,但是它是SUS的XSI扩展。所有UNIX系统都会提供这两个实现。
  3. struct dirent结构体定义在<dirent.h>头文件中,它与实现相关,但是必须包含以下两个成员:
    1
    2
    ino_d d_ino;
    char d_name[];

POSIX.1并没有定义d_ino,而POSIX.1的XSI扩展定义了d_ino。

  1. DIR是一个内部结构,这几个函数使用这个内部结构保存当前正在被读的目录的有关信息。opendir和fdopendir返回DIR,而其他函数把DIR当做参数。
    使用opendir打开文件时,readdir返回的是目录项中的第一项。
    使用fdopndir打开文件时,readdir返回的第一项取决于传递给fopendir的文件描述符的当前文件偏移量。

函数chdir, fchdir和getcwd

每一个进程都有一个当前工作目录,**当前工作目录是进程的一个属性,**这个目录是搜索所有相对路径名的起点,不以"/"开始的路径名都是相对路径名。可以使用chdir后者fchdir改变当前进程的工作目录。它们的原型如下:

1
2
3
4
5
6
#include <unistd.h>

int chdir(const char *path);
int fchdir(int fd);

char *getcwd(char *buf, size_t size);

chdir的参数是path,而fchidir的参数是文件描述符。

chdir只会改变当前进程的work dir

**需要注意的一点是,chdir和fchdir只改变调用这个函数本身的进程,并不影响其他进程。**比如在shell中运行一个程序,在这个程序中更改了进程的当前工作目录,结束这个程序的执行时,shell的当前工作目录并不会改变,因为shell和我们刚才执行的程序属于两个不同的进程。因此,如果要改变shell进程自己的工作目录,应该使用shell直接调用chdir函数,所以cd命令内建在shell中。
getcwd是获得进程当前工作的绝对路径名。内核并不保存目录的完整路径名(linux除外),为了获得进程当前工作的绝对路径名。getcwd需要从当前工作目录开始,找到它的上一级目录,读取目录项,找到和工作目录i节点编号相同的目录项,得到对应的文件名。就这样一层一层的向上找,这就找到了绝对路径名。getcwd会followsymbolic link,但是不会知道它是由哪里链接到这里的。

和进程相关的目录getcwd

当一个应用程序需要在经过一些列目录操作之后返回它刚开始的工作目录时。可以先使用getcwd获得最开始的工作目录,保存起来,最后再使用chdir进行恢复。
fchdir可以有更简单的操作,在刚开始时,保存目录的文件描述符。最后使用fchdir直接打开这个文件描述符。

参考文献

1.《APUE》第三版
2.https://unix.stackexchange.com/questions/21251/execute-vs-read-bit-how-do-directory-permissions-in-linux-work
3.https://superuser.com/questions/168578/why-must-a-folder-be-executable/168583
4.https://stackoverflow.com/questions/22122405/where-is-dir-defined

data structure bineary search trees vs binary heaps

发表于 2019-11-20 | 更新于 2019-12-17 | 分类于 数据结构

参考文献

1.https://stackoverflow.com/questions/6147242/heap-vs-binary-search-tree-bst
2.https://cs.stackexchange.com/questions/27860/whats-the-difference-between-a-binary-search-tree-and-a-binary-heap

UNIX file I/O

发表于 2019-11-18 | 更新于 2019-11-24 | 分类于 UNIX

注意事项

  1. creat以只写方式打开文件,不能进行读操作。
  2. 为什么有了open要有creat,早期的open只支持0,1,2三个flag,不能打开不存在的文件,需要有单独的系统调用创建文件。而有了新的open以后就不需要creat了。
  3. open和dup返回的文件描述符一定是最小的未使用的文件描述符。
  4. 以O_APPEND打开的文件,如果进行write的话,即使使用lseek定位到非文件结尾处,最后也是在文件结尾处进行写。因为使用O_APPEND的write是由两个系统调用函数lseek和“普通的”write构成的一个操作。而read操作可以使用lseek进行定位。
  5. 所有的磁盘I/O都要经过内核的block buffers块缓存区,也称为内核的(buffer cache)缓冲区高速缓存。有一个例外就是对原始磁盘设备的I/O,先不考虑这种情况。read和write的数据都要被内核进行缓冲,术语unbuffered I/O指的是在用户的进程中不会对这两个函数进行自动缓冲,每次read和write都会进行一次系统调用。

文件I/O

UNIX系统中的大多数文件I/O只用到了5个函数:open,read,write, lseek和close。不同的缓冲长度对read和write的速度影响。
本章介绍的函数通常被称为不带缓冲的I/O,不带缓冲的I/O指的是每个read和write都调用内核中的一个系统调用,它们不是ISO C的组成部分,但是,它们都是POSIX.1和SUS的组成部分。
只要涉及在多个进程之间共享资源,原子操作的概念就非常重要。本章还进行一步讨论在多个进程之间如何共享文件,以及所涉及的内核有关数据结构。相应的函数有:dup, fcntl,sync, fsync和ioctl等。

文件描述符

对于内核而言,所有打开的文件都是通过文件描述符引用。文件描述符是一个非负整数。当打开或者创建一个新文件时,内核向进程返回一个文件描述符。当读,写一个文件时,使用open或者creat返回的文件描述符标识该文件,将其作为参数传递给read或者write。
UNIX系统shell把文件描述符0和进程的标准输入关联,文件描述符1和标准输出关联,文件描述符2和标准错误关联。为了提高系统的可读性,通常把它们换成符号常量STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO,它们都在头文件<unistd.h>中定义。
文件描述符的变化范围是0到OPEN_MAX-1,早起的UNIX系统实现采用的上限值是19,现在的很多系统将它增加到63。(对于Linux, FreeBSD等的很多版本,文件描述符的变化范围几乎是无限的,只受到硬件资源的约束)

open,openat和creat, close

函数原型:

1
2
3
4
5
6
7
8
9
10
11
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

int creat(const char *pathname, mode_t mode);

int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);

参数pathname是路径名,可以是相对路径也可以是绝对路径,flags的选项有很多,它们定义在<fcntl.h>头文件中。flag参数必须在O_RDONLY,O_WRONLY和O_RDWR之中选且只能选一个。然后还有很多其他的可选flag,常见的有:O_APPEND,O_CREAT,O_EXCL, O_DIRECTORY等,使用man 2 open就可以查看。

openat vs open

open和openat返回的一定是最小的没有使用的文件描述符。可以利用这一点可以在标准输入,标准输出,或者标准错误上打开新的文件。一个应用程序可以先关闭标准输出,然后打开另一个文件,执行打开操作前就能了解到该文件一定会在文件描述符1上打开。
dirfd参数是open和openat的区别,它们之间的关系有以下三种:

  1. pathname指定的是绝对路径名,dirfd参数被忽略,open和openat一样。
  2. pathname指定的是相对路径名,dirfd制定了相对路径名在文件系统中的开始地址,dirfd参数通过打开相对路径名的目录来获取。
  3. pathname指定了相对路径名,dirfd的参数是特殊值AT_FDCWD,这种情况下,路径名是在当前工作目录中获取,openat和open在操作上类似。

openat作用

为什么增加openat函数,它的目的是解决两个问题:

  1. 让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。同一进程中的所有线程共享相同的当前工作目录。
  2. 避免time-of-check-to-time-of-use错误。它的基本思想是,如果有两个基于文件的系统调用,第二个调用的结果依赖于第一个调用的结果,那么程序是脆弱的。因为两个调用并不是原子操作,在两个函数调用之间,文件可能改变了,这就造成了第一个调用的结果不再有效,使得程序的最终结果是错误的。

文件名和路径名过长

当文件名和路径名过长时,是截断为系统允许的最长量还是返回出错信息?这个是由系统的历史形成的。通常BSD和Linux总是会返回出错,而System V和Solaris等不一定。
具体的可以根据POSIX.1定义的常量_POSIX_NO_TRUC决定是截断还是出错。根据文件系统的类型,这个值可以变换。可以使用fpathconf或者pathconf查询目录具体支持哪种行为。
如果_POSIX_NO_TRUC有效,当路径名超过PATH_MAX或者路径名中的任一文件名超过NAME_MAX时,返回出错,并将errno设置为ENAMETOOLONG。

create和open

create其实相当于指定了open的flags为O_WRONLY|O_CREAT|O_TRUNC。
为什么有了open还要有creat,在早期的UNIX版本中,flags只能为0,1或者2。无法打开一个不存在的文件。因此需要另一个系统调用creat创建新文件。现在的open系统调用提供了O_CREAT和O_TRUNC选项,也就不需要creat了。

creat的不足:creat以只写方式打开所创建的文件,即创建新文件之后,只能对新文件进行写操作,不能进行读操作。如果要创建一个临时文件,先写文件,然后再读文件。在open的老版本时,即不能打开不存在的文件时,需要先使用creat创建新文件,然后关闭该文件,然后使用open读文件。现在的话,可以使用以下方式实现创建新文件并进行读写:

1
open(path, O_RDWR|O_CREAT|O_TRUNC, mode);

close函数

调用close关闭一个已经打开的文件。函数原型:

1
2
3
#include <unistd.h>

int close(int fd);

创建文件

  1. 创建一个只写文件(如果文件存在,将文件清零):

    1
    2
    creat(filename, mode);
    open(filename, O_WRONLY|O_CREAT|O_TRUNC, mode);

  2. 创建一个读写文件(如果文件存在,将文件清零):

    1
    open(filename, O_RDWR|O_CREAT|O_TRUNC);

  3. 检测并创建一个只写文件( 如果文件存在,错处,返回-1):

    1
    open(filename, O_WRONLY|O_CREAT|O_EXCL, mode);

  4. 检测并创建一个读写文件( 如果文件存在,错处,返回-1):

    1
    open(filename, O_RDWR|O_CREAT|O_EXCL, mode);

lseek, read, write

它们的原型如下所示:

1
2
3
4
5
6
#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

当前文件偏移量

  1. 每一个打开文件都有一个和它相关联的当前文件偏移量(current file offset)。它通常是一个非负整数,用来度量从文件开始处的字节数。
  2. 通常情况下,读写操作都是从current file offset开始的,并且使偏移量增加读写的字节数。
  3. 除了指定O_APPEND选项外,打开一个文件时,默认的current file offset都是0。
  4. whence有三个取值,SEEK_SET, SEEK_CUR, SEEK_END。lseek成功执行,返回的offset等于whence+offset,对于SEEK_CUR和SEEK_END来说,参数offset可正可负,只要保证返回的current file offset非负即可。
  5. lseek中的l表示long。
  6. current file offset可以大于文件长度,这种情况会在文件中构成一个空洞。空洞不要求占据磁盘上的存储区。

read

1
ssize_t read(int fd, void *buf, size_t count);

read函数从文件描述符标示的文件中读取至多count个字节到buf指定的位置。如果操作成功的话,返回读到的字节数,如果已经到了文件结尾,返回0。出错的话,返回-1,设置errno。
在以下几种情况下,读到的字节数可能少于count:

  1. 读普通文件时,在读满count个之前就已经到了文件尾端。
  2. 某个信号造成中断时,而已经读取了部分数据时。
  3. 从终端设备读时,通常一次最多读一行。
  4. 从网络读时,网络中的缓冲机制。
  5. 从管道或者FIFO读取时,管道包含的字节数少于count。
  6. 从面向记录的设备读时,一次最多返回一个记录。
  7. 第二个参数void*表示通用指针。
  8. 返回值ssize_t是有符号类型,因为它需要返回正整数字节,0和-1。
  9. 第三个参数size_t是一个无符号类型。

write

1
ssize_t write(int fd, const void *buf, size_t count);

它的返回值通常和count一样,否则就是出错了。出错的常量原因有:

  1. 磁盘满了,
  2. 超过了一个给定进程的文件长度限制。
  3. 如果lseek返回的当前文件偏移量不在文件结尾,write会覆盖掉相应位置的数据。

对于普通文件,写操作从文件的当前偏移量开始,如果打开文件时指定了O_APPEND选项,那么文件偏移量设置在文件结尾处。在一次写成功之后,文件偏移量增加实际写的字节数。

创建含有空洞的文件

当lseek使得当前文件偏移量超过了现有文件长度,再继续进行write之后,当前文件偏移量和文件长度之间的内容就是空洞,它一般不占用磁盘空间,但是使用ls时,会把它计算成字节长度。

I/O的效率

进程终止时,UNIX系统内核会关闭所有打开的文件描述符,但是并不会关闭标准输入和输出。
在选取read和write的buffer大小时,也有一定技巧。大多数文件系统都使用了预读(read ahead)技术。当进行顺序读取时,系统试图读入比应用所要求的更多数据,并且假设应用很快就会读这些数据。
在使用ext4文件系统时,它的磁盘块长度是4096,所以当BUFFER大于等于4096时,读写时间几乎不变。

文件共享

I/O数据结构

UNIX支持在不同进程之间共享打开文件。这需要使用到内核用于I/O的数据结构。内核使用三种数据结构表示打开文件:进程表记录项,文件表项和节点表项。

  1. 进程表记录项。每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,每隔描述符占用一项,其中内容有:文件描述符标志和指向文件表项的指针。
  2. 文件表项。内核为所有打开文件维持一张文件表。每个表项包含:文件状态标志,当前文件偏移量和指向该文件节点表项的指针
  3. 节点表项。每个打开设备都有一个节点结构。包含文件的所有者,文件长度,指向文件实际数据块在磁盘上所在的指针等。

如果两个进程打开了同一个文件,每个进程都会获得各自相应文件的一个文件表项,这两个文件表项中的节点表项指针指向同一个节点表项。也有可能多个进程的文件描述符指向同一个文件表项。
自己的总结,每一个文件都有一个节点表项,记录文件长度和数据存储地址,而文件表项记录的是在节点表项的哪个位置进行什么操作,进程表记录项记录了每个进程打开了几个文件,每个文件的文件表项在哪里。

write和lseek对当前文件偏移量的影响

  1. write在写入完成后,在文件表项的当前文件偏移量上加上写入的字节数,如果当前文件偏移量超过了当前文件长度,更新节点表项中的文件长度,相当于文件长度增加了。
  2. lseek只修改文件表项中的当前文件偏移量,不进行任何I/O操作。
  3. lseel定位到文件尾端的时候,文件表项中当前文件偏移量被设置为节点表项中的当前文件长度。
  4. 使用O_APPEND打开文件的时候,文件表项中的文件状态标志也会被修改,对于使用O_APPEND操作打开的文件,进行write操作相当于先将当前文件偏移量设置为节点表项中的文件长度,然后再write,即使先使用lseek将当前文件偏移量设置为SEEK_SET也不行,也是进行追加。所以在每次append之前不用先进行lseek,lseek了也白做。但是read可以使用lseek正常进行。

原子操作

如果一个操作是原子操作,那么这个操作的所有步骤要么不执行,要不全部执行。

追加文件

指定open的O_APPEND选项实现追加操作,

1
2
fileno=open(filename, O_RDWR|O_APPEND);
write(file, buf, BUFSIZE);

追加文件是一个原子操作,如果不是原子操作的话,就相当于:

1
2
3
fileno = open(filename, O_RDWR);
lseek(fileno, 0, SEEK_END);
write(fileno, buf, BUFSIZE);

如果是单进程,上面两段代码是等价的,但是如果是多进程的话,下面代码就可能会出错。进程A lseek,进程B lseek,进程A write,进程B write。进程B的操作会覆盖进程A的操作。
所以这也就解释了使用选项O_APPEND后的操作,因为这个append的write是由两个系统调用组成的原子操作,先lseek,再普通的write。所以在调用write之前不用lseek,就算你lseek了也是白lseek。

读写原子操作

读写的原子操作原型如下:

1
2
3
4
#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

调用pread相当于调用lseek和read的原子操作,但是pread不改变当前文件偏移量。
调用write相当于调用lseek和write的原子操作,但是pwrite不改变当前文件偏移量。

创建文件原子操作

检查文件是否存在和创建文件是一个原子操作。如果这个操作不是原子操作,比如说是由open和creat两个函数调用组成的一个操作,它们不是一个原子操作。当前进程确定一个文件不存在,决定创建该文件。在open和creat调用之间,另一个进程创建了这个文件,并写入了数据。当前进程会再次创建这个文件,覆盖掉另一个进程写入的数据。

dup和dup2复制文件描述符

UNIX系统提供了两个原子操作dup和dup2对一个指定的文件描述符进行复制。如果得到的新文件描述符和fd不同,那么这两个文件描述符共享同一个文件表项。

1
2
3
4
5
6
7
8
9
#include <unistd.h>

// dup返回的文件描述符一定是当前可用文件描述符中的最小值。和open一个文件类似。
int dup(int fd);

//可以通过fd2指定返回的新的文件描述符。
// 如果fd2和fd相等,返回fd2
// 如果fd2和fd不等,关闭fd2,然后返回fd2。
int dup2(inf fd, int fd2);

非原子操作的文件描述符复制可以通过fcntl实现。

sync,fsync和fdatasync

UNIX系统在内核中设置了缓冲区高速缓存或者页高速缓存,大多数磁盘的I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些写入磁盘,这方方式叫做延迟写。
等到内核需要使用缓冲区存放其他磁盘块数据时,它会把所有延迟写数据写入磁盘。为了保证磁盘上实际文件系统和缓冲区中内容的一致性,UNIX提供了三个函数,它们的原型如下:

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>

// 1.将所有修改过的块缓冲区排入队列,然后就返回,并不等待实际写磁盘操作结束。
// 通常情况下,update系统守护进程一般每隔30秒调用一次`sync`函数,这就保证了定期将内核块缓冲区的内容写入磁盘。命令sync(1)也会调用`sync`函数。
void sync(void);

// 2.只对文件描述符`fd`指定的一个文件起作用,等到写磁盘操作结束才返回。更新文件的数据和属性。
int fsync(int fd);

// 3.只对文件描述符`fd`指定的一个文件起作用,等到写磁盘操作结束才返回。只更新文件的数据。
int fdatasync(int fd);

fcntl

fcntl是文件控制函数,它的原型如下:

1
2
3
4
#include <unistd.h>
#include <fcntl.h>

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

fcntl有很多种功能,这一节先介绍以下五种:

  1. 复制一个已有的描述符,设置cmd为F_DUPFD或者F_DUPFD_CLOEXEC。
  2. 获取和设置文件描述符标志,设置cmd为F_GETFD或者F_SETFD。当前只有一个文件描述符标志,就是FD_CLOEXEC。
  3. 获取和设置文件状态标志,设置cmd为F_GETFL或者F_SETFL。
    获取文件状态标志时,介绍open时给出了许多文件状态标志。对于五个互斥的权限,需使用O_ACCMODE取得访问方式位,然后与相应的权限比对。对于其他的权限,将返回值和相应的标志进行与操作,判断是否设置了相应位。
    设置文件状态标志位时,可以更改的几个权限有,O_APPEND, O_NONBLOCK, O_SYNC, O_DSYNC, O_RSYNC, O_FSYNC, O_ASYNC。
  4. 获取和设置异步I/O所有权,设置cmd为F_GETOWN或者F_SETOWN。
  5. 获取和设置记录锁,设置cmd为F_GETLK,或者F_SETLK或者F_SETLKW。

在修改文件描述符标志或者文件状态标志时,必须要先获得现在的标志值,然后对它进行修改,获得新的标志值,然后进行设置。不能单单设置一个标志值,否则会关闭以前设置的标志位。

ioctl

这个有点看不懂。

/dev/fd

UNIX提供了/dev/fd目录,其中包含了名为0, 1, 2的文件。打开/dev/fd/0,/dev/fd/1, /dev/fd/2相当于复制描述符n。即:

1
2
fd = open("/dev/df/0", mode);
fd = dup(0);

上述两行代码是相等的。文件描述符0和fd共享同一个文件表项。在Linux中,文件描述符被映射成指向底层物理文件的符号链接。比如打开/dev/fd/0时,实际上打开的是与标准输入关联的文件。返回的新文件描述符的mode和/dev/fd文件描述符的mode并不相关。所以,即使我们使用O_RDWR mode打开/dev/fd/0,也不能对fd进行写操作。
Linux下提供了/dev/stdin,/dev/stdout, /dev/stderr,它们和/dev/fd/0等都是一样的。在shell中,可以使用dev/fd作为参数,把标准输入和输出当做一个文件,可以像处理其他文件一样进行操作。

参考文献

1.《APUE》第三版

How C program works

发表于 2019-11-18 | 更新于 2019-11-19 | 分类于 C/C++

参考文献

1.https://mapan1984.github.io/program/2018/03/10/C-编译过程与库文件的作用/#c语言编译过程

C and C++ lvalue and rvalue

发表于 2019-11-14 | 更新于 2019-12-17 | 分类于 C/C++

对象(object)和值(value)

C11标准

C11标准的定义如下([14]3.15, 3.19):

3.15 object
region of data storage in the execution environment, the contents of which can represent values
NOTE When referenced, an object may be interpreted as having a particular type; see 6.3.2.1.
3.19 value
precise meaning of the contents of an object when interpreted as having a specific type

object是一块内存空间,它的内容可以表示值。当被使用时,一个对象可以解释为一种特定的类型。value是以具体类型解析object中的内容。

C++11标准

C++11标准的定义如下([17]1.8):

An object is a region of storage. [ Note: A function is not an object, regardless of whether or not it occupies storage in the way that objects do. — end note ]
An object can have a name.
An object has a storage duration which influences its lifetime.
An object has a type.
The term object type refers to the type with which the object is created.

C++ 中的obejct也是一个内存空间。object可以有名字,可以有类型(内置类型还是复合类型都行),有一个duration。也就是C++ Primer第五版中说的对象是具有某种数据类型的内存空间,可以有名,可以没有名字。

C语言中的lvalue和rvalue

左值和右值的定义

C中早期的定义:
左值是一个表达式,可以出现在赋值操作的左边或者右边,而右值只能出现在左边。
C11中左值的定义:

An lvalue is an expression (with an object type other than void) that potentially designates an object;64) if an lvalue does not designate an object when it is evaluated, the behavior is undefined. When an object is said to have a particular type, the type is specified by the lvalue used to designate the object. A modifiable lvalue is an lvalue that does not have array type, does not have an incomplete type, does not have a constqualified type, and if it is a structure or union, does not have any member (including, recursively, any member or element of all contained aggregates or unions) with a constqualified type.

可修改左值和不可修改左值

  1. 左值指向的位置的内容,如果可以被修改,那么这个左值是一个可修改左值,否则就是不可修改左值。

  2. 如果一个表达式指向内存中的一个位置,并且它的类型是算术类型,struct,union或者pointer,那么它就是一个可修改左值。

  3. 可修改左值不能含有数组类型,不完整的类型,const修饰的类型,它们都是不可修改左值。如果struct或者union要是可修改左值,那么它们不能有const成员。

  4. 为什么可修改左值不能是array,我们不能对数组赋值,但是可以通过下标操作对数组进行赋值,所以数组名字不能是一个可修改的左值。或者说数组名字其实是数组首元素的地址。这里的数组赋值说的是:

    1
    2
    3
    int a[] = {1, 2, 3};
    int b[] = {4, 5, 6};
    b = a; //想要做的操作是把数组a赋值给数组b,这是错误的。

  5. 为什么没有说函数不能是可修改左值,函数名指定的是一个函数不是一个对象,所以函数名不能是左值,自然也不能是一个可修改左值。

  6. 数组和指针都是左值,但是数组是不可修改左值,而指针是可修改左值(非const)。数组作为作为右值表示的是数组首元素的地址,数组作为左值,表示的是数组类型,是不能修改的左值。而指针变量可以作为左值,因为我们可以取得它的地址,指针变量作为右值是指针变量存储的值,即它指向变量的地址。

左值类型

C语言中的左值有以几种:

  1. 任意类型变量的名字
  2. 下标运算符[]
  3. 指针的成员访问操作->和.
  4. 单目运算符解引用*的表达式,不能指向一个数组
  5. 指针的解引用操作,不能是一个函数指针
  6. 数组,const对象,是一个不可修改左值,比如const int a = 0;,a是一个不可修改左值
  7. 字符串字面值常量是一个不可修改左值[12],因为C中没有字符串类型,字符串常量都是以字符数组类型存储的,而在C中,除了左值以外没有任何方式可以让数组存在于表达式中。

左值和右值的转换

左值到右值的隐式类型转换[13]

  1. C语言存在左值到右值的默认类型转换,当运算符需要右值操作对象时,而给出的是左值操作对象时,编译器会默认将左值转换成右值。
  2. 数组到指针的转换。在需要右值操作对象的时候,编译器换将数组名转换为其首元素的地址,类型为指向元素的指针。
  3. 函数到指针的转换。

一般情况下,对象之间的运算,对象是以右值的形式参与的。比如+运算符需要两个右值运算数:

1
2
3
int a = 1;
int b = 2;
int c = a + b;

在上面的例子中,a和b都是左值,在int c = a+b;中,它们经历了隐式的类型转换,将左值转换为了右值。
除了数组,函数,不完整的类型,所有的左值都可以转换为右值,但是右值不能转换为左值。

右值产生左值

1
2
3
int a[] = {1, 2, 3};
int *p = &a[0]; //a和&a[0]都是数组首元素的值
*(p+1) = -1; //p+1是右值,但是*(p+1)是左值

左值产生右值

1
2
int var = 10;
int *p = &var; //var是左值,但是&var是右值。

单目运算符&需要一个左值作为它的运算对象,当且仅当n是一个变量时,&n是一个有效的表达式,&12是错误的。

C++中表达式的value category

C++ 中,一个表达式有两个基本属性,基本类型和值类别。在C++ 中有五种value category,它们的关系如下:
value_category
每一个表达式属于三种基本value中的一个:lvalue, xvalue和prvalue,表达式的这种属性叫做value category。

lvalue

An lvalue (so called, historically, because lvalues could appear on the left-hand side of an assignment expression) designates a function or an object. [Example: If E is an expression of pointer type, then *E is an lvalue expression referring to the object or function to which E points. As another example,the result of calling a function whose return type is an lvalue reference is an lvalue. — end example ]

左值指定了一个函数或者对象(变量)。它存放在内存中的某个位置,并且允许使用取值地址符&获取这块内存的地址。如果E是指针类型的表达式,那么*E是E指向的函数或者对象的左值表达式。左值分为可修改左值和不可修改左值,像常量,数组名,等属于不可修改左值,而其它的左值都是可修改左值。如果一个表达式不是左值,那么它就被定义为右值。
怎么样判断左值,满足以下两点中任何一点就是一个左值:

  1. 是否有名字
  2. 是否能够取到它的地址

C++ Primer中给出的一个方法:当一个对象被用作右值的时候,用的是对象的值。当一个对象被用作左值的时候,用的是对象在内存中的位置。
示例

1
2
3
char ch = 'a';
char *cp = &ch; //ch可以当做左值,也可以当做右值
&ch = 3; //错误,因为&ch我们只能取得它的值,并不能获取它在内存中的地址,即它只是一个右值,不能当做左值。

对象(变量)和指针变量中存放的内容(即地址)的区别,对象可以直接进行赋值。指针变量中存放的是一个地址,地址本身就是一个数字,是一个右值,不能对其进行赋值,对这个地址进行解引用,得到指针指向对象的左值表达式。

xvalue

An xvalue (an “eXpiring” value) also refers to an object, usually near the end of its lifetime (so that its resources may be moved, for example). An xvalue is the result of certain kinds of expressions involving rvalue references (8.3.2). [Example: The result of calling a function whose return type is an rvalue reference is an xvalue. — end example ]

xvalue也指向一个对象,通常在对象声明周期的最后。一个xvalue是和右值引用相关的特定表达式的结果。

prvalue

A prvalue (“pure” rvalue) is an rvalue that is not an xvalue.

prvalue是不是xvalue的rvalue。下列表达式是prvalue`表达式:

  • 一个字面值常量(除了字符串常量),比如42, true或者nullptr
  • 返回值类型是非引用类型的函数调用或者重载的operator表达式的结果。
  • 内置的后置自增自减运算符, a++, a--。
  • 内置的算术表达式,a+b, a&b, a<<b。
  • a>b, a==b, a>=b,内置的比较表达式
  • &a,内置的取地址符
  • a.m表达式的成员对象,其中m是一个member enumerator或者一个非静态的成员函数,或者[a是一个rvalue,m是一个非引用类型的非静态数据成员,until C++ 11]。

glvalue

A glvalue (“generalized” lvalue) is an lvalue or an xvalue.

一个glvalue是一个lvalue或者一个xvalue。下面的表达式是xvalue表达式:

  • 返回值类型是引用类型的函数调用或者重载的operator表达式的结果。
  • a[n],内置的下标表达式,a是一个array rvalue。
  • a.m,对象成员的表达式,其中a是一个rvalue,m是一个非引用类型的非静态数据成员。

rvalue

An rvalue (so called, historically, because rvalues could appear on the right-hand side of an assignment expression) is an xvalue, a temporary object (12.2) or subobject thereof, or a value that is not associated with an object.

一个右值可以是一个xvalue,一个临时对象,或者一个没有和对象关联的值。

C++和C中lvalue的区别

《C++ Primer》中说C++ 和C中的左值和右值不一样,我怎么觉得都一样呢。(好吧,自己还是道行太浅了)。
举例来说:

  1. 比如说++i和--i操作,在C中,它是一个右值,而在C++ 中,它是左值,而i++和i--在C和C++ 中都是右值[9]。
  2. 在C语言中,三目运算符(?:)的结果一定是右值,而在C++中,如果:旁边的两个操作数是左值,那么结果也是左值[10]。

C++中的左值运算

  1. 赋值运算需要一个非常量左值作为左侧运算对象,得到的结果也仍然是一个左值。
  2. 取地址符用作一个左值运算对象,返回一个指向该运算对象的地址,这个值是一个右值。
  3. 内置解引用运算符,下标运算符,迭代器解引用运算符,string和vector的求值结果都是左值。
  4. 内置类型和迭代器的递增递运算符作用于左值运算对象,其前置版本所得的结果也是左值,即++iter, ++i等。

参考文献

1.《C和指针》
2.《C++ Primer》
3.https://www.geeksforgeeks.org/lvalue-and-rvalue-in-c-language/
4.https://segmentfault.com/a/1190000003793498
5.https://stackoverflow.com/questions/45656162/why-cant-a-modifiable-lvalue-have-an-array-type
6.https://www.quora.com/What-is-lvalue-and-rvalue-in-C
7.https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c
8.https://eli.thegreenplace.net/2011/12/15/understanding-lvalues-and-rvalues-in-c-and-c
9.https://www.zhihu.com/question/29936562/answer/46129706
10.https://www.zhihu.com/question/313519801/answer/642403872
11.https://www.zhihu.com/question/36052573/answer/65743965
12.https://stackoverflow.com/questions/10004511/why-are-string-literals-l-value-while-all-other-literals-are-r-value
13.https://www.zhihu.com/question/25814721/answer/31648501
14.https://stackoverflow.com/questions/3601602/what-are-rvalues-lvalues-xvalues-glvalues-and-prvalues
15.http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1570.pdf
16.https://stackoverflow.com/questions/54621080/lvalues-in-the-iso-c11-standard
17.http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf
18.https://en.cppreference.com/w/cpp/language/value_category

1…789…34
马晓鑫爱马荟荟

马晓鑫爱马荟荟

记录硕士三年自己的积累

337 日志
26 分类
77 标签
RSS
GitHub E-Mail
© 2022 马晓鑫爱马荟荟
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v6.6.0