这是该系列第三篇,也是最后一篇

前言

本篇博客是笔者在学习Unix课程时所积累的学习笔记。
希望对后来学习Unix的友友复习准备Unix的期末考试有帮助。

系统调用

文件I/O

引言

基本文件I/O函数:open、creat、read、write、lseek、close
术语:不带缓冲的I/O(指每一个read、write都调用内核中的一个系统调用),低级例程。

文件描述符

一个非负的整数,一个结构数组的下标,进程打开的文件表项的下标。
open、creat函数会返回一个文件描述符
文件结构

文件描述符0、1、2默认打开,分别对应于标准输入(键盘)、标准输出(显示器)、标准错误输出(显示器)文件。在unistd.h中定义为STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO

函数介绍

open

#include <fcntl.h>
int open(const char *pathname, int oflag, [mode_t mode])
返回值:若成功,返回非负整数,即文件描述符。一定是当前“进程文件描述符表”中最小未使用的描述符。出错返回-1。
pathname:常量,文件名,绝对路径或相对路径均可。
oflag:打开方式选项。O_RDONLY、O_WRONLY、O_RDWR三者必须选其一。O_CREAT、O_APPEND、O_TRUNC等任意选择。多个选项进行“或”运算构成oflag选项。
mode:仅当oflag具有O_CREAT选项时,需要此参数,用于指定新建文件的访问权限。

creat

#include <fcntl.h>
int creat(const char *pathname, mode_t mode)
返回值:若成功,返回非负整数,即文件描述符。出错返回-1
pathname:常量,文件名,绝对路径或相对路径均可。
mode:指定新建文件的访问权限。

若原有文件存在,则原有文件的属性和内容将会被覆盖。


等价于:
open(pathname, O_WRONLY|O_CREAT|O_TRUNC, mode)
语义上creat简单,涉及原子操作,应当使用open。

close

#include <unistd.h>
int close(int filedes)
返回值:若成功,返回0,出错返回-1
filedes:文件描述符

进程终止时,内核自动关闭其打开的所有文件

lseek

作用:移动文件“读写指针”(或称“文件偏移量”)。读写操作会自动移动文件读写指针。
#include <unistd.h>
off_t lseek(int filedes, off_t offset, int whence)
off_t: 与系统有关,通常是长整型
filedes: 文件描述符
whence: 移动偏移量的方式。0:绝对方式;1:相对方式;2:相对文件尾部
offset: 移动的距离,非绝对方式移动时,可以为负。
返回值: 新的文件偏移量。

read

作用:从已打开的文件中读取数据,自动移动文件读写指针。
#include <unistd.h>
ssize_t read(int filedes, void *buff, size_t nbytes)
ssize_t: 通常是整型,size_t通常是无符号整型。
filedes: 文件描述符
buff: 存放数据的缓冲区
nbytes: 需要读取的字节数
返回值:实际读取的字节数。正常读取时,返回值等于nbytes;遇到文件结束时,小于nbytes;出错返回-1

write

作用:将数据写入已打开的文件,自动移动文件读写指针。
#include <unistd.h>
ssize_t write(int filedes, void *buff, size_t nbytes)
ssize_t: 通常是整型,size_t通常是无符号整型。
filedes: 文件描述符
buff: 存放数据的缓冲区
nbytes: 需要写入的字节数
返回值:实际写入的字节数。正常写入时,返回值等于nbytes;磁盘空间满时,小于nbytes(这种情况也可以认为是出错);出错返回-1

dup2

#include <unistd.h>
int dup2(int filedes1, int filedes2)
作用:将文件描述符filedes1的表项复制给filedes2的表项。如果filedes2已经打开,则先将其关闭。
返回值:正常返回filedes2,出错返回-1

# include <unistd.h>
int link(const char *existingpath, const char *newpath)
作用: 创建一个新的目录项newpath,指向一个现有的文件existingpath。

int unlink(const char *pathname)
作用:删除一个目录项,对应文件的链接数减1。

