linux对于文件io提供的系统调用大概是5个,分别是 open,read,write,lseek,close 。
linux可以把所有的东西都可以看成文件,对于一个文件,基本的操作就是可读,可写,可控制,可关闭。
-
关于write
-
write 函数的流程
调用write函数的将数据写入磁盘的过程
进入内核空间 --> 将数据写入内核空间里面的缓冲区 --> 写队列 --> 磁盘
一般默认的write函数,只负责将数据写到内核空间的缓冲区里面,然后返回,它并不等待写磁盘的操作结束。 系统的守护进程会周期的调用sync函数,将内核空间的write缓冲区的数据放到写队列。fsync函数可以确保修改过的数据,直到写到磁盘上面再返回。
-
write 函数的追加写
write函数的追加写的实现是一个原子操作,因为如果两个进程同时打开一个文件,近些追加写的时候。如果获得 文件偏移 和 写操作 这个两个操作是独立的话,那么就可以出现被覆盖的情况。所以,write的追加写的操作是一个原子的操作。
-
-
关于read
-
其他的api
dup/dup2 #复制fd fcntl #用于文件的属性控制 ioctl #用于文件的属性控制
-
dup/dup2的用法
todo
-
fcntl的用法:
网络编程里面设置非阻塞
-
-
进程打开文件对应的数据结构
-
linux 的文件类型
-
普通文件
-
目录文件
-
块特殊文件
磁盘文件就是块特殊文件。
-
字符特殊文件
在系统中,所有设备,要不是字符特殊文件,要不就是块特殊文件。比如终端输入就是字符特殊文件。
-
FIFO
-
SOCKET
-
符号链接
-
-
目录
每一个进程都一个自己当前的工作目录,这个目录也是搜索所有相对路径名的起点。 附加一个问题,自己可以去实现cd命令。
-
linux 文件系统
todo
-
缓冲
标准io库提供缓冲的目的是为了尽可能的减少调用read 和 write 的次数。于是,标准io库提供了缓冲的机制。标准io库的缓冲机制分为三种:
-
全缓冲
全缓冲表示直到写满缓冲区的时候,才进行实际的io操作。
-
行缓冲
行缓冲表示遇到/n就进行fflush的操作,或者是达到了行缓冲的大小,就执行fflush(好像默认是1024)。这个是stdin 和 stdout的默认操作行缓冲。
-
不带缓冲
不缓冲就是不缓冲。stderr就是不缓冲。
(这个说的stdin,stdout 是行缓冲 指的是他们默认连接至终端)
-
-
关于 stdin , stdout , stderr
stdin,stdout,stderr默认是连接至终端,也就是从终端获取输入和输出。但是也可以通过重定向修改默认的路径。
./a.out < /etc/passwd > std.out 2 > std.err # 这个表示 # stdin 是从 /etc/passwd 获得输入 # stdout 输出道std.out 文件里面 # stderr 输出道std.err 文件里面
如果stdin,stdout被重定向到文件,那么它们就变成全缓冲了。
#include <iostream> #include <string> using namespace std; int main() { string str; cin>>str; cout<<str<<endl; return 0; }
# 将输入重定向到1.txt ,将输出重定向到 out.txt ./a.out < 1.txt > out.txt
-
程序启动
-
进程终止
-
理解fork
-
父进程打开三个文件,然后fork出子进程,然后两个进程一个写文件,会发生什么?
fork之后,父进程和子进程是共享同一个文件偏移量的。如果子进程写了文件之后,写完之后,会修改文件的偏移。于是,父进程再写的时候,就会在新的偏移后面写。(但是这个只能保证不会出现父子进程写的内容相互覆盖,并不能保证内容的顺序)
比如,子进程写的是abc,父进程写的是123。可能出现的输出是a123bc,或是是1ab23c 等。但是不会出现覆盖的情况,输出的长度一定是6。
-
fork 和 vfork的区别?
区别:
- 没有完全拷贝父进程的进程空间里面的全部内容
- 子进程先执行
-
进程调用
- nice 值
-
linux 早期是不支持线程的,直到2.6的版本才开始支持线程库。现在linux的线程库NPTL(native posix thread library), NPTL现在是glibc的一个部分。
关于线程同步,本来是单独写了一个md文件,写了一个使用底层api的例子
-
线程模型
线程可以分成两种,一种是内核线程,一种是用户线程。内核线程,由内核来调度,用户线程,由线程库来进行调度。两种模型,各有利弊。NPTL的实现方式是,每个线程都是内核线程。
-
锁的类型
-
互斥锁
-
读写锁
-
自旋锁
-
-
POSIX信号量
在linux上面,信号量的api有两组。第一组是IPC的信号量,用于进程之间通信。第二组是POSIX信号量,主要用于线程同步的。这两组api的名字虽然很像,但是语义完全不一样。
POSIX的信号量
sem_init(); sem_destory(); sem_wait(); sem_trywait(); sem_post();
IPC的信号量
semget() semctl()
-
条件变量
举个小例子,更好理解。 https://github.com/zhaozhengcoder/CoderNoteBook/blob/master/example_code/condition.cc
-
屏障
-
线程的属性
-
分离属性
以分离属性启动的线程退出的时候,将自行释放其占用的资源。不需要主线程调用pthread_join来回收。
-
线程堆栈的起始地址和大小
linux 默认为每个线程分配了足够的堆栈空间8MB , 可以使用ulimit -a 查看。
-
线程的优先级
schedparam
-
线程的策略
schedpolicy
-
-
可重入
先举一个例子,比如信号的处理函数,信号的发生是不会提前直到的。比如,程序执行到write一个文件,第一步修改偏移,第二步是开始写入数据。
但是,比如在完成第一步的时候,信号发生。但是,在信号处理函数中,再一次修改的了write的偏移。于是,在信号处理函数返回的时候,就会出现问题。
一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS 调度下去执行另外一段代码,而返回控制时不会出现什么错误。
参考:
-
线程的私有数据
信号是有用户,系统或者是进程发送给目标进程的信息,以通知目标进程某个状态已经发生改变或系统的异常。
-
linux信号可以由如下的条件产生:
- 对于前台进程,用户可以通过终端的输入来发送信号。比如ctrl+c , 或者 kill 命令发送信号。
- 系统异常的时候,会产生信号 (比如,非法的内存访问)
- 进程调用api,产生信号。如果调用alarm函数,产生sigalarm信号;或是是调用kill函数,产生信号。
-
信号的处理:
- 对于一些信号,有它系统默认的处理方式
- 对于一些信号,用户可以自定义信号的处理函数来进行处理
-
常用的信号
SIGKILL SIGALRM SIGTERM SIGSTP SIGCHLD SIGPIPE SIGURG
-
信号处理函数
#include<unistd.h> #include<signal.h> void handler() { printf("hello\n"); } main() { // 注册对SIGALRM的处理函数 signal(SIGALRM,handler); alarm(5); for(int i=1; i<7; i++) { printf("sleep %d ...\n",i); sleep(1); } }
-
信号处理的编程模型 —— 统一事件源
信号处理有一个问题就是,信号处理函数要尽可能快的执行完毕,因为在信号处理期间,以确保该信号不被屏蔽很久。
一种典型的方式是,把信号主要处理的逻辑放在程序的主循环里面,主循环再根据接受到的信号选择相应的逻辑代码,选择相应的逻辑函数进行处理。信号处理函数通过管道将信号“传递”给主循环。 具体可以看 游双的《linux服务器高性能编程》 p184页。
文件锁除了可以给文件加锁之外,另外还可以用于进程之间的同步,比如两个进程都需要访问一块共享内存,这样就会出现类似于线程之间“race condition”的情况。这样,实现一个进程级别的互斥锁。
记录锁不仅仅可以用来同步不同进程对同一文件的操作,还可以通过对同一文件加记录锁,来同步不同进程对某一共享资源的访问,如共享内存,I/O设备。
-
文件锁
这里有一个文件锁的小例子
这里可以思考一个问题,nginx的work进程之间的互斥锁是如何实现的?
我看了一下nginx实现的这一部分,nginx的锁是自己实现的,原理好像是两种方式:
-
共享内存
线程中实现锁就是通过一个共享的堆上的内存(通过malloc实现),进程中实现锁也是通过这样一个共享的区域来实现进程的同步。说白了就是共享一个变量,然后通过这个变量来控制多个进程同步运行
-
文件锁
-
-
记录锁