Linux系统进程
- 程序开始
- 编译链接的引导代码
操作系统下的应用程序在main执行前也需要先执行段引导代码才能去执行main,但写应用程序时不用考虑引导代码的问题,编译连接时(准确说是链接时)由链接器将编译器中事先准备好的引导代码给链接进去,和应用程序一起构成最终的可执行程序。- 运行时的加载器
加载器是操作系统中的程序,当执行一个程序时(e.g., ./a.out,代码中用exec族函数来运行)加载器负责将这个程序加载到内存中去执行这个程序。
- 程序结束
- 正常终止:return、exit、_exit
- 非正常终止:自已或他人发信号终止进程(e.g., ctrl + c )
atexit
atexit注册多个进程终止函数时,先注册的后执行。(先进后出,和栈一样)
return和exit效果一样,都会执行进程终止函数;_exit不会执行进程终止函数,即刻终止进程。
#include <stdio.h>
#include <stdlib.h>
//以下func1,func2,func3都是进程终止函数
void term_func1(void){
puts("term func1");
}
void term_func2(void){
puts("term func2");
}
void term_func3(void){
puts("term func3");
}
int main(int argc , char *args[]){
primtf("hello world");
atexit(term_func1);
atexit(term_func2);
atexit(term_func3);
exit(0);
}
- 进程环境
- 环境变量,可以认为是操作系统的全局变量。
export
查看环境变量- 环境变量表
每一个进程中都有一份所有环境变量构成的一个表格,也就是说当前进程中可以直接使用这些环境变量。进程环境表其实是一个字符串数组,用environ变量指向它。
extern char **environ;
int i=0;
while(*(environ+i)!=NULL){
printf("%s\n",*(environ+i));
i++;
}
- 进程运行的虚拟地址空间
操作系统中每个进程在独立地址空间中运行,每个进程的逻辑地址空间均为4GB(32位系统)。
进程隔离,提供多进程同时运行。
- 进程
进程是一个动态过程而不是像文件一样的静态实物。进程就是程序的一次运行过程,一个静态的可执行程序a.out的一次运行过程(.
/a.out去运行到结束)就是一个进程。
- 进程控制块PCB(process control block)
内核中专门用来管理一个进程的数据结构。- 进程ID(PID, process ID)
用数字标识不同的进程,用getpid获取。- 多进程调度
操作系统同时运行多个进程,其实宏观上是并行,微观上是串行。实际上,现代操作系统最小的调度单元是线程而不是进程。
getpid
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
- 父进程
fork
子进程继承父进程中打开的文件。父进程先open打开一个文件得到fd,然后再fork创建子进程。之后在父子进程中各自write向fd中写入内容,结果是接续写。实际上本质原因是父子进程之间的fd对应的文件指针是彼此关联的。
父进程open打开1.txt然后写入,子进程打开1.txt然后写入,结果是分别写。原因是父子进程分离后才各自打开的1.txt,这时候这两个进程的PCB已经独立了,文件表也独立了,因此2次读写是完全独立的。- 僵尸进程
进程结束时,操作系统会释放此进程占用的资源,但并操作系统不能回收进程本身占用的内存(进程结构体),需要父进程回收。在子进程结束后,父进程还没有回收子进程本身占用的内存,这时的子进程称为僵尸进程。父进程通过调用wait或waitpid来回收子进程。- 孤儿进程
父进程先于子进程结束,此时操作系统会负责回收。pid_t wait(int *wstatus);
子进程结束时,系统向其父进程发送SIGCHILD信号,父进程调用wait函数后阻塞,父进程被SIGCHILD信号唤醒然后去回收僵尸子进程;若父进程没有任何子进程则wait返回错误。wait主要是用来回收子进程资源,回收同时还可以得知被回收子进程的pid和退出状态。
status: status用来返回子进程结束时的状态,父进程通过wait得到status后就可以知道子进程的一些结束状态信息。
pidt: 返回值是本次wait回收的子进程的PID。当前进程有可能有多个子进程,wait函数阻塞直到其中一个子进程结束wait就会返回,wait的
返回值就可以用来判断到底是哪一个子进程本次被回收了。
WIFEXITED、WEXITSTATUS等宏用来判断退出状态。
- exec族函数
fork子进程是为了执行新程序 ,可以直接在子进程的if中写入新程序的代码。这样可以,但是不够灵活,因为我们只能把子进程程序的源代码贴过来执行,但使用exec族可以运行新的可执行程序。
int execl(const char *pathname, const char *arg, ... /* (char *) NULL */);
可以用来执行一个程序,区别是传参的格式不同。execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成。(l其实
就是list的缩写)int execv(const char *pathname, char *const argv[]);
execv是把参数列表事先放入一个字符串数组中,再把这个字符串数组传给execv函数。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid;
pid = fork();
if (pid == -1) {
perror("fork失败");
exit(-1);
} else if (pid == 0) {
// 子进程
// 使用execl执行/bin/ls -l命令
if (execl("/bin/ls", "ls", "-l", NULL) == -1) {
perror("execl失败");
exit(-1);
}
} else {
// 父进程
// 可以在这里做其他事情,或者等待子进程结束
// 这里简单地等待子进程结束
wait(NULL);
printf("子进程执行完毕。\n");
}
exit(0);
}
//execv_test.c
#include<unistd.h>
int main()
{
char * argv[ ]={"ls","-al","/usr",(char*)0};
execv("/bin/ls",argv);
return 0;
}
- 进程状态
进程的5种状态:
- 就绪态 这个进程当前所有运行条件就绪,只要得到了CPU时间就能直接运行。
- 运行态 就绪态时得到了CPU就进入运行态开始运行。
- 僵尸态
- 等待态(浅度睡眠&深度睡眠)进程在等待某种条件,条件成熟后可进入就绪态。等待态下就算给CPU调度进程也无法执行。
- 停止态
- system函数
system函数= fork+exec,属于原子操作,一旦开始就会不被打断的执行完。原子操作的好处就是不会被人打断(不会引来竞争状态),坏处是自己单独连续占用CPU时间太长影响系统整体实时性,因此应该尽量避免不必要的原子操作,就算不得不原子操作也应该尽量原子操作的时间缩短。
- 进程关系
- 无关系 两个进程独立的,无法相互访问。
- 父子进程关系 继承
- 进程组(group)多个进程构成一个进程组,每个进程组都有唯一的进程组ID(整数,也可以存放在pid_t类型中)。
- 会话(session)多个进程组构成一个会话。
ps
process status
ps -ajx 偏向显示各种有关的ID号
ps -aux 偏向显示进程各种占用资源
kill
kill -信号编号 进程ID,向一个进程发送一个信号
kill -9 xxx,将向xxx这个进程发送9号信号,也就是要结束进程
- 守护进程
守护进程(daemon),进程名后面加字母d标识,是一类在后台运行的特殊进程,与控制台脱离。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。
服务器(Server),服务器程序就是一个一直在运行的程序,可以给我们提供某种服务(比如,nfs服务器给我们提供nfs通信方式),当我们程序需要这种服务时我们可以调用服务器程序(和服务器程序通信以得到服务器程序的帮助)来进程这种服务操作。服务器程序一般都实现为守护进程。
- syslogd
守护进程无法输入输出,只能用syslogd调试信息。操作系统中有一个守护进程syslogd(开机运行,关机时才结束),这个守护进
程syslogd负责进行日志文件的写入和维护。syslogd是独立于任意一个进程而运行的。
- openlog
当前进程和syslogd进程是没有任何关系的,但是当前进程可以通过调用openlog打开一个和syslogd相连接的通道,然后通过syslog向syslogd发消息,然后由syslogd来将其写入到日志文件系统中。- syslogd
- closelog
- cron
操作系统时间管理,定时执行程序。- 将一个进程实现成守护进程
create daemon函数要素
(1)子进程等待父进程退出
(2)子进程使用setsid创建新的会话期,脱离控制台
(3)调用chdir将当前工作目录设置为/
(4)umask设置为0以取消任何文件权限屏蔽
(5)关闭所有文件描述符
(6)将0、1、2定位到/dev/null
void create_daemon() {
pid_t pid;
pid = fork();
if (pid == -1) {
perror("fork error\n");
exit(-1);
} else if (pid) {
exit(0);
}
if (-1 == setsid()) {
perror("setsid error\n");
exit(-1);
}
chdir("/");
umask(0);
int cnt = sysconf(_SC_OPEN_MAX);
int i;
for (i = 0; i < cnt ; ++i) {
close(i);
}
open("/dev/null", RDWR);
open("/dev/null", RDWR);
open("/dev/null", RDWR);
}
- 让守护进程只执行一次
- 进程间通信(IPC, Inter-Process Communication)
复杂、大型的程序,因为设计的需要就必须被设计成多进程程序,常见的如GUI、服务器。
(1)管道(无名管道)和有名管道
(2)SystemVIPC:信号量、消息队列、共享内存
(3)Socket域套接字
(4)信号
- 管道(无名管道)
(1)管道通信的原理:内核维护的一块内存,有读端和写端(管道是单向通信的)
(2)管道通信的方法:父进程创建管理后fork子进程,子进程集成父进程的管道fd
(3)管道通信的限制:只能在父子进程间通信、半双工
(4)管道通信的函数:pipe、write、read、close- 有名管道(fifo)
(1)有名管道的原理:实质也是内核维护的一块内存,表现形式为一个有名字的文件
(2)有名管道的使用方法:固定一个文件名,2个进程分别使用mkfifo创建fifo文件,然后分别open打开获取到fd,然后一个读一个写
(3)管道通信限制:半双工(注意不限父子进程,任意2个进程都可)
(4)管道通信的函数:mkfifo、open、write、read、close- SystemVIPC介绍
SystemVIPc的基本特点:
(1)系统通过一些专用API来提供SystemVIPC功能
(2)分为:信号量、消息队列、共享内存
(3)其实质也是内核提供的公共内存
- 消息队列
(1)本质上是一个队列,队列可以理解为(内核维护的一个)FIFO
(2)工作时A和B2个进程进行通信,A向队列中放入消息,B从队列中读出消息。- 信号量
(1)实质就是个计数器
(2)通过计数值来提供互斥和同步- 共享内存
(1)大片内存直接映射
(2)类似于LCD显示时的显存用法