remove

#include <stdio.h>
int remove(const char *pathname)
作用:删除一个文件或目录的链接。

rename

#include <stdio.h>
rename(const char *oldname, const char *newname)
作用:文件或目录更名。

mkdir

# include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode)
作用:创建一个目录。

rmdir

# include <unistd.h>
int rmdir(const char *pathname)
作用:删除一个目录。

目录文件读函数

1
2
3
4
5
#include <dirent.h>
DIR* opendir(const char *pathname) //打开目录
struct dirent* readdir(DIR *dp) //读取目录
int closedir(DIR *dp) //关闭目录
void seekdir(DIR *dp, long loc) //寻找目录

只有内核才可以写。

位置函数

改变进程的当前工作目录 //cd命令
# include <unistd.h>
int chdir(const char *pathname)
int fchdir(int filedes)
获取当前工作目录 //pwd命令
#include <unistd.h>
char *getcwd(char *buff, size_t size)

文件组织架构

  • 一般UNIX文件组织架构如下:

内核数据结构
fd就是前面一直提到的文件描述符。

  • 不同进程打开相同文件的情况:

相同文件
其实也很好理解,首先由于fd要互斥,所以fd一定不同。
且重复打开某个文件,肯定要求读写指针不能相同,否则就会互相影响。
但文件是同一个文件,所以连到同一个V节点。

  • fork之后的情况

fork
这个也很好理解,fork就相当于复制了一份原进程,自然保持一模一样。

  • 重定向

重定向
dup2(3,0) 将进程文件表中的表项3复制给表项0 ——标准输入重定向
dup2(3,1) 将进程文件表中的表项3复制给表项1 ——标准输出重定向

现在大概了解重定向的实现原理了吧~

文件属性和目录

结构

1
2
3
4
5
6
7
8
9
10
11
12
13
struct stat {
mode _t st_mode; //文件类型和权限
ino_t st_ino; //i节点号
dev_t st_dev; //文件系统设备号(磁盘和分区)
dev_t st_rdev; //设备文件的设备号
nlink_t st_nlink; //链接数
uid_t st_uid; //文件所有者的用户ID
gid_t st_gid; //文件所有者的组ID
off_t st_size; //文件长度(字节数),普通文件
time_t st_atime; //最后一次访问的时间
time_t st_mtime; //最后一次修改文件内容的时间
time_t st_ctime; //最后一次修改文件属性的时间
}

配套函数

int stat(const char *pathname, struct stat *buf)
作用:获取文件信息。
int fstat(int filedes, struct stat *buf)
作用:由文件描述符取得文件的状态。

两者返回的结果都是stat结构体,只是使用场景不同。

文件类型

普通文件、目录文件、字符设备文件、块设备文件、 FIFO、符号链接、套接字

判断文件类型

1
2
3
4
5
6
7
S_ISREG(mode_t mode)   //是否普通文件
S_ISDIR(mode_t mode) //是否目录文件
S_ISCHR(mode_t mode) //是否字符设备文件
S_ISBLK(mode_t mode) //是否块设备文件
S_ISFIFO(mode_t mode) //是否管道文件
S_ISLNK(mode_t mode) //是否符号链接
S_SOCK(mode_t mode) //是否套接字

文件的权限

  • 9个普通权限位
    S_IRUSER, S_IWUSER, S_IXUSER //用户权限位
    S_IRGRP, S_IWGRP, S_IXGRP //用户组权限位
    S_IROTH, S_IWOTH, S_IXOTH //其他用户权限位
  • 3个特殊权限位(针对可执行文件)
    S_ISUID 执行时设置有效用户ID,如passwd命令文件
    S_ISGID 执行时设置有效用户组ID
    S_SVTX 第一次执行时,保存正文,即常驻内存。

创建文件

