一、文件I/O函数
1、open和openat
调用open或openat函数可以打开或创建一个文件。
函数原型:
int open(const char *path, int oflag, ... );
int openat(int fd, const char *path, int oflag, ... );
//成功则返回文件描述符,出错则返回-1
- path表示要打开或创建文件的名字;
- oflag表示文件的打开方式:O_RDONLY(只读打开)、O_WRONLY(只写打开)、W_RDWR(读写打开)、O_APPEND(追加写) …
- fd参数把open与openat两个函数区分开,openat可以指定路径名。
2、creat
调用creat函数创建一个新文件,现在基本不用了,用open函数取代,这里就不做过多介绍。
函数原型:
int creat(const char *path, mode_t mode);
//成功返回只写打开的文件描述符,出错返回-1
3、close
调用close函数关闭一个打开的文件,关闭文件时还会释放该进程加在改文件上的所有记录锁。
函数原型:
int close(int fd);
//成功返回0,出错返回-1
当一个进程终止时,内核自动关闭它所有的打开文件,很多程序都利用了这一功能而不显式地用close关闭打开文件。
4、lseek
每个打开的文件都有一个与其相关联的“当前文件偏移量”,通常,读、写操作都从该偏移量处开始,并使偏移量增加所读写的字节数。除非指定O_APPEND选项,否则默认打开一个文件的偏移量是0,也可以用lseek显式地为一个打开文件设置偏移量。
注意:lseek函数只修改当前文件偏移量,不进行任何I/O操作。
函数原型:
off_t lseek(int fd, off_t offset, int whence);
//成功则返回新的文件偏移量,出错则返回-1
- 若whence是SEEK_SET,则设置偏移量为距文件开头offset个字节;
- 若whence是SEEK_CUR,则设置偏移量为当前值加上offset,offset可正可负;
- 若whence是SEEK_END,则设置偏移量为文件长度加上offset,offset可正可负。
5、read
调用read函数从打开文件中读数据。
函数原型:
ssize_t read(int fd, void *buf, size_t nbytes);
//返回读到的字节数,若已到文件尾返回0,出错返回-1
- buf通常指向一个字符数组,表示为
char buf[BUFFSIZE]
; - nbytes表示buf所指字符数组的大小。
6、write
调用write函数向打开文件写数据。
函数原型:
ssize_t write(int fd, const void *buf, size_t nbytes);
//成功则返回已写的字节数,出错返回-1
- 参数同read。
7、UNIX内核用于所有I/O的数据结构
UNIX系统支持在不同进程间共享打开文件,内核使用三种数据结构表示打开文件:
(1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,每个描述符占用一项,每项有两个元素:
- 文件描述符标志;
- 指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表,每个表项包含:
- 文件状态标志(读、写、同步、非阻塞等);
- 当前文件偏移量;
- 指向该文件v节点表项的指针。
(3)每个打开文件都有一个v节点结构,包含了文件类型和对此文件进行各种操作函数的指针,还包含了i节点。这些信息是在打开文件时从磁盘上读入内存的,i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。
注:Linux没有使用v节点,而是使用了通用的i节点结构。
如上图所示,我们假定第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有它自己对该文件的当前偏移量。
8、dup和dup2
调用dup或dup2可以用来复制一个现有的文件描述符。
函数原型:
int dup(int fd);
int dup2(in fd, int fd2);
//成功返回新的文件描述符,出错返回-1
- 由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值;
- dup2可用fd2参数指定新描述符的值。
9、sync、fsync和fdatasync
当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘,这种方式被称为
延迟写
。调用sync函数可以用来冲洗(flush)内核的块缓冲区。
函数原型:
void sync(void);
int fsync(int fd);
int fdatasync(int fd);
//成功返回0,出错返回-1
- sync只是将所有修改过的块缓冲区排入写队列,然后返回,并不等待实际写磁盘操作结束;
- fsync只对fd指定一个的文件起作用,并等待写磁盘操作结束才返回;
- fdatasync除了更新文件的数据外,还会同步更新文件的属性。
10、fcntl
fcntl函数可以改变已经打开文件的属性,这个函数十分复杂,这里只做简单概括。
函数原型:
int fcntl(int fd, int cmd, ... );
//成功返回依赖于cmd,出错返回-1
该函数有以下五个功能:
- 复制一个已有的描述符(
cmd = F_DUPFD
或F_DUPFD_CLOEXEC
); - 获取/设置文件描述符标志(
cmd = F_GETFD
或F_SETFD
); - 获取/设置文件状态标志(
cmd = F_GEETFL
或F_SETFL
); - 获取/设置异步I/O所有权(
cmd = F_GETOWN
或F_SETOWN
); - 获取/设置记录锁(
cmd = F_GETLK
、F_SETLK
或F_SETLKW
)。
二、进程控制相关函数
1、fork
函数原型:
pid_t fork(void); //创建一个新线程
//在子进程中返回0,在父进程中返回子进程ID,出错返回-1
在调用fork()函数后,会得到一个子进程,子进程的地址空间是基于父进程的地址空间拷贝出来的,父子进程各自的虚拟地址空间是相互独立的,不会互相干扰和影响。拷贝完成后的这个时间点,两个地址空间中的用户区数据(内存四区[代码区、全局区、堆区、栈区]、文件描述符、环境变量)是相同的,不同的地方有:
- 父子进程的进程ID不同;
- 子进程不继承父进程设置的文件锁;
- 子进程的未处理闹钟被清除;
- 子进程的未决信号集设置为空集。
写时复制(Copy On Write)是什么?
在调用fork()后,父子进程要共享程序文本和数据,所有映射了两个进程的数据页都是只读的。但只要有一个进程更新了一点数据,就会触发只读保护(异常),并引发操作系统陷阱,然后生成一个该页的副本,这样每个进程都有自己的专属副本后就可以进行写操作了,这也意味着那些从来不会执行写操作的页面是不需要复制的。这种方法称为写时复制,它通过减少复制而提高了性能。现在的fork实现都是用了写时复制技术来提升效率,但vfork的完全不复制还是比fork的这种部分复制要快一些。
PCB(进程控制块)里有什么?
进程id,进程状态(就绪、运行、阻塞),当前工作目录,umask掩码,文件描述符,未决信号集(记录在当前进程中产生的哪些信号还没有被处理掉),用户id和组id。
2、vfork
vfork函数用于创建一个新进程,而该新进程的目的是执行一个新程序,它不会将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit)。
函数原型:
pid_t vfork() //创建一个新进程
//在子进程中返回0,在父进程中返回子进程ID,出错返回-1
vfork会保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数中任意一个时,父进程会恢复运行。
fork()与vfork()区别:
- fork()后子进程拷贝父进程的数据段、堆栈段,但共享正文段;vfork()后的子进程在调用exec或exit之前都在父进程的空间内运行,没有拷贝操作;
- fork()后子进程对变量进行修改不会影响的父进程的变量,因为触发了写时复制,vfork()后子进程对变量进行修改会影响父进程,因为它就是在父进程的地址空间中运行的;
- fork()父子进程的执行次序不确定,vfork()保证子进程先运行,且在子进程调用exec或_exit之后父进程才会被调度运行。
3、exec族函数
函数原型:
int execl (const cahr *pathname, const char *arg0, ... );
int execv (const char *pathname, char *const *argv[]);
int execlv(const char *pathname, const char *arg0, ... );
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... );
int execvp(const char *filename, char *const argv[]);
//成功则不返回,出错则返回-1
一个进程先调用fork创建一个自身的副本,然后其中一个副本(通常为子进程)调用exec把自身替换成新的程序。比如硬盘上有一个可执行程序文件,在调用exec后,exec会把当前进程映像替换成新的程序文件,而且该新程序通常从main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID不改变,
exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
我们称调用exec的进程为调用进程,称新执行的程序为新程序。这六个函数的区别在于:
- 待执行的程序文件是由文件名还是由路径名指定;
- 新程序的参数是一一列出还是由一个指针数组来引用;
- 把调用进程的环境传递给新程序还是给新程序指定新的环境。
这六个函数的关系如下图所示,只有execve是内核中的系统调用,其他5个都是调用execve的库函数。
要理解fork与execve,需要先理解程序与进程:
程序是一堆代码和数据,可以作为目标文件存在于磁盘上,或者作为段存在于地址空间中。进程是程序执行中的一个具体的实例,程序总是运行在某个进程的上下文中。fork函数在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品;execve函数在当前进程的上下文中加载并运行一个新的程序,它会覆盖当前进程的地址空间,但并没有创建一个新进程,新的程序仍然有相同的PID,并且继承了调用execve函数时已经打开的所有文件描述符。
4、exit与_exit
而exit的作用就是结束,清理,就是说先检查缓冲区,把没有写入的数据写入到文件,然后调用_exit关闭文件。而_exit是立刻关闭文件,文件缓冲区的内容也就消失了,这个时候就不可能再输出到显示设备了。
注意:_exit函数由exit调用。
5、wait和waitpid
调用wait或waitpid时,会出现以下情况:
- 如果该进程其所有子进程都还在运行,则阻塞;
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回;
- 如果它没有任何子进程,则立即出错返回。
函数原型:
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
//成功返回进程ID,出错返回0或-1
- statloc指向终止进程的终止状态,若不关心可设置为空;
- pid可以实现等待一个指定的进程终止;
- option可以支持作业控制。
wait和waitpid的区别:
- waitpid可等待一个特定进程,而wait则返回任一终止进程的状态;
- waitpid提供了一个wait的非阻塞版本,有时希望获取一个子进程的状态,但不想阻塞;
- waitpid的option选项支持作业控制。
6、getpid和getppid
getpid可以获得当前进程ID,getppid可以获得当前进程的父进程ID。
三、线程控制相关函数
1、pthread_cread
函数原型:
int pthread_create(pthread_t *restrict tidp, const pthread_atr_t * restrict addt,
void *(*start_rtn)(void *), void *restrict arg);
//成功返回0,出错返回错误编号
- 新创建的线程ID会被设置成tidp指向的内存单元;
- attr参数用于定制各种不同的线程属性;
- 第3个参数是函数指针,新创建的线程从start_rtn函数的地址开始运行;
- arg为要传入的函数参数,是一个指向结构体的指针。
2、pthread_exit
如果进程中任意线程调用了pthread_exit,那么整个进程就会终止。
函数原型:
void pthread_exit(void *rval_ptr);
3、pthread_join
调用线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。
函数原型:
int pthread_join(pthread_t thread, void **rval_ptr);
//成功返回0,出错返回错误编号
- thread表示要等待退出的线程ID;
- rval_ptr包含线程退出的返回码。
当一个线程退出时,进程中的其他线程可以通过调用pthread_join函数获得该线程的退出状态。
4、pthread_cancel
线程可以通过调用pthreead_cancel函数来请求取消同一进程中的其他线程。
函数原型:
int pthread_cancel(pthread_t tid);
//成功返回0,出错返回错误编号
注意:pthread_cancel并不等待线程终止,它仅仅提出请求。
5、pthread_detach
如果对现有的某个线程的终止状态不感兴趣的话,可以调用pthread_detach函数让操作系统在线程退出时收回它所占用的资源,且在分离后我们不能用pthread_join函数等待它的终止状态。
函数原型:
int pthread_detach(pthread_t tid);
//成功返回0,出错返回错误编号
6、pthread_self
函数原型:
pthread_t pthread_selt(void);
//返回调用线程的线程ID