导入
概念在概念篇中已分析,在此不再赘述。
文章目录
进程的三个基本状态——执行态、就绪态、等待态
关系转换图如下:
点击某个程序后,系统会创建此进程,然后交给CPU管理,给它分配运行所需要的资源(比如:内存等等),如果资源全部分配成功,则进入就绪状态。如果多个进程都进入就绪状态,CPU也不能同时执行,所以CPU会把它们进行排队,挨个调度执行,这里排的队就叫“就绪队列”;当就绪队列中的某个进程被CPU执行时,该进程就进入了执行态,也就是说,从就绪态到执行态需要获取CPU的执行权;之前提过,CPU是通过分配时间片来一个个运行程序的,一个进程执行完一个时间片之后,就是暂时失去CPU的执行权,再次进入就绪状态,去就绪队列排队,等待CPU的再次调度,依次循环直到进程退出;如果进程在运行过程中出现异常(比如:存储空间不够),就会进入等待态,进行休整(等待获取足够存储空间,否则一直被阻塞),休整完毕后(获得足够存储空间被唤醒),进入就绪状态,等待CPU的调度。
三个状态的转换仅此而已,没有其他情况,也就是说,进程不会由等待态直接进入执行态,也不会从就绪态直接进入等待态,等待态里的进程都是些出现问题的进程,它就像一个修车厂,存的都是有故障的车,坏的车来,修好了等着上路。
PCB进程控制块(Program Control Block)
简单了解了进程状态后,接下来重点是如何创建、控制进程,在此之前先谈谈CPU或OS凭借什么控制进程,那就是——PCB
话说在OS中,有一个特殊的数据结构叫做PCB,体现在代码层面可以理解为C语言中的结构体,或者Java中的类。这个PCB就像一个封装着姓名、学号、性别、年级等基本信息的学生结构体或者学生类一样,它里面记录了进程的全部信息,系统每创建一个进程,就会开辟一段内存空间(类似于手机的运行内存)来存放该进程的PCB数据结构,此时CPU通过PCB就可以对进程进行相关的调度、资源分配等各种操作;PCB是进程的唯一标志,体现在其中的进程号上。在Linux中,PCB存放在task_struct结构体中。
进程创建
进程号
如何区分不同的进程呢?就是用PCB中提到的进程号,其类型为pid_t ,范围在0~32767,这些进程号是唯一的,它所标记的进程自然也是唯一的,但这些进程号可以被重复使用,某个进程获得某个进程号,当进程运行结束时,该进程号被释放,后还可以被其他进程获取使用;在Linux系统中,0和1号进程号已被使用,由内核创建,0号进程为调度进程或交换进程(swapper)用于进程之间的切换与调度,1号进程为init进程,顾名思义,初始化进程,可以创建其他进程,由此可知,进程可以创建进程,且除了0号进程,所有进程都直接或间接被1号进程创建,所以说,init是他们的父进程(ppid),他们是init的子进程(pid),相互关联的进程构成进程组(pgid),接收同一终端发送的信号。
各进程号获取函数如下:
getppid(); getpid(); getpgid(pid_t pid);
进程类型
进程创建函数——fork()/vfork()
pid_t fork(void)
功能:
fork()函数用于从一个已存在的进程中创建一个新进程,
新进程称为子进程,原进程称为父进程。
返回值:
成功:子进程中返回0,父进程中返回子进程ID。
失败:返回-1。
使用fork函数得到的子进程是父进程的一个复制品,
它从父进程处继承了整个进程的地址空间。
子进程创建完毕后,如果是单个CPU,则子进程与父进程就开始并发执行,至于谁先执行,取决于内核的调度算法。
因此,使用fork函数的代价是很大的。
此时引出了vfork()函数;
特点:
1.vfork()函数创建的子进程与父进程共享数据;
2.父进程等待子进程结束后方可执行。
进程控制
进程挂起——sleep(seconds);
进程在一定的时间内没有任何动作,称为进程的挂起
unsigned int sleep(unsigned int sec);
功能:
进程挂起指定的秒数,直到指定的时间用完或收到信号才解除挂起。
返回值:
若进程挂起到sec指定的时间则返回0,若有信号中断则返回剩余秒数。
注意:
进程挂起指定的秒数后程序并不会立即执行,系统只是将此进程切换到就绪态。
进程等待——wait(NULL)/waitpid(pid,NULL,0)
平时用wait(NULL),指定等待进程用waitpid(pid, NULL, 0)
pid_t wait(int status)
功能:
等待子进程改变状态,如果子进程终止了,此函数会回收子进程的资源*。调用wait函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒。若调用进程没有子进程或它的子进程已经结束,该函数立即返回。
参数:
函数返回时,参数status中包含子进程退出时的状态信息。子进程的退出信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段。
返回值:
如果执行成功则返回子进程的进程号。出错返回-1,失败原因存于errno中。取出子进程的退出信息
WIFEXITED(status):
如果子进程是正常终止的,取出的字段值非零。
WEXITSTATUS(status):
取出的字段值为子进程调用exit函数返回的值(8~16位)。在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏
*pid_t waitpid(pid_t pid, int status,int options)
功能:
等待子进程改变状态,如果子进程终止了,此函数会回收子进程的资源。
返回值:
如果执行成功则返回子进程ID。
出错返回-1,失败原因存于errno中。
参数pid的值有以下几种类型:
pid>0:
等待进程ID等于pid的子进程。
pid=0
等待同一个进程组中的任何子进程,如果子进程已
经加入了别的进程组,waitpid不会等待它。
pid=-1:
等待任一子进程,此时waitpid和wait作用一样。
pid<-1:
等待指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
status参数中包含子进程退出时的状态信息。
options参数能进一步控制waitpid的操作:
0:
同wait,阻塞父进程,等待子进程退出。
WNOHANG:
没有任何已经结束的子进程,则立即返回。
WUNTRACE如果子进程已暂停则马上返回,且子进程的结束状态不予以理会。
返回值:
如果设置了选项WNOHANG,调用waitpid时若没有任何已经结束的子进程可收集,返回0;若有已经结束的子进程可收集,则返回子进程进程号。出错返回-1(当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出
错返回);这时errno被设置为ECHILD。
进程终止
系统调用——exit(value)
exit函数:结束进程执行
#include <unistd.h>
void exit(int value)
参数:
value:返回给父进程的参数(低8位有效)。
库函数——_exit(value)
_exit函数:结束进程执行
#include <unistd.h>
void _exit(int value)
参数:
value:返回给父进程的参数(低8位有效)。
exit和_exit函数的区别:
exit为库函数,而_exit为系统调用
进程替换
注册处理函数——atexit(main)
进程在退出时可以用atexit函数注册退出处理函数,说白了就是进程退出前执行的函数,注意调用顺序和注册顺序相反即可,可能是将将入口函数地址暂时存放到栈中去了,后入先出。
#include <stdlib.h>
int atexit(void (*function)(void));
功能:
注册进程正常结束前调用的函数。
参数:
function:进程结束前,调用函数的入口地址。
一个进程中可以多次调用atexit函数注册清理函数,
正常结束前调用函数的顺序和注册时的顺序相反。
进程间通信(IPC)
进程是一个独立的资源分配单元,不能直接进行相互访问,需要借助第三方(无名管道和有名管道)来完成
无名管道(pipe)——pipe(int array[2])
从内存中开辟一块4Kb的空间,供子父进程以半双工的方式传送无格式数据进行通信。
特点:
管道不是普通的文件,不属于某个文件系统,其只存在于内存中。
管道没有名字,只能在具有公共祖先的进程之间使用。
管道的缓冲区是有限的。管道是一个固定大小的缓冲区。在Linux中,该缓冲区的大小为4Kbyte。
管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算。
一个消息等。
半双工通信,数据只能从管道的一端写入,从另一端读出。
从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。
int pipe(int filedes[2]);
功能:经由参数filedes返回两个文件描述符
参数:
filedes为int型数组的首地址,其存放了管道的
文件描述符fd[0]、fd[1]。
filedes[0]为读而打开,filedes[1]为写而打开
管道,filedes[0]的输出是filedes[1]的输入。
返回值:
成功:返回 0
失败:返回-1
文件描述符——dup() / dup2()
文件描述符是非负整数,是文件的标识。
内核利用文件描述符(file descriptor)来访问文件。
利用open打开一个文件时,内核会返回一个文件描述符。
每个进程都有一张文件描述符的表,进程刚被创建时,其计录了默认打开的标准输入、标准输出、标准错误输出三个设备文件的文件描述符0、1、2。
在进程中打开其他文件时,系统会返回文件描述符表中最小可用的文件描述符,并将此文件描述符记录在表中。
关于文件描述符的操作,反映到代码层面就是使用dup()和dup2()两个函数,直接上代码,通过代码结合文件描述符的描述理解更直观。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
//进程打开,创建一张文件描述符的表,里面默认的文件描述符有
//0(标准输入) 1(标注输出) 2(标准错误)
int main(){
//此时文件描述符表(后面简称:表)中的文件描述符为: 0 1 2
int fd1;
int fd2;
char buf[] = "Hello World";
char buf1[] = "Hi Everyone";
//下一行行代码可分步剖析:
/*1.执行dup()函数后,返回表中最小可用的一个文件描述符,此时最小可用的为 3
2.将文件描述符 3 赋予整形变量fd2
3.将文件描述符 1 传入dup()函数中,此时fd2代表的文件描述符3就有了文件描述符1的功能
即,标准输出功能 ,可以将数据写出到某文件中
*/
fd2 = dup(1); //fd2 = 3 ------> 1
//以可读可写的方式打开一个名为file的文件,可执行权限加满
if((fd1 = open("file",O_CREAT|O_RDWR,0777)) < 0)
/*当通过open打开一个文件时,返回一个表中最小可用的文件描述符代表此文件,此时应该是 4 ,fd1 就是4*/
{
perror("open error");
exit(-1);
}
//打印一下fd1,验证一下是否是 4
printf("fd1 = %d\n",fd1);
//dup2(fd1 ,1); 4 -------------- 1
//下行代码关闭文件描述符 1 ,此时表中文件描述符为:0 2 3 4
close(1);// 0 2 3 4
/*再次执行dup()函数,返回表中最小可用的文件描述符,看上面,应该是 1 ,此时将fd1所代表的文件描述符(4)的功能赋予 1,也就是将 1 重定向到 4,即,通过 1 可以操作文件file*/
dup(fd1); //1------>4 = file
/*此时的prinf()函数紧跟在dup()后面,成为一组,即,输出内容跟dup返回的文件描述符以及其重定向有关联,平时printf函数是将输出内容打印到控制台,但此处就会将“Hello World”输出到文件file中,因为dup返回的文件描述符 1 重定向到file的文件描述符 4 ,输出自然会到file中,这地方思维跨度有点儿大,逻辑不是很清晰,比如printf是如何跟dup组队的,没有调用系统调用函数write,怎么就会往存在于磁盘中的文件写数据,这个按老师的说法可以理解为操作系统在背后操作的,有意见找操作系统去,哈哈*/
printf("Hello world\n"); //take care of the "\n" line buffer
//再关闭文件描述符 1 ,剩下:0 2 3 4
close(1);//0 2 3 4
/*通过dup,再返回 1 ,且将其重定向到 fd2代表的文件描述符(3),之前分析过,文件描述符 3 是重定向到具有标准输出功能的文件描述符 1 ,此行代码效果就是让文件描述符 1 恢复标准输出的功能*/
dup(fd2); // 1 -----> fd2 = 3 -->1
//此时再输出,内容就会打印到控制台上
printf("Print to the control\n"); //take care of the "\n" line buffer
return 0;
}
运行效果如下:
命名管道(FIFO)——mkfifo(“path+name”,0777)
命名管道(FIFO)和管道(pipe)基本相同,但也有一些显著的不同,其特点是:
FIFO在文件系统中作为一个特殊的文件而存在。
虽然FIFO文件存在于文件系统中,但FIFO中的内容却存放在内存中,在Linux中,该缓冲区的大小为4Kbyte。
FIFO有名字,不同的进程可以通过该命名管道进行通信。
FIFO所传送的数据是无格式的。
从FIFO读数据是一次性操作,数据一旦被读,它就从FIFO中被抛弃,释放空间以便写更多的数据当共享FIFO的进程执行完所有的I/O操作以后,FIFO将继续保存在文件系统中以便以后使用。
FIFO文件的创建
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo( const char *pathname, mode_t mode);
参数:
pathname:FIFO的路径名+文件名。
mode:mode_t类型的权限描述符。
返回值:
成功:返回0
失败:如果文件已经存在,则会出错且返回-1。
FIFO文件的读写
因为使用pipe的进程通过继承获得了pipe的文件描
述符,所以pipe仅需要创建而不需要打开。但是FIFO则需要打开,因为使用它们的进程可以没有任何关系。
一般文件的I/O函数都可以作用于FIFO,如open、close、read、write等。
当打开FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:
不指定O_NONBLOCK(即不写|O_NONBLOCK):
只读open要阻塞到某个其他进程为写而打开此FIFO。
只写open要阻塞到某个其他进程为读而打开此FIFO。
注:
不指定O_NONBLOCK时
除了只读、只写open会阻塞,调用read函数从FIFO里读数据时read也会阻塞。通信过程中若写进程先退出了,则调用read函数从FIFO里读数据时不阻塞;若写进程又重新运行,则调用read函数从FIFO里读数据时又恢复阻塞。通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会退出。
指定O_NONBLOCK:
只读方式打开:如果没有进程已经为写而打开一个FIFO, 只读open成功,立即返回。
只写方式打开:如果没有进程已经为读而打开一个FIFO,那么只写open将出错返回-1。
核心历程
pipe()——父子进程间通信
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(){
int fd_pipe[2];//int 类型数组存放pipe文件描述符
char buf[] = "Hello World";//读写内容
pid_t pid;
//1.创建子进程
pid = fork();
//2.创建无名管道
if(pipe(fd_pipe) < 0){
perror("pipe error\n");
}
if(pid == 0 ){
//3.子进程往管道内写数据
write(fd_pipe[1],buf,sizeof(buf));//系统调用:写出
printf("Son process write to the pipe success\n");
_exit(0);//系统调用exit、
}
else{//父进程往从管道里读
memset(buf,0,sizeof(buf));//系统调用:清空父进程的buf缓存区
//4.父进程从管道内读数据
read(fd_pipe[0],buf,sizeof(buf));//系统调用:读入
printf("Father process read from the pipe success\n");
printf("Father's buf = %s\n",buf);
}
}
运行结果
mkfifo()——任意进程间通信
read进程
/*read进程*/
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd;
int fifo;
//1.创建名为“my_fifo”且可读可写的命名管道<S_我读用户/我写用户>
fifo = mkfifo("my_fifo",S_IWUSR | S_IRUSR);
//2.open打开,打开方式为只读且不阻塞
//(不会因为以只写方式打开此文件的wirte进程没有运行而阻塞)
//注意加循环,且先运行read进程,再运行write进程,否则报为设备接收错误
fd = open("my_fifo",O_RDONLY|O_NONBLOCK);
printf("fd = %d\n",fd);
while(1) {
char buff[1024];
memset(buff, 0, sizeof(buff));
//3.实时读取管道内容
int ret = read(fd, buff, 1024);
if (ret > 0) {
printf("Person One Say:%s\n",buff);
}
}
//4.关闭
close(fd);
return 0;
}
write进程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd; // file description
int fifo; // return 0/-1
char send[] = "hello world";
//1.创建名为“my_fifo”且可读可写的命名管道<S_我读用户/我写用户>
//若已经存在就罢
fifo = mkfifo("my_fifo",S_IRUSR | S_IWUSR);
//2.open打开,打开方式为只写且不阻塞
//(不会因为以只读方式打开此文件的read进程没有运行而阻塞)
fd = open("my_fifo",O_WRONLY | O_NONBLOCK);
while(1)
{
printf("input: ");
fflush(stdout);//清除缓存
char buff[1024] = {0};
scanf("%s", buff);//获取键盘输入内容
//3.将键盘输入内容写入管道
write(fd, buff, sizeof(buff));
}
//4.关闭
close(fd);
return 0;
}
运行结果
补充
不带缓冲
系统调用函数如:write(), 一经调用,便立即将所写内容写入磁盘中;看似非常方便,其实这需要操作系统在背后进行用户空间和内核空间的切换,若频繁写数据,空间就需要频繁切换,大大增加了操作系统的负担
行缓冲
为了减轻操作系统的负担,引入了C语言的标准IO库函数,将其封装到系统调用之上;
此时想往磁盘中写数据时,调用库函数,会开辟一段缓冲区,将要写入磁盘的数据暂时存放到缓冲区内,一旦检测到换行符“\n”后,就会立即冲洗缓冲区,此时会将换行符之前的内容一次性写入磁盘中,比如printf(“\n”)函数中就会有换行符。比起不带缓冲,操作系统切换的频率大大降低。
全缓冲
与行缓冲大部分类似,只是它的缓冲区冲洗条件变成了缓冲区全部填满,此方式更加降低了操作系统来回切换空间的频率,提高其利用率。
exec函数族
exec可理解为“执行”,后面的“函数族”代表不止一个,这里有6个,下面是百度上找的相关介绍,主要作用就是,正在运行的进程执行到此函数时便不再执行后面的代码,而是去运行其他程序并结束本进程
废话不多说,上代码!
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
pid_t pid;
char *argv[] = {"hello",NULL};
pid = vfork(); //1. son process run first /they use the same sources
if(pid == 0)
{
printf("Son process pid = %d\n",getpid());
printf("Son process ppid = %d\n",getppid());
/*execl:参数一(可执行文件所在目录)
参数二(可执行文件名称)
参数三(可执行文件某属性)
以“参数NULL”结束
执行效果为,子进程打印完其进程号和父进程号后,遍历当前目录下的文件并结束子进程
*/
//if(execl("/bin/ls","ls","-hl",NULL) < 0)
/*
execv:参数一(可执行文件目录+文件名)
参数二(char * 数组,格式见 argv[])
执行效果为,子进程打印完其进程号和父进程号后,执行hello文件,打印新进程的进程号和父进程号并结束子进程
*/
if(execv("/home/wangxianjie/test/pipe_test/hello",argv) < 0) // run ohter function
{
perror("execerror");
}
}
else{
printf("Father process pid = %d\n",getpid());
printf("Father process ppid = %d\n",getppid());
sleep(1);
}
return 0;
}
运行效果(以execv为例)如下:
结束
此篇小帆用于笔记整理,内容仅供参考,如有错误,望大佬指点