fork函数
pid_t fork(void)
父进程返回正整数,子进程返回0,在执行fork函数之前,操作系统只有一个进程,fork函数之前的,代码只会被执行一次,在执行fork函数之后,操作系统有两个几乎一样的进程,fork函数之后的代码会被执行两次
子进程偷梁换柱
(1)execl和execv
这两个函数是最基本的exec,都可以用来执行一个程序,区别是传参的格式不同。execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成(l其实就是list的缩写),execv是把参数列表事先放入一个字符串数组中,再把这个字符串数组传给execv函数。
(2)execlp和execvp
这两个函数在上面2个基础上加了p,较上面2个来说,区别是:上面2个执行程序时必须指定可执行程序的全路径(如果exec没有找到path这个文件则直接报错),而加了p的传递的可以是file(也可以是path,只不过兼容了file。加了p的这两个函数会首先去找file,如果找到则执行执行,如果没找到则会去环境变量PATH所指定的目录下去找,如果找到则执行如果没找到则报错)
(3)execle和execvpe
这两个函数较基本exec来说加了e,函数的参数列表中也多了一个字符串数组envp形参,e就是environment环境变量的意思,和基本版本的exec的区别就是:执行可执行程序时会多传一个环境变量的字符串数组给待执行的程序。
#include
#include
#include
#include
#include
int main(void){
pid_t pid = -1;
pid_t ret = -1;
int status = -1;
pid = fork();
if (pid > 0)
{
// 父进程
printf("parent, 子进程id = %d.\n", pid);
}
else if (pid == 0)
{
// 子进程
//execl("/bin/ls", "ls", "-l", "-a", NULL); // ls -l -a
//char * const arg[] = {"ls", "-l", "-a", NULL};
//execv("/bin/ls", arg);
//execl("hello", "aaa", "bbb", NULL);
//char * const arg[] = {"aaa", "bbb", NULL};
//execv("hello", arg);
//execlp("ls", "ls", "-l", "-a", NULL);
char * const envp[] = {"AA=aaaa", "XX=abcd", NULL};
execle("hello", "hello", "-l", "-a", NULL, envp);
return 0;
}
else
{
perror("fork");
return -1;
}
return 0;
}
进程的退出
_exit:立即退出,不处理IO缓冲区
exit:会先处理IO缓冲区
等待子进程的终结
pid_t wait(int *status)
WIFEXITED、WIFSIGNALED、WEXITSTATUS,这几个宏用来获取子进程的退出状态。
WIFEXITED:宏用来判断子进程是否正常终止(return、exit、_exit退出),如果子进程正常退出,则该宏为真
WIFSIGNALED:宏用来判断子进程是否非正常终止(被信号所终止)
WEXITSTATUS:宏用来得到正常终止情况下的进程返回值的
几个概念
进程组
作用:对相同类型的进程进行管理
进程组的诞生
在Shell里面直接执行一个应用程序,对于大部分进程来说,自己就是进程组的首进程,进程组只有一个进程
如果进程调用了fork函数,那么父子进程同属一个进程组,父进程为首进程
在Shell中通过管道执行连接起来的应用程序,两个程序同属一个进程组,第一个程序为进程组的首进程
进程组id:pgid,由首进程pid决定
会话
作用:管理进程组
会话的诞生
调用setsid函数,新建一个会话,应用程序作为会话的第一个进程,称为会话首进程
用户在终端正确登录之后,启动shell时linux系统会创建一个新的会话,shell进程作为会话首进程
会话id:会话首进程id,SID
前台进程组
Shell进程启动时,默认是前台进程组的首进程。前台进程组的首进程会占用会话所关联的终端来进行,shell启动其他应用程序时,其他程序成为首进程
后台进程组
后台进程中的程序是不会占用终端在shell进程里启动程序时,加上&符号可以指定程序运行在后台进程组里面
前台切换到后台
ctrl+z
jobs:查看有哪些后台进程组
fg+job id 可以把后台进程组切换为前台进程组
终端:
1、物理终端:串口终端,lcd终端
2、伪终端:ssh远程连接产生的终端、桌面系统启动的终端
3、虚拟终端:Linux内核自带的,ctrl+alt+f6可以打开7个虚拟终端
守护进程
会话用来管理前后台进程组,会话一般关联着一个终端当终端被关闭了之后,会话中的所有进程都会被关掉
守护进程不受终端影响。就算终退出,也可以继续在后台运行
如何来写一个守护进程
1.创建一个子进程,父进程直接退出
方法通过fork()函数
2.创建一个新的会话,摆脱终端的影响
方法通过setsid函数
3.改变守护进程的当前工作目录,改为"/"
方法通过chrdir()函数
4.重设文件权限掩码
新建文件的权限受文件权限掩码影响
022:只写
新建文件默认执行权限:666
真正的文件执行权限:666&(~umask)
方法:通过umask
5.关闭不需要的文件描述符
0,1,2:标准输入、输出、错误
// 函数作用就是把调用该函数的进程变成一个守护进程
void create_daemon(void){
pid_t pid = 0;
pid = fork();
if (pid 0)
{
perror("fork");
exit(-1);
}
if (pid > 0)
{
exit(0); // 父进程直接退出
}
// 执行到这里就是子进程
// setsid将当前进程设置为一个新的会话期session,目的就是让当前进程
// 脱离控制台。
pid = setsid();
if (pid 0)
{
perror("setsid");
exit(-1);
}
// 将当前进程工作目录设置为根目录
chdir("/");
// umask设置为0确保将来进程有最大的文件操作权限
umask(0);
// 关闭所有文件描述符
// 先要获取当前系统中所允许打开的最大文件描述符数目
int cnt = sysconf(_SC_OPEN_MAX);
int i = 0;
for (i=0; i {
close(i);
}
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
}
普通进程伪装成守护进程
nohup
ps命令:
如:ps axjf | grep "程序名"
aux
axjf
a:显示一个终端的所有进程
u:显示进程的归属用户及内存使用情况
x:显示没有关联控制终端的进程
j:显示进程归属进程组的id、会话id、父进程id
f:以ascii码形式显示出进程的层次关系
ps aux
user:进程是哪个用户产生的
pid:进程的身份证号码
%cpu:表示进程占用了cpu计算能力的百分比
%mem:表示进程占用了系统内存的百分比
vsz:进程使用的虚拟内存大小
rss:进程使用的物理内存大小
tty:表示进程关联的终端
stat:表示进程当前状态
start:表示进程的启动时间
time:记录进程运行的时间
command:表示进程执行的具体程序
常见的状态有以下几种
-D:不可被唤醒的睡眠状态,通常用于I/O情况。
-R:该进程正在运行。
-S:该进程处于睡眠状态,可被唤醒
-T:停止状态,可能是在后台暂停或进程处于除错状态。
-X:死掉的进程。
-Z:僵尸状态。
-N:低优先级。
-s:进程是会话首进程。
-l:多线程(小写L)。
-+:位于后台。
进程的5种状态
(1)就绪态。这个进程当前所有运行条件就绪,只要得到了CPU时间就能直接运行。
(2)运行态。就绪态时得到了CPU就进入运行态开始运行。
(3)僵尸态。进程已经结束但是父进程还没来得及回收
(4)等待态(浅度睡眠&深度睡眠),进程在等待某种条件,条件成熟后可进入就绪态。等待态下就算你给他CPU调度进程也无法执行。浅度睡眠等待时进程可以被(信号)唤醒,而深度睡眠等待时不能被唤醒只能等待的条件到了才能结束睡眠状态。
(5)暂停态。暂停并不是进程的终止,只是被被人(信号)暂停了,还可以回复的。
ps axjf
ppid:表示进程的父进程id
pid:进程的身份证号码
pgid:进程所在组的id
sid:进程所在会话的id
tty:表示进程关联的终端
tpgid:值为-1,表示进程为守护进程
stat:表示进程当前状态
uid:启动进程的用户id
time:记录进程运行的时间
command:表示进程的层次关系
使用场景:
关注进程本身:ps aux
关注进程间的关系:ps axjf
linux内核提供多种进程间通信机制
(1)无名管道和有名管道
(2)SystemV IPC:信号量、消息队列、共享内存
(3)Socket域套接字
(4)信号
linux的IPC机制-管道
管道(无名管道)
(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
SystemV IPC介绍
1、SystemV IPC的基本特点
(1)系统通过一些专用API来提供SystemV IPC功能
(2)分为:信号量、消息队列、共享内存
(3)其实质也是内核提供的公共内存
2、消息队列
(1)本质上是一个队列,队列可以理解为(内核维护的一个)FIFO
(2)工作时A和B2个进程进行通信,A向队列中放入消息,B从队列中读出消息。
3、信号量
(1)实质就是个计数器(其实就是一个可以用来计数的变量,可以理解为int a)
(2)通过计数值来提供互斥和同步
4、共享内存
(1)大片内存直接映射
(2)类似于LCD显示时的显存用法
使用syslog来记录调试信息
(1)一般log信息都在操作系统的/var/log/messages这个文件中存储着,但是ubuntu中是在/var/log/syslog文件中的。
syslog的工作原理
(1)操作系统中有一个守护进程syslogd(开机运行,关机时才结束),这个守护进程syslogd负责进行日志文件的写入和维护。
(2)syslogd是独立于我们任意一个进程而运行的。我们当前进程和syslogd进程本来是没有任何关系的,但是我们当前进程可以通过调用openlog打开一个和syslogd相连接的通道,然后通过syslog向syslogd发消息,然后由syslogd来将其写入到日志文件系统中。
(3)syslogd其实就是一个日志文件系统的服务器进程,提供日志服务。任何需要写日志的进程都可以通过openlog/syslog/closelog这三个函数来利用syslogd提供的日志服务。这就是操作系统的服务式的设计。
#include
#include
#include
#include
int main(void){
printf("my pid = %d.\n", getpid());
openlog("b.out", LOG_PID | LOG_CONS, LOG_USER);
syslog(LOG_INFO, "this is my log info.%d", 23);
syslog(LOG_INFO, "this is another log info.");
syslog(LOG_INFO, "this is 3th log info.");
closelog();
}