程序如何结束:
1.正常终止:return,exit,_exit(这些都是程序中写好的终止)
2.非正常终止:自己或他人发信号终止程序(当然需要有相应的权限)
atexit注册进程终止处理函数(就是进程在终止时执行的函数)
return exit 函数结束后会调用atexit
_exit 不会调用atexit
进程环境
1.环境变量,也可以理解为进程中的全局变量
(1)export命令查看环境变量
(2)进程环境表
每个进程中都有一份所有环境变量构成的一个表格,也就是说我们当前进程中可以直接使用这些环境变量。
(3)程序中通过environ全局变量访问环境变量,或者使用getenv这个函数查询环境变量
linux 1G给操作系统(实际用不到1g,空的不会对应到物理内存) 3g给用户进程,意义,进程隔离,多进程同时运行,方便虚拟内存安排(C语言以后不用提供链接脚本,直接就从0开始分配了)。
什么是进程:
进程就是程序一次运行的过程
进程控制块PCB,内核中专门用来管理一个进程的数据结构
getpid当前进程
getppid父进程
getuid当前用户id(root账户还是啥的)
geteuid(当前有效用户id)
getgid(当前用户组id)
getegid(有效组id)
多进程调用原理
1.操作系统同时运行多个进程
2.宏观上并行,微观上串行
3.实际上现代操作系统最小的调度单元是线程而不是进程
fork创建子进程
为了减少开销,fork会复制一份父进程,然后再修改复制出来的进程
fork会返回两次,这是为啥?
1.当fork以后,有一份父进程,一份子进程,代码段,数据段等都一模一样,然后子进程也会被加入到CPU等待列表中,所以会执行两次,(父进程执行一次,子进程执行一次)
为啥子进程也从那一点开始执行,因为父进程和子进程一模一样。
有时候再运行的时候发现父进程已经结束了,所以在子进程运行的时候发现父进程的pid已经改变,变为init进程
父子进程对文件的操作
子进程继承父进程中打开的文件
1.现象为接续写,本质是父子进程之间fd对应的文件指针式彼此关联的。
父子进程各自独立打开同一文件实现共享
1.父进程open 1.txt,子进程打开1.txt,然后写入,发现父子进程分别写,因为在各自打开的时候,两个进程
的PCB已经独立了,文件表也独立了。
2.但是o_append标志可以把父子进程各自独立打开的fd文件指针给关联起来,实现接续写
进程的诞生和消亡
进程的诞生
在用户态中的进程都是fork(或者vfork)出来的
进程的消亡
1.正常终止和异常终止
2.进程在运行时需要消耗系统资源(内存,IO),进程终止时理应完全释放
僵尸进程
1.子进程先于父进程结束,而父进程没有结束,没来及回收(如果父进程继续执行,需要执行wait,waitpid)
2.父进程可以使用wait或waitpid以回收子进程剩余内存资源
3.子进程先于父进程结束,父进程结束时一样会回收子进程剩余待回收内存资源
为什么要有这么一种叫法,因为每个进程退出时,操作系统会自动回收这个进程涉及的所有资源
(譬如open打开的文件没有close在程序终止时也会被关闭回收),但是操作系统只是回收了这个进程工作时消耗的内存和IO,
而并没有回收这个进程本身占用的内存(8KB,主要task_strut和栈内存,因为这些不是操作系统负责的,需要别人辅助清理,这个别人就是
父进程,帮助收尸,所以一个进程必须要有父进程)
孤儿进程
父进程先于子进程,子进程成为一个孤儿进程
所有的孤儿进程都自动成为一个特殊的进程(进程1,也就是init进程(ubuntu不是1,它有自己的设计,就不管了))
父进程wait回收子进程
wait工作原理
1.子进程结束时,系统向其父进程发送SIGCHILD信号(SIGCHILD信号就是为了父子进程之间的异步通信)
2.父进程调用wait函数后阻塞
3.父进程被SIGCHILD信号唤醒然后去回收僵尸子进程
4.若父进程没有任何子进程则wait返回错误
waitpid
1.基本功能一样,都是用来回收子进程
2.waitpid可以回收指定pid的子进程
3.waitpid可以阻塞或者非阻塞两种工作模式
exec函数
1.可以直接在if语句中执行新的程序
execl和execv
execlp和execvp(可以使用当前环境变量的默认值)
execle和execvpe(可多加环境变量)
进程状态
1.进程链表
2.就绪态链表
1.就绪态,等待CPU去执行
2.运行态,在CPU中开始运行
3.僵尸态,进程已经结束,但父进程还来得及回收
4.等待态,浅度睡眠(进程等待时可以被唤醒(唤醒一般只信号))或者深度睡眠(进程等待时不能被唤醒,只能等待条件才能结束睡眠),就算给它CPU它也无法运行,它需要一定的条件激活
5.停止态(暂停态),暂停并不是进程终止,只是被人(信号)暂停了,还可以恢复
为什么有那么多种状态:
尽量充分的运用CPU
system函数
system = fork+exec
原子操作,原子操作意思就是整个操作一旦开始就不会被打断的执行,即fork+exec中间不夹杂任何别的操作,CPU全给你霸占,不会引来竞争状态
进程关系
1.无关系
2.父子进程关系
3.进程组(group),好多个进程加起来,为什么要建立组,让关系更密切,好管理(比如组内可查看这个文件,组外的不行)
4.会话(session)
如何向进程发送信号
例如:kill -9 xxx 其实就是向进程xxx发送9信号
什么是守护进程
1.daemon,表示守护进程,简称为d,进程名后面跟着d的都是守护进程,比如kthreadd
2.长期运行(一般是开机运行直到关机关闭)
3.与控制台脱离(表现为普通进程如果终端被关闭,则这个终端中运行的所有进程都会被关闭,背后的问题在于会话,所谓的终端其实就是ps以后TTY列就是显示的终端)
4.服务器,服务器就是一直在运行的程序,譬如nfs服务器给我们提供nfs通信方式。
5.是用户进程,不是内核程序
常见守护进程
1.syslogd,系统日志守护进程,提供syslog功能,使用syslog来记录调试信息
2.cron,实现我们操作系统的时间管理,实现定时执行程序的功能
3.暴风影音的一些守护进程,经常向我们推送广告
编写简单守护进程
1.任何一个进程都可以将自己实现成守护进程
2.使用create_daemon()将进程变成一个守护进程,在main函数中调用
实现步骤:
(1)子进程等待父进程退出
(2)子进程使用setsid创建新的会话期,目的就是脱离控制台
(3)调用chdir将当前工作目录设置为/
(4)umask设置为0以取消任何文件权限屏蔽,确保进程有最大的文件操作权限
(5)关闭所有文件描述符,这是清除继承自父进程的垃圾
(6)将0(表述输入),1(标准输入),2(标准输出)定位到/dev/null
void create_daemon()
{
pid_t pid = 0;
pid = fork();
if(pid<0)exit(-1);
if(pid>0)exit(0);//父进程退出
//子进程干活
//把我们当前进程设置为新的会话期session
sid = setsid();
chdir(“/”);
umask(0);
int i =0;
int xxx = sysconf(_SC_OPEN_MAX);//获取当前操作系统配置信息,记录了很多信息,比如一个进程可以打开的最大文件数,用户可创建最大进程数等
for(i=0;i=xxx;i++)
{
close(i);
}
//将0,1,2定位到/dev/null,这里表示垃圾堆,回收站
open(“/dev/null”,O_RDWR);//0
open(“/dev/null”,O_RDWR);//1
open(“/dev/null”,O_RDWR);//2
}
使用syslog(openlog,syslog,closelog)来记录调试信息,比如说守护进程无法在控制台输出,就可以使用这个,
一般会在/var/log/messages存储,ubuntu在/var/log/syslog文件
其实这个syslog是通过一个守护文件来执行的,syslogd这个守护进程
让程序不能被多次运行
1.有时候并不让程序运行多次,比如守护进程就想运行一次。
2.单例运行功能
实现方法
1.用一个文件的存在与否来做标志,然后在运行程序时去创建这个文件,然后结束时删掉这个文件。
进程之间的通信(IPC)
1.同一进程的通信
(1)同一个进程在一个地址空间中,同一个进程的不同模块,不同函数,不同文件都是简单的通信,很多时候都是用全局变量,函数传参的方式
(2)2个不同的进程处于不同的地址空间(为了进程安全,这样有好处也有坏处),因此要互相通信很难。
通信机制
1.无名管道和有名管道
2.SystemV(一种Unix内核) IPC:信号量,消息队列,共享内存
3.Socket域套接字(BSD内核)
4.信号
无名管道(一般直接称为管道)
1.原理:管道其实就是内核维护的一块内存,有读端和写端(管道是单向通信的)
2.通信方法:父进程创建管道后fork子进程,子进程继承父进程管道fd
3.通信的限制:只能在父子进程间通信,半双工
4.管道用到的函数:pipe(创建管道),write,read,close
管道不是内存吗?内存不是允许随便读吗?为什么读了一次以后就没了
因为管道有这样的机制,机制的原因,只能读一次
有名管道(fifo)
解决无名管道问题,可以在不是父子进程中进行通信
1.原理:实质也是一块内核维护的内存,表现形式为一个有名的文件(真的文件,该文件映射到维护的内存)
2.通信方法:固定一个文件名,2个进程分别使用mkfifo创建fifo文件,然后分别open打开
获取到fd,然后一个读一个写
3.通信的限制:半双工
4.使用的函数:mkfifo,open,write,read,close
SystemV IPC
特点:
1.系统通过一些专用API来提供SystemV IPC功能
2.分为:信号量,消息队列,共享内存(通过这3种方法实现进程通信,为什么不统一一种,因为都不一样,都有自己的实地用法)
3.其实质也是内核提供的公共内存
信号量(小信息交换):
1.实质就是一个计数器(其实就是一个可以用来计数的变量,可以理解为2进程共享int a,a++)
2.主要用来通过计数值来提供互斥和同步
消息队列(合适用来广播,一对一单播):
1.本质上是一个队列,可以理解为fifo。
2.工作时A和B2个进程进行通信,A向队列放入消息,B从队列中读出消息
共享内存(用于大容量的信息交换,例如传递图像信息)
1.大片内存直接映射(例如将图片在内存共享,通过虚拟地址到物理地址的映射,映射到同一块物理内存)
2.类似于LCD显示时的显存用法
剩余的2类IPC
1.信号
2.Unix域套接字 socket
什么是信号
1.信号的目的:用来通信,是一种通信途径
2.信号是异步的
3.信号本质是int型数字编码(事先定义好的,比1表示什么,2表示什么),所以信号发送的内容很受限,不可能是文字,图片这些
在进程表的表现中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不用的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少次。(可能在_eprocess中找到)
信号由谁发出
1.用户在终端按下按键
2.硬件异常后由操作系统内核发出信号
3.用户使用kill 命令向其它进程发出信号
4.某种软件条件满足后也会发出信息,如alarm闹钟时间到会产生sigalarm信号(其实也是内核发出的)
信号由谁处理,如何处理
发给哪个进程,哪个进程处理
1.忽略信号
2.捕获信号(信号绑定了一个函数)
3.默认处理(忽略或者终止进程)
常见信号介绍
1.SIGINT 2 Ctrl+C时OS送给前台进程(后台进程就不行)组中每个进程
2.SIGABRY 6 调用abort函数,进程异常终止
3.SIGPOLL SIGIO 8 指示一个异步IO事件
4.SIGKILL 9 杀死进程的终极办法(进程无法拒绝这个进程)
5.SIGSEGV 11 无效存储访问(比如强势修改代码段,错误内存访问等等)时OS发出该信息
6.SIGPIPE 13 涉及管道和socket
7.SIGALATM 14 涉及alarm函数的实现
8.SIGTERM 15 kill命令发送的OS默认终止信号
9.SIGCHLD 17 子进程终止或停止时OS向其父进程发此信号,比如说父进程wait子进程,当子进程结束的时候就发送这个信号告诉父进程
10.
SIGUSR1 10 用户自定义信号,作用和意义由应用自己定义
SIGUSR2
进程对信号的处理
例如用signal函数处理sigint,但考虑到移植性性或者各个版本不一样时,使用sigaction函数代替
typedef void (*sighandler_t)(int)
void func(int sig)
{
printf(”func for signal %d\n”,sig)
}
int main()
{
//虽然定义了处理方法,但原本的关闭进程就没了,这函数的返回值是上一个func函数指针
sighandler_t ret = (sighandler_t)-2
ret = signal(SIGINT,func)
//signal(SIGINT,SIG_DEL)
//signal(SIGINT,SIG_IGN)
while(1)
}
闹钟函数alarm,时间到了会回给你一个SIGALRM的信号
pause函数让程序停止,不浪费CPU