linux进程全解
程序的开始与结束
main函数由谁调用
编译链接时的引导代码。
操作系统下的应用程序其实在main执行前需要执行一段引导代码才能去执行main。链接器将编译器中的引导代码连击与应用程序一起构成最终的可执行程序。
运行时的加载器。
加载器是操作系统中的程序,当执行一个程序时(譬如./a.out,譬如代码中使用exec族来运行)加载器负责将程序加载到内存中。
argc 与 argv
程序如何结束
正常终止:return、三个exit
非正常终止:自己或者他人发信号终止程序
atexit注册进程终止处理函数
#include <stdio.h>
#include <stdlib.h>
void func1(void)
{
printf("func1\n");
}int main()
{
printf("Hello world ! ");
atexit(func1);
return 0;
}
atexit使得func1函数在最后(终止)运行。
return, exit, _exit区别:return 与 exit效果一样,会执行进程处理函数,_exit 不会执行atexit注册的进程终止处理函数。
进程环境
环境变量
export命令查看环境变量
进程环境表介绍
每个进程都有一份所有环境变量构成的一个表格,也就是我们当前进程中可以直接使用这些变量。进程环境表其实是一个字符串数组,我们用environ变量指向它
#include <stdio.h>
int main()
{
extern char **environ;//声明就能用
int i=0;
while(NULL != environ[i])
{
printf("%s\n",environ[i]);
i++;
}
return 0;
}
//与export打印一致
程序中通过environ全局变量使用环境变量
我们写的程序中可以无条件直接使用系统中的环境变量,一旦程序中使用了环境变量那么程序就和操作系统环境有关。
获取指定环境变量
getenv与setenv,得到/设置当前进程的环境变量
进程运行的虚拟地址空间
操作系统中每个进程在独立地址空间中运行
进程的正式引入
进程是一个动态过程而不是一个静态实物。进程就是程序的一次运行过程,一个静态的可执行程序(a.out)的一次运行过程(运行到结束)为一个进程。
进程控制块PCB(process control block)
内核中专门来管理一个进程的数据结构。记录这个进程的各种信息。每个进程又一个PCB。
进程ID(PID)
OS给内个进程提供一个ID来每个进程进行编号。进程的唯一标识,,
getpid、getppid(获取父进程的ID)、getuid、getgid、getegid
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t p1 = -1;
pid_t p2 = -1;
printf("hello.\n");
p1 = getpid();
p2 = getppid();
printf("pid = %d.\n",p1);
printf("ppid = %d.\n",p2);
return 0;
}
实际用户ID和有效用户ID区别
多进程调度原理
操作系统同时运行多个进程,裸机运行一个进程,可以理解为单进程操作系统,
宏观上的并行,微观上的串行
实际上现代操作系统最小的调度单元是线程而不是进程
fork创建子进程
创建子进程的原因
每次程序的运行都需要一个进程
多进程实现宏观上的并行
fork的内部原理
进程的分裂生成模式
如果OS需要一个新进程来运行一个程序,OS会用现有的进程来复制生成一个新进程。成为父子进程、
fork演示
fork调用一次会返回两次,返回值等于0的为子进程,大于0的为父进程
fork创建完返回值在子进程中等于0,在父进程中等于本次fork创建的进程ID
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t p1 = -1;
p1 = fork();
if(p1 == 0)
{
printf("子进程, pid = %d.\n", getpid());
}if(p1 > 0)
{
printf("父进程, pid = %d.\n", getppid());
}if(p1 < 0)
{
perror("fork");
return -1;
}
//后续的操作,在父子进程中都存在。
printf("hello world, pid = %d.\n",getpid());return 0;
}
fork相当于创建了连个main函数分开执行。
父子进程对文件的操作
子进程继承父进程中的打开的文件
父进程先打开文件,再fork创建子进程,父子进程对文件进行写入操作为接续写(与O_APPEDN类似)。
父子进程各自独立打开同一文件实现共享
父进程open打开后写入,子进程open打开后写入,结果是独立/分别写
加入O_APPEDN:可以改为接续写
子进程最终的目的是要独立去运行另外的程序。
进程的诞生与消亡
进程0与进程1、fork、vfork
进程的消亡
正常终止与异常终止
进程在运行时消耗系统资源(内存、IO),进程终止时须完全释放资源
linux系统设计规定:每个进程退出时,OS会自动回收这个进程涉及到的所有资源。但是OS知识回收了这个进程工作时消耗的内存与IO,而没有回收进程本身占用的内存(nkb,主要是task_struct和栈内存)。
进程本身的nkb内存OS不能回收,需要辅助回收,因此我们每个进程都需要一个帮助其回收的人,就是次进程的父进程。(防止父进程没有显示调用wait或waitpid回收子进程)
僵尸进程
子进程先于父进程结束。子进程结束后父进程此时不一定立即帮子进程回收,这一段时间子进程被称为僵尸进程。
子进程除task_struct和栈外其余内存空间皆已清理
父进程可以使用wati或waitpid以显式回收子进程的剩余待回收内存资源并且获取子进程退出状态。
父进程也可以不适用wait或者waitpid回收子进程,此时父进程结束时一样会回收子进程的剩余待回收内存资源。
孤儿进程
父进程先于子进程结束,子进程就成了孤儿进程。
linux系统规定:所有的孤儿进程都自动成为进程1(init的进程)的子进程。
wait & waitpid
wait工作原理
子进程结束后,系统向父进程发送SIGCHILD信号
父进程调用wait函数后阻塞,若父进程没有任何子进程啧wait返回错误。
父进程被SIGCHILD信号唤醒然后去回收僵尸子进程
父子进程是异步的,SIGCHILD信号机制就是为了解决父子进程之间异步通信问题。让父进程及时去回收僵尸子进程。
wait实战编程
pid_t = wait(&staus)
wait的参数status,status用来返回子进程结束时的状态,父进程通过wait得到status后就可以知道子进程的一些结束状态信息。
wait返回值pid_t,这个返回值就是本次wait回收的子进程的PID。当前进程有多个子进程,wait函数阻塞直到其中一个子进程结束wait就会返回,wait的返回值就可以用来判断到底是哪一个进程本次被回收了
fork后wait回收实例
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{
pid_t pid = -1,ret;
int status = -1;
pid = fork();
if( pid > 0 )
{
//父
//sleep(1);
printf("parent.\n");
ret = wait(&status);printf("子进程已经被回收,子进程的pid = %d.\n",ret);
}
if(pid == 0)
{
//子
printf("child.\n");
}
if( pid < 0 )
{
perror("fork");
return -1;
}return 0;
}
WIFEXITED、WIFSIGNALED、WEXITSTATUS宏来获取子进程退出状态
WIFEXITED():返回进程是否正常退出。(return、exit、_exit退出)
WIFSIGNALED():用来判断子进程是否非正常终止(被信号终止)
WEXITSTATUS():用来得到正常终止下的终止值(下图所示)
waitpid与wait的区别
基本功能一致,都是用来回收子进程。
waitpid可以回收指定PID的子进程
waitpid可以阻塞式或非阻塞式两种工作模式
返回值为-1表示回收失败,0表示要回收的子进程尚未结束,pid表示指定的子进程回收成功。
代码示例
使用waitpid实现wait的效果。
ret = waitpid(-1, &status, 0);
//-1表示回收任意一个进程都可以,0表示默认的方式(阻塞式)来等待,ret是本次回收的子进程PID
回收指定PID进程
ret = waitpid(pid, &status, 0);
非阻塞式访问
ret = waitpid(pid, &status, WNOHANG);//非阻塞式
等待回收PID为pid的子进程,当前进程如果没有一个ID号为pid的子进程,则返回值为-1;如果成功回收pid这个子进程则返回值为回收的经常的pid
竟态初步引入
竟态全称:竞争状态,多进程环境下,多个进程同时抢占系统资源(内存、CPU、文件IO)。
竞争状态会导致OS结果不稳定,因此我们要尽量消灭竞争状态(使用OS提供的一系列的机制,在合适的地方使用合适的方法)、
exec 族函数
为什么需要exec函数
fork创建子进程后,需要子进程去执行新的程序,父子进程需要同时被OS调度,因此,子进程可以单独执行一个程序,此程序与父进程程序在宏观上可以同时进行。
exec族可以把一个编译好的可执行程序直接加载运行。
exec族典型父子进程程序:子进程需要运行的程序被单独编写、单独编译连接成一个可执行程序(叫做hello(例)),主程序为父进程,fork创建子进程后,在子进程中使用exec来执行hello
exec族六个函数
execl和execv
这两个函数是最基本的exec,都可以用来执行一个程序,区别是传参的格式不同execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成( ‘l’ 其实是list的缩写),execv是把参数列表放入字符串数组,将字符串数组传给execv函数
execlp和execv
execl和execv执行程序时必须执行可执行程序的全路径,(如果exec没有找到path的文件直接报错),execlp和execv 传递的可以是file(也可以是path,兼容了file。xeclp和execv 首先回去寻找file, 如果找到则执行,没找到就回去环境变量PATH所指定的目录下查找,再找不到报错)
execle和execvpe
较基本的exec来说 ‘e’ 的意思是environment环境变量。可执行程序时会多传递一个环境变量的字符串数组给待执行的程序。
exec实战
execl运行 ls -l -a
which ls 查看ls的路径
execl("/bin/ls", "ls", "-a", "-l", NULL);
execl运行 ls
char *const arg[]={"ls", "-a", "-l", NULL};
execv("/bin/ls",arg);
execl 运行自己写的程序
execl("./hello", NULL);
会有错误,后面解决。
execlp和execvp
execlp先从环境变量里找,找不到再从当前目录找
execle与execvpe
main函数原型:不止int main(int argc, char **argv),
int main(int argc, char **argv, char **env)//env就是我们给main函数额外传递的环境变量字符串数组,程序会自动从父进程继承一份环境变量(默认的)。如果使用exec时使用execlp或者execvpe去给传递一个envp数组,则程序中的实际环境变量是我们传递的这一份(取代了默认从父进程继承的那一份)
进程状态和system函数
进程的五种状态
就绪态:进程当前所有运行条件就绪,得到CPU就可以直接运行
运行态:就绪态得到CPU就进入运行态开始运行
僵尸态:进程已经结束但是父进程还没来得及回收
等待态:(浅度睡眠 & 深度睡眠)运行条件不满足,等待运行条件满足即可以进入就绪态,浅度睡眠等到时,进程可以被唤醒,深度睡眠等待时不可以唤醒,只能等待运行条件满足。
停止态(暂停态,不是进程的终止)只是被信号暂停了,还可以恢复
system函数
system函数 = fork + exec
原子操作:一旦开始就不会被打断的操作。优势:不会被打断,降低竞争的风险。劣势:自己单独连续占用CPU时间太长影响系统实时性。
system("ls -al ");//system 调用 ls -la
进程关系
无关系
父子进程关系
进程组(group)进程构成的进程组
会话(session):进程组构成的更高一级的组
守护进程
进程查看命令 ps
ps -ajx 偏向显示各种有关的ID号
ps -aux 偏向显示进程各种占用资源
进程发送信号指令 kill
kill -信号编号 进程ID kill -9 xxx(向xxx进程发送九号信号,结束进程)
什么是守护进程(daemon)
简称d(进程后面带d的基本为守护进程)
长期运行(开机运行直到关机)
与控制台脱离(普通进程都和运行该进程的控制台绑定,表现为如果终端被强制关闭了,则终端运行的所有进程都被关闭(同属于一个会话))
服务器(Server),服务器程序时一直运行的程序,可以提供某种服务。(一般都实现为守护进程)
常见守护进程
ps -aux | grep "syslogd" 查看syslogd守护进程(系统日志守护进程),提供syslog服务
cron 实现操作系统的时间管理,linux中实现定时执行程序的功能
编写简单守护进程
任何一个进程都可以将自己实现为守护进程
create daemon函数
子进程等待父进程退出
子进程使用setsid创建新的会话期,脱离控制台
调用chdir将当前工作目录设置为 /
umask设置为以取消任何文件权限屏蔽
关闭所有文件描述符
将0、1、2文件描述符定位到/dev/null
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>void create_daemon(void);
int main( void )
{
create_daemon();while(1)
{
printf("I am running. \n");
sleep(1);
}return 0;
}//把调用该函数的进程变成守护进程
void create_daemon(void)
{
pid_t pid = 0;
pid = fork();
if( pid < 0 )
{
perror("fork");
exit(-1);
}
if( pid > 0 )
{
exit(0);
}//以下为子进程
//将当前进程设置为新的会话期session,让当前进程脱离终端
pid = setsid();
if(pid < 0)
{
perror("setsid");
exit(-1);
}//更改工作目录
chdir("/");//umask设置为0确保将来进程有最大的文件操作权限
umask(0);//关闭所有文件描述符
//先要获取当前系统中所允许打开的最大文件描述符数目
int cnt = sysconf(_SC_OPEN_MAX);
for(int i = 0; i < cnt; i++ )
{
close(i);
}//将0、1、2文件描述符定位到/dev/null
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
}
使用syslog来记录调试信息
openlog、syslog、closelog(写入日志)
各种参数(提供的各种宏,可以查看man手册syslog)
void openlog(const char *ident, int option, int facility);
ident识别的字符串(当前应用程序的名字),
void syslog(int priority, const char *format, ...);
priority日志的重要程度
void closelog(void);
编程实战
一般log信息在OS的/var/log/messages中存储,ubuntu中在/var/log/syslog下
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>int main( void )
{
//打开记录
openlog("a.out", LOG_PID | LOG_CONS, LOG_USER);
syslog(LOG_INFO, "this is my log info .\n");
closelog();return 0;
}
syslog工作原理
OS有一个守护进程syslogd(开机运行,关机结束),这个守护进程syslogd负责进行日志文件的写入与维护
syslogd是独立于我们任意一个进程二运行的。我们当前进程和syslogd进程本来时没有任何关系的,但是我们当前进程可以通过调用openlog打开一个和syslogd相连通的通道,然后通过syslog向syslogd发消息,由syslogd来将其写入到日志文件系统中
syslogd其实就是一个日志文件系统的服务器进程,提供日志服务。人格需要写日志的经常都可以通过openlog/syslog/closelog这三个函数来利用syslogd提供的日志服务。这就是OS的服务体系。
让程序不能被多次运行
守护进程长时间运行,./a.out执行一次就有一个进程,执行多次就有多个进程
守护进程一般都是服务器,服务器程序只要运行一个就行。
程序的单例运行:./a.out运行程序时,如果当前没有这个程序的进程运行则运行,如果已经有这个程序的进程在运行则直接退出
实现方法:
用一个文件的存在来做标志。程序在执行初去判断一个特定的文件是否存在,若存在则表明进程已经运行。运行时去创建此文件。
errno在<errno.h>头文件中
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>#define FILE "/var/aston_test_single"
void delFile(void);
int main( void )
{
//判断文件是否存在
int fd = -1;
fd = open(FILE, O_RDWR | O_TRUNC | O_CREAT | O_EXCL, 0664);
if( fd < 0 )
{
if(errno == EEXIST)
{
printf("进程已经存在!\n");
return -1;
}
}
atexit(delFile);
int i = 0;
for(i = 0; i < 10; i++)
{
printf("I am running....\n");
sleep(1);
}
return 0;
}void delFile(void)
{
remove(FILE);
}
进程间通信
同一个进程在一个地址空间中, 同一个进程的不同模块(不同函数、不同文件)之间。一般使用全局变量来通信,也可以通过函数形参实参来传递。
两个不同的进程处于不同的地址空间,通信很难。
大部分进程不需要考虑进程通信。大部分程序时单进程多线程。
复杂、大型的城区,因为设计的需要必须设计成多进程程序。常见的如GUI、服务器
IPC技术一般在中小型程序中用不到,在大型程序中会用到。
linux内核多种进程间通信机制
无名管道与有名管道
管道(无名管道):
通过内核维护的一块内存进行通信,有读端和写端(管道是单向通信的)
父进程创建管理后fork子进程,子进程继承进程的管道fd
pipe得到两个fd,一个是写fd,一个是读fd。
只能在父子进程间通信、半双工
有名管道(fifo)
实质也是内核维护的一块内存,表现形式为一个有名字的文件
固定一个文件名,两个进程分别使用mkfifo创建fifo文件,分别open打开获取fd,一个读一个写
半双工(不限父子进程,任意两个进程都可)
mkfifo、open、write、read、close
SystemV IPC:信号量、消息队列、共享内存
系统通过专业API来通过SystemV IPC功能
分为:信号量、消息队列、共享内存
实质就是内核提供的公共内存
消息队列
本质上是一个队列,可以理解为FIFO(first in first out)
工作时A和B两个进程进行通信,A向队列中放入消息,B从队列中读出消息
信号量
实质是一个计数器(可以用来计数的变量)
通过计数值来提供互斥和同步
共享内存
大片内存直接映射
类似于LCD显示时的显存用法