我们要了解Liunx内核提供的进程、管道、信号等内核对象,了解这些内核对象的应用将有助于我们更好地了解Linux内核究竟为应用层提供了什么特性,也有且于后续编写更底层的驱动程序。
1.简单了解进程
ps -aux
3.3父进程与子进程
进程启动时,启动进程是新进程的父进程,新进程是启动进程的子进程。
4.程序与进程
程序的概念
程序是一个普通文件,是为了完成特定任务而准备好的指令序列与数据的集合,这些指令和数据中以“可执行映像”的格式保存在磁盘中。正如我们所写的一些代码,经过编译器编译后,就会生成对应的可执行文件,那么这个就是程序,或者称之可执行程序。
进程的概念
进程
程序变成进程
5.总结
6.进程状态
在linux上输入以下命令:
ps -ux
从上图中可以看到进程的状态有比较多种,有些是 S,有些是 Ss,还有些是 Sl、Rl、R+ 等状态,具体是什么含义呢?
7.进程状态转换
8.1 system()进程实验
下面对system函数做一个简单的介绍:
头文件
#include
定义函数
int system(const char * string);
函数说明
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命>令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。
返回值 =-1:出现错误,
返回值 =0:调用成功但是没有出现子进程
返回值>0:成功退出的子进程的id
如果system()在调用/bin/sh时失败则返回127,其他失败原因返回-1。若参数string为空指针(NULL),则返回非零值>。如果system()调用成功则最后会返回执行shell命令后的返回值,但是此返回值也有可能为 system()调用/bin/sh失败所返回的127,因此最好能再检查errno 来确认执行成功。
#include
#include
#include
#include
int main(void)
{
pid_t result;
printf("This is a system demo!\n\n");
/*调用system函数*/
result=system("ls -l");
printf("Done!\n\n");
return result;
}
ls -l &
#include
#include
#include
#include
int main(void)
{
pid_t result;
printf("This is a system demo!\n\n");
result=system("ls -l &");
printf("Done!\n\n");
return result;
}
可以看出来,在 ls 命令还未来得及打印出它的所有输出结果之前,system() 函数就程序就打印出 字符串 Done 然后退出了。在 system() 程序退出后,ls 命令继续完成它的输出。这类的处理行为往往会给用户带来很大的困惑,也不一定如用户所预料的结果一致,因此如果读者想要让进程按 照自己的意愿执行,就需要能够对它们的行为做更细致的控制,接下来讲解其他方式启动新的进 程。
pid_t fork(void);
#include
#include
#include
#include
int main(void)
{
pid_t result;
printf("This is a fork demo!!\n\n");
result = fork();
if(result == -1)
{
printf("fork error!!\n");
}
else if(result == 0)
{
printf("The returned value is %d,In child process!! My PID is %d\n\n",result,getpid());
}
else
{
printf("The returned value is %d,In father process!! My PID is %d\n\n",result,getpid());
}
return result;
}
~
我们来分析一下这段代码:
int execl( const char *path,const char *arg,...)
#include
#include
int main(void)
{
int err;
printf("this is a execl function test demo!\n\n");
err = execl("/bin/ls","ls","-la",NULL);
if(err < 0){
printf("execl fail!\n\n");
}
printf("Done!\n\n");
}
程序先打印出它的第一条消息”this is a execl function test demo!”,接着调用 exec 系列函数(实验中使用 execl() 函数),这个函数在/bin/ls 目录中搜索程序 ls,然后它将会替exec_demo 本身的进程,程序运行结果与在终端中使用以下所示的 shell 命令一样,如下图。
#include
#include
int main(void)
{
pid_t result;
result = fork();
if(result > 0)
{
execlp("ls","ls","-l",NULL);
printf("error!!\r\n");
return -1;
}
return 0;
}
#include
#include
int main(void)
{
pid_t result;
char *arg[]={"ls","-l",NULL};
result = fork();
if(result > 0)
{
execv("/bin/ls",arg);
printf("error!!\r\n");
return -1;
}
return 0;
}
#include
#include
int main(void)
{
pid_t result;
char *arg[]={"env",NULL};
char *env[]={"PATH=/tmp","name=lzf",NULL};
result = fork();
if(result > 0)
{
execve("/usr/bin/env",arg,env);
printf("error!!\r\n");
return -1;
}
return 0;
}
①.l后缀和v后缀两者必须选其一来使用
#include #include
void _exit(int status);void exit(int status);
#include
#include
#include
int main()
{
pid_t result;
result = fork();
if(result == -1)
{
printf("error\n\n");
}
else if(result == 0)
{
printf("son");
_exit(0);
}
else
{
printf("father");
exit(0);
}
}
输出结果:father
从图中可以看出,_exit() 函数的作用最为简单:直接通过系统调用使进程终止运行。当然,在终 止进程的时候会清除这个进程使用的内存空间,并销毁它在内核中的各种数据结构;
而 exit() 函 数则在这些基础上做了一些包装,在执行退出之前加了若干道工序:比如 exit() 函数在调用 exit 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,这就是“清除 I/O 缓冲”。
#include #include
void _exit(int status);void exit(int status);
pid_t wait(int *wstatus);
#include
#include
#include
#include
#include
int main()
{
pid_t result;
int status;
result = fork();
if(result == -1)
printf("error!!\r\n");
if(result == 0)
{
printf("son!!\r\n");
exit(77);
}
else
{
wait(&status);
if(WIFEXITED(status) == 1)
printf("exit value:%d\r\n",WEXITSTATUS(status));
return 0;
}
}
#include
#include
#include
#include
#include
int main()
{
pid_t pid, chil_pid;
int status;
pid = fork();
if(pid < 0){
printf("Error fork\n");
}
else if (pid == 0){
printf("I am a child process!my pid is %d!\n\n",getpid());
sleep(3);
printf("I am about to quit the process!\n\n");
exit(0);
}
else{
child_pid = wait(&status);
if(child_pid == pid){
printf("Get exit child process id:%d\n",child_pid);
printf("Get child exit status:%d\n\n",status);
}else {
printf("Some error occured.\n\n");
}
exit(0);
}
}
pid_t waitpid(pid_t pid, int *wstatus,int options);
进程状态:
- TASK_RUNNING:就绪/运行状态
- TASK_INTERRUPTIBLE:可中断睡眠状态
- TASK_UNINTERRUPTIBLE:不可中断睡眠状态
- TASK_TRACED:调试态
- TASK_STOPPED:暂停状态
- EXIT_ZOMBIE:僵死状态
- EXIT_DEAD:死亡态
14.对进程的管理
进程组、会话、终端
进程组
作用:对同等类型的进程进行管理
进程组的诞生
①在shell里面执行一个应用程序,对于大部分进程来说,自己就是进程组的首进程。进程组只有一个进程
②如果进程调用了fork函数,那么父子进程同属一个进程组,父进程为首进程
③在shell中通过管道执行连接起来的应用程序,两个程序同属于一个进程组,第一个程序为进程组的首进程
进程组id:PGID,由首进程的pid来决定的
会话:SID
作用:管理进程组
会话的诞生
调用setsid函数,新建一个会话,应用程序作为会话的第一个进程,称为会话的第一个进程,称为会话首进程。
用户在终端正确登录之后,启动shell时Linux系统会创建一个新的会话,shell进程作为会话首进程
会话id:会话首进程id,SID
前台进程组
shell进程启动时,默认是前台进程组的首进程,前台进程组的首进程会占用会话所关联的终端来运行,shell启动其他应用程序时,其他应用程序成为首进程。
后台进程组
后台进程中的程序是不会占用终端
在shell进程里启动程序时,加上&符号可以指定程序运行在后台进程组里面
Ctrl+z让前台进程组程序变成后台进程组程序
如何让后台进程组程序变成前台进程组程序
jobs:查看有哪些后台进程组
fg+jobid可以把后台进程组切换为前台进程组
终端
①物理终端
-串口终端
-LCD终端
②伪终端
-ssh远程联接产生的终端
-桌面系统启动的终端
③虚拟终端
Linux内核自带的
Ctrl+alt+f0~f6可以打开7个虚拟终端
14.守护进程
会话管理前后台进程组
会话关联着一个终端
当终端被关闭了之后,会话中的所有进程都会被关掉
守护进程
不受终端影响,就算终端退出,也可以继续在后台运行
如何来写一个守护进程
1.创建一个子进程,父进程直接退出
方法:通过fork()函数
2.创建一个新的会话,摆脱终端的影响
方法:通过setsid()函数
3.改变守护进程的当前工作目录,改为“/”
方法:通过chrdir()函数
4.重设文件权限的掩码
新建文件的权限受文件掩码影响
0002是当前文件权限掩码
002(000 000 010):只写
新建文件的默认执行权限:666(110 110 110)
真正的文件执行权限:666&~umask
重设文件权限的掩码
方法:通过umask()函数
5.关闭不需要的文件描述符
0,1,2:标准输入、输出、出错
方法:通过close()函数关闭文件描述符
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXFILE 3
int main()
{
pid_t pid;
int fd,len,i,num;
char *buf="the dameon is running\n";
len=strlen(buf)+1;
pid =fork();
if(pid<0){
printf("fork fail\n");
}
if(pid>0)
exit(0);
setsid();
chdir("/");
umask(0);
for(i=0;i
sudo vi /var/log/dameon.log
普通进程伪装成守护进程
nohup
ps命令详解
aux
axjf
①a:显示一个终端所有的进程
②u:显示进程的归属用户及内存使用情况
③x:显示没有关联控制终端的进程
④j:显示进程归属的进程组id、会话id,父进程id
⑤f:以ASCII形式显示出进程的层次关系
ps aux
①USER:进程的归属用户
②PID:进程的身份证号码
③%CPU:表示进程占用了CPU计算能力的百分比
④%MEM:表示进程占用了系统内存的百分比
⑤VSZ:进程使用的物理内存大小
⑥TTY:表示进程关联的终端
⑦STAT:表示进程当前状态
⑧START:表示进程启动时间
⑨TIME:记录进程的运行时间
⑩COMMAND:表示进程执行的具体程序
ps axjf
①PPID:表示进程为父进程id
②PID:进程的身份证号码
③PGID:进程所在进程组的id
④SID:进程所在会话的id
⑤TTY:表示进程关联的终端
⑥TPGID:表示进程是否是一个守护进程,值为1,表示进程为守护进程
⑦STAT:表示进程当前状态
⑧UID:启动进程的用户id
⑨TIME:记录进程的运行时间
⑩COMMAND:表示进程的层次关系
使用场景
关注进程本身:ps aux
关注进程间的关系:ps axjf
15.僵尸进程和托孤进程
进程的正常退出步骤:
①子进程调用exit()函数退出
②父进程调用wait()函数为子进程处理其他事情
僵尸进程
子进程退出后,父进程没有调用wait()函数处理身后事,子进程变为僵尸进程
#include
#include
#include
#include
int main()
{
int pid;
if((pid = fork())<0 ){
perror("fail to fork");
return -1;
}
else if(pid == 0){
printf("child exit now.\n");
exit(0);
}
else{
while(1);
}
return 0;
}
z:表示僵尸态
托孤进程
父进程比子进程先退出,子进程变为孤儿进程,Linux系统会把子进程托孤给1号进程(init进程)
15.进程间通信(ipc)
进程间通信
①数据传输
②资源共享
③事件通知
④进程控制
Linux系统下的ipc
①早期Unix系统ipc
-管道
-信号
-fifo(数据传输)
②system-v ipc(贝尔实验室)
-system-v消息队列
-system-v信号量
-system-v共享内存
③socket ipc(BSD)
④posix ipc(IEEE)
-posix消息队列
-posix信号量
-posix共享内存
16.无名管道
pipe函数
头文件:
#include
函数原型:
int pipe(int pipefd[2]);
返回值:
成功:0
失败: -1
特点
①特殊文件(没有名字),无法使用open,但是可以使用close。
②只能通过子进程继承文件描述符的形式来使用
③write和read操作可能会阻塞进程
④所有文件描述符被关闭之后,无名管道被销毁
使用步骤
①父进程调用pipe函数,pipe函数就会创建无名管道
②fork子进程
③close无用端口
④write/read读写端口
⑤close读写端口
#include
#include
#include
#include
#include
#include
#include
#define MAX_DATA_LEN 256
int main()
{
pid_t pid;
int pipe_fd[2];
int status;
char buf[MAX_DATA_LEN];
const char data[] = "Pipe Test Program";
int real_read,real_write;
memset((void*)buf, 0,sizeof(buf));
/* 创建管道 */
if(pipe(pipe_fd) < 0)
{
printf("pipe create error\n");
exit(1);
}
/* 创建一子进程 */
if((pid = fork()) == 0)
{
/* 子进程关闭写描述 */
close(pipe_fd[1]);
/* 子进程读取管道内容 */
if((real_real = read(pipe_fd[0],buf,MAX_DATA_LEN))>0)
{
printf("%d bytes read from the pipes is '%s'\n",real_read,buf);
}
/* 关闭子进程描述符 */
close(pipe_fd[0]);
exit(0);
}
else if (pid > 0)
{
/* 父进程关闭读描述 */
close(pipe_fd[0]);
if((real_write = write(pipe_fd[1],data,strlen(data)))!= -1)
{
printf("Parent write %d bytes : '%s'\n",real_write,data);
}
/* 关闭父进程写描述符 */
close(pipe_fd[1]);
/* 收集子进程退出信息 */
wait(&status);
exit(0);
}
}
17.有名管道
无名管道只能在父子进程之间进行数据传输
mkfifo函数
头文件:
#include
#include
函数原型:
int mkfifo(const char *filename,mode_t mode);
返回值:
成功:0
失败:-1
特点
①有文件名,可以使用open函数打开
②任意进程间数据传输
③write和read操作可能会阻塞进程
④write具有“原子性”
使用步骤
①第一个进程mkfifo有名管道
②open有名管道,write/read数据
③close有名管道
④第二个进程open有名管道,read/write数据
⑤close有名管道
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MYFIFO "/tmp/myfifo"
/* 4096 定义在于limits.h中 */
#define MAX_BUFFER_SIZE PIPE_BUF
/* 参数为即将写入的字符串 */
int main(int argc,char *argv[])
{
char buff[MAX_BUFFER_SIZE];
int fd;
int nread;
/* 判断有名管道是否存在,若尚未创建,则以相应的权限创建 */
if(access(MYFIFO,F_OK) == -1){
if((mkfifo(MYFIFO,0666) < 0)&& (errno != EEXIST)){
printf("Cannot create fifo file\n");
exit(1);
}
}
/* 以只读堵塞方式打开有名管道 */
fd=open(MYFIFO,O_RDONLY);
if(fd == -1){
printf("Open file error\n");
exit(1);
}
/* 循环读取有名管道数据 */
while(1){
memset(buff,0,sizeof(buff));
if((nread = read(fd,buff,MAX_BUFFER_SIZE))>0){
printf("Read '%s' from FIFO\n",buff);
}
}
close(fd);
exit(0);
}
#include
#include
#include
#include
#include
#include
#include
#include
/* 有名管道文件名 */
#define MYFIFO "/tmp/myfile"
/*4096 定义在于limits.h中 */
#define MAX_BUFFER_SIZE PIPE_BUF
/* 参数为即将写入的字符串 */
int main(int argc,char *argv[])
{
int fd;
char buff[MAX_BUFFER_SIZE];
int nwrite;
if(argc <= 1){
printf("Usage: ./fifo_write string\n");
exit(1);
}
/* 填充命令行第一个参数到buff */
sscanf(argv[1],"%s",buff);
/* 以只写阻塞方式打开FIFO管道 */
fd = open(MYFIFO,O_WRONLY);
if( fd == -1){
printf("Open fife file error\n");
exit(1);
}
if((nwrite = write(fd,buff,MAX_BUFFER_SIZE))>0){
printf("Write '%s' to FIFO\n",buff);
}
close(fd);
exit(0);
}
18.信号简介
信号的基本概念
软件模拟中断,进程接受信号后做出相应响应
Linux下有64种信号的类型
怎么产生信号?
硬件
-执行非法指令
-访问非法内存
-驱动程序
-...
软件
控制台:
①Ctrl+c:中断信号
②Ctrl+l:退出信号
③Ctrl+z:停止信号
kill命令
sudo kill -9 3301
程序调用kill()函数
信号的处理方式:
-忽略:进程当信号从来没有发生过
-捕获:进程会调用相应的处理函数,进行相应的处理
-默认:使用系统默认处理方式来处理信号
19.常用信号分析
信号名 信号编号 产生原因 默认处理方式
SIGHUP 1 关闭终端 终止
SIGINT 2 ctrl+c 终止
SIGQUIT 3 ctrl+\ 终止+转储
SIGABRT 6 abort() 终止+转储
SIGPE 8 算术错误 终止
SIGKILL 9 kill -9 pid 终止,不可捕获/忽略
SIGUSR1 10 自定义 忽略
SIGSEGV 11 段错误 终止+转储
SIGUSR2 12 定义 忽略
SIGALRM 14 alarm() 终止
SIGTERM 15 kill pid 终止
SIGCHLD 17 (子)状态变化 忽略
SIGTOP 19 ctrl+z 暂停,不可捕获/忽略
20.signal_kill_raise函数
signal函数(在程序里面设置对信号的处理方式)
头文件:
#include
函数原型:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
参数:
-signum:要设置的信号
-handler:
①SIG_IGN:忽略
②SIG_DFL:默认
③void (*sighandler_t)(int):自定义
返回值:
成功:上一次设置的handler
失败:SIG_ERR
#include
#include
#include
#include
#include
#include
/* 信号处理函数 */
void signal_handler(int sig)
{
printf("\nthis signal number is %d\n",sig);
if(sig == SIGINT){
printf("I have get SIGINT!\n\n");
printf("The signal has been restored to the default processing mode!\n\n");
/* 恢复信号为默认情况 */
signal(SIGINT,SIG_DFL);
}
}
int main(void)
{
printf("\nthis is an alarm test function\n\n");
/* 设置信号处理的回调函数 */
signal(SIGINT,signal_handler);
while(1){
printf("waiting for the SIGINT signal , please enter \"ctrl+c\"...\n");
sleep(1);
}
exit(0);
}
kill函数
头文件:
#include
#include
原型函数:
int kill(pid_t pid,int sig);
参数:
-pid:进程id
-sig:要发送的信号
返回值:
成功:0
失败:-1
raise函数(和kill函数很类似)
头文件:
#include
原型函数:
int raise(int sig);
参数:
sig:发送信号
返回值:
成功:0
失败:非0
#include
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid;
int ret;
if((pid = fork()) < 0){
printf("Fork error\n");
exit(1);
}
if(pid == 0){
printf("Child is waiting for SIGSTOP signal!\n\n");
raise(SIGSTOP);
printf("Child won't run here forever!\n");
exit(0);
}
else{
sleep(3);
if((ret = kill(pid,SIGKILL)) == 0){
printf("Parent kill %d!\n\n",pid);
}
wait(NULL);
printf("parent exit!\n");
exit(0);
}
}
21.信号集处理函数
屏蔽信号集
屏蔽某些信号
-手动
-自动
未处理信号集
信号如果被屏蔽,则记录在未处理信号集中
-非实时信号(1~31),不排队,只留一个
-实时信号(34~64),排队,保留全部
信号集相关API
①int sigemptyset(sigset_t *set);
-将信号集合初始化为0
②int sigfillset(sigset_t *set);
-将信号集合某一位设置为1