并发
进程是运行起来的程序,是OS(操作系统)进行资源分配和调度的基本单位。
当只有一个cpu,进程遇到阻塞事件的时候(可能是要去磁盘中读取数据),我们知道cpu的执行速度和磁盘的IO速度相差特别大,cpu不可能一直等待这个进程去读数据,该遇到阻塞事件的进程会被挂起,cpu会执行下一个进程。
所以cpu会在各个进程之间进行快速的切换,而每个进程的执行也只有那么一瞬间,这也就造成cpu好像在同时执行多个程序的错觉
进程状态
一个进程开始执行,会遇到以下几种状态
运行态
进程正在占用cpu时间片运行时候
就绪态
该进程准备好了可以运行,但是cpu正被其他进程占用
阻塞态(挂起态)
该进程遇到了阻塞事件,进程会挂起,需要等待阻塞事件结束后才能继续运行,此时及时给cpu的使用权,该进程也无法运行
状态的切换
运行态到阻塞态:当进程遇到阻塞事件的时候
阻塞态到就绪态:当阻塞事件结束时
就绪态到运行态:操作系统的进程调度程序选中后,分配cpu的使用权,开始运行
运行态到就绪态:进程运行过程中,分配的时间片用完了,操作系统会将其变成就绪态,接着从就绪态的进程中选择一个运行
处于就绪态的程序有很多,操作系统该如何去选择,处于运行态的进程也有很多,他们该运行多久,这需要程序调度算法来分配,一般位时间片轮转算法
PCB
进程控制块
每一个进程在内核中都有一个进程控制块,PCB是一个结构体,里面有很多成员变量
1.pid进程id
2.进程的状态(运行态、阻塞态、就绪态)
3.进程的虚拟地址
4.文件描述符表
5.进程切换时需要保存和回复的一些变量,cpu寄存器
6.描述控制终端的信息
7.当前工作目录
8.umask掩码
9.uid、gid
零号进程会产生一号进程和二号进程,其他进程都是一号和二号的子进程
子进程
进程产生的俩种方式
运行一段程序,会产生进程
在程序中调用系统调用函数fork,能产生子进程
为什么要这么做?
如果进程遇到了阻塞事件,但是现在不想等阻塞事件结束,就可以调用fork函数,产生子进程,越过阻塞事件,执行下面的程序
fork函数
创建出子进程,子进程和原来的进程的代码是一样的
不光是代码复制过来了,数据段和堆栈都复制了一份
从第10行fork开始分叉
fork()返回值为0–>子进程
返回值>0–>父进程
就比如把你复制了一份,这个克隆人具有和你一样的记忆,但是你和克隆人之前对这个世界的影响在只能是你的影响,不会是双份的
父子进程共享同一个文件描述符
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>int main(int argc, char* argv[])
{int fd=open("a.txt",O_RDWR);int pid=fork();if(pid == 0){//子进程int write_count=write(fd,"hello\n",6);if(write_count == -1){perror("write:");return -1;}printf("fd = %d\n",fd);printf("write_count = %d\n",write_count);}if(pid > 0){//父进程sleep(1);char buf[1024];int read_count=read(fd,buf,sizeof(buf));if(read_count == -1){perror("read:");return -1;}printf("fd = %d\n",fd);printf("read_count = %d\n",read_count);}while(1);return 0;
}
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>int main(int argc, char* argv[])
{printf("hello");int fd=open("a.txt",O_RDWR);int pid=fork();if(pid == 0){//子进程int write_count=write(fd,"hello\n",6);if(write_count == -1){perror("write:");return -1;}printf("fd = %d\n",fd);printf("write_count = %d\n",write_count);}if(pid > 0){//父进程sleep(1);char buf[1024];int read_count=read(fd,buf,sizeof(buf));if(read_count == -1){perror("read:");return -1;}printf("fd = %d\n",fd);printf("read_count = %d\n",read_count);}while(1);return 0;
}
缓冲区机制
C语言中,printf输出的内容首先到放到缓冲区中,等到缓冲区刷新后,才会输出到屏幕上
在 C 语言中,
printf()
函数不会自动刷新缓冲区。这意味着,如果你在printf()
语句中不添加换行符\n
,输出内容可能不会立即显示在屏幕上。原因如下:
缓冲区机制:
- 为了提高输出效率,C 语言中的标准输出(通常是屏幕)使用了缓冲区机制。
- 当你使用
printf()
输出内容时,这些内容首先被写入到内存中的缓冲区,而不是直接输出到屏幕上。缓冲区刷新:
- 缓冲区会在以下情况下自动刷新(即将缓冲区中的内容输出到屏幕):
- 遇到换行符
\n
- 缓冲区满了
- 程序正常退出
- 如果
printf()
语句中不包含换行符\n
,缓冲区可能不会立即刷新,导致输出结果不会立即显示在屏幕上。因此,在
printf()
中不加\n
的情况下,输出内容可能会在缓冲区中暂时保存,直到缓冲区满或程序结束时才会被刷新到屏幕上。如果需要立即看到输出结果,建议在printf()
语句中添加换行符\n
或使用fflush(stdout)
函数来手动刷新缓冲区。
多进程的创建
让子进程不再创建子进程
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>int main(int argc, char* argv[])
{int pid;for(int i=1;i<=14;i++){pid = fork();if(pid == 0) break;}printf("process\n");return 0;
}
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>int main(int argc, char* argv[])
{2 (0,1)2 03 1 (0,1)4 1 1if(fork()&&fork()){fork();}printf("123\n");2 (0,1)3 14 0 (0,1)5 0 1if(fork()||fork()){fork();}printf("123\n");return 0;
}
查看进程
ps ajx | grep ./main(进程名);
结束进程的函数
系统调用函数 -exit()
库函数 exit()
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{printf("hello world\n");exit(0);printf("abcd\n");return 0;
}
孤儿进程
父进程先结束,子进程会变成孤儿进程,但是孤儿进程只会存在那么一瞬间,因为会有一个专门回收的init进程去领养孤儿进程
孤儿说明儿子还存在,但是爸爸死了
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{int pid=fork();if(pid == 0){while(1){//孤儿进程,被init进程领养,因为每个进程必须有父进sleep(1);printf("child pid = %d,father pid = %d\n",getpid(),getppid());}}if(pid > 0){sleep(10);printf("pid = %d,ppid = %d\n",getpid(),getppid());}return 0;
}
僵尸进程
字面意思,儿子死了,但是没死透,因为它的父亲没了,没人给它收尸
一个人离开世界应该是肉体和精神都消失在这个世界里面,僵尸进程就说明精神死了,但是它的肉体仍然存在这个世界里面。
专业词汇:
子进程先结束,但是父进程尚未回收,子进程残留的资源PCB存放在内核中,变成了僵尸(Zombie)进程
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{int pid=fork();if(pid == 0){//僵尸进程printf("child pid = %d,father pid = %d\n",getpid(),getppid());return 0;}while(1);return 0;
}
进程回收
一个进程在结束的时候会关闭所有文件描述符,释放在用户空间分配的内存,但是它的PCB还保留着,内核在其中保存了一些信息,如果是正常退出则保留退出状态,如果是异常退出则保存着异常终止的信号。
子进程的父进程可以调用wait或者waitpid获取这些信息,然后彻底的除掉这个进程。
wait函数
作用
1.阻塞等待子进程的结束
2.在父进程中回收任意一个子进程的残留资源
3.获取子进程的结束状态
头文件
#include <sys/types.h>
#include <sys/wait.h>
函数形式
int wpid=wait(status);
status是传出参数,是进程的状态
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{int pid;for(int i=0;i<5;i++){pid = fork();if(pid == 0){break;}}if(pid>0){int wpid=wait(NULL);printf("hello wpid = %d\n",wpid);while(1);}else{sleep(20);return 0;}while(1);return 0;
}
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/wait.h>
int main(int argc, char* argv[])
{int pid;for(int i=0;i<4;i++){pid = fork();if(pid == 0) break;}if(pid > 0){int status;int wpid = wait(&status);printf("wpid = %d\n",wpid);//当前回收的进程是id是多少// if(WIFEXITED(status)){//如果程序正常退出// printf("child ps return status = %d\n",status);// }if(WIFSIGNALED(status)){//如果程序异常退出printf("kill child ps signal = %d\n",WTERMSIG(status));}while(1);}else{sleep(5);while(1);}return 0;
}
waitpid函数
指定进程id去回收
int wpid=waitpid(wpid,&status,option)
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/wait.h>
int main(int argc, char* argv[])
{int pid;int wpid;for(int i=0;i<4;i++){pid = fork();if(pid == 0) break;if(i == 2){wpid=pid;printf("wpid = %d\n",wpid);}}if(pid > 0){int ret=waitpid(wpid,NULL,0);printf("wpid = %d\n",ret);while(1);}else{sleep(5);return 5;}return 0;
}
若参三为WNOHANG,如果要回收的程序还在运行,则直接返回0,不会杀死该进程(不会阻塞)
参一pid
pid>0,回收指定pid的子进程
pid==0,回收和当前调用进程(父进程)一个组的子进程
pid==-1,回收任意子进程,相当于wait
pid<-1,回收abs(pid)这个组的一个子进程
exec函数族
fork函数创建的子进程后执行的是和父进程相同的程序,如果想在一个程序执行过程中执行另一个程序的话,就需要调用exec函数,通过该调用,被调用的进程会替代当前运行的程序,将当前进程的代码段和数据段全部替换成被调用程序的代码段数据段,但是进程的id不变,进程的名字替换成了被调用程序的名字,肉体还在,精神是别的人的
int execl(path,参1,参2 ,参3 ,NULL);
path–>被调用程序的路径
main.c
在main中调用execl函数,执行a程序,用a程序覆盖main程序,但是进程的id没有改变,进程的名字变成了a程序的
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/wait.h>
int main(int argc, char* argv[])
{printf("cur pid = %d\n",getpid());execl("./a","./a","aa.txt","bb.txt",NULL);//执行其他程序printf("不会执行了\n");return 0;
}
a.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[])
{printf("cur pid = %d\n",getpid());printf("argc = %d\n",argc);printf("agrv[0] = %s\n",argv[0]);printf("agrv[1] = %s\n",argv[1]);printf("agrv[2] = %s\n",argv[2]);printf("agrv[3] = %s\n",argv[3]);return 0;
}
int execlp(const char * file,参1,参2,参3,NULL)
file一般为shell命令或者是环境变量
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/wait.h>
int main(int argc, char* argv[])
{execlp("ls","ls","/",NULL);return 0;
}
ls的底层是把目录里面的内容输出到终端中,现在把文件描述符值为1的指向该为一个文件,就可以实现输出重定向的内容(ls / >>a.txt)
#include<stdio.h> #include<string.h> #include<unistd.h> #include<pthread.h> #include<fcntl.h> #include<sys/wait.h> int main(int argc, char* argv[]) {int fd=open("b.txt",O_RDWR | O_CREAT,0644);if(dup2(fd,1)==-1){perror("dup2:");return -1;}execlp("ls","ls",NULL);return 0; }
覆盖重定向和追加重定向
一个shell命令默认输出到终端上