一、系统调用
UNIX/Linux系统绝大部分功能都是通过系统调用实现,比如:open/close…
UNIX/Linux把系统调用都封装成了C函数的形式,但他们并不是标准C的一部分。
标准库中的函数绝大部分时间都工作在用户态,但部分时间也需要切换到内核(进行了系统调用),比如:fread/fwirte/malloc/free。
我们自己所编写的代码也可以直接调用系统接口进入内核态(进行系统调用),比如:brk/sbrk/mmap/munmap
系统调用的功能代码存在于内存中,接口定义在C库中,该接口通过系统中断实现调用,而不是普通函数进行跳转。
注意:从用户态切换到内核态或从内核态返回到用户态都会消耗时间。
time a.out
real 0m0.137s 总执行时间=用户态+内核态+切换消耗
user 0m0.092s 用户态执行时间
sys 0m0.040s 内核态执行时间
strace 程序 可以跟踪系统调用
二、一切皆文件
在UNIX/Linux系统下,几乎所有资源都是以文件形式提供了,所以在UNIX/Linux系统下一切皆文件,操作系统把它的服务、功能、设备抽象成简单的文件,提供一套简单统一的接口,这样程序就可以像访问磁盘上的文件一样访问串口、终端、打印机、网络等功能。
大多数情况下只需要 open/read/write/ioctl/close 就可以实现对各种设备的输入、输出、设置、控制等。
UNIX/Linux下几乎任何对象都可以当作特殊类型的文件,可以以文件的形式访问。
目录文件 里面记录的是一些文件信息,相关条目。
设备文件 在系统的/dev目录下存储了所有的设置文件
stderr
stdin
stdout
普通文件
链接文件
管道文件
socket文件
三、文件相关系统调用
open 打开或创建文件
creat 创建文件
close 关闭文件
read 读文件
write 写文件
lseek 设置文件读写位置
unlink 删除链接
remove 删除文件
四、文件描述符
文件描述符是一个非负整数,表示一个打开的文件,由系统调用open/creat/socket返回值。
为什么使用文件描述符而不像标准库那使用文件指针?
因为记录文件相关信息的结构存储在内核中,为了不暴露内存的地址,因此文件结构指针不能直接给用户操作,内核中记录一张表,其中一列是文件描述符对应一列文件结构指针,文件描述符就相当于获取文件结构指针的下标。
内核中已经有三个已经打开的文件描述符,它们的宏定义在unistd.h:
stdin 0 STDIN_FILENO
stdout 1 STDOUT_FILENO
stderr 2 STDERR_FILENO
0,1,2 都代表的是终端
五、open/creat/close
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
功能:打开文件
pathname:文件的路径
flags:打开的权限
O_RDONLY, 只读
O_WRONLY, 只写
O_RDWR,读写
O_NOCTTY, 当打开的是终端设备文件,不要把该文件当作主控终端。
O_TRUNC,清空
O_APPEND,追加
返回值:文件描述符
int open(const char *pathname, int flags, mode_t mode);
功能:创建文件
pathname:文件的路径
flags:打开的权限
O_CREAT, 文件不存在则创建
O_EXCL,如果文件存在,则创建失败
mode:设置文件的权限
S_IRWXU 00700 read,write and execute permission
S_IRUSR 00400 user has read permission
S_IWUSR 00200 user has write permission
S_IXUSR 00100 user has execute permission
S_IRWXG 00070 read,write and execute permission
S_IRGRP 00040 group has read permission
S_IWGRP 00020 group has write permission
S_IXGRP 00010 group has execute permission
S_IRWXO 00007 others have read, write and
S_IROTH 00004 others have read permission
S_IWOTH 00002 others have write permission
S_IXOTH 00001 others have execute permis‐sion
int close(int fd);
功能:关闭打开的文件
六、read/write
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
功能:从文件中读取数据到内存
fd:文件描述符,open函数的返回值
buf:数据的存储位置
count:读取的字节数
返回值:成功读取到的字节数
ssize_t write(int fd,const void *buf, size_t count);
功能:把数据写入到文件
fd:文件描述符,open函数的返回值
buf:要写入的数据内存首地址
count:要写入的字节数
返回值:成功写入的字节数
注意:如果把结构体以文本形式写入到文件,需要先把结构体转换成字符串。
七、lseek
off_t lseek(int fd, off_t offset, int whence);
功能:设置文件位置指针
返回值:文件位置指针所在的位置 功能类似ftell
练习1:实现一个Linux系统下的计算文件大小的函数,使用系统调用完成。
练习2:实现带覆盖检查的cp命令。
八、dup/dup2
int dup(int oldfd);
功能:复制文件描述符,操作系统会从末的文件描述符中选择一个返回。
oldfd:被复制的文件描述符
int dup2(int oldfd, int newfd);
功能:复制指定的文件描述符,如果newfd已经被使用,则先关闭,再复制。
九、标准IO与系统IO比较
练习3:分别使用标准IO与系统IO随机写入1000000个整数到文件,比较哪种更快,为什么?
因为标准IO使用了缓冲技术,当数据写入时并没有立即把数据交给内核,而是先存放在缓冲区中,当缓冲区满时,会一次性把缓冲中的数据交给内核写到文件中,这样就减少内核态与用户态的切换次数。
而系统IO每写一次数据就要进入一次内核态,这样就浪费了大量时间进行内核态与用户态的切换,因此用时更长。
如果为系统IO,设置更大的缓冲区,它会比标准IO更快。
real 0m0.113s 标准IO
user 0m0.100s
sys 0m0.008s
real 0m2.845s 系统IO
user 0m0.268s
sys 0m2.572s
real 0m0.064s 设置过缓冲区的系统IO
user 0m0.060s
sys 0m0.000s
一、sync/fsync/fdatasync
1、硬盘上一般都有一些缓冲区以此来提高数据的写入效率,操作系统写入数据其实只是写入缓冲区,直到缓冲区满,才排队写入硬盘中。
2、这种操作降低的写入的次数,但是提高了数据写入的延时,导致缓冲区中的数据与磁盘中的内容不同步。
void sync(void);
功能:把所有缓冲区中的数据全部同步到磁盘
注意:只是发面将数据同步到磁盘的命令,并不等待执行完比才返回,而是命令发布后立即返回。
int fsync(int fd);
功能:指定fd文件的缓冲区数据同步到磁盘,只针对一个文件,数据同步到磁盘后才返回。
int fdatasync(int fd);
功能:指定fd文件的缓冲区数据同步到磁盘,但仅是文件的数据并不同步文件属性。
二、fcntl
int fcntl(int fd, int cmd, … /* arg */ )
cmd:操作指令,不同的操作指令决定,后续参数的个数和类型
注意:这是个变长参数的函数
int fcntl(int fd, int cmd, long newfd)
cmd:F_DUPFD
功能:复制文件描述符,与fd操作同一个文件
返回值:如果newfd没有使用则返回newfd,如果newfd已经被占用,则返回一个不小于newfd的文件描述符。
练习1:利用fcntl,实现dup和dup2的功能。
int fcntl(int fd, int cmd,void/long)
功能:设置或获取文件描述符标志
cmd:
F_GETFD void
返回值:0新进程保持打开状态,1新进程中关闭该文件描述符。
F_SETFD long
目前只能设置FD_CLOEXEC标志位。
返回值:成功返回0,失败返回-1
int fcntl(int fd, int cmd,void/long)
功能:获取文件状态标志(此文件打开的权限以及打开的方式)。
cmd:
F_GETFL void
O_CREAT,O_EXCL,O_NOCTTY,O_TRUNC 不能获取到
返回值:带有文件状态标志的int类型变量,需要与各标志相与得到。
F_SETFL long
仅能设置的有
O_APPEND,O_ASYNC,O_DIRECT,O_NOATIME,O_NONBLOCK
返回值:成功返回0,失败返回-1
int fcntl(int fd, int cmd,struct* flock);
功能:为文件加锁,能锁整个文件,或锁一部分内容。
一旦进程结束,锁会自动解锁。
cmd:
F_GETLK 获取锁的信息
F_SETLK 设置文件锁
F_SETLKW 测试锁
注意:加锁并不能让其它进程打不开文件或不能操作,而是使用者都要遵守锁的约定,确保文件不混乱(劝谏锁)。
struct flock
{
short l_type;// 锁的类型 F_RDLCK,F_WRLCK, F_UNLCK
short l_whence;// SEEK_SET, SEEK_CUR, SEEK_END
off_t l_start;// 锁的偏移值
off_t l_len;// 锁的长度,为0表示锁到文件尾
pid_t l_pid;// 加锁的进程号
};
读锁 与 读锁 成功
读锁 与 写锁 失败
写锁 与 写锁 失败
三、stat/fstat/lstat
#include <sys/stat.h>
功能:用来获取文件属性,返回值:成功返回0,失败返回-1
int stat(const char *path, struct stat *buf);
path:需要文件路径
int fstat(int fd, struct stat *buf);
fd:需要打开后的文件描述符
int lstat(const char *path, struct stat *buf);
stat/fstat会跟踪链接目标,而lstat不跟踪链拉目标。
struct stat {
dev_t st_dev; // 设备ID
ino_t st_ino; // 节点号
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 硬链拉数
uid_t st_uid; // 用户ID
gid_t st_gid; // 组ID
dev_t st_rdev; // 特殊设备ID
off_t st_size; // 文件的总字节数
blksize_t st_blksize; // IO块数
blkcnt_t st_blocks; // 占用块(512字节)数
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后文件属性修改时间
S_ISREG(m) 测试是否是标准文件
S_ISDIR(m) 目录
S_ISCHR(m) 字符设备文件
S_ISBLK(m) 块设备文件
S_ISFIFO(m) 管道文件
S_ISLNK(m) 软链接文件
S_ISSOCK(m) socket文件
S_IFMT 0170000 获取文件类型出错
S_IFSOCK 0140000 socket文件
S_IFLNK 0120000 软链接文件
S_IFREG 0100000 标准文件
S_IFBLK 0060000 块设备文件
S_IFDIR 0040000 目录
S_IFCHR 0020000 字符设备文件
S_IFIFO 0010000 管道文件
S_ISUID 0004000 set UID bit
S_ISGID 0002000 set-group-ID bit (see below)
S_ISVTX 0001000 sticky bit (see below)
S_IRWXU 00700 属主的读写执行权限
S_IRUSR 00400 属主的读
S_IWUSR 00200 属主的写
S_IXUSR 00100 属主的执行
S_IRWXG 00070 属组的读写执行权限
S_IRGRP 00040 属组的读
S_IWGRP 00020 属组的写
S_IXGRP 00010 属组的执行
S_IRWXO 00007 其它用户的读写执行权限
S_IROTH 00004 其它用户的读
S_IWOTH 00002 其它用户的写
S_IXOTH 00001 其它用户的执行
练习2:使用stat函数,获取文件的属性,显示出文件的类型和权限,参考ls -l的第一列内容。
struct tm *localtime(const time_t *timep);
功能:使用一个记录秒数据的变量,获取当前时间
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
练习3:获取文件的最后访问时间,最后修改时间,最后文件属性修改时间。
四、access
int access(const char *pathname, int mode);
功能:测试当前用户对文件的访问权限,或者文件是否存在
pathname:文件路径
mode:
F_OK 是否存在
R_OK 是否有读权限
W_OK 是否写权限
X_OK 是否有执行权限
返回值:0表示有,-1表示没有
五、umask
mode_t umask(mode_t mask);
功能:设置并获取权限屏蔽码,功能与umask命令一样,一旦设置成功新创建文件就不会具有mask中的权限。
返回值:旧的权限屏幕码
注意:该权限屏蔽码只对当前进程有效,进程结束后就会变成默认的。
六、chmod/fchmod
功能:修改文件的权限,返回值:成功返回0,失败返回-1
int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
注意:它们不受权限屏蔽码的干扰
七、truncate/ftruncate
功能:修改文件的大小,如果文件的,成功返回0,失败返回-1
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
八、link/unlink/remove/rename
返回值:成功返回0,失败返回-1
int link(const char *oldpath, const char *new‐path);
功能:创建硬链拉文件,硬链接指向的是文件的内存,因此当链接目标被删除后,依然可以访问文件的内容。
int unlink(const char *pathname);
功能:删除硬链拉文件
注意:普通文件就是硬链接数量为1的文件,当把一个文件的硬链接数删除到0个时,这个文件就被删除了。
int remove(const char *pathname);
功能:删除文件,该函数是标准库中的删除文件函数,底层调用了unlink系统调用。
int rename(const char *oldpath, const char *new‐path);
功能:文件重命名
九、symlink/readlink
int symlink(const char *oldpath, const char *new‐path);
功能:创建软件链拉(目录文件只能创建软链接)
oldpath:链接目标
new‐path:链接文件
返回值:成功返回0,失败返回-1
ssize_t readlink(const char *path, char *buf,size_t bufsiz);
功能:读取软链接文件的内容而非链接目标(open打开软链接文件是打开的是目标文件)
path:链接文件的路径
buf:读取数据的存储位置
bufsiz:读取多少个字节
返回值:成功读取到的字节数
十、mkdir/rmdir
返回值:成功返回0,失败返回-1
int mkdir(const char *pathname, mode_t mode);
功能:创建目录,目录一定要有执行权限,否则无法进入
int rmdir(const char *pathname);
功能:删除空目录(只能删除空目录)
十一、chdir/fchdir/getcwd
char *getcwd(char *buf, size_t size);
功能:获取当前进程的工作目录,工作目录是指当不加路径信息时,创建/打开时从那个目录下查找,工作目录默认是程序所在的目录。
int chdir(const char *path);
功能:修改进程的工作目录
返回值:成功返回0,失败返回-1
int fchdir(int fd);
功能:修改进程的工作目录
fd:被open打开的目录文件的fd
返回值:成功返回0,失败返回-1
十二、opendir/fdopendir/closedir/readdir/rewinddir/telldir/seekdir
#include <dirent.h>
DIR *opendir(const char *name);
功能:打开一个目录流
返回值:目录流(链表)
DIR *fdopendir(int fd);
功能:使用文件描述获取目录流,fd必须是目录文件的。
struct dirent *readdir(DIR *dirp);
功能:从目录流中读取一个文件结点信息
struct dirent {
ino_t d_ino; // i节点号
off_t d_off; // 下一个文件结点信息的偏移量
unsigned short d_reclen; // 当文件结点信息的长度
unsigned char d_type; // 文件类型
char d_name[256]; // 文件的名字
DT_BLK This is a block device.
DT_CHR This is a character device.
DT_DIR This is a directory.
DT_FIFO This is a named pipe (FIFO).
DT_LNK This is a symbolic link.
DT_REG This is a regular file.
DT_SOCK This is a UNIX domain socket.
DT_UNKNOWN The file type is unknown.
void rewinddir(DIR *dirp);
功能:把目录流的位置指针调整到开头
long telldir(DIR *dirp);
功能:获取当前目录流的位置指针在第几个文件结点
void seekdir(DIR *dirp, long offset);
功能:调整当前目录流的位置指针
offset:根据开头位置指针,进行偏移
一、信号的基本概念
1、中断:中止(注意不是终止)当前正在执行的任务,转而执行其它任务(可能返回也可能不返回),中断分为硬件中断(硬件设备产生的中断)和软件中断(其它程序产生的中断)。
2、信号:是一种软件中断,提供了一种异步执行任务的机制。
3、常见的信号
SIGINT(2) Ctrl+c 产生的信号
SIGQUIT(3) Ctrl+\ 产生的信号
SIGABRT(6) 调用abort函数,产生此信号
SIGFPE(8) 浮点数例外,除以0、浮点溢出等
SIGKILL(9) 不能被捕获或忽略,常用于杀死进程
SIGSEGV(11) 段错误信号,非法访问内存产生的信号
SIGTSTP(20) Ctrl+z 生产的信号
SIGCHLD(17) 子进程状态改变信号
注意:在终端中执行 kill -l 可以显示出所有信号
4、不可靠信号
建立在早期机制上的信号被称为不可靠信号,SIGHUP(1)~SIGSYS(31)。
不支持排队可能会丢失,同一个信号产生多次,进程可能只接收到一次。
5、可靠信号
采用新的机制产生的信号,SIGRTMIN(34)~SIGRTMAX(64).
支持排队,不会丢失。
6、信号的来源
硬件产生:除0,非法内存访问。
这些异常是硬件(驱动)检查到,并通知内核,然后内核再向引发这些异常的进程发送相应信号。
软件产生:通过kill/raise/alarm/setitmer/sigqueue函数产生。
7、信号的处理
1、忽略
2、终止进程
3、终止进程并产生core文件
4、捕获信号并处理。
二、信号的捕获
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理注册函数
signum:信号的编号,1~31 也可以是宏
handler:
SIG_IGN 忽略该信号
SIG_DEL 默认处理
函数指针
注意:在某些UNIX系统上,signal注册的函数只执行一次,执行完后就恢复成默认处理方式,如果长期使用该函数处理信号,可以在函数结束前再注册一次。
SIGKILL/SIGSTOP 既不能被捕获,也不能被处理
SIGSTOP信号会让进程暂停,当再次收到SIGCONT信号时会继续执行。
普通用户只能给自己的进程发送信号,而root可以给任何进程发送信号。
练习1:实现一个"死不掉的进程",当收到信号后,给出信号产生的原因。
三、发送信号
1、键盘
Ctrl+c SIGINT
Ctrl+\ SIGQUIT
Ctrl+z SIGTSTP
2、错误
除零 SIGFPE
非法访问内存 SIGSEGV
3、命令
kill -signum pid
ps -aux 查看所有进行
4、函数
int kill(pid_t pid, int sig);
功能:向指定的进程发送信号
pid:进程id
pid > 0 向进程号为pid的进程发送信号
pid = 0 向同组一进程组的进程发送信号
pid = -1 向所有(有权力发送信号的)进程发送信号
pid < -1 向进程号为abs(pid)的进程组发送信号
sig:信号的编号
sig值为0时,kill不会发送信号,但会进行错误检查(检查进程号或进程组id号是否存在)。
int raise(int sig);
功能:发向当前进程发送信号
四、休眠
int pause(void);
功能:一旦执行进程就会进入无限的休眠(暂停),直到遇到信号。
先执行信号处理函数才会从休眠中醒来。
unsigned int sleep(unsigned int seconds);
功能:休眠指定的秒数,当有信号来临时会提前醒来,提前醒来会返回剩余的秒数,或者睡够了,返回0。
五、闹钟
unsigned int alarm(unsigned int seconds);
功能:告诉内核在seconds秒之后,向当前进程发送SIGALRM信号。
返回值:如果之前设定的时间还没有到,则会重新设置(覆盖),并返回之前设置的剩余秒数。
六、信号集与信号屏蔽
信号集:信号的集合,由128位二进制组成,每一位代表一个信号。
int sigemptyset(sigset_t *set);
功能:清空信号集,把所有位设置为0。
int sigfillset(sigset_t *set);
功能:填满信号集,把所有位设置为1。
int sigaddset(sigset_t *set, int signum);
功能:往信号集中添加一个信号。
int sigdelset(sigset_t *set, int signum);
功能:从信号集中删除一个信号。
int sigismember(const sigset_t *set, int signum);
功能:判断信号集中是否有signum信号
信号屏蔽:当做一些特殊操作时会希望,有些信号来而有些信号不要来,而与设置信号忽略不同的,信号屏蔽只是暂时不来,而可以获取这段时间发生那些信号。
每个进程都有一个信号掩码(信号集),其中包括了需要屏蔽的信号,可以通过sigprocmask函数,检查、修改进程的信号掩码。
int sigprocmask(int how, const sigset_t *set,
sigset_t *oldset);
功能:检查、修改进程的信号掩码
how:
SIG_BLOCK
设置当前信号掩码与set的并集为新的信号掩码,添加。
SIG_UNBLOCK
新的信号掩码是当前掩码与set补集的交集,删除。
SIG_SETMASK
把set当作新的信号掩码,重新设置。
set:可以为空,则获取信号掩码。
oldset:旧的信号屏幕掩码
int sigpending(sigset_t *set);
功能:获取信号屏蔽期间发生的信号,当信号屏蔽解除后就没了。
注意:在信号屏蔽期间发生的信号,无论多少次(不可靠信号),只能捕获一次。
练习:学生信息管理系统,在保存数据在加载数据时屏蔽Ctrl+c和Ctrl+\,等数据加载、保存完成后再处理该信号。
七、带附加信息的信号捕获
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
功能:向内核注册信号处理函数
signum:信号编码
act:信号处理方式
oldact:获取到此信号旧的处理方式,可以为NULL。
struct sigaction {
void (*sa_handler)(int); //简单的信号处理函数指针
void (*sa_sigaction)(int, siginfo_t *, void *);
// 可以带附加信息的信号处理函数指针
sigset_t sa_mask; // 当执行信号处理函数时需要屏蔽的信号
int sa_flags;
SA_RESETHAND 表示信号只处理一次,然后就恢复默认处理方式。
SA_RESTART 系统调用如果被signum信号中断,自行重启
SA_NOCLDSTOP 当子进程暂时时,不用通知父进程。
SA_NODEFER 当执行信号处理函数时,不屏蔽正常的处理的信号。
SA_SIGINFO 使用第二个函数指针处理信号
void (*sa_restorer)(void); // 保留,暂不使用
int sigqueue(pid_t pid, int sig, const union sig‐
val value);
功能:信号发送函数,与kill不同的是可以附加一些额外数据
pid:目标进程号
sig:要发送信号
value:联合,成员可以是整数或指针
八、计时器
1、系统为每个进程维护三个计时器
ITIMER_REAL 真实计时器,程序运行的实际所用时间
ITIMER_VIRTUAL 虚拟计时器,程序运行在用户态所消耗的时间
ITIMER_PROF 实用计时器,程序在用户态在内核态所消耗的时间。
实际时间(真实计算器) = 用户时间(虚拟)+内核时间+睡眠时间
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
功能:给当前进程设置定时器,与alarm的区别是更精确,可以选择以那个时间段计算(选择使用那个计时器)。
which:选择使用哪些计时器
curr_value:
struct timeval it_value; 第一次触发时钟信号所需要的时间
struct timeval it_interval; 每一次触发时钟信号所需要的时间
int getitimer(int which, struct itimerval *curr_value);
功能:获取当前进程的定时器
which:选择使用哪些计时器
curr_value:
struct timeval it_value; 第一次触发时钟信号所需要的时间
struct timeval it_interval; 每一次触发时钟信号所需要的时间