1.文件描述符
文件描述符被内核用以描述某个进程打开的某个文件。
2.标准输入输出错误
每当运行一个程序,该程序会打开三个文件描述符,为标准输入输出错误,他们的文件描述符值为0,1,2,在程序中可以使用STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO来操作。
如:read(STDIN_FILENO,buf,bufsize);
./a.out > outfile < infile 将标准输出重定向到outfile文件,标准输入重定向到infile文件,文件不存在则会创建
3.错误处理errno
1.每条线程都有一个独立的errno,所是线程安全的
2.如果没有出错,errno值不会被清除,只有在函数出错时才检查errno
3.任何函数都不会把errno设置为0
char* strerror( int errnum);
将参数errnum(一般就是errno)映射为一个出错消息字符串
void perror( const char* msg);
先输出msg指向的串,然后输出一个冒号一个空格,接着是errno出错消息,最后是换行
4.其他
UNIX为每个系统调用在标准c中设置一个具有同样名字的库函数,由库函数调用系统调用
用户CPU时间:执行用户指令的时间
系统CPU时间:进程执行系统调用的时间
列举了标准定义的一些编译时和运行时数值,如果不跨平台大概是用不到这部分的
1.杂
当文件偏移量大于当前文件长度时,产生空洞文件
缓冲区大小为4096时,调用read write等,效率比较好
进程表里有一张文件文件描述符表,存储当前进程打开的所有描述符
内核维护一张文件表,表内是文件表项,表示打开的文件,其中每项包含:文件状态标志,当前文件偏移量,指向文件v节点的指针
注:不同进程打开同一个v节点,会在内核中产生两个不同的文件表项,他们有不同的状态标志 偏移量,但是指向同一个v节点,多线程访问时会有问题。
2.函数大集合
int open(const char * pathname, int flags);
int openat(int fd, const char * pathname, int flags);
打开某个pathname,可以是目录也可以是文件
openat()在pathname是绝对路径时忽略fd参数,变得和open()一样
pathname是相对路径时,以fd参数的工作目录作为基准
列几个常用的flag:
O_RDONLY O_WRONLY O_RDWR O_EXEC O_SEARCH 这五个模式五选一,分别是只读 只写 读写 执行打开 搜索打开
O_APPEND O_CLOEXEC O_CREAT O_DIRECTORY O_EXCL O_NOCTTY O_NOFOLLOW O_NONBLOCK O_SYNC O_TRUNC O_TRUNC O_DSYNC O_RSYNC
in creat(const char * pathname, mode_t mode);
创建一个文件,但是完全可以被open取代
int close(int fd);
关闭一个文件描述符
off_t lseek(int fd,off_t offset,int whence);
设置文件偏移量,第二参数是偏移量(正数负数皆可),第三参数是从哪里开始偏移:
SEEK_SET 从文件开始处偏移
SEEK_CUR 从文件当前处偏移
SEEK_END 从文件末尾处偏移
注:lseek的返回值特殊,判断失败时应判断 == -1,而不是<0
ssize_t read(int fd, void *buf, size_t count);
int write(int fd, void *buf, int nbyte);
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);
是为了解决以下可能出现的情况:
多进程访问时,是有可能在write的中途挂起一个进程,然后另一个进程接着write,pread和pread不会在中途挂起,是一个原子操作,要么什么都不做,要么就完成。
int dup(int fd);
int dup2(int fd1,int fd2);
在文件描述符表中复制一个文件描述符,两个文件描述符指向同一个表项。
int sync(void);
int fsync(int fd);
int fdatasync(int fd);
sync将所有修改过的快缓冲区排入写队列,然后返回,不等待实际同步操作结束
fsync只对某个描述符起作用,且会等待同步完成在返回
fdatasync只写数据,对于文件属性不更新。
int fcntl(int fd, int cmd, long arg);
int ioctl(int fd,unsinged long cmd,...)
感觉这两个不太会用到,14、18章还要讲,先放着吧
2020年7月14日22:08:11 1,2,3章
关于文件和目录操作的函数就不写了,好懒啊。
写写那些关于文件和目录的概念吧。
1.stat结构
存储了关于文件的用户ID、组ID、权限位等等一系列关于文件的信息;
2.七种文件类型
普通文件:包含某种形式的数据
目录文件:包含关于其他文件的名字与指向这些文件有关信息的指针
块特殊文件:提供对设备的带缓冲访问
字符特殊文件:提对设备不带缓冲的访问
FIFO:用于进程间通信
套接字:进程间网络通信
符号链接:此类型文件指向另一个文件
3.有效ID和实际ID
通常情况下,进程的有效用户ID就是实际用户ID,但也有特殊情况
4.文件权限
S_ISUID S_ISGID S_ISVTX
S_IRUSR S_IWUSE S_IXUSE
S_IRGRP S_IWGRP S_IXGRP
S_IROTH S_IWOTH S_IXOTH
5.粘着位
如果一个程序设置了粘着位,程序终止时,程序正文的副本保存在交换区,以便下次快速加载,不过现在没什么用了
6.软硬符号链接
软链接:是一个对文件的间接指针
硬链接:直接指向文件的i节点
7.文件的三个时间
文件数据的最后访问时间
文件数据的最后修改时间:文件内容最后一次被改
i节点状态最后的更改时间:不该变数据却改变i节点的操作:修改访问权限、用户ID等
2020年7月16日20:18:59 4章
本章函数的用途和坑都已写出,在写详细怎么使用就和抄书没区别了
1.流
流不同于文件描述符,是一个文件指针FILE *。
关于流的定向,当其最初被创建,是没有定向的,对其使用的第一个IO函数决定了一个流是单字节还是多字节。
fwide
函数可以设置流的定向,但是不会改变已经定向的流。
2.缓冲
全缓冲 数据塞满缓冲区后才进行实际IO操作,
行缓冲 在缓冲区满或遇到换行符时进行IO操作
无缓冲
可以用 setbuf
和 setvbuf
来设置是哪种形式的缓冲,还可以自己提供缓冲区
但是setbuf 是不安全的,因为他要求用户提供缓冲区却又不提供长度,容易溢出
setvbuf的使用,作为参数的缓冲区应该是被malloc的,因为如果使用栈变量,栈帧销毁后,其指向被销毁的空间。
还有一个清洗缓冲区的函数fflush
,如果参数为空,所有的流都被清洗。
3.流的操作
打开流
fopen
打开一个文件,并返回文件指针
freopen
在指定的流(文件指针)上打开文件。若流已经定向,还会清除定向
fdopen
将描述符和文件指针结合,实际上就是把描述符转换成文件指针以方便调用那些函数。
fileno
函数,就是接收一个文件指针将其转换为描述符返回
flocse
关闭文件流指针。
每个流文件指针都会维护 出错标志 和 文件结束标志
通过 ferror
和 feof
两个函数来判断这两个标志是否为真
通过 clearerr
来清除这两个标志使其为假
还有一个把字符回送到流中的函数 ungetc
定位流
ftell fseek rewind
ftell取得文件流当前的位置,fseek设置流的定位,rewind将流设置到文件起始位置
4.批量读写
行式IO
gets puts fgets fputs
书上是建议使用 fgets 和 fputs,因为这两个函数将字符串写到指定的流,但是结束符不写出。永远记住需要自己处理换行符。
二进制IO(按对象读写)
fread fwrite
读写二进制数组,数组元素类型也可以是结构
但有一个坑,如果要在不同机器间传递就会有大小端问题
5.格式化输出输入
关于输出和输入的格式说明
输出:%[flags] [fldwidth] [precision] [lenmodifier] convtype
flags ’ 将证书按千位分组输出, - 左对齐, +显示正负号, (空格)空格填充,0 0填充空白
fldwidth是最低宽度,precision是精度,两者之间用点隔开
lenmodifier是关于数值的长度,hh是char类型1字节,h是short类型2字节,l是long的长度
convtype就是什么类型,如c字符,s字串
输入:%[*] [fldwidth] [m] [lenmodifier]convtype
* 这个星号看不太懂啥意思,大概是不写入提供的变量?
m 书上木有说
printf
把数据写到标准输出
fprintf
把数据写到指定的流
dprintf
把数据写到指定的文件描述符
snprintf
把数据写到用户提供的缓冲区(另一个函数sprint
是不安全的)
scanf
fscanf
从指定的流上读
sscanf
从用户提供的缓冲区读
6.临时文件和内存流
关于临时文件和内存流,他们也是提供流的一种方式而已。
临时文件就是创建临时文件获取一个生命周期和进程生命周期相等流
内存流是用户自己提供缓冲区,然后系统将这片缓冲区视作一个流,最后将其文件流指针返回。
临时文件的函数:tmpnam tmpflie mkdtemp mkstemp
内存流的函数: fmemopen
关于tmpnam tmpflie有一个坑:返回路径名和创建文件之间有一段时间,这段时间里其他进程可以创建同名文件。mkdtemp mkstemp不会有这个问题,不过这个问题概率有点低。
这一章没看到有很有用的东西,最后那个时间例程有点作用,但是觉得C++标准库的时间库函数足够了,还能跨平台。
2020年7月18日20:11:50 5,6章
1.进程的启动和终止
进程的启动通过exec
进程的终止有8种方式,其中五种正常退出,三种异常退出。(详见第八章)
正常
①从main返回 ②exit ③_exit或_Exit ④最后一个线程从启动例程返回 ⑤最后一个线程调用pthread_exit
异常
⑥调用abort ⑦接到一个信号 ⑧最后一个线程对取消请求做出响应
exit函数总是执行标准IO库的清理关闭操作。缓冲区数据被写到文件。
2.函数int atexit(void (\*func)(void))
注册一个函数指针,在exit时调用顺序与注册顺序相反,另,先调用这些注册的函数,再清洗文件流。
3.环境表 & 存储空间布局
每个程序都有一个环境表,和参数表一样是一个字符指针数组。
emsp; 关于存储空间布局,从低地址到高地址依次是:正文段 初始化数据 未初始化数据 堆 栈 命令行参数和环境变量。
其中正文段是可执行程序。
4.存储空间分配的相关函数
void * malloc(size_t size);
void * calloc(size_t nobj,size_t size);
void * realloc(void * ptr, size_t new size);
void * free(void * ptr);
calloc分配nobj*size的空间,realloc对一块申请的内存进行增长或缩短。但是要注意,realloc可能重新申请一块更长的,然后将原数据复制过去,所以任何指针都不应该指向可能会被realloc的内存。
注:假如申请一块n个字节的空间,那么可能实际申请n+2或者n+4个字节的空间,然后把地址偏移2字节或4字节,开头偏移的2或4字节用来存储一些信息。(这里2和4只是假定)
5.大重点: setjmp 和 longjmp 的介绍及使用注意
int setjmp(jmp_buf evn);
void longjmp(jmp_buf evn, int val);
关于参数jmp_buf evn
最好把该参数声明做一个全局变量,因为这样方便使用。该参数指明了longjmp应该跳回哪个setjmp,因为会有多个对应。
setjmp的返回值会有两种情况
一种就是正常路过这个函数,他会返回0。
一种就是经由longjmp跳转回来。这时候longjmp的第二参数作为setjmp的返回值.
应重点注意的事项
static int globval; // 一个全局变量
int autoval; // 一个栈自动变量
register int regival; // 一个寄存器变量
volatile int volaval; // 一个易失变量
static int statval; // 一个静态变量
在setjmp和longjmp中间被修改的变量,longjmp跳转时,关于这五种类型的是否回滚。
书中的解释是这个是不确定的,他们没有标准,不同的环境结果也是不一样的。但是书中给出了一个例子来展示。
在编译不优化的情况下:所有变量都没有被回滚。
在编译优化的情况下:只有自动变量和寄存器变量被回滚到了setjmp之前的值
结论: 如果不想变量回滚就把他设置成volatile类型,还有尤其注意中间修改了哪些变量。
6.不重要的两部分写在最后
获取 设置 添加 删除进程的环境变量
虽然这一块不大可能用到还是把这几个函数写出来吧
注:不论是何种操作,其参数的格式都应该为 name=value
getenv取得某个环境变量,putenv添加某个环境变量,setenv设置某个环境变量unsetenv删除某个环境变量。
获取 设置 某些系统限制
struct rlimit{ rlim_t rlim_cur; rlim_t rlim_max}
int getrlimit (int resource, struct rlimit * rlptr)
int setrlimit (int resource, const struct rlimit * rlptr)
1.概念汇总
0号进程:调度进程,是内核的一部分
1号进程:intit进程,读取系统初始化文件,引导启动一个系统。是一个普通用户进程,但以超级用户权限运行,作为大部分进程的父进程。
2号进程:页守护进程负责虚存分页操作。
僵死进程:子进程终止后,如果父进程未调用wait或waitpid等获取子进程信息,子进程会一直被等待调用wait,以正常结束。
进程终止:进程A终止时,内核查询所有进程,判断是否有A的子进程,若果有,就把这些子进程的父进程改为1号进程。同为1号进程的行为还会对过继过去的子进程调用wait,使其正常终止。
2.获取ID的一组函数 & fork & vfork & exec
当前进程ID:pid_t getpid(void);
父进程ID:pid_t getppid(void);
调用进程的实际用户ID:uid_t getuid(void)
调用进程的有效用户ID:uid_t geteuid(void);
调用进程的实际组ID:gid_t getgid(void)
调用进程的有效组ID:gid_t getegid(void);
pid_t fork(void);
pid_t vfork(void);
fork和vfork的区别:fork之后父子进程不一定是哪一个先运行,vfork的目的就是为了创建新进程,其假设vfork之后会exec,保证子进程先运行。另一个区别是fork的父子进程,都有独立的进程空间。而vfork的父子进程共享同一进程空间。(子进程修改变量会影响父进程)。
小坑1:进程fork会,复制文件描述符,指向同一文件表项,也就是说文件偏移量也是共享的。
小坑2:如果某个流是带缓冲的,fork之前可能已经把数据写到文件上了,但是缓冲区存有之前的数据未被修改,fork之后,子进程退出,清洗流时,会再一次把缓冲区数据写进文件。
int execvp(const char * filename,char * const argv[]);
exec后,会继承父进程的一些属性:实际用户ID和实际组ID、附属组ID、进程组ID、会话ID、控制终端、当前工作目录、文件模式创建屏蔽字、未处理信号等等…
3.exit
不管进程如何终止,内核最后都会关闭所有描述符(可能会清洗流),释放存储器。
①从main返回:return返回
②exit:调用atexit注册的函数,清洗流
③_exit或_Exit:不清洗流
④最后一个线程从启动例程返回:
⑤最后一个线程调用pthread_exit
⑥调用abort:产生SIGABRT信号
⑦接到一个信号
⑧最后一个线程对取消请求做出响应:取消请求的相关概念在11章和12章
4.wait等一组函数
pid_t wait (int * status);
wait会阻塞等待子进程结束,如果没有子进程就返回错误。可以通过以下四个宏来判断子进程的状态:
WIFEXITED(status)正常终止,该宏返回真
WIFSIGNALED(status)异常终止,该宏返回真
WIFSTOPPED(status)若为暂停子进程返回的状态,该宏返回真。暂停状态在第九章。
WIFCONTINUED(status)暂停状态子进程继续执行后返回,该宏返回真。
pid_t waitpid(pid_t pid,int * status,int options);
参数pid:
pid < -1 等待组ID等于pid绝对值的任一子进程
pid == -1 等待任一子进程
pid == 0 等待 组ID 等于 调用进程组ID的任一子进程
pid > 0 等待进程ID与其相等的子进程。
参数options: WNOHANG 若没有子进程结束,不阻塞,返回0
WUNTRACED 进程处于 停止 状态且未报告,返回其状态。
WCONTINUED 停止状态的进程继续执行后,返回其状态
waitid wait3 wait4
这几个函数作用不大,当个纪念。
5.system & 获取用户登录名字 & 进程调度
int system(char *command);
system函数可以执行一个命令行参数。实现方式是fork一个进程,然后exec调用某个shell,执行参数command的命令。
获取用户登录名字char *getlogin(void);
进程调度优先级别
nice值有关于进程优先级,范围在0~(2*NZERO-1)之间。一般默认nice值为NZERO。nice值越小,优先级越高。
int nice(int inc);
设置进程nice值,返回新的nice值,出错返回-1;
int getpriority(int which,int who);
可以获取一组进程相关的nice值
which参数:PRIO_PROCESS表示进程,PRIO_PGRP表示进程组,PRIO_USER表示用户ID
who参数:0时取决于which。
int setpriority(int which,int who, int prio);
参数和上面一个,prio设置为新的nice值。
6.一些没看明白的
用户ID组ID 解释器文件 进程会计 进程时间
2020年7月20日23:07:48 7,8章
1.在一个终端登录Linux时,会有包含但不限于以下操作:
系统自举——》创建init进程——》init使系统进入多用户模式——》init读取文件/etc/ttys,对每个允许登录的终端设备调用一次fork——》生成的每个子进程调用exec getty 程序——》getty对终端调用open打开终端——》getty输出“login:”等信息,等待用户输入用户名——》输入用户名后,exec调用/bin/login程序,并把用户名的信息以及环境变量作为exec的参数传递——》login根据传递过来的用户名调用getpwnam取得用户的口令文件项——》调用getpass提示用户输入密码——》调用crypt对密码加密后,和口令文件项的密码比对,是否密码正确
连续几次失败后,login调用exit——》init得知login失败后,重新fork调用getty重复此过程
若正常登陆后——》更改工作目录为用户的起始目录、更改终端用户使登陆用户成为其拥有者、更改访问权限、调用setgid和initgroups设置进程组ID、初始化环境——》调用exec,使用用户ID启动其对应的shell程序
最后当此shell终止,其父进程init收到SIGHLD。
2.Telnet远程登录linux
init调用一个shell——》shell启动一个守护进程inetd进程,inetd等待一些网络连接——》shell终止时,inetd父进程变成init——》客户端通过Telnet来远程连接linux主机——》Telnet进程打开一个伪终端设备——》然后调用fork——》父进程处理网络连接,子进程执行login——》之后就和终端登录一样了。
3.进程组 会话 控制终端 作业控制
进程组:是一个或多个进程的集合。每个进程组有一个组长进程。进程组ID 等于 组长进程的ID。
会话(如本章最后的图):是一个或多个进程组的组合。是一个通过终端或者远程伪终端登录的 某个用户的 所有进程组集合。例如一个会话包含:一个只有 shell进程 的进程组。以及该shell启动的其他程序A、B,而程序A、B又各自fork形成其进程组。
控制终端:一个会话通常有一个控制终端(大概是一个shell),一个会话中的进程组可分为 一个前台进程组 以及 多个后台进程组。
①无论何时键入中断键(ctrl+C 产生SIGINT信号),终端信号都会发送给前台进程。还有退出字符(ctrl+\ 产生SIGQUIT信号),挂起字符(ctrl+Z 产生SIGTSTP),
②建立与控制终端连接的会话首进程称为控制进程(大概就是shell那个)。
③如果检测到网络连接断开,,则挂断信号发送至控制进程(会话首进程)。
④当没有控制终端的会话首进程打开一个终端设备,该终端自动成为这个会话头进程的控制终端。
作业控制:如一开始登陆后,会话中只有一个shell进程组,shell进程组中只有shell进程。后来通过shell,启动了其他的 前端进程 和 若干后端进程。启动后端进程时,在命令最后加 & 符号既可。
4.孤儿进程组
我的理解是,孤儿进程组只是一个概念,他并不是什么坏事。
定义:一组进程中,每个成员的父进程要么是该组的一个成员。要么不是该所属会话的成员。
如本章最后会话图中:proc1是proc2的父进程,或者反过来。或者是proc1 proc2的父进程不在该会话中,如proc1 proc2的父进程是init。
假设proc1的父进程是proc3,那么proc1 proc2的进程组就不是孤儿进程组。
5.关于本章的函数集合
pid_t getpgrp();
返回进程组ID
pid_t getpgid();
getpgid(0);
等价于getpgrp();
setpgid(pid_t pid, pid_t pgid);
将pid的进程组ID设置成pgid,pid为0时,使用调用者进程ID
pid_t setsid();
建立一个新会话。
如果调用进程是一个进程组组长,那么会出错返回。
若调用此函数的进程不是进程组的组长,则创建一个新会话。
该进程成为一个新进程组的组长进程,组ID是调用进程ID。
该进程没有控制终端,之前即使有一个控制终端,也会被切断。
pid_t getsid(pid_t pid);
若pid是0,getsid返回调用进程的 会话首进程 的 进程组ID
pid_t tcgetpgrp(int fd); int tcsetpgrp(int fd, pid_tpgrpid); pid_t tcgetsid(int);
没啥意义的三个函数,还是写一写。
6.关于停止状态
在作业控制那一块,但是shell控制,kill什么的,但是kill不是杀死吗,没看明白。
关于本章的大部分函数都统一列在本章最后,前半部分说明概念和书中暴露出的问题。
当线程遇上信号,有特殊情况,在十一章。
1.概念:
在
关于信号名和信号编号的转换使用函数strsignal
,sig2str
,str2sig
当产生一个信号,有三种处理方式:
①忽略。IGKILL和SIGSTOP这两个信号不能被忽略。
②捕获。通过signal
函数为某个特定的信号设置一个信号处理函数,产生该信号时,调用这个被设置的函数。
③默认动作。信号的默认动作不外乎三个,忽略、终止、终止+core文件。其中core文件是进程的内存映像。(本章最后附一张书上的图,是所有信号的默认动作)
另外在某些条件下是不产生core文件的,在P252,这里留个印象吧,不想抄书了。
程序启动(包括exec)时,所有信号都是默认或者忽略(exec后,原先的函数地址不可用)。
fork时,子进程继承父进程的信号处理方式。
不可靠信号:在信号发生之后和调用 该信号的处理函数之间有一个时间窗口,在此期间可能发生另一个中断信号。
被中断的系统调用:即慢系统调用被信号打断是否会重启。unp第五章说明过这个问题,并给出一种解决方法。本章后面给出了使用sigaction另一种解决方法。
可重入函数:P262给了一张表(附在本章最后),列出了可重入函数,这些可重入函数在信号处理期间会阻塞任何引起不一致的信号发送。
不可重入函数:使用了静态数据结构,调用malloc或free,是标准IO函数(这里不太清楚,书中只说了printf会被打断)
信号未决:一个信号产生时,内核在进程表整设置某个标志。设置这个标志的动作称为信号递送。在信号产生后,递送到进程前(或者是在信号产生后,因为阻塞没有被递送),称信号为 未决。
只有信号被递送后才决定对信号的处理方式,也就是说在未决状态下,可以改变对信号的处理方式。
2.一些大概常用的信号及解释:
SIGCHLD 进程终止时发送给父进程
SIGCLD 语义和上面那个不同,①若该信号设置为SIG_IGN,则子进程不产生僵死进程②如果设置为捕捉该信号,则立刻检查是否有子进程已经结束。
SIGABRT 调用abort
函数产生此信号,进程异常终止。
SIGALRM 函数alarm
设置的定时器超时
SIGBUS 内存故障
SIGEMT、SIGIOT、SIGTRAP 硬件故障
SIGFPE 算术运算异常SIGINT
SIGIO 异步IO事件
SIGKILL 不可被忽略的信号之一,作为信号被杀死的可靠方法存在
SIGSTOP 不可忽略的信号之二,停止一个进程,但不是终止,停止状态,作业控制等。
SIGPIPE 管道的 读进程 已终止时 写管道,产生该信号
SIGPWR 电池电压过低
SIGQUIT 终端上键入退出键(ctrl+)
SIGSEGV 无效的内存引用,野指针指针越界,段错误,段违例
SIGSYS 无效的系统调用,高版本程序在低版本系统运行
SIGURG 带外数据
3.signal函数:
typedef void Sigfunc(int);
Sigfunc * signal(int signo, Sigfunc * func);
关于参数func,有三个常量:SIG_IGN、SIG_DEF、SIG_ERR,前两个可作func的参数,分别是对某个信号忽略和默认行为,第三个可用来判断signal的返回值如if(signal(SIGURG,SIG_DEF) == SIG_ERR)
3.函数集合及一些暴露的问题:``
int kill(pid_t pid, int signo);
给进程或进程组发送信号。
pid == -1 把信号发送进程有权向他们发信号的所有进程
pid < 0 发送给进程组ID等于PID绝对值,且有权发送的所有进程
pid == 0 发给同一进程组且有权发送的所有进程。
pid > 0 直接发给某个进程。
int raise(int signo);
自己给自己发送一个信号
unsigned int alarm(unsigned int seconds);
设置一个定时器,超时是产生SIGALRM信号,不捕获就会终止调用进程。如果上次设置的定时器未超时,就在此调用alarm,返回值是上次定时器剩下的时间。
int pause(void);
只有执行了一个信号处理函数并返回时,pause才返回,返回-1,errno设置为EINTR。
一个问题,如果alarm的值很小,在调用到pause之前就返回并执行信号处理函数,那么pause可能一直阻塞下去。书中尝试使用setjmp
和longjmp
解决,但是这两个函数在这里有一些问题,应该使用sigsetjmp
和siglongjmp
。
int sigemptyset(sigset_t *set);
将所有信号的标记设为0
int sigfillset(sigset_t * set);
将所有信号的标记设为1
int sigaddset(sigset_t *set,int signo);
增加一个信号到信号集
int sigdelset(sigset_t * set,int signo);
从一个信号集删除一个信号
int sigismember(const sigset_t *set,int signo);
判断信号是否在某个信号集
关于信号集:表示多个信号。
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oldset);
该函数用来屏蔽信号,即对某些信号阻塞,不接受这些信号。如果产生了这些信号,那么他们处于未决状态。
在调用该函数后如果有任何未决且不再阻塞的信号,在该函数返回前至少,至少有一个被递送给进程(这里指同一信号多次发生)
若参数oldset不为空,那么返回信号屏蔽字
how参数:
SIG_BLOCK 对set参数所指的信号阻塞
SIG_UNBLOCK 对set参数所指的信号解阻塞
SIG_SETMASK 进程设置set作为新的屏蔽字。
int sigpending(sigset_t *set)
查看当前进程哪些信号是被屏蔽阻塞的,通过参数set返回。
书中的例子展示了一个情况:调用sigprocmask屏蔽中断信号,然后产生多个中断信号,这时这些信号未决,接着在把中断信号解开=阻塞,然后收到一次中断信号。
int sigation(int signo,const struct sigaction *act ,struct sigaction *oldact);
检查或修改与指定信号关联的处理动作,包括处理函数、信号屏蔽字、还有一个标志选项(关于这些标志选项也在后面放一张图吧,不想写了),这些东西全都包含在sigaction结构中。
另外用sigaction函数实现了signal和signal_intr。
前者在为某个信号设置函数时,只要这个信号不是SIGALRM,那么进行某种设置,使发生这个信号后 会尝试主动重启被打断的系统调用。后者的行为则是设置尝试不会启重启被中断的系统调用。**从而避免了不明确的行为。**感觉还是unp第五章那个解决方法好用。
#include
#include "ourhdr. h"
Sigfunc * signal (int signo, Sigfunc * func)
{
struct sigaction act, oact ;
act.sa_ handler = func;
sigemptyset (&act.sa_ mask) ;act.sa_ flags = 0;
if (signo == SIGALRM)
{
#ifdef SA INTERRUPT
act.sa_ flags I= SA_ INTERRUPT; /* SunOS */
#endif
}
else
{
#ifdef SA RESTART
act.sa_ flags l= SA_ RESTART; /* SVR4, 4.3+BSD */
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ ERR) ;
return (oact .sa_ handler) ;
}
int sigsetjmp(sigjmp_buf env, int savemask)
和int siglongjmp(sigjmp_buf env, int val)
使用sigsetjmp
和siglongjmp
是因为setjmp
和longjmp
有一个坑,后者在跳转后不会恢复原来的信号屏蔽字。
两者用法一样,区别是只有savemask不为0,sigsetjmp就把信号屏蔽字保存在env参数里。
这里还有一个坑,evn参数可能还未被初始化就产生了信号,引发调用。解决方法是设立一个flag,在主进程sigsetjmp后设置该flag表示已初始化,在信号处理函数中判断flag,如果为0,就不跳转(啊这一块好迷啊,虽然知道是为了解决什么问题,但是没明白问题本身以及是如何解决的)。
void abort(void)
abort不会返回到调用端,但是允许程序捕捉SIGABRT,意在让程序做结束前的处理。
char * strsignal(int signo)
接受信号编号,返回信号的字符形式
4.其他没怎么认真看的:
int sigsuspend(const sigset_t *mask);
这里书上原话是:如果在接触阻塞时刻和pause之间发生信号,那么就会产生问题。因为可能不会再见到该信号。理解不了,放着吧。
system
啊直接跳过。
sleep nanosleep clock_nanosleep
书中讲了一下这三个函数的实现,不过没必要深究,行为就是将程序挂起一段时间。
sigqueue
这个东东试图让信号排队。
sig2str
和str2sig
这两个函数是Solaris的函数,不知道能不能通用,留个纪念,就是信号编号和信号字符串的互转。
关于线程的使用,曾经在c++并发编程实战一书中了解过,相比之下,c++标准库的线程看起来更易用,一个是对象式,本书则是函数式。本章只做一些概念上的笔记,关于线程的应用,等过些日子再看一遍并发编程实战,写到另一篇里吧。
1.互斥量
锁,多线程并发访问时防止殷勤数据不一致情况。多个锁的情况下按相同顺下上锁避免死锁
2.读写锁
可以同时被多次锁定读操作,但只要有一个锁定写操作的请求,读写锁会阻塞随后的读模式请求。
3.条件变量
是一种线程同步的方式,首先对声明一个一个条件变量对象。生产者中,加锁解锁,然后通过条件变量发出通知。消费者中,加锁,对条件变量和锁等待,等待过程中,会暂时解锁,当生产者发出通知后,重新上锁,条件变量的等待返回,执行操作,最后解锁。
4.自旋锁
自旋锁和普通的锁相似,区别是普通的锁在lock后阻塞挂起,自旋锁lock后不挂起等待,而是持续请求,以期望在短时间内获取到锁。
5.屏障
另一种线程同步的方式,强化版的join函数,首先声明一个屏障对象,设置某个数值,这个数值是要等待的线程数。或者说是拓补图,所有的前置条件达成(线程到达屏障函数),才能进行下一步。书中的例子:对800W个数分成8个线程进程堆排序,最后合并,合并的前置条件就是八条排序线程到达屏障,主线程也到达屏障函数处,那么一开始设置一个值为9的屏障,主线程启动八条线程后到达屏障处,屏障计数加一,另外八条排序线程,每有一个线程执行到屏障函数处,计数就加1,最终计数到9时,突破屏障,所有线程继续执行。
6.线程限制
书中展示了:
①线程退出时销毁线程特定数据的最大次数
②创建键的最大数量
③线程栈可用的最小字节数
④进程可创建的最大线程数。
①②都是关于线程特定数据的,在第13节。线程特定数据是一个概念。
7.0属性
所有关于属性的操作基本都是如下清一色操作:
①首先声明一个特定的属性对象,如是线程属性就声明线程属性对象,互斥量属性就是互斥量属性对象。
②接下来通过该对象的特定初始化函数初始化,通过对应的get/set函数设置或者取得对象属性。
③最后通过特定的析构函数清理属性对象。
这里的特定函数意为:线程属性对象和互斥量属性对象 的 初始化和析构函数 是不同的函数。
7.线程属性
有四个关于线程的属性:
①直接以分离模式启动
②线程栈的最低地址
③线程栈的最小长度
④线程栈末尾的警戒缓冲区大小。
8.互斥量属性
①进程共享属性:顾名思义,在进程间共享互斥量
②健壮属性:持有互斥量的进程终止时不需要特别动作(大概是释放清理之类的?)
③类型属性:有四种类型的互斥量可以设置。一是标准互斥量,不做任何检查和死锁检测。二是提供检查的互斥量,三是递归锁类型。四是默认行为,书中的描述是取决于不同的实现,生产厂家可以随意实现为任意一种。
9.读写锁属性
只有一个进程共享属性,同上。
10.条件变量属性
①进程共享属性:顾名思义,在进程间共享互斥量
②时钟属性:设置条件变量的超时时间,但是条件变量也有指定时间的超时函数,意义不大。
11.屏障属性
只有一个进程共享属性,同上。
12.重入
有些系统提供的函数不可重入,就像信号那章讲的一样,使用静态数据结构等。在最后补一张这个不可重入函数的表把,甚至system rand等函数都不可重入。
13.线程特定数据
关于这个东西怎么讲,很鸡肋的感觉。虽然说线程独立的errno通过这个东西实现,但是感觉完全用不上。
就是线程独有的数据,创建一个键,给这个键通过特定函数pthread_setspecific()
设置一个void指针,这个指针指向数据,关于数据如何解读需要自己把握,键只管指向。使用时通过一个get函数获取。
每个键只指向关联指向一个数据,键存储在内存单元中,可被所有线程使用。
我的理解是:键指向数据,但是线程和键并不关联,只是使用的时候应该明白,某个线程和某些键是对应的。
14.取消线程
就是字面意思,让线程停止,中断。
通过函数pthread_setcancelstate()
设置线程的取消状态。
之后通过函数pthread_testcancel()
,尝试在取消点结束线程。
取消点一般是某个系统调用,send write poll等。
15.线程遇上信号
每个线程都有自己的线信号屏蔽字,但是信号的处理确是所有进程共享的。
进程产生的信号会递送到某个线程,至于会递送的哪个线程,我的理解大概是调用sigwait()
那个线程。
书中展示了一个例子:对主线程调用pthread_sigmask()
函数阻塞某些信号后。启动一条专门处理信号的线程。这个线程是一个无限循环,调用sigwait()
等待信号到来,sigwait()
接受一个信号屏蔽字参数,它暂时解阻塞这些信号,以期望等待到这些信号,接着通过switch处理。
16.线程遇上fork
fork之后,子进程只有一个线程,谁调用的fork,那么子进程就是哪条线程。
然后继承地址空间副本,包括互斥量条件变量的。这里主要想表达的就是在fork之后处理掉这些互斥量条件变量。
如果fork之后exec,那么原先地址空间被抛弃,也就不需要处理他们了。
否则就需要处理那些变量。
提供了如下函数来帮做操作。
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
prepare在fork之前被调用,接下来child,最后parent函数。
17.pread和pwrite
线程安全的读写
2020年7月31日23:09:11 11,12章
关于如何通过编程实现启动守护进程,在unp第十三章里,有详细如何启动以及为什么这么的原因。
相较之下,本书中关于守护进程概念更清晰,unp中关于细节解释更清晰。
1.守护进程
守护进程是长期生存的进程,他们常常在系统引导装入时启动,系统关闭时中间。他们没有控制终端,所以说他们在后台运行。
系统上有很多守护进程。如kswapd是内存换页守护进程,flush守护进程在可以内存达到设置的最小阈值时将脏页面冲洗至磁盘等等。
inetd作为一个守护进程,监听网络接口,来提供各种网络服务。
2.syslog守护进程
守护进程没有控制终端,当出现错误信息的报告时,通过syslog函数,将错误信息统一递送给syslog守护进程处理。
1.非阻塞IO、IO复用、readv和writev
关于这三块内容就不写了,分别在UNP第十六章 第六章 第十四章
2.记录锁
记录锁是操作系统级别,关于文件的锁。这个锁可以关于整个文件,也可以只关于文件的某一部分。
int fcntl(int fd, int cmd, ...);
使用注意
l_len字段若为0,那么不管追加多少数据都在锁的范围。
锁的范围可以超过文件尾,但不能在开始位置之前。
加读锁,描述符必须是读打开,加写锁,描述符必须写打开。
再用F_GETLK和F_SETLK之间有时间窗口,其他进程可能先一步加锁。
当一个描述符关闭,进程通过该描述符引用的所有锁都释放。
fork之后不会继承记录锁。但是exec后会继承。
关于参数cmd:
F_GETLK 第三参数填写好后,调用,判断是否能够成功上锁。如果第三参数描述的范围上有锁,那么就把这个已经存在的锁的信息填到第三参数返回。如果这个范围上无锁,那么除了l_type字段被更改为F_UNLCK外,其他都不变。
F_SETLK 非阻塞版本,试图设置一个锁。出错则errno设置为EACCES或EAGAIN。
F_SETLKW 阻塞版本,如果不能取得锁,那么挂起等待。
关于第三参数指向的结构struct flock:
struct flock
{
short l_type; // F_RDLCK 共享读锁 F_WRLCK 独占写锁 F_UNLCK 解锁
short l_whence; // 和lseek函数的参数一样,从哪里开始偏移 SEEK_SET SEEK_CUR SEEK_END
off_t l_start; // l_whence + l_start,最终的锁的起始点。
off_t l_len; // 从起始点锁起的文件长度。
pid_t l_pid; // 哪个进程持有的锁,仅由系统返回填写,不由用户填写。
};
3.存储映射IO
将文件内容直接映射到用户提供的某块内存里。
映射区长度会自动对齐到系统页长整数倍。修改多出的空白部分,不会写 也不会追加到文件。
fork会继承存储映射区。
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
strat参数是内存地址,为0则表示系统选择地址。
len是映射的字节数
prot是映射区的属性,读还是写。PROT_READ PROT_WRITE PROT_EXEC PROT_NONE,对这些按位或,同时使用这些宏也不能也不能超过open文件时的权限。
flag参数。MAP_SHARED表示修改映射区也会修改文件。MAP_PRIVATE对文件创建一个副本,修改映射不会修改文件。
fd参数是被映射文件的描述符。
offset是起始偏移量。
int mprotect(const void *start, size_t len, int prot);
更改一个现有映射的权限。
int msync ( void * addr, size_t len, int flags);
MAP_SHARED不会实时同步写,可以通过msync主动冲洗
int munmap(void *start, size_t length);
进程终止会自动关闭映射。或通过该函数主动关闭。!注:调用该函数不会主动冲洗数据,为同步的将会丢失。
2020年8月2日16:52:45 13,14章
这一章我没有认认真真去看,翻看这章的时候我时常回想起muduo中进程间通信只用TCP,以前我还在异或为什么,现在我有些理解了,各种繁复的结构、使用限制、安全性问题让人头大,相比之下TCP调几个函数,想写就写,想读就读,真的是让人舒服。
管道
通过调用pipe()函数,来返回一对文件描述符,一般用于父子进程间,父进程关闭其中一个,子进程关闭另一个,实现单向的数据传递。
管道在进程终止时完全删除。
命名管道FIFO
管道的思路是先调用mkfifo创建一个fifo文件,然后向对待正常文件一样,单个或多个进程使用open打开,对其读写。
最后一个引用被解除时,被创建的fifo文件仍让留在系统中,但其中的数据被删除。
XSI
XSI IPC有三种:消息队列 信号量 共享存储
每个IPC结构都有一个称为 标识符 的内部名。以及一个称为 键 的外部名。
这些结构在系统中没有名字,不能以文件形式查看或删除他们,因为没有描述符,更不能对他们调用IO函数。
消息队列
消息队列是消息链表,存储在内核,有消息队列标识符 标识。通过调用函数对某个队列插入取出,而且可以使用某个结构来优先取出某些数据。
消息队列中的数据不会被删除,知道数据被取出 | 被主动删除
信号量
有些类似互斥量和操作系统里的进程同步原语,没怎么看明白。
共享存储
即共享内存。
popen()和pclose()
FILE * popen(const char * cmdstring, const char * type)和pclose(FILE *fp)
popen先fork,然后exec执行其参数cmdstring启动另一个程序。type参数用来区分读写。
r模式下,被exec的进程的 标准输出 会流向 popen所返回的文件指针。
w模型下,父进程向该文件指针写的数据 会流向 被exec进程的标准输入。
本章就是关于网络编程socket那一块的,相关信息在UNP的笔记中有更详细的记录,跳过。
2020年8月5日16:41:13 15,16章
关于这章偷懒吧。主要讲了两部分内容,一部分是通过unix域套接字建立连接。另一部分是通过unix域套接字传递文件描述符。这两部分书中都给出了完整的实现代码,需要直接调用就可以了。稍微写一下。
1.完全不相干的进程使用UNIX域套接字建立连接
socketpair()
函数用来在父子进程间建立连接。以下则是在完全不相干的进程间建立连接。
书中提供了三个自定义函数,行为是通过一个本机上文件,被动端绑定这个文件来实现监听,主动端通过connect这个文件来实现连接。
int serv_listen(const char * );
int serv_accept(int listenfd, uid * uidptr);
int cli_conn(const char * name);
注:这三个函数是apue自己实现的,代码在www.apuebook.com上可以找到。这几个函数是没有处理被中断的系统调用的情况的。
服务器进程调用serv_listen()
函数,声明一个名字(文件系统中某个路径名)上监听客户进程的连接请求,如果成功,返回一个unix域套接字类型的监听套接字。
serv_accept()
通过serv_listen()
返回的监听套接字调用。第二参数是一个值-结果参数,用来返回声明的路径名的所有者。成功则把新建立的连接通过返回值返回。
cli_conn()
的参数需要和服务端所声明的那个路径一模一样。然后作为参数建立连接。
2.使用UNIX域套接字在进程间传递描述符
发送进程把描述符发送的时候,描述符引用计数+1,然后关闭该描述符。
int send_fd(int fd, int fd_to_send);
int send_err(int fd, int status, const char * errmsg);
int recv_fd(int fd, ssize_t (*userfunc)(int , const void *, size_t));
第二个函数没看懂,不过并不影响。
send_fd()
用来发送描述符,fd参数是用来通信的那个uniix域套接字,fd_to_send是被发送的那个描述符。
recv_fd
用来接收描述符,成功返回接收的描述符。若返回负值,则说明这个负值是有send_err()
发送的错误状态,接下来调用userfunc来处理该消息。
2020年8月7日18:52:48 17章
今天简略翻看了一下这几章,分别讲的是对终端、伪终端的一些属性的操作。然后是一个数据库的实现。最后一章和打印机通信。对于我这个只是在unix环境下编程的人来说并用不上。apue就算到此结束了。等回过头来再把文章里面留下的坑填一填。
2020年8月8日17:02:22