1.基础入门 1.1基本函数 open()函数:打开一个文件
int open(char *pathname , int flags , mode_t mode)
mode:设置文件的权限(参数3使用前提:参数2指定了O_CREAT)
返回值:
成功:打开文件所得到对应的文件描述符(整数)
失败:-1,设置errno
read()函数:往一个文件读内容
ssize_t read(int fd , void *buf , size_t count)
参数:
fd:文件描述符
buf:存数据的缓冲区
count设置读字节数大小
count:缓冲区大小
write()函数
ssize_t write(int fd , const void *buf , size_t count)
参数:
fd:文件描述符
buf:待写出数据的缓冲区
count:写入数据字节数大小
返回值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main (int argc,char *argv[]) { char buf[3 ]; int n=0 ; int fd1 = open(argv[1 ],O_RDONLY); int fd2 = open(argv[2 ],O_RDWR|O_CREAT|O_TRUNC,0664 ); while ((n = read(fd1,buf,3 )) != 0 ){ write(fd2,buf,n); } printf ("fd1 = %d\n" ,fd1); printf ("fd2 = %d\n" ,fd2); close(fd1); close(fd2); return 0 ; }
1.2基本知识 1.PCB进程控制块:本质是结构体,每个进程都有一个唯一的 PCB。
成员:文件描述符表——>文件描述符:0/1/2/3/…/1023
key value(指针:指向一个结构体)
0 – STDIN_FILENO ——–>标准输入
1 – STDOUT_FILENO —–>标准输出
2 – STDERR_FILENO ——>标准错误
注意:每次得到的文件描述符默认是表中可用最小的
2.阻塞、非阻塞
是设备文件、网络文件的属性。产生阻塞的场景:读设备文件、读网络文件(读常规文件无阻塞概念)
dev/tty:终端文件。标准输入、标准输出、标准错误都在终端显示
非阻塞设置:open(“/dev/tty”,..|O_NONBLOCK)
此时若返回-1,并且errno = EAGIN或EWOULDBLOCK,说明不是read失败,而是read在以非阻塞方式读一个设备文件或网络文件,并且文件无数据(默认是阻塞状态)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int main (void ) { char buf[10 ]; int fd,n; fd = open("/dev/tty" ,O_RDONLY|O_NONBLOCK); if (fd<0 ){ perror("open /dev/tty" ); exit (1 ); } tryagain: n=read(fd,buf,10 ); if (n<0 ){ if (errno != EAGAIN){ perror("read /dev/tty" ); exit (1 ); }else { write(STDOUT_FILENO,"try again\n" ,strlen ("try again\n" )); sleep(2 ); goto tryagain; } } write(STDOUT_FILENO,buf,n); close(fd); return 0 ; }
3.参数
传入参数:
指针作为函数参数
有const关键字修饰
指针指向有效区域,在函数内部做读操作
传出参数:
传入传出参数:
指针作为函数参数
在函数调用之前,指针指向的空间有实际意义
在函数内部,先做读操作,后做写操作
函数调用结束后,充当函数返回值
4.文件存储
dentry:目录项,其本质是结构体,重要成员变量有两个{文件名,inode,…},而文件内容(data)保存在磁盘块中。
inode:其本质是结构体,存储文件的属性信息。如权限、类型、大小、盘块位置…。大多数的inode都存储在磁盘上,少数常用、近期使用的inode会被存储在内存上。
1.3进阶函数 1.fcntl函数
获取文件状态:F_GETFL
设置文件状态:F_SETFL
获取fd描述符的stdin属性信息:int flgs = fcntl(fd,F_GETFL);
添加非阻塞状态:flgs |= O_NONBLOCK;
把设置的状态信息设置到终端设备的状态信息中:fcntl(fd,F_SETFL,flgs);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define MSG_TRY "try again\n" int main (void ) { char buf[10 ]; int flags,n; flags = fcntl(STDIN_FILENO,F_GETFL); if (flags == -1 ){ perror("fcntl error" ); exit (1 ); } flags |= O_NONBLOCK; int ret = fcntl(STDIN_FILENO,F_SETFL,flags); if (ret == -1 ){ perror("fcntl error" ); exit (1 ); } tryagain: n=read(STDIN_FILENO,buf,10 ); if (n<0 ){ if (errno != EAGAIN){ perror("read /dev/tty" ); exit (1 ); }else { sleep(3 ); write(STDOUT_FILENO,MSG_TRY,strlen (MSG_TRY)); goto tryagain; } } write(STDOUT_FILENO,buf,n); return 0 ; }
2.lseek函数
off_t lseek(int fd , off_t offset , int whence);
参数:
返回值:
成功:较起始位置偏移量
失败:-1 设置errno
应用场景:
补:可以使用truncate函数,直接拓展文件大小
如int ret = truncate(“dict.txt”,250);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int main (void ) { int fd,n; char msg[] = "It's a test4 for lseek\n" ; char ch; fd = open("lseek.txt" ,O_RDWR|O_CREAT,0644 ); if (fd<0 ){ perror("open lseek.txt error" ); exit (1 ); } write(fd,msg,strlen (msg)); lseek(fd,0 ,SEEK_SET); while ((n = read(fd,&ch,1 ))){ if (n < 0 ){ perror("read error" ); exit (1 ); } write(STDOUT_FILENO,&ch,n); } close(fd); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 int main (void ) { int fd = open("lseek.txt" ,O_RDWR); if (fd == -1 ){ perror("open error" ); exit (1 ); } int lenth = lseek(fd,0 ,SEEK_END); printf ("file size:%d\n" ,lenth); return 0 ; }
3.stat/lstat函数
stat底层是一个结构体,里面有文件的信息
int stat(const char *path , struct stat *buf);
参数:
path:文件或目录路径
buf:(传出参数)存放文件属性
返回值:
系统提供函数
获取文件大小:buf.st_size
获取文件类型:buf.st_mode
获取文件权限:buf.st_mode
符号穿透:stat会;lstat不会
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int main () { struct stat sbuf ; int ret = stat("document" ,&sbuf); if (ret == -1 ){ perror("stat error" ); exit (-1 ); } printf ("file size:%ld\n" ,sbuf.st_size); if (S_ISREG(sbuf.st_mode)){ printf ("It's a regular\n" ); }else if (S_ISDIR(sbuf.st_mode)){ printf ("It's a dir\n" ); }else if (S_ISFIFO(sbuf.st_mode)){ printf ("It's a pipe\n" ); }else if (S_ISLNK(sbuf.st_mode)){ printf ("It's a sym link\n" ); } return 0 ; }
4.link/unlink函数
link函数:可以为已经存在的文件创建目录项(硬链接)。即增加了一个指向已有inode的新目录项(新目录项与旧目录项的区别就只有文件名)
1 2 link("a.txt" ,"b.txt" ); unlink("a.txt" );
unlink函数:删除一个文件的目录项。从某种意义上说,只是让文件具备了释放的条件。
unlink函数的特征:清除文件时,如果文件的硬链接数到0了,没有dentry对应,但该文件仍不会马上被释放(只是目录中我们看不到该文件了而已),要等到所有打开该文件的进程关闭该文件,系统才会挑时间将该文件释放掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int main (void ) { int fd; char *p = "test of unlink\n" ; char *p2 = "after write something\n" ; fd = open("temp.txt" ,O_RDWR|O_CREAT|O_TRUNC,0644 ); if (fd < 0 ){ perror("open temp error" ); exit (1 ); } sleep(5 ); int ret = unlink("temp.txt" ); if (ret < 0 ){ perror("unlink error" ); exit (1 ); } ret = write(fd,p,strlen (p)); if (ret == -1 ){ perror("--------write error" ); } printf ("hi! I'm lxx\n" ); ret = write(fd,p2,strlen (p2)); if (ret == -1 ){ perror("------Write error" ); } printf ("enter anykey continue\n" ); getchar(); close(fd); }
5.readlink()函数
作用:读取符号链接(软链接)文件本身内容,得到链接所指向的文件名。
如有软链接:t.soft -> /home/hoem1/test
终端执行:readlink t.soft 得到:/home/hoem1/test
6.目录操作函数
DIR * opendir(char *name);
int closedir(DIR *dp)
struct dirent *readdir(DIR *dp);
其中:
1 2 3 4 5 struct dirent { inode char dname[256 ] ...... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int main (int argc,char *argv[]) { DIR *dp; struct dirent *sdp ; dp = opendir(argv[1 ]); if (dp == NULL ){ perror("opendir error" ); exit (1 ); } while ((sdp = readdir(dp)) != NULL ){ if ((strcmp (sdp->d_name,"." ) == 0 )||(strcmp (sdp->d_name,".." ) == 0 )) continue ; printf ("%s\t" ,sdp->d_name); } printf ("\n" ); closedir(dp); return 0 ; }
案例:递归遍历目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 void isFile (char *name) ; void read_dir (char *dir) { char path[256 ]; DIR *dp; struct dirent *sdp ; dp = opendir(dir); if (dp == NULL ){ perror("opendir error" ); return ; } while ((sdp = readdir(dp)) != NULL ){ if (strcmp (sdp->d_name,"." ) == 0 || strcmp (sdp->d_name,".." ) == 0 ){ continue ; } sprintf (path,"%s/%s" ,dir,sdp->d_name); isFile(path); } closedir(dp); return ; } void isFile (char *name) { int ret = 0 ; struct stat sb ; ret = stat(name,&sb); if (ret == -1 ){ perror("stat error" ); return ; } if (S_ISDIR(sb.st_mode)){ read_dir(name); } printf ("%s\t%ld\n" ,name,sb.st_size); return ; } int main (int argc, char *argv[]) { if (argc == 1 ){ isFile("." ); }else { isFile(argv[1 ]); } return 0 ; }
7.dup/dup2函数
int dup(int oldfd);
参数:oldfd:已有文件描述符
返回值:新文件描述符
int dup2(int oldfd,int newfd);
1 2 3 4 5 6 7 8 9 10 int main (int argc,char *argv[]) { int fd1 = open(argv[1 ],O_RDWR); int fd2 = open(argv[2 ],O_RDWR); int fdret = dup2(fd1,fd2); printf ("fdret = %d\n" ,fdret); int ret = write(fd2,"1234567" ,7 ); dup2(fd1,STDOUT_FILENO); printf ("--------------------886\n" ); return 0 ; }
fcntl函数是实现dup
int fcntl(int fd,int cmd,…);
1 2 3 4 5 6 7 8 9 10 11 12 13 int main (int argc,char *argv[]) { int fd1 = open(argv[1 ],O_RDWR); printf ("fd1 = %d\n" ,fd1); int newfd = fcntl(fd1,F_DUPFD,0 ); printf ("newfd = %d\n" ,newfd); int newfd2 = fcntl(fd1,F_DUPFD,7 ); printf ("newfd = %d\n" ,newfd2); int ret = write(newfd2,"yyyyyy" ,6 ); printf ("ret = %d\n" ,ret); return 0 ; }
2.进程 2.1进程与程序 程序:死的,只占用磁盘空间 —剧本
进程:活的,运行起来的程序,占用内存、cpu等系统资源 —戏剧
1.进程控制块PCB
我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
内部成员:
进程id:系统中每个进程有唯一的id,在c语言中用pid_t类型表示,其实就是一个非负整数;
进程的状态:有初试、就绪、运行、挂起和停止五种状态
进程切换时想要保存和恢复的一些CPU寄存器;(了解)
描述虚拟地址空间的信息;(了解)
描述控制终端的信息;(了解)
当前所处工作目录;
umask掩码:rwx对应124,当umask为022时,在你创建文件和目录时,默认权限是777-022=755,即rwxr_xr_x;(了解)
文件描述符表:是map结构,key为正整数,value为指针,指针指向结构体;它包含很多指向file结构体的指针(每个进程都有文件描述符表)
和信号相关的信息;
用户id和组id;
会话和进程组;(了解)
进程可用使用的资源上限;(了解)
2.进程共享
父进程在fork()之后:
相同:全局变量、data段、text段、栈、堆、环境变量、用户id、宿主目录、进程工作目录、信号处理方式…
不相同:进程id、fork返回值、父进程id、进程运行时间、闹钟(定时器)、未决信号集
注:对于全局变量,父子进程间遵循读时共享写时复制 的原则
3.孤儿进程/僵尸进程
3.相关函数 3.1fork()函数 功能:是创建一个新的进程
pid_t fork(void);
注意:创建出来的子进程可用执行父进程中fork()函数下面的代码。在父进程中,fork返回的是子进程id号;在子进程中,fork返回的是0;
补:getpid()返回的是当前进程;getppid()返回的是父进程;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main (int argc,char * argv[]) { pid_t pid = fork(); if (pid == -1 ){ perror("fork error" ); exit (1 ); }else if (pid == 0 ){ printf ("---child is created,pid = %d,parent-pid = %d\n" ,getpid(),getppid()); }else if (pid > 0 ){ printf ("---parent process: my child is %d,my pid = %d,my parent pid = %d\n" ,pid,getpid(),getppid()); } printf ("======================end of file\n" ); sleep(3 ); }
在上面代码中,子进程和父进程都会执行sleep(3)这行代码,不是说只有让父进程执行,而子进程不执行,sleep(3)只是防止父进程结束退出程序了,子进程还没有打印父进程。如果父进程结束了,子进程才打印父进程,则父进程是1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int main (int argc,char * argv[]) { int i; pid_t pid; for (i = 0 ;i < 5 ; i++){ if (fork() == 0 ){ break ; } } if (i == 5 ){ sleep(5 ); printf ("我是父进程\n" ); }else { sleep(i); printf ("我是第%d个子线程\n" ,i+1 ); } return 0 ; }
3.2 execlp()函数 功能:指定进程执行相应的程序
int execlp(const *file , const *arg , …);
该函数常用来调用系统程序。如ls、date、cp、cat等命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int main (int argc,char * argv[]) { pid_t pid = fork(); if (pid == -1 ){ perror("fork error" ); exit (1 ); }else if (pid == 0 ){ execlp("ls" ,"ls" ,"-l" ,"-h" ,NULL ); perror("exec error" ); exit (1 ); }else if (pid > 0 ){ sleep(1 ); printf ("我是父进程: %d\n" ,getpid()); } return 0 ; }
3.3 execl函数 功能:既可以执行自己写的程序,也可用执行系统程序
int execl(const char* path , const char *arg , ….);
该函数是通过 路径+程序名 来加载进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int main (int argc,char * argv[]) { pid_t pid = fork(); if (pid == -1 ){ perror("fork error" ); exit (1 ); }else if (pid == 0 ){ execl("/bin/ls" ,"ls" ,"-l" ,NULL ); perror("exec error" ); exit (1 ); }else if (pid > 0 ){ sleep(1 ); printf ("我是父进程: %d\n" ,getpid()); } return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 int main (void ) { int fd; fd = open ("ps.out" ,O_WRONLY|O_CREAT|O_TRUNC,0644 ); if (fd < 0 ){ perror ("open ps.out,error" ); exit (1 ); } dup2 (fd,STDOUT_FILENO); execlp ("ps" ,"ps" ,"ax" ,NULL ); return 0 ; }
3.4wait()函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态;如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个过程。
该函数的三个功能:
阻塞等待子进程退出;
回收子进程残留资源
获取子进程结束状态(退出原因)
pid_t wait(int *status);
可使用wait函数传出的参数status来保存进程的退出状态:
WIFEXITED(status)为非0,表示进程正常结束
如上正常退出,使用WEXITSTATUS(status)来获取进程的退出状态(正常的参数)
WIFSIGNALED(status)为非0,表示进程异常终止
如上异常退出,使用WTERMSIG(status)来获取使进程终止的那个信号的编号
WIFSTOPPED(status)为非0,表示进程处于暂停状态
如上为暂停状态,使用WSTOPSIG(status)来取得使进程暂停的那个信号的编号
WIFCONTINUED(status)为真,表示进程暂停后已经继续运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int main (void ) { pid_t pid,wpid; int status; pid = fork(); if (pid == 0 ){ printf ("---child,my id = %d,going to sleep 3s\n" ,getpid()); sleep(3 ); printf ("--------------child die------------------\n" ); }else if (pid > 0 ){ wpid = wait(&status); if (wpid == -1 ){ perror("wait error" ); exit (1 ); } printf ("---parent wait finish:%d\n" ,wpid); }else { perror("fork" ); return 1 ; } return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int main (void ) { pid_t pid,wpid; int status; pid = fork(); if (pid == 0 ){ printf ("我是子线程%d\n" ,getpid()); printf ("--------------child die------------------\n" ); return 93 ; }else if (pid > 0 ){ wpid = wait(&status); if (wpid == -1 ){ perror("wait error" ); exit (1 ); } if (WIFEXITED(status)){ printf ("我是正常终止,返回%d\n" ,WEXITSTATUS(status)); } if (WIFSIGNALED(status)){ printf ("child kill with signal %d\n" ,WTERMSIG(status)); } printf ("父线程等待完毕,回收的子线程是%d\n" ,wpid); }else { perror("fork" ); return 1 ; } return 0 ; }
3.5waitpid()函数 pid_t waitpid(pid_t pid , int *status , int optains)
参数:
pid:指定某个子进程进行回收(大于0:回收指定ID的子进程;-1:回收任意子进程,相当于wait;0:回收和当前调用waitpid一个组的所有子进程;小于-1:回收指定进程组内的任意子进程)
status:是传出参数,结合系统提供的宏函数,可以得到子进程的一些信息
options:通过特定参数,可以完成特定功能,如WNOHANG指以非阻塞方式回收
返回值:
waitpid()与wait()的区别是:waitpid能指定某个进程进行回收
注意:一次wait/waitpid函数调用,只能回收一个子进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int main (int argc,char *argv[]) { int i; pid_t pid,wpid,tmpid; for (i=0 ;i<5 ;i++){ pid = fork(); if (pid==0 ){ break ; } if (i == 2 ){ tmpid = pid; printf ("指定回收的子线程 = %d\n" ,tmpid); } } if (i==5 ){ sleep(5 ); wpid = waitpid(tmpid,NULL ,0 ); if (wpid == -1 ){ perror("waitpid error" ); exit (1 ); } printf ("成功回收子线程:%d\n" ,wpid); }else { sleep(i); printf ("我是第%d个子线程%d\n" ,i+1 ,getpid()); } return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int main (int argc,char *argv[]) { int i; pid_t pid,wpid; for (i=0 ;i<5 ;i++){ pid = fork(); if (pid==0 ){ break ; } } if (i==5 ){ while ((wpid = waitpid(-1 ,NULL ,WNOHANG)) != -1 ){ if (wpid>0 ){ printf ("成功回收:%d\n" ,wpid); }else if (wpid == 0 ){ sleep(1 ); } } }else { sleep(i); printf ("我是第%d个子线程%d\n" ,i+1 ,getpid()); } return 0 ; }
4.IPC方法
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)。
在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。现在常用的进程间通信方式有:
管道(使用最简单)
信号(开销最小)
共享映射区(无血缘关系)
本地套接字(最稳定)
4.1管道
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
1.管道的局限性:
2.常见的通信方式有:单工通信、半双工通信、全双工通信
3.pipe函数
int pipe(int fd[2]); —> 创建,并打开管道
参数:
返回值:成功:0;失败:-1,设置errno
4.管道的读写行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void sys_err (const char *str) { perror(str); exit (1 ); } int main (int argc,char *argv[]) { int ret; int fd[2 ]; pid_t pid; char *str = "hello pipe\n" ; char buf[1024 ]; ret = pipe(fd); if (ret == -1 ){ sys_err("pipe error" ); } pid = fork(); if (pid>0 ){ close(fd[0 ]); write(fd[1 ],str,strlen (str)); sleep(1 ); close(fd[1 ]); }else if (pid == 0 ){ close(fd[1 ]); ret = read(fd[0 ],buf,sizeof (buf)); write(STDOUT_FILENO,buf,ret); close(fd[0 ]); } return 0 ; }
案例:使用管道实现父子进程间通信,完成ls | wc -l,假定子进程实现ls,父进程实现wc
ls | wc -l 的含义是将ls命令的输出通过管道传递给wc -l命令,然后统计输出的行数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void sys_err (const char *str) { perror(str); exit (1 ); } int main () { int fd[2 ]; int ret; pid_t pid; ret = pipe(fd); if (ret == -1 ){ sys_err("pipe error" ); } pid = fork(); if (pid == -1 ){ sys_err("fork error" ); }else if (pid == 0 ){ close(fd[0 ]); dup2(fd[1 ],STDOUT_FILENO); execlp("ls" ,"ls" ,NULL ); sys_err("exclp ls error" ); }else if (pid > 0 ){ close(fd[1 ]); dup2(fd[0 ],STDIN_FILENO); execlp("wc" ,"wc" ,"-l" ,NULL ); sys_err("exclp wc error" ); } return 0 ; }
分析:ls命令正常会将结果集写出到stdout,但现在会写入管道的写端;wc -l正常应该会从stdin读取数据,但此时会从管道的读端读。
案例:使用管道实现兄弟进程间通信,兄:ls,弟:wc -l,父:等待回收子进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 int main () { int fd[2 ]; int ret; pid_t pid; ret = pipe(fd); if (ret == -1 ){ sys_err("pipe error" ); } int i; for (i=0 ;i<2 ;i++){ pid = fork(); if (pid == -1 ){ sys_err("fork error" ); } if (pid == 0 ){ break ; } } if (i == 2 ){ close(fd[0 ]); close(fd[1 ]); wait(NULL ); wait(NULL ); }else if (i == 0 ){ close(fd[0 ]); dup2(fd[1 ],STDOUT_FILENO); execlp("ls" ,"ls" ,NULL ); sys_err("exclp ls error" ); }else if (i == 1 ){ close(fd[1 ]); dup2(fd[0 ],STDIN_FILENO); execlp("wc" ,"wc" ,"-l" ,NULL ); sys_err("exclp wc error" ); } return 0 ; }
4.2 FIFO
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程;但通过FIFO,不相关的进程也能交换数据。FIFO是Linux基础文件类型中的一种。但FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信,并且可以有多个写端和多个读端,但读完就没有了。
创建管道方式:
注意:可以通过终端mkfifo 命名管道文件名 来创建,也可以通过c程序
写端的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void sys_err (const char *str) { perror(str); exit (1 ); } int main (int argc,char *argv[]) { int fd,i; char buf[4096 ]; if (argc<2 ){ printf ("ENter like this:./a.out fifoname\n" ); return -1 ; } fd = open(argv[1 ],O_WRONLY); if (fd < 0 ){ sys_err("open error" ); } i = 0 ; while (1 ){ sprintf (buf,"hello lxx %d\n" ,i++); write(fd,buf,strlen (buf)); sleep(1 ); } close(fd); return 0 ; }
读端的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void sys_err (const char *str) { perror(str); exit (1 ); } int main (int argc,char *argv[]) { int fd,len; char buf[4096 ]; if (argc<2 ){ printf ("ENter like this:./a.out fifoname\n" ); return -1 ; } fd = open(argv[1 ],O_RDONLY); if (fd < 0 ){ sys_err("open error" ); } while (1 ){ len = read(fd,buf,sizeof (buf)); write(STDOUT_FILENO,buf,len); sleep(3 ); } close(fd); return 0 ; }
4.3存储映射I/O
存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样就可以在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现
1.创建映射区
void *mmap(void * addr , size_t length , int prot , int flags , int fd , off_t offset);
2.释放映射区
int munmap(void *addr , size_t length);
参数:
3.使用注意事项:
用于创建映射区的文件大小为0,实际指定非0大小创建映射区,出“总线错误”;
用于创建映射区的文件大小为0,实际指定0大小创建映射区,出“无效参数”;
用于创建映射区的文件读写属性为只读,映射区属性为读、写,出“无效参数”;
创建映射区,需要read权限(因为创建时,需要查看映射指定的文件);当访问权限指定为MAP_SHARED(共享)时,mmap的读写属性应该<=文件的open权限(只给mmap写属性不可以)–>因为共享时,对内存操作都会同样的对磁盘(文件)操作;
文件描述符fd,在mmap创建映射区完成即可关闭,后续访问文件用地址访问;
映射区访问权限为MAP_PRIVATE(私有)时,对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上
映射区访问权限为MAP_PRIVATE(私有)时,在需要open文件时,有读权限来创建映射区即可;
4.mmap函数的保险调用:
fd = open(“文件名”,O_RDWR);
mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void sys_err (const char *str) { perror(str); exit (1 ); } int main (int argc,char *argv[]) { char *p = NULL ; int fd; fd = open("testmap" ,O_RDWR|O_CREAT|O_TRUNC,0644 ); if (fd == -1 ){ sys_err("open error" ); } ftruncate(fd,20 ); int len = lseek(fd,0 ,SEEK_END); p = mmap(NULL ,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 ); if (p == MAP_FAILED){ sys_err("mmap error" ); } strcpy (p,"hello mmap" ); printf ("-----------%s\n" ,p); int ret = munmap(p,len); if (ret == -1 ){ sys_err("imunmap error" ); } return 0 ; }
5.mmap进程通信
父子进程通信
父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
MAP_PRIVATE(私有映射):父子进程各自独占映射区(修改,互相都看不到);
MAP_SHARED(共享映射):父子进程共享映射区;
无血缘关系间的进程通信
两个进程打开同一个文件(创建的映射区);一个进程写入,另外一个进程读出。
mmap:数据可以重复读取(当创建的映射区为4字节时,只要读的速度快于写的速度,那么可以重复读同一个正整数多次,直到新数据写入);而fifo只能读取数据一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 int var = 100 ;void sys_err (const char *str) { perror(str); exit (1 ); } int main (void ) { int *p; pid_t pid; int fd = open("temp" ,O_RDWR|O_CREAT|O_TRUNC,0644 ); if (fd == -1 ){ sys_err("open error" ); } ftruncate(fd,4 ); int len = lseek(fd,0 ,SEEK_END); p = mmap(NULL ,4 ,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 ); if (p == MAP_FAILED){ sys_err("mmap error" ); } close(fd); pid = fork(); if (pid == 0 ){ *p = 2000 ; var = 1000 ; printf ("child,*p = %d,var = %d\n" ,*p,var); }else if (pid > 0 ){ sleep(1 ); printf ("parent,*p = %d,var = %d\n" ,*p,var); wait(NULL ); int ret = munmap(p,4 ); if (ret == -1 ){ perror("munmap error" ); exit (1 ); } } return 0 ; } child,*p = 2000 ,var = 1000 parent,*p = 2000 ,var = 100
5.信号
信号是一种软件中断,通知程序某种事件的发生。常见的信号有SIGABRT(当进程调用abort函数的时候自动发送), SIGALRM(当timer被触发的时候自动发送),等等。
5.1常识 1.信号的共性:简单、不能携带大量信息、满足条件才发送
2.信号的特质:信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。所有信号的产生及处理全部都是由内核完成的
3.产生信号
4.信号的状态
5.信号处理的方式:执行默认动作、忽略(丢弃)、捕抓(自定义)
6.阻塞信号集(信号屏蔽字):本质就是位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,再解除屏蔽前,就一直处于未决态
7.未决信号集:本质就是位图。用来记录信号的处理状态,该信号集中的信号表示已经产生,但尚未被处理
5.2信号四要素及常规信号 信号使用之前,应先确定其四要素,而后再用。即信号编号、信号名称、信号对应事件、信号默认处理动作。
1.常规信号:
1)SIGHUP:当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程。
2)SIGINT:当用户按下了<Ctrl+c>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号,默认动作为终止进程。
3)SIGQUIT:当用户按下<Ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号,默认动作为终止进程。
5)SIGTRAP:该信号由断点指令或其他trap指令产生,默认动作为终止进程,并产生core文件。
6)SIGABRT:调用abort函数时产生该信号,默认动作为终止进程并产生core文件。
7)SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
8)SIGFPE:在发生致命的运算错误时发出,不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误,默认动作为终止进程并产生core文件。
9)SIGKILL:无条件终止进程,本信号不能被忽略处理和阻塞,默认动作为终止进程,它向系统管理员提供了可以杀死任何进程的方法。
10)SIGUSR1:用户定义的信号,即程序员可以在程序中定义并使用该信号,默认动作为终止进程。
11)SIGSEGV:指示进程进行了无效内存访问,默认动作为终止进程并产生core文件。
12)SIGUSR2:用户自定义信号,程序员可以在程序中定义并使用该信号,默认动作为终止进程。
13)SIGPIPE:Broken pipe向一个没有读端的管道写数据,默认动作为终止进程。
14)SIGALRM:定时器超时,超时的时间由系统调用alarm设置,默认动作为终止进程。
15)SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止,通常用来要示程序正常退出,执行shell命令kill时,缺省产生这个信号,默认动作为终止进程。
17)SIGCHLD:子进程状态发生变化时,父进程会收到这个信号,默认动作为忽略这个信号。
18)SIGCONT:如果进程已停止,则使其继续运行,默认动作为继续/忽略。
19)SIGSTOP:停止进程的执行,信号不能被忽略处理和阻塞,默认动作为暂停进程。
注意:只有每个信号所对应的事情发生了,该信号才会被递送(但不一定递达),不应该乱发信号。
2.kill函数与kill命令
kill函数:给指定进程发送指定信号(不一定是杀死)
int kill(pid_t pid , int signum);
参数pid: >0:发送信号给指定进程。=0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。<-1:取绝对值,发送信号给该绝对值所对应的进程组的所有组员。=-1:发送信号给,有权限发送的所有进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void sys_err (const char *str) { perror(str); exit (1 ); } int main (int argc,char *argv[]) { pid_t pid = fork(); if (pid > 0 ){ printf ("parent,pid = %d\n" ,getpid()); while (1 ); }else if (pid == 0 ){ printf ("child pid = %d,ppid = %d\n" ,getpid(),getppid()); sleep(2 ); kill(getppid(),SIGKILL); } }
kill命令:如杀死一个进程(kill -9 进程号)
5.3 alarm函数
设置定时器(闹钟),在指定seconds后,内核会给当前进程发送SIGALRM(14)信号,进程收到该信号,默认终止动作。
每个进程都有且只有唯一个定时器。
unsigned int alarm(unsigned int seconds);
参数:定时秒数
返回值:上次定时剩余秒数,无错误现象。
常用:取消定时器alarm(0),返回旧闹钟剩余的秒数
注意:定时与进程状态无关,无论进程处于何种状态,alarm都计时。
1 2 3 4 5 6 7 8 9 int main (void ) { int i; alarm(1 ); for (i = 0 ;;i++){ printf ("%d\n" ,i); } return 0 ; }
[补]使用time命令查看程序执行的时间(如time ./alarm_count),alarm_count是上面代码的函数
得出:实际执行时间 = 系统时间+用户时间+等待时间(最多)—->程序运行的瓶颈在于IO,优化程序,首选优化IO。
5.4setitimer函数
setitimer函数可以替代alarm函数,精度到微妙,还可以实现周期定时
int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);
参数:
new_value:定时秒数(结构体类型见代码)
old_value:传出参数,上次定时剩余时间
返回值:
提示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void myfunc (int signo) { printf ("hello world\n" ); } int main (void ) { struct itimerval it ,oldit ; signal(SIGALRM,myfunc); it.it_value.tv_sec = 2 ; it.it_value.tv_usec = 0 ; it.it_interval.tv_sec = 5 ; it.it_interval.tv_usec = 0 ; if (setitimer(ITIMER_REAL,&it,&oldit) == -1 ){ perror("setitimer error" ); return -1 ; } while (1 ); return 0 ; }
5.5信号集和未决信号集 1.信号集操作函数
自定义信号集(用于和mask发生或与关系):sigset_t set;
清空信号集(全置为0):sigemptyset(sigset_t *set);
信号集全置1:sigfillset(sigset_t *set);
将一个信号添加到集合中:sigaddset(sigset_t *set,int signum);
将一个信号从集合中移除:sigdelset(sigset_t *set,int signum);
判断一个信号是否在集合中(在是1,不在是0):sigismember(const sigset_t *set,int signum);
2.设置信号屏蔽字和解除屏蔽
int sigprocmask(int how , const sigset_t *set , sigset_t *oldset);
用来屏蔽信号、解除屏蔽也使用该函数。其本质是读取或修改进程的信号屏蔽字(PCB中)
注意:屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略是表示将信号丢弃处理
3.查看未决信号集
int sigpending(sigset_t *set);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 void sys_err (const char *str) { perror(str); exit (1 ); } void print_set (sigset_t *set ) { int i; for (i=1 ;i<32 ;i++){ if (sigismember(set ,i)){ putchar ('1' ); }else { putchar ('0' ); } } printf ("\n" ); } int main (int argc,char *argv[]) { sigset_t set ,oldset,pedset; int ret = 0 ; sigemptyset(&set ); sigaddset(&set ,SIGINT); sigaddset(&set ,SIGQUIT); sigaddset(&set ,SIGKILL); ret = sigprocmask(SIG_BLOCK,&set ,&oldset); if (ret == -1 ){ sys_err("sigprocmask error" ); } int i; for (i=0 ;i<100 ;i++){ ret = sigpending(&pedset); if (ret == -1 ){ sys_err("sigpending error" ); } print_set(&pedset); sleep(1 ); if (i==10 ){ sigprocmask(SIG_UNBLOCK,&set ,&oldset); } } return 0 ; }
5.6sigaction函数
signal函数和sigaction函数都是只注册一个信号的捕抓函数,捕抓是有内核来完成的
int sigaction (int signum , const struct sigaction *act , struct sigaction *oldact);
参数:
singum:捕抓的信号
act:传入参数,新的处理方式
oldact:传出参数,旧的处理方式
信号捕抓的特性:
进程正常运行时,默认PCB中有一个信号屏蔽字,假定为#,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉,捕捉到该信号之后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由#来指定,而是用sa_mask来指定,调用完信号处理函数,再恢复为#。
捕捉函数执行期间,本信号自动被屏蔽(sa_flgs=0)。
捕捉函数执行期间,被屏蔽信号多次发生,解除屏蔽后只处理一次
案例:用signal()函数对信号2进行捕抓
1 2 3 4 5 6 7 8 9 10 11 12 13 void sys_err (const char *str) { perror(str); exit (1 ); } void sig_cath (int signo) { printf ("catch you!%d\n" ,signo); return ; } int main (int argc,char *argv[]) { signal(SIGINT,sig_cath); while (1 ); return 0 ; }
案例:用sigaction()函数对信号2进行捕抓(可以设置多个函数的捕抓)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void sys_err (const char *str) { perror(str); exit (1 ); } void sig_cath (int signo) { if (signo == SIGINT) printf ("catch you!%d\n" ,signo); else if (signo == SIGQUIT) printf ("---catch you---%d\n" ,signo); return ; } int main (int argc,char *argv[]) { struct sigaction act ,oldact ; act.sa_handler = sig_cath; sigemptyset(&(act.sa_mask)); act.sa_flags = 0 ; int ret = sigaction(SIGINT,&act,&oldact); if (ret == -1 ){ sys_err("sigaction error" ); } ret = sigaction(SIGQUIT,&act,&oldact); while (1 ); return 0 ; }
案例:通过信号捕捉回收子进程(当一个时间点有多个进程死亡时,此时只能处理一个,其他死亡的子进程没有回收就会是僵尸进程)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 void sys_err (const char *str) { perror(str); exit (1 ); } void catch_child (int signo) { pid_t wpid; int status; while ((wpid = waitpid(-1 ,&status,0 ))!=-1 ) { if (WIFEXITED(status)) printf ("---catch child id %d,ret = %d\n" ,wpid,WEXITSTATUS(status)); } return ; } int main (int argc,char *argv[]) { pid_t pid; sigset_t 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.sa_mask); act.sa_flags = 0 ; sigaction(SIGCHLD,&act,NULL ); sigprocmask(SIG_UNBLOCK,&set ,NULL ); printf ("我是父进程%d\n" ,getpid()); while (1 ); }else { printf ("我是子进程%d\n" ,getpid()); return i; } }
6.会话与守护进程 6.1进程组和会话
进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid和kill函数的参数中都曾使用过。操作系统设计的进程组的概念,是为了简化对多个进程的管理。当父进程,创建子进程的时候,默认子进程与父进程属于同一个进程组。进程组ID = 第一个进程ID(组长进程)。所以,组长进程标识,其进程组ID = 其进程ID。
而会话就是进程组的集合。
1.创建一个会话需要注意以下6点:
调用进程不能是进程组组长,该进程变成新会话的首进程
该进程成为一个新进程组的组长进程
需要root权限 (Ubuntu不需要)
新会话丢弃原有的控制终端,该会话没有控制终端
如果该进程调用的是组长进程,则出错返回
建立新会话时,先调用fork,父进程终止,子进程调用setsid()
2.getsid()函数:获取进程所属的会话ID
pid_t getsid(pid_t pid);
注意:组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程
3.setsid()函数:创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID
pid_t setsid(void);
返回值:成功(返回调用进程的会话ID);失败(返回-1)
注意:调用setsid函数的进程,既是新的会长,也是新的组长。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int main (void ) { pid_t pid; if ((pid = fork())<0 ){ perror("fork" ); exit (1 ); }else if (pid == 0 ){ printf ("child process PID is %d\n" ,getpid()); printf ("Group ID of child is %d\n" ,getpgid(0 )); printf ("Session ID of child is %d\n" ,getsid(0 )); sleep(10 ); setsid(); printf ("changed:\n" ); printf ("child process PID is %d\n" ,getpid()); printf ("Group ID of child is %d\n" ,getpgid(0 )); printf ("Session ID of child is %d\n" ,getsid(0 )); sleep(20 ); exit (0 ); } return 0 ; }
6.2守护进程
daemon进程。通常运行于操作系统后台,脱离控制终端。一般不与用户直接交互。周期性的等待某个事件发送或周期性执行某一动作。不受用户登录注销影响,通常采用以d结尾的命名方式。
1.创建守护进程,最关键的一步是调用setsid函数,创建一个新的Session,并成为Session leader。
2.守护进程的创建步骤:
fork子进程,让父进程终止
子进程调用setsid()创建新会话
通常根据需要,改变工作目录位置chdir()
通常根据需要,重设umask文件权限掩码
通常根据需要,关闭/重定向文件描述符012
守护进程 业务逻辑。—->while()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 void sys_err (const char *str) { perror("str" ); exit (1 ); } int main (int argc,char *argv[]) { pid_t pid; int ret,fd; pid = fork(); if (pid>0 ){ exit (0 ); } pid = setsid(); if (pid == -1 ){ sys_err("setsid error" ); } ret = chdir("/home/c_c++后端/系统编程/session_dir" ); if (ret == -1 ){ sys_err("chdir error" ); } umask(0022 ); close(STDIN_FILENO); fd = open("/dev/null" ,O_RDWR); if (fd == -1 ){ sys_err("open error" ); } dup2(fd,STDOUT_FILENO); dup2(fd,STDERR_FILENO); while (1 ); return 0 ; }
7.线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程是独立调度和分派的基本单位,同一进程中的多条线程将共享该进程中的全部系统资源,但同一进程中的多个线程有各自的调用栈、寄存器环境和线程本地存储。
1.线程与进程区别:
注意:线程是最小的执行单位;进程是最小分配资源单位,可看成是只有一个线程的进程。进程里创建线程后,进程也叫线程了
2.查看某个进程里的线程:
ps -Lf 进程pid:线程号是LWP那一栏(相当于进程号,线程号是接在进程号后面的)
3.线程共享资源:文件描述符、每种信号的处理的方式、当前工作目录、用户ID和组ID、内存地址空间(.text/.data/.bss/heap/共享库)、全局变量;
4.线程非共享资源:线程id、处理器现场和栈指针(内核栈)、独立的栈空间(用户空间栈)、errno变量、信号屏蔽字、调度优先级
5.线程优缺点
7.1线程控制原语 1.线程函数
pthread_t pthread_self(void);
注意:线程ID是在进程中来标识线程身份的,进程通过线程ID来对其加以区分;而LWP是线程号,标识线程身份给CPU用的(CPU用线程号来划分时间片)
2.创建线程
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void*),void *arg);
注意:主函数利用pthread_create()创建新线程时,主函数会立即返回值并执行下面代码,不会因为新线程没有执行完回调函数而阻塞。
当新线程启动后,它会在后台执行回调函数,同时主线程继续执行自己的任务。如果回调函数执行时间较长,主线程仍然不会被阻塞,它会继续往下执行,直到遇到需要等待新线程完成任务的代码段。
3.退出当前线程
void pthread_exit(void *retval);
注意:exit()是退出当前进程;return;是返回到调用者那里去
pthread_exit(NULL)的作用是在线程中显式地退出线程的函数调用。它用于终止当前线程的执行,并将线程的退出状态设置为NULL。当调用pthread_exit(NULL)时,当前线程会立即退出,并将控制返回给创建该线程的线程。这意味着线程的执行会终止,但其他线程仍然可以继续执行。
4.阻塞等待线程退出,获取线程退出状态
int pthread_join(pthread_t thread,void** retval);
注意:在进程中,进程结束是exit(1),退出状态是int型,所以回收进程wait的参数是int*型;在线程中,线程结束是pthread_exit(void*),退出状态是void*型,所以回收线程pthread_join参数是void**型。
5.杀死(取消)线程,类似于进程中的kill()
int pthread_cancel(pthread_t thread);
参数:待杀死的线程id
返回值:成功(0);失败(errno)
注意:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
如果子线程没有到达取消点,那么使用pthread_cancel无效。我们可以在程序中。手动添加一个取消点,使用pthread_testcancel()。
取消点:通常是一些系统调用creat,open,pause,close,read,write。(可以理解为是线程有进入内核,如果线程执行的内容是if,while,for这些则不能用pthread_cancel()杀死)
案例:终止线程的三种方法,注意取消点的概念。
6.线程出错要用这个函数打印
char *strerror(int errnum);
使用:fprintf(stderr,”pthread_join error:%s”,strerror(ret));
7.实现线程分离
int pthread_detach(pthread_t thread);
参数:待分离的线程id
返回值:成功(0),失败(errno)
注意:pthread_detach不能与pthread_join一起用,当使用线程分离时,被分离的子线程就不归主线程管了,子线程执行完由系统回收,不需要主线程再调用pthread_join来回收,如果调用,会回收失败。
8.线程属性,用来设置分离属性
pthread_attr_t attr; //创建一个线程属性结构体变量
pthread_attr_init(&attr); //初始化线程属性
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); //设置
pthread_create(&tid,&attr,tfn,NULL); //设置线程属性,创建为分离态
pthread_attr_destroy(&attr); //销毁线程属性
案例:创建一个线程执行回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void *tfn (void *arg) { printf ("我是子线程,先慢慢执行\n" ); sleep(3 ); return NULL ; } void sys_err (const char *str) { perror(str); exit (1 ); } int main (int argc,char *argv[]) { pthread_t tid; int ret = pthread_create(&tid,NULL ,tfn,NULL ); if (ret != 0 ){ sys_err("pthread_create error" ); } printf ("我是父进程,先结束了\n" ); pthread_detach(tid); }
案例:循环创建多个子线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void *tfn (void *arg) { int i = (int )arg; sleep(i); printf ("我是第%d子线程:pid=%d,tid=%lu\n" ,i+1 ,getpid(),pthread_self()); return NULL ; } void sys_err (const char *str) { perror(str); exit (1 ); } int main (int argc,char *argv[]) { pthread_t tid; int ret; int i; for (i=0 ;i<5 ;i++){ ret = pthread_create(&tid,NULL ,tfn,(void *)i); if (ret != 0 ){ sys_err("pthread_create error" ); } } sleep(i); printf ("主线程里面,pid=%d,tid=%lu\n" ,getpid(),pthread_self()); return 0 ; }
案例:使用pthread_exit()函数来退出子线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void *tfn (void *arg) { int i = (int )arg; sleep(i); if (i==2 ){ pthread_exit(NULL ); } printf ("我是第%d子线程:pid=%d,tid=%lu\n" ,i+1 ,getpid(),pthread_self()); return NULL ; } void sys_err (const char *str) { perror(str); exit (1 ); } int main (int argc,char *argv[]) { pthread_t tid; int ret; int i; for (i=0 ;i<5 ;i++){ ret = pthread_create(&tid,NULL ,tfn,(void *)i); if (ret != 0 ){ sys_err("pthread_create error" ); } } sleep(i); printf ("主线程里面,pid=%d,tid=%lu\n" ,getpid(),pthread_self()); return 0 ; }
案例:通过pthread_join()回收子线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 struct thrd { int var; char str[256 ]; }; void sys_err (const char *str) { perror(str); exit (1 ); } void *tfn (void *arg) { struct thrd *tval = (struct thrd *)arg; tval->var = 93 ; strcpy (tval->str,"hello lxx" ); return (void *)tval; } int main (int argc,char *argv[]) { pthread_t tid; struct thrd arg ; struct thrd *retval ; int ret = pthread_create(&tid,NULL ,tfn,(void *)&arg); if (ret != 0 ){ sys_err("pthread_create error" ); } ret = pthread_join(tid,(void **)&retval); if (ret != 0 ){ sys_err("pthread_join error" ); } printf ("child thread exit with var = %d,str = %s\n" ,retval->var,retval->str); pthread_exit(NULL ); }
案例:通过pthread_cancel()终止子线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void *tfn (void *arg) { while (1 ){ printf ("thread:pid = %d,tid = %lu\n" ,getpid(),pthread_self()); sleep(1 ); } return NULL ; } int main (int argc,char *argv[]) { pthread_t tid; int ret = pthread_create(&tid,NULL ,tfn,NULL ); if (ret != 0 ){ fprintf (stderr ,"pthread_create error:%s\n" ,strerror(ret)); exit (1 ); } printf ("main:pid = %d,tid = %lu\n" ,getpid(),pthread_self()); sleep(5 ); ret = pthread_cancel(tid); if (ret != 0 ){ fprintf (stderr ,"pthread_cancel error:%s\n" ,strerror(ret)); exit (1 ); } }
案例:通过pthread_detach()设置线程分离
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void *tfn (void *arg) { printf ("thread: pid = %d,tid = %lu\n" ,getpid(),pthread_self()); return NULL ; } int main (int argc,char *argv[]) { pthread_t tid; int ret = pthread_create(&tid,NULL ,tfn,NULL ); if (ret != 0 ){ fprintf (stderr ,"pthread_create error:%s\n" ,strerror(ret)); exit (1 ); } ret = pthread_detach(tid); if (ret != 0 ){ fprintf (stderr ,"pthread_detach error:%s\n" ,strerror(ret)); exit (1 ); } sleep(1 ); ret = pthread_join(tid,NULL ); printf ("join ret = %d\n" ,ret); if (ret != 0 ){ fprintf (stderr ,"pthread_join error:%s\n" ,strerror(ret)); exit (1 ); } printf ("main: pid = %d,tid = %lu\n" ,getpid(),pthread_self()); pthread_exit((void *)0 ); }
案例:通过线程属性设置分离属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void *tfn (void *arg) { printf ("thread:pid = %d,tid = %lu\n" ,getpid(),pthread_self()); return NULL ; } int main () { pthread_t tid; pthread_attr_t attr; int ret = pthread_attr_init(&attr); if (ret != 0 ){ fprintf (stderr ,"attr_init error:%s\n" ,strerror(ret)); exit (1 ); } ret = pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); if (ret != 0 ){ fprintf (stderr ,"attr_setdetachastate error:%s\n" ,strerror(ret)); exit (1 ); } ret = pthread_create(&tid,&attr,tfn,NULL ); if (ret != 0 ){ fprintf (stderr ,"pthread_create error:%s" ,strerror(ret)); } printf ("main:pid = %d,tid = %lu\n" ,getpid(),pthread_self()); ret = pthread_attr_destroy(&attr); if (ret != 0 ){ fprintf (stderr ,"attr_destroy error:%s\n" ,strerror(ret)); exit (1 ); } pthread_exit((void *)0 ); }
案例:终止线程的三种方法,注意取消点的概念。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void *tfn1 (void *arg) { printf ("thread 1 returning\n" ); return (void *)111 ; } void *tfn2 (void *arg) { printf ("thread 2 returning\n" ); pthread_exit((void *)222 ); } void *tfn3 (void *arg) { while (1 ){ printf ("thread 3 returning\n" ); sleep(1 ); } return (void *)333 ; } int main () { pthread_t tid; void *tret = NULL ; pthread_create(&tid,NULL ,tfn1,NULL ); pthread_join(tid,&tret); printf ("thread 1 exit code = %d\n\n" ,(int )tret); pthread_create(&tid,NULL ,tfn2,NULL ); pthread_join(tid,&tret); printf ("thread 2 exit code = %d\n\n" ,(int )tret); pthread_create(&tid,NULL ,tfn3,NULL ); sleep(3 ); pthread_cancel(tid); pthread_join(tid,&tret); printf ("thread 3 exit code = %d\n" ,(int )tret); return 0 ; }
7.2线程使用注意事项 线程使用注意事项:
主线程退出其他线程不退出,主线程应该调用pthread_exit()
避免僵尸线程,使用pthread_join、pthread_detach、pthread_create(指定分离属性)
malloc和mmap申请的内存可以被其他线程释放(线程共享堆)
应该避免在多线程模型中调用fork。除非马上exec。因为如果调用fork,则子进程中只有调用fork的线程存在,其他线程在子进程中均默认被pthread_exit
信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
8.线程同步 协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。
锁的使用:建议锁,对公共数据进行保护。所有线程应该在访问公共数据前先拿锁再访问。但锁本身不具备强制性。
8.1互斥锁mutex 1.使用互斥锁的一般步骤:
pthread_mutex_t lock; //创建锁
pthread_mutex_init; //初始化
pthread_mutex_lock; //加锁
访问共享数据(stdout)
pthread_mutex_unlock(); //解锁
pthread_mutex_destroy; //销毁锁
补:restrict关键字:用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
2.初始化互斥锁的两种方法:
pthread_mutex_t mutex; //定义一把锁
pthread_mutex_t_init(&mutex,NULL); //动态初始化
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; //静态初始化
注意:尽量保证锁的粒度(范围),越小越好。访问共享数据前加锁,访问结束立即解锁。
方便记忆:互斥锁,本质是结构体。我们可以看成整数,初值为1,即pthread_mutex_init()调用成功;加锁理解为–操作,阻塞线程;解锁理解为++操作,唤醒阻塞在锁上的线程。
try锁:尝试加锁,成功–操作;失败就返回,同时设置错误号EBUSY。
案例:通过互斥锁进行对公共区域的访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 pthread_mutex_t mutex; void *tfn (void *arg) { srand(time(NULL )); while (1 ){ pthread_mutex_lock(&mutex); printf ("hello " ); sleep(rand()%3 ); printf ("lxx\n" ); pthread_mutex_unlock(&mutex); sleep(rand()%3 ); } return NULL ; } int main (void ) { pthread_t tid; srand(time(NULL )); int ret = pthread_mutex_init(&mutex,NULL ); if (ret != 0 ){ fprintf (stderr ,"mutex init error:%s\n" ,strerror(ret)); exit (1 ); } pthread_create(&tid,NULL ,tfn,NULL ); while (1 ){ pthread_mutex_lock(&mutex); printf ("HELLO " ); sleep(rand()%3 ); printf ("WORLD\n" ); pthread_mutex_unlock(&mutex); sleep(rand()%3 ); } pthread_join(tid,NULL ); pthread_mutex_destroy(&mutex); return 0 ; }
8.2读写锁
锁只有一把,以读方式给数据加锁为读锁;以写方式给数据加锁为写锁。相较于互斥量(互斥锁)而言,当读线程多的时候,读写锁可以提高访问效率。
1.读写锁的特性
读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞
读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞
读写锁是“读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求,优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高。
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。即写独占,读共享
2.主要应用函数
pthread_rwlock_init //自定义读写锁
pthread_rwlock_destroy //销毁读写锁
pthread_rwlock_rdlock //读模式加锁
pthread_rwlock_wrlock //写模式加锁
pthread_rwlock_tryrdlock //尝试读模式加锁
pthread_rwlock_trywrlock //尝试写模式加锁
pthread_rwlock_unlock //读写锁解锁(通用)
以上函数的返回值都是:成功(返回0);失败(返回错误号)
pthread_rwlock_t rwlock //定义一个读写锁变量rwlock
3.死锁
是使用锁不恰当导致的现象,如:
对一个锁反复lock
两个线程,各自持有一把锁,请求另一把
案例:模拟3个线程不定时写同一个全局资源,5个线程不定时读同一个全局资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 int counter; pthread_rwlock_t rwlock; void *th_write (void *arg) { int t; int i = (int )arg; while (1 ){ pthread_rwlock_wrlock(&rwlock); t = counter; usleep(1000 ); printf ("======Write %d:%lu:counter = %d ++counter = %d\n" ,i,pthread_self(),t,++counter); pthread_rwlock_unlock(&rwlock); usleep(10000 ); } return NULL ; } void *th_read (void *arg) { int i = (int )arg; while (1 ){ pthread_rwlock_rdlock(&rwlock); printf ("--------Read %d: %lu: %d\n" ,i,pthread_self(),counter); pthread_rwlock_unlock(&rwlock); usleep(2000 ); } return NULL ; } int main () { int i; pthread_t tid[8 ]; pthread_rwlock_init(&rwlock,NULL ); for (i = 0 ; i < 3 ; i++){ pthread_create(&tid[i],NULL ,th_write,(void *)i); } for (i = 0 ; i < 5 ; i++){ pthread_create(&tid[i+3 ],NULL ,th_read,(void *)(i+3 )); } for (i = 0 ; i < 8 ; i++){ pthread_join(tid[i],NULL ); } pthread_rwlock_destroy(&rwlock); return 0 ; }
8.3条件变量
条件变量本身不是锁,但它也可以造成线程阻塞,通常与互斥锁配合使用。
1.主要应用函数:
pthread_cond_init函数 //初始化条件变量
pthread_cond_destroy函数 //销毁条件变量
pthread_cond_wait函数 //等待条件满足
pthread_cond_timewait函数 //等待条件满足(超时不等)
pthread_cond_signal函数 //唤醒阻塞在条件变量的线程(一个)
pthread_cond_broadcast函数 //唤醒阻塞在条件变量的线程(多个)
以上函数返回值:成功(返回0);失败(返回错误号)
pthread_cond_t cond; //定义一个条件变量cond
2.初始化条件变量的两种方法:
pthread_cond_t cond; //定义一个条件变量
3.pthread_cond_wait函数作用:
注意:上面两步为一个原子操作(中间不会分开执行)
当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex)
案例:模拟一个消费者-生产者模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 void err_thread (int ret,char *str) { if (ret != 0 ){ fprintf (stderr ,"%s:%s\n" ,str,strerror(ret)); pthread_exit(NULL ); } } struct msg { int num; struct msg *next ; }; struct msg *head ;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t has_data = PTHREAD_COND_INITIALIZER; void *produser (void *arg) { while (1 ){ struct msg *mp = malloc (sizeof (struct msg)); mp->num = rand()%1000 +1 ; printf ("--------produce %d\n" ,mp->num); pthread_mutex_lock(&mutex); mp->next = head; head = mp; pthread_mutex_unlock(&mutex); pthread_cond_signal(&has_data); sleep(rand()%3 ); } return NULL ; } void *consumer (void *arg) { while (1 ){ struct msg *mp ; pthread_mutex_lock(&mutex); while (head == NULL ){ pthread_cond_wait(&has_data,&mutex); } mp = head; head = mp->next; pthread_mutex_unlock(&mutex); printf ("-------------cconsumer id:%lu : %d\n" ,pthread_self(),mp->num); free (mp); sleep(rand()%3 ); } return NULL ; } int main (int argc,char *argv[]) { int ret; pthread_t pid,cid1,cid2; srand(time(NULL )); ret = pthread_create(&pid,NULL ,produser,NULL ); err_thread(ret,"pthread_create produser error" ); ret = pthread_create(&cid1,NULL ,consumer,NULL ); err_thread(ret,"pthread_create consumer1 error" ); ret = pthread_create(&cid2,NULL ,consumer,NULL ); err_thread(ret,"pthread_create consumer2 error" ); pthread_join(pid,NULL ); pthread_join(cid1,NULL ); pthread_join(cid2,NULL ); return 0 ; }
9.信号量
应用于线程、进程间同步,因为互斥锁对于多个线程访问同一个公共内存空间时,只能一个一个访问,虽然保证了数据正确性的目的,但导致了线程的并发性下降。
信号量是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
信号量机制是一种用于控制多个进程/线程对共享资源访问。它通过保持对可用资源数量的计数来协调多个线程或进程之间的访问,从而避免竞争条件和数据不一致的问题。信号量通过一个计数器来表示可用资源的数量
PV 操作是操作系统中基于信号量的进程/线程同步机制,P(sem_wait)用于请求资源(可能阻塞),V(sem_post)用于释放资源(可能唤醒),广泛应用于互斥控制和进程协调。
1.主要应用函数
sem_init //信号量初始化
sem_destroy //信号量销毁
sem_wait //信号量加锁
sem_trywait
sem_timedwait
sem_post //信号量解锁
以上函数返回值:成功(返回0);失败(-1)
2.int sem_init(sem_t *sem,int pshared,unsigned int value);
参1:信号量
参2:0—>用于线程间同步;1—>用于进程间同步
参3:N值(指定同时访问的线程数)
3.sem_wait():一次调用,做一次–操作,当信号量的值为0时,再次–就会阻塞(对比pthread_mutex_lock)
4.sem_post():一次调用,做一次++操作,当信号量的值为N时,再次++就会阻塞(对比pthread_mutex_unlock)
案例:用信号量实现生产者-消费者模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #define NUM 5 int queue [NUM]; sem_t blank_number,product_number; void *producer (void *arg) { int i=0 ; while (1 ){ sem_wait(&blank_number); queue [i]=rand()%100 +1 ; printf ("------produce------%d\n" ,queue [i]); sem_post(&product_number); i = (i+1 )%NUM; sleep(rand()%1 ); } } 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 ); } } 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 ; }