新文件的用户ID、组ID等于创建进程的有效用户ID和组ID
新文件的权限由指定权限和屏蔽字共同决定。

  • 指定权限,open和creat函数中给出的权限。
  • 屏蔽字(umask值),被umask屏蔽的权限位即使在open或creat中指定了也会被屏蔽掉。
  • #include <sys/stat.h>
  • mode_t umask(mode_t mode)
  • umask函数设置当前屏蔽字,返回以前的屏蔽字。

进程控制

获取进程

#include <unistd.h>
pid_t getpid(void) 获取调用者的进程ID
pid_t getppid(void) 获取调用者的父进程ID。

创建进程

#include <unistd.h>
pid_t fork(void)
功能:创建一个新的进程,新进程是旧进程的副本。旧进程叫父进程,新进程叫子进程。
返回值:fork函数调用一次,返回两次。在父进程中返回子进程的ID,在子进程中返回0,出错返回-1
目的:并行,运行新程序。

运行新程序

父进程fork一个子进程后,子进程往往需要调用一个exec函数来运行一个新程序。
否则,这个fork我个人认为是没意义的。
exec函数用一个新程序替换调用进程原有的代码、数据、堆栈等,新程序从头开始执行。exec不产生新的进程,所有调用前后的进程号不发生变化。
exec函数有很多,并不指具体一个函数:

1
2
3
4
5
6
7
#include <unistd.h>
int execl(const char *pathname, const char *arg0,…,(char *)0)
int execv(const char *pathname, char *const argv[])
int execle(const char *pathname, const char *arg0,…,(char *)0, char *const envp[])
int execve(const char *pathname, char *const argv[], char *const envp[])
int execlp(const char *filename, const char *arg0,…,(char *)0)
int execvp(cons char *filename, char *const argv[])

1) execl和execlp比较直观,类似命令行输入,推荐使用。
2)execve是系统调用,其他是库函数。
3)返回值:正常不返回,出错返回-1

进程等待

通过wait函数实现:

1
2
3
4
#include <sys/wait.h>
pid_t wait(int *statloc)
pid_t waitpid(pid_t pid, int *statloc, int options)
waitid、wait3、wait4等

进程通信

  • 全称:IPC(Interprocess Communication)
  • 包括:半双工管道、半双工命名管道(FIFO)、全双工管道、全双工命名管道、消息队列、信号量、共享存储、套接字、STREAMS

    Linux支持以上全部,其他UNIX一般只支持一部分。

  • 使用习惯:
    套接字可支持不同主机的进程间通信(网络通信),其他为主机内通信。
    主机内通信常用半双工管道,网络通信常用套接字。

半双工管道

是所有UNIX系统都提供的一种通信方式。
特点:
1)半双工的工作方式。通信方向是双向的,但只能选择其中一种。
2)只能在具有公共祖先的进程之间使用。通常是父子进程之间。
创建:
#include <unistd.h>
int pipe(int filedes[2])
创建一个管道,若成功返回0,不成功返回-1;
由参数filesdes返回两个文件描述符: filedes[0]、filesdes[1], filedes[0]为读而打开, filedes[1]为写而打开。
半双工管道
创建管道之后的情形如上图所示。将数据写入fd[1],从fd[0]中读出。
管道在单个进程中没有意义,通常在pipe函数之后,立刻会调用fork,产生一个子进程,情形如下图所示:
父子管道
fork之后,选择通信方向:
如果父进程写、子进程读,则父进程close fd[0],子进程close fd[1];
反之,子进程关闭fd[0],父进程关闭fd[1]。
规则:
1)写端关闭时,read函数返回0,表示文件结束;
2)读端关闭时,write函数返回-1,并且出现SIGPIPE异常(信号)。
常量PIPE_BUF规定了内核管道缓冲区大小,每次write的字节数需小于PIPE_BUF
举例:(父进程写,子进程读)

1
2
3
4
5
6
7
8
9
10
11
#include <sys/wait.h>
int fd[2]; char line[MAXLINE];
pipe(fd);
if (pid=fork() >0){ //父进程
close(fd[0]);
write(fd[1],"hello world!\n", 12);
}
else if (pid==0){ //子进程
close(fd[1]);
read(fd[0], line, MAXLINE);
}

