系统调用:由操作系统实现并提供给外部程序的编程接口(API)
函数实现从用户区到内核区的数据传输就是依靠系统调用
在man手册的第二卷存放的就是系统调用。但是严格来说应该叫做系统函数,因为它对真正的系统调用做了一次浅封装,即仅仅是用另一个名字不同的函数来封装系统调用。如man手册中的open、write、read函数,其真正的系统调用为sys_open、sys_write、sys_read
open和close函数需要头文件#include
,参数中的宏定义需要头文件#include
open函数
原型:
int open(const char *pathname, int flag);
int open(const char *pathname, int flag, mode_t mode);
参数:
|
来连接返回值:一个文件描述符,当为-1时表示打开失败,出现错误
close函数
原型:
int close(int fd);
参数:
返回值:0表示关闭成功,-1表示关闭失败
open常见错误:①打开文件不存在;②以写方式打开只读文件(打开文件没有对应权限);③以只写方式打开目录
errno:最后一次错误的序号,是一个全局变量。当程序出现错误时的一个用于指明错误的错误号,使用时需要头文件#include
strerror:string strerror(int errno);
用于解释errno所代表的错误内容
perror:void perror(const char *s);
直接输出自定义的错误信息,参数就是用户自定义的错误信息字符串
需要头文件#include
read函数
原型:ssize_t read(int fd, void *buf, size_t count);
参数:
返回值:返回一个正数表示读取成功,是读取到的字节数;0表示已经读取到文件尾,读取结束;-1表示读取失败,设置errno;-1且errno=RAGIN或EWOULDBLOCK表示不是读取失败,而是read在以非阻塞方式读一个设备文件或网络文件,且文件无数据
write函数
原型:ssize_t write(int fd, const void *buf, size_t count);
参数:
返回值:返回一个正数表示写入成功,是写入的字节数;0表示写入完毕,没有更多内容可写入;-1表示写入失败,设置errno
strace:strace ./test
可以跟踪命令执行时的系统调用
优先使用库函数而不是系统调用,系统调用效率不一定比库函数高
预读入缓输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I8HunCfY-1664298269830)(https://gitee.com/AnonymYH/markdown-image/raw/master/img/20200723142043813.png)]
系统维护一个内核缓冲区,当进行写入操作(缓输出)时,数据从用户去写入内核区缓存时即认为是写入成功,但是系统会利用算法在合适的时机把内核缓冲区的数据真正写入到磁盘上;进行读取操作(预读入)时,如果读取的字节数太小,系统仍会把一整块数据从磁盘读取到内核缓冲区上,在下次读取时不需要再去进行磁盘上的物理操作,节省时间
每一个进程对应一个PCB,PCB进程控制块是一个结构体struct task_struct
,其中一个指针成员指向文件描述符表,其中存放着该进程所使用的文件对应的文件描述符。一个进程能打开的最大文件数默认为1023
文件描述符:是一个键值对(key, value)
,其中key为一个整数,value是一个指针,指向一个文件结构体struct file
,文件结构体中保存着文件的各种描述信息。通过整数key即可访问文件
新打开的文件的文件描述符一定是当前文件描述符表中最小的整数
阻塞场景:读常规文件不会发生阻塞,只有读设备文件、网络文件时才会发生阻塞
终端对应的文件为/dev/tty
阻塞是设备文件、网络文件的属性。可以通过open函数的flag参数O_NONBLOCK将文件设置为非阻塞状态
当设备文件、网络文件设置为非阻塞时,设备无数据时read不会等待输入而是直接返回-1,并且将errno设置为EAGIN或EWOULDBLOCK
阻塞和非阻塞都不是最好的读取设备或网络文件的方式,最好的方式为设备或网络文件需要被读时向系统主动发出请求,如epoll
需要头文件#include
和#include
获取/修改文件状态
原型:int fcntl(int fd, int cmd, .../*arg*/ );
参数:
返回值:-1时表示cmd操作失败
修改文件状态:利用F_GETFL获取文件状态的位图flag,利用位运算修改flag中对应位的值即表示修改文件状态,修改后利用F_SETFL将文件状态设置为flag使修改生效
需要头文件#include
设置文件中的偏移位置(文件读写位置)
原型:off_t lseek(int fd, off_t offset, int whence);
参数:
返回值:正数表示从文件起始位置开始的偏移量;-1表示失败,设置errno
文件读写使用的是同一偏移位置
可以用lseek(fd,0,SEEK_END)
返回值统计文件大小;用lseek(fd,size,SEEK_END)
拓展文件大小,但是想要真正拓展文件大小必须引起IO操作,所以还需要在结尾调用write(fd,"\0",1)
拓展文件还可以使用truncate(path, length)
实现,将路径path的文件拓展length长度。成功返回0,失败返回-1并设置errno
传入参数定义:
如char *strcpy(char *dest, const char *src);
中的src
传出参数定义:
如char *strcpy(char *dest, const char *src);
中的dest
传入传出参数定义:
如char *strtok_r(char *str, const char *delim, char **saveptr);
中的saveptr
inode:索引节点,其实是一个结构体struct
,存储文件的属性信息。大多数的inode都存储在磁盘上(存储在内存中的inode为VFS inode),inode中的某个成员指向磁盘上文件存储的盘块位置
dentry:目录项,是一个结构体,主要包含文件名和inode号,通过inode号就能找到磁盘上的inode结点。创建硬链接就是相当于创建了一个新的dentry,其中的inode号指向同一个inode结点
需要头文件#include
和#include
获取文件属性(从inode中获取)
原型:int stat(const char *path, struct stat *buf);
参数:
返回值:0表示获取成功;-1表示获取失败,设置errno
用stat函数去获取符号链接的属性时或发生stat穿透,获取到的是符号链接指向的文件的属性而不是链接本身的属性。解决方法为,使用lstat函数获取符号链接属性
lstat函数:int lstat(const char *path, struct stat *buf);
原型和返回值与stat函数相同,不会发生符号穿透
判断文件类型:利用获取的属性中的成员buf.st_mode
作为参数传入宏函数S_ISREG()普通文件、S_ISDIR()目录、S_ISCHR()字节设备、S_ISBLK()块设备、S_ISFIFO()管道、S_ISLNK()符号链接、S_ISSOCK()套接字
成员变量st_mode是一个16位的位图,0-3位表示文件类型,4-6表示特殊权限码,7-15表示u、g、o的权限码。通过执行与运算屏蔽其他位来获取每部分的值
需要头文件#include
access:int access(const char *pathname, int mode);
测试指定文件是否有mode所表示的权限。参数mode表示权限码。返回0表示具有该权限;-1表示不具有该权限,设置errno
需要头文件#include
chmod:int chmod(const char *path, mode_t mode);
修改文件访问权限为mode表示的权限。返回0表示修改成功了;-1表示修改失败,设置errno。也可以用int fchmod(int fd, mode_t mode);
用文件描述符来指定需要修改的文件
需要头文件#include
link函数
为已存在的文件创建硬链接,即目录项
原型:int link(const char *oldpath, const char *newpath);
参数:
返回值:0表示创建成功;-1表示创建失败,设置errno
unlink函数
删除文件的一个目录项,使硬链接计数减一
原型:int unlink(const char *pathname);
参数:
返回值:0表示删除成功;-1表示删除失败,设置errno
清除文件时,如果文件的硬链接计数为0,没有目录项dentry对应时,文件并不会马上被释放,而是等到所有打开该文件的进程关闭该文件系统才会在合适的时间释放。所以unlink函数只是让文件具备了被释放的条件,并不是立刻删除了文件
==隐式回收:==当进程结束运行时,所有该进程打开的文件都会被关闭,申请的内存空间会被释放。是系统特性,但是不允许程序员依赖此特性
需要头文件#include
读取符号链接文件本身,得到链接指向的文件名
原型:ssize_t readlink(const char *path, char *buf, size_t bufsiz);
参数:
返回值:0表示读取成功;-1表示读取失败,设置errno
在终端可以直接使用readlink命令来读取链接文件的内容
需要头文件#include
重命名一个文件
原型:int rename(const char *oldpath, const char *newpath);
参数:
返回值:0表示重命名成功;-1表示重命名失败,设置errno
需要头文件#include
getcwd函数
获取进程当前工作目录
原型:char *getcwd(char *buf, size_t size);
参数:
返回值:返回路径表示成功;NULL表示失败
chdir函数
改变当前进程的工作目录
原型:int chdir(const char *path);
参数:
返回值:0表示修改成功;-1表示修改失败,设置errno
目录文件也是文件,文件内容是该目录下所有子文件的目录项dentry
目录文件权限:
目录设置黏着位:目录的删除、修改只能由文件所有者或者root用户操作。写操作不受影响。黏着位只对目录有效,对文件无效
需要头文件#include
opendir函数
根据传入的目录名打开一个目录
原型:DIR *opendir(const char *name);
参数:
返回值:返回指向该目录的结构体指针表示成功;NULL表示打开失败
closedir函数
关闭打开的目录
原型:int closedir(DIR *dirp);
参数:
返回值:0表示关闭成功;-1表示关闭失败,设置errno
需要头文件#include
读取目录。一次读取一个目录项
原型:struct dirent *readdir(DIR *dirp);
参数:
返回值:返回目录项结构体指针表示成功;NULL表示读取结束;NULL且设置errno时表示读取错误
读取返回指针的成员变量d_name可以获取目录项对应的文件名dirp->d_name
需要头文件#include
回卷目录读写位置至起始位置
不是系统调用,是库函数
原型:void rewinddir(DIR *dirp);
参数:
需要头文件#include
telldir函数
获取目录读写位置
原型:long telldir(DIR *dirp);
参数:
返回值:返回目录的读写位置表示获取成功;-1表示获取失败,设置errno
seekdir函数
修改目录读写位置
原型:void seekdir(DIR *dirp, long loc);
参数:
需要头文件#include
新建或令已有的一个文件描述符指向旧文件
原型:
int dup(int oldfd);
int dup2(int oldfd, int newfd);
参数:
返回值:返回新的文件描述符表示成功;-1表示失败,设置errno
可以用于实现重定向,让标准输出的文件描述符STDOUT_FILENO指向想要重定向的文件dup2(fd, STDOUT_FILENO)
fcntl函数也能实现dup函数的功能。参数cmd取值为F_DUPFD,就会返回一个新建的文件描述符。也可以再添加一个整数参数指定文件描述符的key值,若该文件描述符未被占用则令其指向旧文件描述符对应的文件;若被占用则返回从该数字开始最小的文件描述符
程序:是存储在磁盘上的编译好的二进制文件
进程:是活跃的程序,占用系统资源,在内存中执行
并发:在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但是任一时刻点上只有一个进程在运行
单道程序设计:所有进程一个个排队依次执行
多道程序设计:在计算机内存中同时存放几道相互独立的程序,在管理程序控制下相互穿插运行
MMU:内存管理单元。负责进行虚拟地址到物理地址的映射管理
孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程。此时init进程会领养孤儿进程,即成为它的父进程
僵尸进程:子进程终止,父进程尚未回收,子进程残留资源即PCB存放于内核中,变成僵尸进程。无法被kill终止,因为僵尸进程本身就已经终止,结束僵尸进程的方式为kill它的父进程,让僵尸进程成为孤儿进程被init收养,init会自动回收它
PCB是一个结构体task_struct
,来维护进程相关信息和资源
task_struct
的主要内部成员:
PATH:可执行文件的搜索路径。可以包含多个路径,执行命令时会依次去每个路径下找
SHELL:当前Shell
TERM:当前终端类型
LANG:语言和locale,决定了字符编码以及时间、货币等信息的显示格式
HOME:当前用户的宿主目录(家)
需要头文件#include
创建一个子进程
原型:pid_t fork(void)
返回值:父进程返回子进程的id;子进程返回0;返回-1表示创建子进程失败
fork之后的代码会被父进程和子进程分别执行,所以会执行两遍
可以通过getpid函数获取当前进程的pid,通过getppid获取当前进程的父进程的pid;通过getuid函数获取当前进程实际用户id,通过geteuid函数获取当前进程有效用户id;通过getgid函数获取当前进程使用用户组id,通过getegid函数获取当前进程有效用户组id
父子进程遵循读时共享写时复制原则。当子进程只是读父进程用户空间的内容,那就不用复制,直接使用父进程用户空间的内容;当父进程或者子进程需要写入内容时,再复制父进程的用户空间的相应内容进行写操作
父子进程相同的部分:data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式
父子进程不同的部分:进程id、fork返回值、各自的父进程、进程创建时间、闹钟、未决信号集
**父子进程共享的部分:**文件描述符、mmap映射区
父子进程的执行顺序取决于内核使用的调度算法
gdb调试时只能跟踪一个进程,当进程中调用了fork函数产生了子进程后,gdb默认跟踪父进程。也可以使用命令set follow-fork-mode child
在fork函数调用之前设置
父进程结束后就会返回至shell进程,此时会打印命令提示符。但是父进程结束时子进程不一定执行结束,所以可能出现命令提示符比子进程的输出先打印的情况,说明bash在子进程之前强占了资源执行了打印
需要头文件#incldue
调用exec函数可以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以进程的id并不改变,换核不换壳
exec执行结束后不能返回给调用者
原型:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
函数名中的p表示PATH环境变量
execlp函数
加载一个进程,借助PATH环境变量。通常来调用系统调用
参数
返回值:正常情况没有返回值,只有当出错时才会返回-1,设置errno
execl函数
加载一个指定路径下的进程
参数:
返回值:同execlp
bash进程使用命令执行其他可执行程序的过程就是依靠exec函数族
其他exec函数都是库函数,只有execve是系统调用,其他exec函数的实现都是通过封装execve实现的
需要头文件#include
回收子进程终止信息。功能包括①阻塞等待子进程退出;②回收子进程残留资源;③获取子进程结束状态(退出原因)
wait函数
原型:pid_t wait(int *status);
参数:
返回值:返回子进程的id表示回收成功;-1表示没有子进程
通过宏函数解释传出参数status表示的子进程退出状态,具体宏函数查看wait函数的man手册
一次wait调用只能回收一个子进程
waitpid函数
原型:pid_t waitpid(pid_t pid, int *status, int options);
参数
返回值:返回终止的子进程id表示回收成功;-1表示回收失败;0表示options指定了WNOHANG即不挂起,且没有子进程结束
IPC:进程间通信。进程地址空间相互彼此独立,每个进程都有各自不同的用户地址空间,想要交换数据需要通过内核,在内核中开辟一块缓冲区,通过共享的一块内核缓冲区进行通信
方法:文件、管道、信号、共享内存、消息队列、套接字、命名管道
伪文件:管道、块设备、字符设备、套接字。不占用磁盘空间
是伪文件,即内核缓冲区
由两个文件描述符引用,一个表示读端,一个表示写端。规定数据从写端流入,从读端流出管道,
数据在管道里只能单向流动,数据不可以反复读,读完就释放
原理:内核使用环形队列机制,借助4K内核缓冲区实现
一般用于有血缘关系的进程之间,如父子进程
需要头文件#include
创建匿名管道,并打开管道
原型:int pipe(int pipefd[2]);
参数:
返回值:0表示创建成功;-1表示创建失败,设置errno
管道的缓冲区大小为4096字节
读管道:
写管道:
一般一个管道只有一个读端和一个写端,在使用管道时要将不使用读/写端的进程的对应端close掉。但是管道允许一个写端与多个读端和多个写端与一个读端
命名管道。匿名管道pipe只能用于有血缘关系的进程之间,FIFO能用于不相关的进程之间的通信
创建FIFO文件:命令mkfifo 管道名
、函数int mkfifo(const char *pathname, mode_t mode);
函数mkfifo
需要头文件#include
和#include
原型:int mkfifo(const char *pathname, mode_t mode);
参数:同open函数
mode & ~umask
返回值:0表示创建成功;-1表示创建失败,设置errno
写入:先以只写方式打开FIFO文件open(path,O_WRONLY)
,然后再向文件里写入数据write(fd,buf,len)
读出:先以只读方式打开FIFO文件open(path,O_RDONLY)
,然后再读出文件里的数据read(fd,buf,size)
消息队列是消息的链表,存放在内核中并由消息队列标识符表示
消息队列提供了一个从一个进程向另一个进程发送数据块的方法,每个数据块都可以被认为是有一个类型,接受者接受的数据块可以有不同的类型
每条消息的最大长度上限为MSGMAX,每个消息队列的最大字节数为MSGMNB,每个消息队列的最大消息数为MSGMNI,具体的值可以通过cat /proc/sys/kernel/msgmax
查看
特点:
需要头文件#include
创建和访问一个消息队列
原型:int msgget(key_t key, int msgflag);
参数:
返回值:成功返回非负整数作为消息队列的标识码,失败返回-1并设置error
需要头文件#include
把从指定路径的文件与一个低序8位的整数标识符组合成一个整数IPC键
原型:key_t ftok(const char* pathname, int proj_id);
参数:
返回值:成功返回IPC键值,错误返回-1并设置error
ftok的实现是通过调用stat函数获取文件的属性,利用属性中的st_dev
文件所在文件系统的信息、st_ino
文件的索引节点号、proj_id
低序8为标识符组合产生一个32为IPC键值,具体组合为**proj_id
的后8位+st_dev
的后8位+st_ino
的后16位**
需要头文件#include
原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
返回值:成功返回0,失败返回-1并设置error
需要头文件#include
原型:int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);
参数:
返回值:成功返回0,失败返回-1并设置error
需要头文件#include
原型:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
返回值:成功返回接收到的数据长度,失败返回-1并设置error
原理:打开的文件是内核中的一块缓冲区。多个无血缘关系的进程,可以同时访问该文件
在读文件时如果没有数据不会阻塞,直接返回0。因为只有读管道、设备和网络文件时才会阻塞,读普通文件不会发生阻塞
阻塞与非阻塞是对于文件而言的,而不是read、write等的属性
两个不相关的进程可以通过打开同一个文件进行读写来进行通信
存储映射I/O:使一个磁盘文件与存储空间中的一个缓冲区相映射
映射区的内容可以重复读取
共享内存进行消息通信是速度最快的
mmap函数
需要头文件#include
创建一个内存映射区
原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数:
返回值:返回映射区的首地址表示成功;MAP_FAILED表示失败,设置errno
munmap函数
需要头文件#include
释放回收指定的内存映射区
原型:int munmap(void *addr, size_t length);
参数:
返回值:0表示是释放成功;-1表示释放失败,设置errno
使用共享映射区:
fd = open("file", O_RDWR)
p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
memcpy(p, &src, sizeof(src))
;读取映射区printf("%s", p)
munmap(p, len)
mmap注意事项:
先mmap,再fork
使用映射区的方式同上
使用两个进程,一个读,一个写
两个进程使用同一个文件分别创建内存映射区,虽然内存映射区不同,但是两个进程对各自内存映射区的操作都会反映到文件上,从而建立了通信
可以不依赖文件创建匿名映射区,只能用于有血缘关系之间的文件
使用:mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0)
信号是信息的载体
共性:简单、不能携带大量信息、满足条件发送
特性:软件层面上的“中断”
信号机制:收到信号后,不管执行到程序的什么位置,都必须暂停运行去处理信号。信号处理完毕后再继续执行
进程收到的所有信号都是由内核负责发送的,内核处理
信号产生:
信号状态:
信号处理:
阻塞信号集(信号屏蔽字):将某些信号加入集合,对它们设置屏蔽。收到对应的信号后,它的处理将延后(解除屏蔽后)
未决信号集:①信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。处理后翻转为0;②信号产生后由于某些原因(主要是阻塞)不能递达。这类信号称为未决信号集。屏蔽解除前。信号一直处于未决状态
kill -l命令可以列出信号表。1-31为普通信号,有默认处理动作;34-64为实时信号,无默认处理动作
信号四要素:编号、名称、事件、默认处理动作
常用的常规信号:
默认动作:
kill命令:kill -信号名或变化 进程pid
向指定进程发送指定信号
需要头文件#include
和#include
向指定进程发送指定信号
原型:int kill(pid_t pid, int sig);
参数:
返回值:0表示发送成功;-1表示发送失败,设置errno
权限保护:root用户可以发送信号给任意用户,普通用户是不能向系统用户和其他普通用户发送信号的
需要头文件#include
设置定时器,经过指定seconds后,内核会给当前进程发送14 SIGALRM信号。默认动作为终止进程
每个进程都有且只有一个定时器
原型:unsigned int alarm(unsigned int seconds);
参数:
返回值:返回剩余的秒数;0表示没有定时器再运行,没有失败情况
取消定时器:alarm(0)
time命令:time ./out
查看程序执行时间
实际运行时间=用户运行时间+内核运行时间+等待时间。其中等待时间主要是等待IO设备阻塞的时间,所以优化程序第一步先优化IO
设置定时器。可实现周期定时
原型:int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);
参数:
返回值:0表示设置成功;-1表示设置是吧,设置errno
itimerval类型:
struct itimerval{
struct timeval{
time_t tv_sec; //秒
suseconds_t tv_usec; //微妙
}it_interval;//两次定时任务之间间隔时间
struct timeval{
time_t tv_sec; //秒
suseconds_t tv_usec; //微妙
}it_value;//定时的时长
}
sigset_t set;
自定义信号集
sigemptyset(sigset_t *set);
清空信号集
sigfillset(sigset_t *set);
全部置1
sigaddset(sigset_t *set, int signum);
将一个信号添加到集合中
sigdelset(sigset_t *set, int signum);
讲一个信号从集合中移除
sigismemeber(const sigset_t *set, int signum);
判断一个信号是否在集合中。返回1表示在;0表示不在
sigprocmask函数
需要头文件#include
设置屏蔽字和解除屏蔽字
原型:sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
返回值:0表示执行成功;-1表示执行失败,设置errno
sigpending函数
需要头文件#include
读取当前进程的未决信号集
原型:int sigpending(sigset_t *set);
参数:
返回值:0表示读取成功;-1表示读取失败,设置errno
signal函数
需要头文件#include
注册一个信号捕捉函数
由ANSI定义,在不同的Unix、Linux版本上可能有不同的行为,应该尽量避免使用它
原型:typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
参数:
返回值:旧的信号捕捉函数
sigaction函数
需要头文件#include
修改信号处理动作。通常在Linux中注册一个信号的捕捉函数
和signal的区别:signal函数修改处理函数后在信号触发一次后就恢复为旧的处理函数,sigaction函数修改处理函数后,后续信号多次触发时执行的均为新的处理函数
原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
返回值:0表示注册成功;-1表示注册失败设置errno
sigaction类型:
struct sigaction
{
void (*sa_handler)(int);//捕捉函数
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;//只在捕捉函数内生效,通常传全0(sigemptyset函数)
int sa_flags;//参数,通常传0表示屏蔽本信号
void (*sa_restorer)(void);
}
act传参构造sigaction结构体时只需要初始化sa_handler、sa_mask、sa_flags即可
信号特性:
内核实现信号捕捉过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lTTQvNH1-1664298269832)(https://gitee.com/AnonymYH/markdown-image/raw/master/img/image-20210815190618056.png)]
回调函数sighandler是被内核所调用的
产生条件:子进程状态发生改变,如子进程终止时、SIGSTOP信号停止时、子进程处于停止态被唤醒时
用SIGCHLD回收子进程:
void catch_child(int signo)
{
pid_t wpid;
wpid = wait(NULL);
printf("catch child id %d\n", wpid);
//修改为循环回收子进程
/*
while((wpid = wait(NULL)) != -1)
{
printf("catch child id %d\n", wpid);
}
*/
return;
}
int main()
{
pid_t pid;
//屏蔽信号
/*
sig_set set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
*/
int i;
for(i = 0; i < 5; i++)
{
if((pid = fork()) == 0)
{
break;
}
}
if(i == 5)
{
struct sigaction act;
act.sa_handler(catch_child);
sigemptyset(&act.as_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);//注册SIGCHLD信号
//接触信号屏蔽
/*
sigprocmask(SIG_UNBLOCK, &set, NULL);
*/
printf("parent, pid=%d\n",getpid());
}
else
{
printf("child, pid=%d\n", getpid());
}
}
当有多个子进程同时发出SIGCHLD信号时,由于SIGCHLD是不排队的,父进程在处理上一个信号的时候,同时收到多个SIGCHLD信号,只会保留一个SIGCHLD,其余信号丢弃,导致这些被丢弃的信号对应的子进程无法被回收,成为僵尸进程。解决方法为在收到一个信号SIGCHLD时处理函数中回收所有已经结束的子进程(循环回收)
如果有子进程在父进程注册信号之前就结束,此时会进行默认处理动作,会出现捕捉不到的情况。解决方法为在fork之前设置SIGCHLD的屏蔽字,在注册信号后再解除屏蔽字阻塞
慢速系统调用:可能使进程永远堵塞的系统调用。如果在阻塞期间收到一个信号,系统调用则被中断不再执行。如read、write、pause、wait
其他系统调用:getpid、getppid、fork
可修改sa_flags参数设置信号中断系统调用后是否重启。SA_RESTART重启、SA_INTERRUPT不重启
进程组:又称作业,代表一个或者多个进程的集合。每个进程都属于一个进程组
会话:会话是一组相关进程组的集合
父进程创建子进程时,默认子进程和父进程属于同一进程组,进程组ID为第一个进程ID(组长进程),即父进程
组长进程可以创建一个进程组、已经该进程组中的进程、终止进程组中的进程。只要进程组中有一个进程存在,进程组就存在,与组长进程无关
创建会话需要注意:
getsid函数:pid_t getsid(pid_t pid);
成功返回调用进程的会话id(SID);失败返回-1,设置errno。pid为0表示查看当前进程SID
setsid函数
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID
原型:pid_t setsid(void);
返回值:返回调用进程的SID表示创建成功;-1表示创建失败,设置errno
定义:是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字
Linux后台的一些系统服务进程,没有控制终端,不能和用户交互。不受用户登录、注销影响,一直运行。如预读入缓输出机制的实现、FTP服务器、NFS服务器等
创建守护进程:
/dev/null
线程是轻量级的进程LWP,本质仍为进程
进程:独立的地址空间,拥有独立PCB
线程:有独立的PCB,但没有独立的地址空间(共享)
进程创建线程后自己变成线程,每个线程都有一个PCB,所有线程共享原进程的地址空间
线程是最小的执行单位;进程是最小的资源单位,相当于只有一个线程的进程
ps -Lf pid
命令:查询线程号,不是线程ID
线程号是CPU给线程分配时间片的标识根据,线程ID是进程用于标识内部线程
linux内核线程实现:内核中看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的,所以没有独立的地址空间
三级页表映射:页面 -> 页目录 -> 页表
线程中共享的资源:文件描述符表、信号处理方式、当前工作目录、用户ID和组ID、内存地址空间(.text、.data、.bss、heap、共享库)
线程中非共享资源:线程id、处理器现场和栈指针、独立的栈空间、errno变量、信号屏蔽字、调度优先级
以下函数在编译时需要加上选项-pthread
以下函数需要头文件#include
pthread_self函数
获取线程ID
原型:pthread_t pthread_self(void)
返回值:0表示获取成功
pthread_create函数
创建一个新线程
原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数:
返回值:0表示创建成功;返回错误号表示创建失败
pthread_exit函数
将当前单个线程退出
exit函数退出的是进程,退出线程需要使用pthread_exit函数。使用return语句返回到调用者也能实现退出线程,但是只用于子线程,用在主线程上相当于退出进程
原型:void pthread_exit(void *retval);
参数:
exit函数:退出当前进程
return语句:返回到调用者
pthread_exit():退出当前线程
pthread_join函数
阻塞等待线程退出,获取线程退出状态
原型:int pthread_join(pthread_t thread, void **retval);
参数:
返回值:0表示退出成功;返回错误号表示退出失败
prthread_cancel函数
杀死线程
原型:int pthread_cancel(pthread_t thread);
参数:
返回值:0表示杀死成功;返回错误号表示杀死失败
该函数杀死线程必须有一个取消点,即一个进入内核态的时期。线程可以通过执行一个系统调用进入内核态,可以在线程中使用**pthread_testcancel()**手动添加一个取消点
被该函数杀死的线程如果用pthread_join函数回收的话,退出状态值为-1
pthread_detach函数
实现线程分离
线程分离状态:线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放
分离的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态,无需使用pthread_join回收
原型:int pthread_detach(pthread_t thread);
参数:
返回值:0表示分离成功;返回错误号表示分离失败
**线程中检查错误:**不能用perror函数,因为线程函数出错时直接返回错误号,所以直接用strerror函数解释函数返回值ret然后使用fprintf函数打印即可
线程属性
初始化线程属性:int pthread_attr_init(pthread_attr_t *attr);
销毁线程属性所占用的资源:int pthread_attr_destroy(pthread_attr_t *attr)
设置分离状态:int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
其中参数detachstate取值为PTHREAD_CREATE_DETACHED分离进程、PTHREAD_CREATE_JOINABLE非分离进程
pthread_join函数能够测试线程是否是分离的。如果是分离的,那回收失败,会返回错误号;如果不是分离的,则可以正常回收
线程使用注意:
同步:协同步调,按预定的先后次序运行
线程同步:指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能
通过互斥量mutex来实现线程同步
互斥量是建议锁,不具备强制性,需要手动在程序逻辑中控制线程同步。只要有一个线程没有使用锁机制,就可能出现数据混乱
pthread_mutex_t结构体:互斥量类型
使用mutex步骤:
需要头文件#include
pthread_mutex_init函数
初始化互斥量
原型:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
**restrict关键字:**限定指针。被restrict限定的指针指向的内存操作只能由该指针完成
参数:
返回值:0表示成功;返回错误号表示失败
使用pthread_mutex_init函数初始化为动态初始化,也可以使用宏进行静态初始化pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_destroy函数:int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_lock函数:int pthread_mutex_lock(pthread_mutex_t *mutex);
当锁被占用时,会阻塞线程
pthread_mutex_trylock函数:int pthread_mutex_trylock(pthread_mutex_t *mutex);
锁被占用时,返回错误号EBUSY
pthread_mutex_unlock函数:int pthread_mutex_unlock(pthread_mutex_t *mutex);
当锁解锁后,会唤醒阻塞的线程
互斥量mutex注意事项:
**死锁:**是不恰当地使用锁导致的现象。①一个线程对一把锁多次加锁;②线程1拥有A锁请求B锁,线程2拥有B锁请求A锁
读写锁只有一把锁,读模式下加锁状态为读锁,写模式下加锁状态为写锁
特点:读共享写独占,写锁优先级高。当读写同时加锁时,优先让写模式加锁;当读写并行阻塞时,写模式排队在读前面
pthread_rwlock_t结构体:读写锁
应用函数:需要头文件#include
函数用法同互斥锁mutex
条件变量本身不是锁,但是通常结合互斥锁mutex来使用
pthread_cond_t结构体:定义条件变量
pthread_condattr_t结构体:初始化属性
应用函数:需要头文件#include
初始化也可以使用宏pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
进行静态初始化条件变量
pthread_cond_wait函数
阻塞等待一个条件变量
作用:①阻塞等待条件变量cond满足;②阻塞等待时释放已掌握的互斥锁,相当于unlock了mutex;③条件满足被唤醒时,解除阻塞,重新加锁,即重新lock了mutex
原型:int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数:
返回值:0表示阻塞成功;返回错误号表示阻塞失败
主程序:
pthread_creaet()
pthread_join()
struct mas
{
int num;
struct msg *next;
}
struct msg *head;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态初始化互斥量
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER;//静态初始化条件变量
int main()
{
int ret;
pthread_t pid, cid;
ret = pthread_create(&tid, NULL, producer, NULL);
if(ret != 0)
{
fprintf(stderr, "pthread_create:%s\n", strerr(ret));
exit(1);
}
ret = pthread_create(&cid, NULL, consumer, NULL);
if(ret != 0)
{
fprintf(strerr, "pthread_create:%s\m", strerr(ret));
exit(1);
}
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
生产者:
pthread_mutex_lock(&mutex)
pthread_mutex_unlock(&mutex)
pthread_cond_signal()
或pthread_cond_broadcast()
void *producer(void *p)
{
struct msg *mg;
for(;;)
{
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;
printf("-Produce ------ %d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);
sleep(rand() % 5);
}
}
消费者:
pthread_mutex_lock(&mutex)
pthread_cond_wait()
进行阻塞等待pthread_mutex_unlock(&mutex)
void *consumer(void *p)
{
struct msg *mp;
for(;;)
{
pthread_mutex_lock(&lock);
while(head == NULL)
{
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = mp-next;
pthread_mutex_unlock(&lock);
printf("-Consume %lu---%d\m", pthread_self(), mp->num);
free(mp);
sleep(rand() % 5);
}
}
相当于初始化值为N的互斥量(把互斥量理解为资源为1的锁)
允许同时有N个线程来访问共享资源
可用于线程同步和进程同步
sem_t结构体:信号量
应用函数:需要头文件#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数pshared为0表示线程间同步,非0表示进程间同步;value指定同时访问的线程数N主程序:
#define NUM 5
int queue[NUM];
sem_t blank_number, product_number;//空闲块数、生产资源数
int main()
{
pthread_t pid, cid;
sem_init(&blank_number, 0, NUM);
sem_init(&product_number, 0, 0);
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(&pid, NULL);
pthread_join(&cid, NULL);
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}
生产者:
sem_wait(&blank_number)
sem_post(&product_number)
void *producer(void *arg)
{
int i = 0;
while(1)
{
sem_wait(&blank_number);
queue[i] = rand() % 1000 + 1;
printf("----Produce---%d\n", queue[i]);
sem_post(&product_number);
i = (i+1) % NUM;
sleep(rand()%1);
}
return NULL;
}
消费者:
sem_wait(&product_number)
sem_post(&blank_number)
void *consumer(void *arg)
{
int i = 0;
while(1)
{
sem_wait(&product_number);
printf("-Consumer---%d\n", queue[i]);
queue[i] = 0;
sem_post(&blank_number);
i = (i+1) % NUM;
sleep(rand() % 3);
}
return NULL;
}