1.之前学习linux C语言时整理的关于进程的概念,这里先记录下
1、1、程序的开始和结束
1、main函数由谁调用
(1)我们之前在写裸机代码的时候,需要有段引导代码start.S
(2)我们操作系统中的应用程序,也是需要一段引导代码的,在我们编写好一个应用程序的时候,我们链接这个应用程序的时候,链接器会从编译器中将那段引导代码加上链接进去和我们的应用程序一起生成可执行程序,用gcc -v xxx.c编译一个程序的时候我们可以看到这些详细的信息,包括预处理,编译,链接,gcc内部有一个默认的链接脚本,所以我们可以不用用链接脚本进行指定。
(3)运行时的加载器,加载器是操作系统的一个代码,当我们执行一个程序的时候,加载器会将这段程序加载到内存中去执行程序编译时用编译器,程序运行时用加载器,由加载器将程序弄到内存中运行。
2、argc,和argv
(1)当我们执行一个程序的时候,带了参数的话,因为我执行这个程序的时候,其实所执行的这个程序也相当于一个参数,所在的环境其实是一个shell的环境,shell这个环境接收到了这个两个参数后会给加载器,加载器找到了main函数,然后将这个两个参数给了main,
3、程序如何结束
(1)程序正常终止:return、exit、_exit,就是程序有目的终止了,知道什么时候终止
(2)程序非正常终止:自己或他人发信号终止进程
4、atexit注册进程终止处理函数
(1)atexit函数注册的那个函数,是在程序正常终止的时候,会被执行的,也就是说,如果一个程序正常终止是执行到return 0,表示这个程序正常终止了,如果你使用的atexit函数的后面还有函数,执行顺序不是先执行atexit函数,在执行后面的那个函数,而是先执行后面的函数,在执行atexit函数,就是说,程序正常终止时,这个atexit注册的那个函数才会被执行
(2)当一个进程中多次用atexit函数注册进程终止处理函数时,在程序正常终止的时候,先注册的会被后执行,后注册的会先执行,因为你先注册了,说明你这个进程正常终止的时候,你这个先注册的会有优先起到作用,跟栈的先进后出一样
(3)return exit _exir 的区别:return 和 exit 来让程序正常终止的时候,atexit注册的进程终止处理函数会被执行,但是_exit终止时就不会执行进程终止处理函数
1、2、进程环境
1.环境变量
(1)我们在命令行下,可以用export看到所有的环境变量
(2)进程所在的环境中,也有环境变量,进程环境表将这些环境变量构成了一个表,当前的进程可以使用这个当前进程环境表的环境变量
(3)进程中通过environ全局变量使用当前进程的环境变量,这个全局变量是由库提供的,只用声明,不用定义的。
environ这个全局变量声明的方法,extern char environ,因为当前进程可以使用的环境变量,是在进程环境表中的,是一个一个的字符串,所以用了二重指针,是一个字符串数组,指针数组,数组中的每个元素都指向一个字符串,进程环境表其实是一个字符串数组,用environ变量指向这个表来使用,所以可以使用environ[i]来遍历所有的环境变量
(4)获取环境变量,getenv函数
getenv函数,C库函数,传一个环境变量的名字,返回一个当前环境变量的值,都是字符串格式的,获取或者改环境变量,只是获取和修改的你当前进程中的环境变量
1、3、进程运行的虚拟地址空间
**(1)操作系统中的每一个进程都以为自己是操作系统中的唯一的一个进程,所以都认为自己享有独立的地址空间,但是实际上不是这样的,每一个进程相当于分时复用的。br/>(2)操作系统为每一个进程分配了逻辑空间4GB(32位系统),每个进程都天真的以为,有4G的内存,其中有1G是给操作系统的,其余的都是给自己用的,但实际上可能整个电脑就只有512MB的物理内存,操作系统可能只需要用到10M内存,这10M内存就真正的映射到了物理内存上,剩下的1G-10M的内存是空的,相当于是虚假的,当你用到了的时候,在将你用到的相应的内存映射到物理内存上,不用的时候,就又不映射到物理内存上。所以虽然你相当于有4GB的逻辑虚拟内存,但是你的物理内存真正并没有那么多,而是你逻辑虚拟内存用一点,对应你就去物理内存中用一点,但你肯定不会一下子用的内存多于物理内存,当你不用时,你要把内存还回去了,所以这样的情况就相当于你的程序可以自己认为自己在4G内存中跑着的,程序用了多少内存都会去物理内存中取一点用,用完后又还回去了,所以这4G内存是虚拟出来的。虽然进程认为自己有4G的内存,但是你的进程可能只会用到4M内存,所以就对应物理内存中的4M,只是欺骗了进程你有4G内存。
所以也就是说每一个进程是工作在4G的逻辑虚拟内存中的,但是你的进程并不可能用到这么真正的对应的物理内存,你在逻辑虚拟内存中用到了1M的内存,就给你分配物理内存中的1M进行对应,用10M就给你10M,只是你自己以为你有4G的内存。
这样做的意义:
@1:为了让进程隔离,为了安全,让进程之间看不到对方。让每个进程都以为自己独立的生活在那4G的内存空间上。如果进程不隔离的话,比如你在用支付宝,这个进程运行着,你又运行了QQ,这个进程也运行着,但是因为进程不隔离,QQ进程能看支付宝进程,那么QQ进程就会用一些手段能获取到你的支付宝密码了。
@2:提供多进程同时运行,因为每个程序在运行的时候,肯定都需要链接到真正的物理地址内存上去运行的,但是你的进程那么多,你的每个进程都是独立的,那么就以为着每个进程都是从0地址这个虚拟地址开始的,那怎么将不同的应用程序链接到不同的物理地址内存中去,正因为有了这个虚拟地址和物理地址的映射关系,操作系统内部对内存这块做了很多的管理,我们就不需要操心了,每一个进程运行的时候,操作系统都会为这个进程分配一个可以链接运行的物理地址,不会干扰到别人的**
1、4、什么是进程
1、进程是一个动态过程而不是实物
(1)进程就是程序的一次运行过程,一个程序a.out被运行后,在内存中运行,重运行开始到结束就是一个进程的开始和结束,a.out运行一次就是进程,运行一次就是一个进程
2、进程控制块PCB(proess control block)
(1)内核中专门用来管理进程的一个数据结构,就叫进程控制块。操作系统用一个进程控制块,来管理和记录这个进程的信息
3、进程ID
**(1)一个程序被运行了,就是一个进程,每一个进程操作系统都会其分配一个ID,就是一个号码,来唯一的标识这个进程,进程ID是进程控制块这个结构体中的一个元素。
(2)在Linux的命令行下,可以用ps命令来打印当前的进程(当前在运行的程序),就有当前进程的ID。 ps -a 可以查看所有的进程 ps -aux 查看操作系统所有的进程(这可能是包括之前运行的进程和当前运行的进程)
(3)在编程的过程中,也可以用函数来获得进程号
getpid、获取当前进程的自己的PID,进程ID号
getppid、 获取父进程的ID号
getuid 获取当前进程的用户ID
geteuid、 获取当前进程的有效用户ID
getgid、 获取当前进程组的ID
getegid 获取当前进程的有效组ID
实际用户ID,有效用户ID,实际组ID,有效组ID(有需要去百度吧)**
4、多进程调度的原理
(1)操作系统同时运行多个进程
(2)宏观上的并行,微观上的串行
(3)实际上现代操作系统最小的调度单元是线程而不是进程
5、fork创建子进程,fork是一个系统调用
(1)每一次程序的运行都需要一个进程,操作系统运行你的程序的时候是需要代价的,代价就是要让这个程序加载到一个进程中去运行,这个进程又不是凭空出现的,是需要创建的,所以操作系统运行一个程序的时候,是要先构建一个进程的,就构建了一个进程的进程控制块PCB,这是一个结构体,既然你要构建,你就是一个结构体变量,是需要内存的,这个进程控制块中,就有这个进程的ID号,就需要将你的程序用这个进程控制块PCB来进行管理和操作。
也就是说,你的程序想要在操作系统中运行的话,就一定要参与进程调度,所以就一定要先构建一个进程,所以先构建一个进程控制快PCB,将这个程序用这个进程控制块来管理,这个进程控制块就是一个结构体变量,里面有好多好多的元素,就相当于一个房子,一个让程序住的房子,把这个程序当成了一个进程来管理,进程控制块中就有这个进程的好多信息,比如PID号等等。
(2)linux 中制造进程的方法就是拿老进程来复制出来一个新进程
(3)fork之前的的代码段只有父进程拥有,fork之后的代码段,父子进程都有,数据段,栈这些内存中的数据,即使在fork之前父子进程也都有,因为fork的时候是将父进程的进程控制块中的代码段复制到子进程的进程控制块中,而全局变量,栈,数据值都是拥有的
6、fork内部原理
(1)进程的分裂生长模式:如果操作系统需要运行一个程序,就需要一个进程,让这个程序在这个进程控制块中。做法是,用一个现有的老的进程复制出一个新的进程(比如用memcopy直接将一个老的进程控制块进行复制,因为是结构体变量嘛),然后在这个复制过来的基础上去修改,比如修改这个进程块中描述当前运行的那个程序的内容,给改成我们这个程序,让我们这个程序的代码段到这个位置。既然是复制的,所以就有了父进程和子进程
总结:一个程序想要在操作系统上运行的话,操作系统是需要先构建一个进程的,为了能其进行调度。构建了一个进程控制块PCB,这是一个结构体,用这个结构体定义了一个变量,让这个变量中进行填充内容,这就是一个进程的样子,里面放了这个要执行的程序的代码段等等,还未这个程序弄了一个进程ID号,不过,在构建一个进程的时候,不是直接从头开始构建的,而是用老的进程复制了一个新的进程,就是用memcopy将老进程也就那个结构体变量中的内存空间复制了一个快出来,在向其中进行修改,修改ID号,修改程序的代码段等等,所以就有了父进程和子进程。
7、fork函数的使用
(1)fork函数调用一次,会返回两次,返回值等于0的就是子进程,返回值大于0的就父进程。
我们程序 p1 = fork();后,在运行到这一句的时候,操作系统就已经将父进程就是当前的进程复制了一份了,子进程就出来了,这个时候在这个程序中,父进程也就是现在的这个程序还在操作系统的中运行这,同时子进程也被操作系统运行着了,所以当我们后面一次判断 if(p1 == 0) 的时候,就是判断这个是子进程还是父进程,0就是子进程,if(p1 > 0)就是判断是否是父进程
(2)因为当我们fork的时候,这个进程的整个进程控制块都被复制了,也就代表着这个程序的代码段也被复制了一份,变成了子进程,这个子进程有的也是这个代码段,所以p1 = fork()后面的代码是两个进程同时拥有的,两个进程都要运行的,所以有了if (p1 == 0)和if(p1 > 0 )这两个if来让程序进到其中的某一个进程中,因为父进程中的p1是大于0的,子进程中的p1是等于0的,这样就可以进到其中的某一个进程中。fork后有了两个进程,都在运行的,宏观上的并行,微观上的并行,由操作系统来调度。
再次总结:
fork函数运行后,就将父进程的进程控制块复制了一个份成为了子进程的进程控制块,这个时候,子进程的进程控制块中就有了父进程控制块的代码段,并且这个时候已经是两个进程了,所以操作系统就会对这两个进程进行调度运行,所以这个时候两个进程都是在运行的,所以fork后面的代码段两个进程都有,都会运行,这个时候我们如果想要进到这两个进程中某一个进程中的话,就需要用到if来进行判断,因为fork会返回两次,一次的返回值给了父进程,一次的返回值给子进程,给父进程的返回值是大于0的,给子进程的返回值是等于0的,因为后面的代码两个进程都会运行,如果我们想要进入到某一个进程中去做事情的话,就可以判断返回值是父进程的还是子进程的来决定到底是哪个进程的,重而可以进入到这个进程中去。父进程的返回值是大于0的,值就是本次fork创建子进程的pid
1、5、父子进程对文件的操作
1、子进程继承父进程中打开的文件
(1)在父进程中open打开一个文件得到fd,之后fork父进程创建子进程,之后再父子进程中wirte向文件中写东西,发现父子进程是接续写的,原因是因为父子进程中的fd对应的文件指针彼此是关联的,很像加上了O_APPEDN标志。
(2)我们父子进程写的时候,有的时候会发现类似于分别写的情况,那是因为父子进程中的程序太短了,可以加一额sleep(1)来休眠一段时间,主要是因为,你的父进程或者子进程在运行完自己的程序的时候,就会close关闭这个文件了,既然有一个进程把文件关闭了,那另一个进程既然写不进去了
2、父子进程各自独立打开同一个文件
(1)父进程open打开一个文件然后写入,子进程也open打开这个文件然后写入,结论是分别写,会覆盖,原因是因为这个时候父子进程已经完全分离后才在各自的进程中打开文件的,这时两个进程的PCB已经是完全独立的了,所以相当于两个进程打开了同一个文件,实现了文件共享。open中加上O_APPEND标志后,可以实现接续写,实现文件指针关联起来
1、6、进程的诞生和消亡
1、进程的诞生
(1)进程0和进程1,进程0是在内核态的时候,内核自己弄出来的,是空闲进程,进程1是内核复制进程0弄出来的,相当于内核中的fork弄出来的,然后执行了进程1中的那个根文件系统中init程序,进而逐步的重内核态到用户态。
(2)进入到了用户太的时候,之后的每一个进程都是父进程fork出来的
(3)vfork,和fork有微小的区别,需要可以自己去百度
2、进程的消亡
(1)正常终止和异常终止,静态的放在那里不动的a.out叫做程序,运行了就叫做进程,进程就是运行的程序
(2)进程在运行的时候是需要耗费系统资源的(内存、IO),内存是指进程中向操作系统申请的内存的资源,创建进程时也需要内存,IO是进程对文件IO和IO硬件设备,比如串口这个IO的资源。所以进程终止的时候,应当把这些资源完全释放,不然的话,就会不断的消耗系统的资源,比如你的进程死的时候,你没有将向操作系统申请的资源换回去,那么内存就会越用越少,形成吃内存的情况
(3)linux系统中,当一个进程退出的时候,操作系统会自动回收这个进程涉及到的所有资源,比如,向我们在一个进程malloc的时候,但是没有free,但是这个进程退出的时候,内存也会被是释放,比如open了一个文件,但是进程结束时我们没有close,但是进程结束时,操作系统也会进行回收。但是操作系统只是回收了这个进程工作时消耗的资源,并没有回收这个进程本身占用的内存(一个进程的创建需要内存,因为是复制父进程的PCB出来的,主要是task_struct和栈内存,有8KB,task_struct就是PCB,描述这个进程的那个结构体,栈内存是这个进程所独有的栈)。
(4)所以进程消耗的资源主要是分为两部分,一部分是这个进程工作时消耗的资源,另一部分是这个进程本身自己存在就需要的资源,操作系统在一个进程结束的时候,回收的只是进程工作时用的资源,并没有回收进程本身自己存在所需要的资源
(5)所以进程自己本身需要的8KB内存,操作系统没有回收,是需要别人辅助回收的,所以需要收尸的人,收进程尸体的人,因为进程的尸体也需要占用8KB的内存,所以需要收尸,这个人就是这个进程的父进程
3、僵尸进程(子进程结束了,但是父进程还没来的及将子进程回收,那8KB内存父进程还没有回收)
(1)僵尸进程就是子进程先与父进程结束的进程:子进程结束后,父进程并不是马上立刻就将子进程收尸的,在这一段子进程结束了,但是父进程尚未帮子进程收尸的这一段时间,这个子进程就是一个僵尸进程
(2)在僵尸进程的这一段时间,子进程除了8KB这一段内存(task_struct和栈)外,其余的内存空间和IO资源都被操作系统回收干净了
(3)父进程可以使用wait或者waitpid以显式的方式回收子进程的待被回收的内存资源,并且获取子进程的退出状态。真因为父进程要调用wait或者waitpid来帮子进程回收子进程结束时剩余的内存资源,所以父进程也是要执行函数的,所以有了子进程的这一段僵尸进程的时间,因为子进程死的太快了,父进程需要等到调用了这个两个函数才可以回收子进程,所以子进程必然会存在僵尸进程的这一段时间。
(4)子进程结束的阶段到父进程调用wait或waitpid函数的这一阶段,就是子进程的僵尸进程阶段
(5)父进程还有一种情况也可以回收子进程剩余的内存资源,就是父进程也死了,这个时候,父进程结束时也会去回收子进程剩余的内存资源,这种回收,是在子进程先结束了,父进程后结束的情况下 。(这样设计是为了防止,父进程活的时候忘记使用wait或waitpid来回收子进程从而造成内存泄漏)
4、孤儿进程
(1)父进程先于子进程结束
(2)Linux中规定,所有的孤儿进程都自动成为进程1,init进程的子进程
1、7、父进程wait回收子进程
1、wait的工作原理,因为父子进程是异步的,所以父进程要wait阻塞等待信号后唤醒在回收
(1)子进程结束时,系统向其父进程发出SIGCHILD信号(SIGCHILD是信号中的一个编号)
(2)父进程调用wait函数后阻塞,wait这个函数是阻塞式的,父进程调用wait这个函数阻塞在这里,等操作系统向我发SIGCHILD信号
(3)父进程wait后阻塞等到操作系统发来的SIGCHILD信号,收到SIGCHILD信号后,父进程被唤醒,去回收僵尸子进程
(4)父进程如果没有任何的子进程,这个时候父进程调用wait函数,wait函数就会返回错误
2、wait函数
(1)wait的参数,status。这个参数是用来返回状态的,是子进程结束时的状态,父进程调用wait通过status这个参数,可以知道子进程结束时的状态。子进程结束的时候有两种状态,一种是正常的结束状态,就是return,exit等造成的结束,一种是异常状态,就是由信号造成的异常终止状态,通过status参数返回的状态可以知道这个僵尸子进程是怎么死的
(2)wait的返回值,pid_t,就是本次wait回收的僵尸子进程的PID号。因为当前进程可能有多个子进程,wait阻塞后,你也不知道将来哪一个子进程会结束先让你回收,所以需要用wait的返回值来表示本次回收的僵尸子进程的PID号,来知道是哪个进程本次被回收了。
所以:wait函数就是用来回收僵尸子进程的,父进程wait后阻塞等到操作系统发来的SIGCHILD信号,收到SIGCHILD信号后,父进程被唤醒,去回收僵尸子进程,并且还会通过返回值pid_t类型的值得到这个僵尸子进程的PID,还会通过status参数得到这个子进程的结束的状态,
(3)WIFEXITED、WIFSIGNALED、WEXITSTATUS,这几个宏是来判断wait函数中status参数返回回来的值代表子进程结束是哪种状态的
WIFEXITED:可以用WIFEXITED宏和status参数值来判断回收的那个子进程是否是正常终止的(return exit _exit退出的),测试status的值是正常退出,这个宏就返回1,不正常就是0。
WIFSGNALED: 用来判断子进程是否非正常终止,是否是不正常终止(被信号所终止),是非正常的就返回1,不是就0
WEXITSTATUS:这个宏可以得到子进程正常结束状态下的返回值的(就是return exit _exit 带的值)
3.wait和waitpid的差别
*(1)wait和waitpid的功能几乎是一样的,都是用来回收僵尸子进程的。不同的是waitpid可以指定PID号,来回收指定的僵尸子进程,waitpid可以有阻塞和非阻塞两种工作方式
pid_t waitpid(pid_t pid, int status, int options);
返回值是被回收的子进程的PID号,第一个参数是指定要回收子进程的PID号,如果第一个参数给了-1,就表示不管哪一个子进程要被回收都可以,就跟wait一样了,第二个参数会得到一个子进程的结束状态,第三个参数是一个可选功能,可以有让这个函数是阻塞还是非阻塞的,第三个参数如果是0的话,就表示没有特别的要求,表示是阻塞式的
WNOHANG,options是这个就表示是非阻塞式的,如果第一个参数给的子进程号,这个子进程没有执行完,没有可回收的就立马返回 ,返回值是0,如果个的子进程号是不对的,就返回-1,如果给的子进程号被成功回收了,就返回这个被回收的子进程的PID号**
4、竞态的概念
(1)竞态的全称就是竞争状态,在多进程的环境下,多个进程会抢占系统的资源,内存,文件IO,cpu。
(2)竞争状态是有害的,我们应该去消灭这种竞态,操作系统为我们提供了很多机制来去消灭这种竞态,利用好sleep就可以让父子进程哪个先执行,哪个后结束,当然还有其他的方法,就是让进程同步
1、8、exec族函数
1、既然是一个族函数,就是有很多函数是以exec开头的
2、我们fork一个子进程,是为了让子进程执行一个新的程序,为了让OS调度,所以创建子进程的目的是为了实现父进程和子进程在宏观上的并行,为了让子进程执行新的程序,所以我们可以有两种实现方法:一种是用if直接进到子进程中,在子进程中直接写新的程序的代码。但是这样不好,因为需要将代码全都写到子进程的这个if中,如果代码太多的会也会不好控制。第二种:就是可以直接用exec族函数来直接运行可执行程序,可以在子进程中,直接使用exec族函数,加载可执行程序来直接运行,不需要源代码。
3、所以有了exec族函数,我们可以将子进程要执行的程序,直接进程单独的编写好,编译连接成一个执行程序,完了再子进程中用exec族的函数可以直接运行这个可执行程序,实现了多么方便的方法当一个进程CTRL + C 退出不了后,可以先ps看下这个进程,然后kill -9 xxx 可以关闭一个进程在命令航洗啊
4、exec族的6个函数
(1)execl和execv:这两个函数主要是第二个参数的格式的问题,第一个函数中的执行的那个可执行程序所带的参数,是一字符串,一个字符串代表一个参数,多个参数,是多个字符串,像列表一样,一个参数一个参数的,参数的结尾必须是NULL表示传参结束了。execl中的l其实就是list的缩写,列表。
而execv是将参数放到一个字符串数组中,
这个两个函数,如果找到了那个pathname的可执行程序,就会执行,找不到就会报错,可执行程序是我们指定的。注意是全路径
(2)execlp和execlp:这连个函数的区别就是多了个p,这个函数的第一个参数是file,是一个文件名,当然也可以是全路径加文件名,这两个函数因为参数是文件名,所以会首先找file这个名的可执行程序,找到后就执行,如果找不到就会去PATH环境变量指定的目录下去寻找,也就是说这两个函数执行的那个可执行程序应该是唯一的,如果你确定只有这一个程序,就应该使用这个,方便多些路径的麻烦
(3)execle和execpe:这两个函数就是多了一个e,多了一个环境变量的字符串数组,可以给那个可执行程序多传递一个环境变量,让那个可执行程序在运行的时候,可以用这个传过去的环境变量
extern char **environ;这个是进程中的那个进程环境表,当前进程的所有环境变量都维护在这个表中,这个environ指针指向那个环境表。自己是就是一个字符串数组指针,就是一个指针数组
@1:int execl(const char path, const char arg, ...);
(1)第一个参数是要执行的那个可执行程序的全路径,就是pathname,第二参数是要执行的那个可执行程序的名字,第三个是变参,说明那个可执行程序可以带的参数,是由那个可执行程序决定,他能接受几个,你就可以传几个,参数是以NULL结尾的,告诉传参没了,可以在命令行下,用which xxx来查这个xxx命令(也是程序)的全路径,一定要是全路径第一个参数
用法是:execl("/bin/ls", "ls", "-l", "-a", NULL);
@2:int execlp(const char file, const char arg, ...);
用法只是第一个参数不同,第一个参数是要执行可执行程序的名字,可以是全路径,也可以是单纯的名字,先去PATH环境变量下的路径中去寻找,如果没有找到这个程序就在指定的目录下或者当前目录下找
@3:int execle(const char path, const char arg, ..., char * const envp[]);
@4:int execv(const char path, char const argv[]);
(1)第一个参数和execl一样,第二参数说明,参数是放在argv这个指针数组中的,字符串数组,数组中的指针指向的东西是可以变的。用的时候是这样用的,先定义一个
char * const arg[] = {"ls", "-l", "-a", NULL};
然后在 execv("/bin/ls", arg);
@5:int execvp(const char file, char const argv[]);
@6:int execvpe(const char file, char const argv[], char *const envp[]);
(3)execle和execpe br/>@1:真正的main函数的原型是这样的
int main(int argc, char argv, char env)
env就是给main函数额外传递的环境变量字符串数组,正常我们直接写一个程序直接编译运行的时候里面的环境变量值是继承父进程的,最早是来源于操作系统中的环境变量
@2:execle 的用法,
在子进程中先定义了一个 char * const envp["AA=XX", "BB=DDD", NULL];
在使用execle("/bin/ls", "ls", "-l", "-a", NULL, env);给ls这个可执行程序传递了环境变量envp中的,这个时候这个执行的程序中的env[0]就等于了AA=XX env[1]就等于了BB=DDD,NULL表示后面没有要传的环境变量,用来当结束。如果你不给这个可执行程序传递环境变量的话,这个可执行程序继承的默认就是父进程中的那一份,如果你传了,就是你传递的那一份环境变量
1、9、进程的状态和system函数
1、进程的几个重要的需要明白的状态
操作系统是怎么调度进程的,操作系统中有一个就绪链表,每一个节点就是一个进程,将所有符合条件可以运行的进程加到了这个就绪链表中。
操作系统中还有一个链表,是所有进程的链表,就是所有的进程都在这个链表中,这个链表中的进程如果有符合就绪态可以运行的了,就会被复制加载到就绪态链表中
(1)就绪态:这个进程当前的所有运行条件具备,只要得到CPU的时间,就可以运行了,只差被OS调度得到CPU的时间了
(2)运行态:进程从就绪态得到了CPU,就开始运行了,当前进程就是在运行态
(3)僵尸态:进程运行结束了,操作系统回收了一部分资源(进程工作时用的内存和IO资源),但是没有被彻底回收,父进程还没将剩余的8KB内存空间(进程自身占用的内存)回收的这段时间。
(4)等待态(浅度睡眠&深度睡眠):进程等待满足某种条件达到就绪态的这段时间,等待态下就算给CPU时间也没法运行。浅度睡眠时,进程可以被(信号,别人打电话告诉你不要等那个卫生纸了,你就不等了)唤醒达到就绪态,得到CPU时间,就可以达到运行态了。深度睡眠时,进程不可以被唤醒了(别人打电话告诉你不要等那个卫生纸了,你就非要等到那个卫生纸才行),不能再唤醒达到就绪态了,只有等待到条件满足了,才能结束睡眠状态
(5)暂停态:暂停态不是说进程终止了,进程终止了就成为僵尸态了才对,暂停态只是说进程被信号暂停了,还可以恢复(发信号)
一个进程在一个生命周期内,是在这5种状态间切换的
2、system函数
(1)system函数 = fork + exec
system函数是原子操作的,而fork+exec操作是非原子操作的
(2)原子操作的意思是,一旦开始,就会不被打断的执行完。fork + exec是非原子的,意思是说你fork后得到一个子进程的时候,CPU的时间有可能是还没执行exec呢,就被调度执行别的进程去了,而system函数是,你创建了子进程让子进程执行那个程序,这之间是立刻的事情,并且执行完
(3)原子操作的好处就是会尽量的避免竞争状态,坏处就是自己连续占用CPU的时间太长了
(4)int system(const char *command);
system函数先调用fork创建一个子进程,再让这个子进程去执行command参数的命令程序,成功运行结束后返回到调用sysytem的进程。详情使用的话,去百度看就行。
3、进程关系
(1)无关系,没有以下三种关系234的情况下,可以认为无关系。无关系就是说,进程间是独立的,进程和进程之间不可以随便的访问被的进程的地址空间,不能随便访问别进程中的东西
(2)父子进程关系
(3)进程组(group):由很多个进程构成了一个进程组,为了让这些进程间的关系更加密切,组外的不可以随便的访问
(4)会话(session):会话就由很多个进程组构成的一个组,为了让一个会话做的事情,会话外的不能随便访问
1、10、守护进程
1、进程查看命令ps
(1)单独ps只能看到当前终端的进程,当前是哪个终端,哪个进程,对应的时间和进程程序是什么
(2)常用的ps带的参数:
ps -ajx:偏向于显示操作系统各种进程的ID号
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
父进程ID 子进程 进程组 会话ID 终端 用户ID 时间 对应的程序名
ps -aux:偏向于显示进程所占系统的资源
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
当前用户 进程ID CPU 内存 虚拟内存大小
2、向进程发信号的命令kill
(1)kill -信号编号 进程ID //向一个进程发送信号
(2)kill -9 xxx //这个是比较常用的,向xxx进程发送编号为9的信号,意思是关闭当前这个进程
3、什么是守护进程
(1)daemon:守护进程的意思,简称d,进程名后面带d的基本表示是一个守护进程
(2)长期运行(一般开机运行直到关机时关闭),而普通进程如ls,运行完一遍进程就结束了
(3)与控制台脱离,也就是说,终端关闭了,守护进程还在运行呢,而其他普通进程,终端关了,进程就关了,普通进程和运行该进程的终端是相绑定的,终端关闭了,这个终端中的所有普通进程都会关闭,原因还是在于会话这个概念,因为一个终端中的所有进程其实是一个会话,而守护进程是不会被关闭的
(4)服务器(server):守护进程一般是用来实现一个服务器的,服务器就是一个一直在运行的程序,当我们需要某种帮助的时候,服务器程序可以为我们提供,比如当我们的程序想要nfs服务器的帮助实现nfs通信,我们就可以调用服务器程序得到服务器程序的帮助,可以调用服务器程序来进行这些服务操作。
服务器程序一般都是守护进程,都是被实现成守护进程,所以具有长期运行和终端控制台脱离的特点,所以守护进程其实就是一种特殊的进程,不受终端控制台约束的进程,为我们整个系统提供某种服务,当我们需要某种服务的时候,就可以调用这个守护进程的服务程序来进行得到帮助
(4)我们自己也可以实现一个守护进程,比如你写的这个程序,你也想达到某种服务,脱离终端控制台的影响,就可以将其设计成守护进程。
4、常见的守护进程就先说两个
(1)syslogd,是系统日志守护进程,提供syslogd功能
(2)cron, 这个守护进程使用来实现操作系统的一个时间管理的,比如在Linux中要实现一个定时时间到了才执行程序的功能就需要cron,比如定期垃圾清理,就可以定好时间,没到这个时间就会执行垃圾清理的这个程序,就要用到cron
守护进程其实就是一个能长期运行,能脱离控制台的约束的一个进程,可以长期运行,我们需要的时候,可以通过调用守护进程来得到想要的。因为一直在运行为我们服务,所以可以说每一个守护进程是一个服务程序,很多个守护进程在一起,就形成了服务器
5、编写简单的守护进程
(1)每一个守护进程都可以由一个普通进程实现成一个守护进程,但是一般情况下,只有你的这个进程你想让他一直长期运行,最终和其他的守护进程一起构成一个服务器的情况下,才会让他变成守护进程
(2)我们可以写一个函数,create_daemon,目的是让一个普通进程一调用这个函数,就会被实现变成守护进程br/>(3)create_daemon函数实现的要素
@1:子进程等待父进程退出
@2:子进程使用setsid将当前的进程设置为一个新的会话期session,目的是让当前的进程脱离控制台,
@3:调用chdir将当前工作目录设置为/, chdir("/");目的是让其不依赖于别的文件系统,设置成根目录就可以让开机时就运行,而且不依赖于别的文件系统br/>@4:umask设置为0以取消任何文件权限屏蔽,确保将来这个进程有最大的文件操作权限
@5:关闭所有文件描述符,因为有可能在创建守护进程之前,调用这个函数的父进程可能已经打开了一些文件了,被当前的进程继承了,如果不关闭的话,这个文件OS就会认为一直有人打开着,因为守护进程是一直在运行的,关闭的文件描述包括0、1、2,关闭方法:
for (i=0; i
xxx是当前系统所拥有的一个进程文件描述符的上限,这个是一个不定值,所以我们要动态获取当前系统对一个进程的文件描述符的最大值。通过一个函数来获取,这个函数叫sysconf,这个函数可以获取当前操作系统的所有配置信息的,原型是 long sysconf(int name);你要获取哪一个参数的信息,就把这个参数的名字传进去就行,参数的名字是用宏来体现的,可以通过man手册来查找,获取系统所允许打开的一个进程文件描述符的上限数目的宏是OPEN_MAX或者_SC_OPEN_MAX,函数的返回值就是想要的数目
@6:将0、1、2定位到/dev/null。 这个设备文件相当于OS中的一个垃圾堆。方法是:
open("/dev/null") //对应返回的文件描述符是0
open("/dev/null") //对应返回的文件描述符是1
open("/dev/null") //对应返回的文件描述符是2
6、使用sysconf来记录调试信息
(1)openlog函数
`void openlog(const char *ident, int option, int facility);`
@1:打开一个日志文件
@2:第一个参数是打开这个文件的程序的名字
@3:第二参数和第三个参数都是一些选项,用宏定义来标识的,看man手册
@4:option
LOG_CONS 这个宏,当日志本身坏了时候,或者写不进去的时候,就会将错误信息输出到控制台
LOG_PID 这个宏,我们写入到日志中信息每一条都会有我们写入日志文件的这个进程的PID
@5:facility 表示当前写入到日志中的信息,是一个什么样的log信息,和什么有关的
LOG_AUTH 有这个宏,表示是和安全,检验,验证有关的
LOG_CRON 这个宏,表示是和定时的守护进程相关的信息 clock daemon(cron an at)
LOG_FTP 如果是一个FTP服务器相关的信息,就是这个宏
LOG_USER 如果我们写入到日志文件中的日志信息是普通的用户层的不是什么特殊的,就这个宏
(2)syslog函数 将信息输出到日志文件中
void syslog(int priority, const char *format, ...);
@1:第一个参数表示这条日志信息的重要程度,也是用宏来标识的
@2:LOG_DEBUG 有这个宏表示是最不重要的日志信息
@3:LOG_INFO 有这个宏表示系统的正常输出消息
@4:LOG_NOTICE 有这个宏表示这条日志信息,是比较显著的,比较显眼的,公告
@5:LOG_WARNING 是一条警告,要注意了
@6:LOG_ERR 错误了,出错了
@7:LOG_CRIT 出现紧急的情况了,要马上去处理
@8:LOG_ALERT 立刻要去行动了,不然会完蛋
@9:LOG_EMERG 系统已经不行了
(3)closelog函数 关闭log文件
void closelog(void);
一般log信息都在OS的/var/log/messages这个文件中存着的,但是Ubuntu中log信息是在/var/log/syslog文件中存着的。都是在虚拟文件系统中
(3)syslog的工作原理
操作系统中有一个syslogd守护进程,开机运行,关机结束这个守护进程来负责日志文件的写入和维护
,syslogd是独立运行的一个进程,你不用他,他也在运行。我们当前的进程可以通过调用openlog这个系统调用来打开一条和syslogd相连接的通道,然后通过syslog向syslogd这个守护进程发消息,然后syslogd在将消息写入到日志文件系统中
所以syslogd其实就是日志文件系统的一个服务器进程,提供日志服务的。
7、让程序不能多次运行
(1)我们弄好了守护进程的时候,守护进程是长时间运行不退出的,除非关机,所以我们运行了几次守护进程,就会出现几个守护进程,这样是不好的。
(2)我们希望一个程序是有单例运行功能的,就是说我们之前运行了一程序后,已经有了一个进程了,我们不希望在出现一个这个进程了,在运行的这个程序的时候就会直接退出或者提示程序已经在运行了
(3)方法就是,在运行这个程序的时候,我们在这个程序的里面开始的位置,我们判断一个文件是否存在,如果这个文件不存在的的话,我们的程序就会运行,并且创建这个文件,就会出现一个进程,如果这个文件存在的话,就会退出这个程序,不让其继续运行,这样就会保证有了单例功能。
(4)可以在一个程序的开始加上一个open("xxx", O_RDWR | O_TRUNC | O_CREAT | O_EXCL, 0664);
如果这个文件存在的话,就被excl这个标志影响,open就会错误,我们可以在后面用if来判断errno这个值是否和EEXIST这个宏相等如果和这个宏相等了,就说明是文件已经存在引起的错误,errno是在errno.h中定义的。当我们这个进程运行结束时,我们让这个进程在结束之前在把这个文件删除掉,删除一个文件用的函数是remove函数,我们用atexit函数来注册一个进程结束之前运行的一个清理函数,让这个函数中用remove函数来删除这个文件
1、11、Linux进程间通信(IPC)
1、进程间通信,就是进程和进程之间的通信,有可能是父子进程之间的通信,同一个进程组之间的通信,同一个会话中的进程之间的通信,还有可能是两个没有关联的进程之间的通信.
2、之前我们都是在同一个进程之间通信的,都是出于一个地址空间中的,譬如在一个进程中的不同模块中(不同的函数,不同的文件a.c,b.c等之间是用形参和实参的方式,和全局变量的方式进行通信的,a.c的全局变量,b.c中也可以用),最后形成了一个a.out程序,这个程序在运行的时候就会创建一个进程,进程里去运行这个程序。这是一个进程中的通信。
3、两个进程之间的通信,两个进程是处在两个不同的地址空间中的,也就是a进程中的变量名字和值在a进程的地址空间有一个,但是在b进程中的变量名字可以在b进程的地址空间中和a进程的变量名字一样,因为两者是在两个不同的地址空间中的,每一个进程都认为自己独自享有4G的内存空间,这两者实现的进程间通信就不能通过函数或者全局变量进行通信了,因为互相的地址看不到,所以两个进程,两个不同地址空间的通信是很难的。
4、一般像大型的程序才会用多进程之间的通信,像GUI,服务器之类的,其他的程序大部分都是单进程多线程的
5、Linux中提供了多种进程间通信的机制
(1)无名管道和有名管道,这两种管道可以提供一个父子进程之间的进程通信
(2)systemV IPC: 这个unix分裂出来的内核中进程通信有:信号量、消息队列、共享内存
(3)Socket域套接字(是BSD的unix分支发展出来的),Linux里面的网络通信方式。最早Linux发明这个是为了实现进程间通信的,实际上网络通信也是进程间的通信,一个电脑上的浏览器进程和另一个电脑上的服务器程序进程之间的通信
1和2只能是同一个操作系统之间的不同的进程之间的通信,3是可以实现两个电脑上的不同操作系统之间的进程间通信
(4)信号,a进程可以给b进程信号或者操作系统可以给不同的进程发信号
6、Linux的IPC机制(进程间通信)中的管道
1、说管道的话一般指的是无名管道,有名管道我们一般都会明确的说有名管道,有名管道是fifo
2、管道(无名管道):我们之间进程之间是不能直接一个进程访问到另一个进程的地址空间然后实现进程间通信的,所以这种管道的通信方式的方法就是,在内核中维护了一个内存区域(可以看成是一个公共的缓冲区或者是一个管道),有读端和写端,管道是单向通信的,半双工的,让a进程和b进程通过这块内存区域来实现的进程间的通信。
(1)、管道信息的函数:pipe、write、read、close、pipe创建一个管道,就相当于创建了两个文件描述符,一个使用来读的,一个是用来写的,a进程两个文件描述符,b进程两个文件描述符,所以是a进程在用写的文件描述符像内核维护的那一个内存区域(管道)中写东西的时候,b进程要通过他的读文件描述符read到管道中的a进程写的内容。所以也就是半双工的通信方式,但是我们为了通信的可靠性,会将这一个管道变成单工的通信方式,将a进程的读文件描述符fd=-1,不用这个读文件描述符,将b进程的写文件描述符等于-1,也不用这个,实现了单工的通信方式,这样可以提高通信的可靠性,如果想要双工的通信,就在pipe创建一个管道,将a进程中的写文件描述符fd废弃掉,将b进程的读文件描述符废弃掉,这样两个管道一个实现从左往右的通信,一个实现从右往左的通信,就实现了双工,同时也提高了通信的可靠性。
(2)、管道通信只能在父子进程之间通信
(3)管道的代码实现思路:一般是先在父进程中pipe创建一个管道然后fork子进程,子进程继承父进程的管道fd
3、有名管道(fifo)
(1)解决了只能在父子进程之间通信的问题
(2)有名管道,其实也是内核中维护的一个内存区域,不过这块内存的表现形式是一个有名字的文件
(3)有名管道的使用方法:先固定一个文件名,然后我们在两个进程分别使用mkfifo创建fifo文件,然后分别open打开获取到fd,然后一个读一个写。
(4)任何两个进程都可以实现半双工的通信,因为有名管道是内核维护的一个内存区域,一个有名字的文件,所以我们要先固定一个文件的名字(这个文件是真是存在的,我们外在弄出来的),比如abcd.txt,然后我们在两个进程中分别用mkfifo(mkfifo的是同一个文件的名字)创建fifo管道文件,然后用open分别打开这个有名字的文件,一个进程得到了两个fd,然后一个读一个写
(5)有名管道的通信函数:mkfifo、open、write、read、close
7、systemV IPC
(1)消息队列:消息队列其实就是内核中维护一块内存区域,这个内存区域是fifo的,是一个队列,我们a进程向这个队列中放东西,我们b进程从这个队列中读东西,fifo(队列)是先进先出的,是一种数据结构。可以实现广播,将信息发送出去,形成多个队列。(适合于广播,或者单播)
(2)信号量(适合于实现进程通信间的互斥):信号量实质就是一个计数器,更本质的来说其实就是一个用来计数的变量,这个信号量一般是用来实现互斥和同步的。比如开始的时候这个信号量int a =1,当我们a进程想要访问一个公共的资源的时候,比如说是一个函数的时候,我们a进程就会去判断这个信号量a是否等于,如果等于1,a进程就知道当前这个公共的资源函数,没有被别的进程使用,a进程就会用这个函数,当用的时候,a进程会将信号量a=1,变成一个其他的值,这样b进程这个时候如果也想用这个公共的资源的时候,就会发现a这个信号变了,所以
b进程就知道有别的进程用这个函数了,所以b进程就会等,等到a进程用完了之后,a进程就会信号量a的变成原先的1,这样b进程就知道别的进程用完了,之后b进程就会用这个函数,这样的类似的思路,就可以实现互斥的概念。同步也是可以实现的用信号量,比如a进程走一步,就将信号量a的值加1,b进程来判断信号量a是否加1了,如果加1了,b进程也跟着走一步,b走完以后就将信号量的值恢复到原先,a开始走,a将信号量加1,b又走,b又将信号量减1,因为两个进程之间本身是异步的,因为互相都看不到对方在做什么,但是可以通过信号量的这个机制,来实现节奏上的同步,队列的感觉。
(3)共享内存(适合于进程通信时需要通信大量的信息量时),跟上面两种的共享的内存不同的是,这个共享内存是大片的内存。两个进程间的的虚拟地址同时映射到了物理内存中一大片
8、剩余两类IPC
(1)信号
(2)unix域套接字,socket
2.python中的多进程
# multiprocessing内置模块包含了多进程的很多方法
from multiprocessing import Process
import os
import time
def func(*args, **kwargs):
while True:
time.sleep(1)
print('子进程 pid: ', os.getpid()) # 查看当前进程的pid
print('func的父进程是: ', os.getppid()) # 查看父进程的pid
print(args)
print(kwargs['haha'])
if __name__ == '__main__': # 在windows操作系统上,创建进程启动进程必须需要加上这句话,其他操作系统不需要
# target为要绑定注册的函数(进程), args为要传给绑定注册的函数(进程)的位置参数. kwargs为要传给绑定注册的函数(进程的动态关键字参数),kwargs参数是一个字典类型
p = Process(target=func,args=(5, 4, 3, 2, 1), kwargs={'haha' : 'hehe'}) # 将func函数注册到进程后得到一个进程对象p # 创建了一个进程对象p
p.start() # 开启了子进程,此时func就是相对于本父进程的另外一个进程,属于另外一个应用程序,操作系统创建一个新的进程func子进程,并且子进程进入就绪态,当有时间片时(也就是其他进程(这里是父进程)阻塞时),func子进程就开始执行了
while True:
time.sleep(2)
print('父进程 pid: ', os.getpid()) # 查看当前进程的pid
print('__main__的父进程是: ', os.getppid()) # 查看父进程的pid