命名管道

命名管道是一种文件

1
2
3
4
$ mkfifo f1  //建立管道
$ ls -l f1 //可以看到f1的权限什么的,确实说明是一个文件
$ cat < f1 //将f1中的内容打印
$ ls -l > f1 //输出目录到f1中

函数:
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode)

进程结束

无论进程如何结束,都会执行内核中的一段代码:关闭所有打开文件,释放占用的内存。
进程结束时,内核会保存其终止状态,直到该进程的父进程取走其状态。

父进程结束,其子进程成为孤儿进程,由进程1(init进程)领养。
子进程先于父进程结束,如果父进程未取走其状态,则该进程仍然占有一定的内核资源,成为“僵死进程”。
由于僵死进程占用资源,因此当一个长期运行的服务类程序调用fork后,应处理僵死进程。

正常结束

  • main函数执行完
  • main函数中执行return
  • 任意位置执行exit、_exit、_Exit

    异常结束

    收到某个信号而结束

    比如kill命令中的-9.

文件共享

fork之后,所有父进程打开的文件描述符都被复制到子进程中。父子进程相同的文件描述符指向相同的内核文件表,具有相同的文件偏移量。如果父子进程同时操作相同的文件,则需要同步机制,否则会产生混乱。

一般情况下,应该尽量回避父子进程同时操作相同文件的情况。1)父进程等待子进程结束后再运行;2)父子进程运行不同的程序段,使用不同的文件。

信号

简介

信号是一种异步事件,通常是非正常情况,如:人为终止进程、除数为0、非法内存访问等。所谓“异步”,指信号的出现是随机的。

产生

  • 人为按下中断键 Ctrl_C
  • 用户使用kill命令
  • 进程调用kill函数
  • 程序运行错误,除数为0、非法内存访问
  • 软件产生的信号:闹钟超时、写一个读端已关闭的管道、网络传来带外数据。——这些基本是可以预见的信号。

    处理

    系统默认处理或忽略。
    捕捉信号 通知内核在某种信号发生时调用一个用户函数。

    函数

    signal函数

    #inclucde <signal.h>
    void (*singnal(int signo, void (*func)(int)))(int)
    这个函数比较抽象,需要好好理解:
    void (*func)(int)一个函数指针,所指向的函数需要一个整型参数,无返回值。
    singnal(int signo, void (*func)(int)), signal函数有两个参数,一个整型,一个函数指针。
    void (*signal(…))(int) signal的返回值也是一个函数指针,所指向的函数需要一个整型参数,无返回值。

    这个和OO电梯多线程里面的signal没啥关系,不要魔怔(其实是我魔怔),这个是用来捕捉信号的~

    捕捉实例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    signal(SIGINT, my_sig);
    signal(SIGALRM, my_sig);
    void my_sig(int signo){
    if (signo==SIGINT){

    }
    else if(signo==SIGALRM){

    }
    }

    alarm函数

    #include <unistd.h>
    unsigned int alarm(unsigned int seconds)
    按秒设置下一次闹钟的时间。
    返回值,如果以前设置过闹钟,再次设置时返回上一次设置时间的余数;否则返回0。
    alarm函数设置的时间到达时,产生SIGALRM信号。该信号的默认动作是终止程序。

    进程控制中的信号处理

    fork时,子进程继承父进程的信号处理。
    exec时,恢复系统默认设置

    重入问题

    信号处理程序可以被中断,中断时可能导致信号处理程序重新进入(再次被调用)。
    printf不可重入,大多数标准IO库函数不可重入。
    信号处理程序应尽可能简单,尽可能不用库函数。

结语

到这里,我的笔记也就分享完毕了,大概核心就是PPT内容加我的注解。感叹Unix虽然只有8节课,但也已经将Unix讲的五脏俱全。

这门课对于OS也还是有一定的帮助,毕竟Unix就是一个活生生的操作系统嘛。