linux调度单元,Linux高级环境编程之7执行单元_进程管理

课程目标

(1) 掌握进程的基本概念,进程属性获取。

(2) 掌握进程的生命周期以及资源申请与释放的过程。

(3) 掌握创建新进程时父子进程资源的管理。

(4) 掌握守候进程等特殊进程的管理。

(5)多进程在编程中的应用。

主要知识点

(1) 程序,进程,进程资源。

(2) 进程的生命周期,进程状态。

(3) 进程创建与父子进程资源。

(4)进程属性管理与进程应用。

进程是unix/Linux中基本的资源管理单元。

进程又是执行的代码片段(一个进程可能创建多个进程,一个进程可以执行多个程序的代码)。将一个代码片段加载到内存并让其执行也就是创建一个(或多个)进程。

进程与程序的关系:程序存储在磁盘上,是一个文件;而进程是一个加载到内存执行的程序段,且有生命周期,创建、执行、退出、等待的状态

一个进程不仅仅占用了加载代码的内存(用户空间),在Linux下,使用task_struct这个结构体来维护整个进程的资源(内核空间)

在内核中task_struct完整的描述了一个进程的所有信息:

volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 描述了状态信息

struct mm_struct *mm, *active_mm; 与内存存储相关信息,描述了在用户空间中的代码段,数据段,堆, 栈, mmap等涉及的所有内存信息

/* open file information */

struct files_struct *files; 打开文件的信息列表,成员fd_array[NR_OPEN_DEFAULT]就是描述了这个进程打开的文件列表。

/* signal handlers */

struct signal_struct *signal; 信号的描述信息

struct sighand_struct *sighand; 信号的描述信息

应用编程时产看进程ps aux

用户空间的进程属性:

PID:进程号,每个进程有唯一的编号

状态:如下描述

Linux系统是一个多任务多用户的系统。为什么会用多进程,意义何在?

现在CPU的速度非常快,而人的反映时间时微秒级,系统的外设的速度的速度也相对比较慢。如果使用单进程任务系统,则CPU有大量空闲。因此,我们让CPU在各个执行单元中不停的调转。当某个进程需要等待其他的资源时,CPU转而执行其它的进程,充分利用了CPU资源。在什么时候执行哪一个进程实质是由调度算法来决定的。

CPU只有一个,而进程有多个,因此某个时刻只能执行一个进程,其他的进程则处于相应的其他状态:

运行状态:占有资源,执行;

就绪状态:除了CPU资源外,其他资源都已获取,等待调度算法来执行;

等待状态:除了CPU资源外,还需要等待其他资源或时间,分为可中断等待(可以被信号打断)和不可中断等待

停止状态:正在被跟踪或者调试的进程

僵死状态:用户资源已经收回,PCB内存资源没有收回,已经不能执行

怎么来划分多个进程呢?

进程是资源管理的基本单元,创建进程时,一个比较独立的任务(事务)创建一个进程,这个事务尽量不与其他进程有太多的耦合性。

2. 进程管理应用及资源

(1)进程创建:进程创建于进程资源获取。fork/vfork

在进程创建过程中:

用户空间中:将程序的代码段,数据段,BBS段,从磁盘加载到内存,并且申请堆栈空间;

内核空间中:为这个进程分配唯一的PID标识,同时在内核中为这个进程申请进程控制块PCB,初始化相关信息

在运行的过程中,涉及打开的文件,关联的终端,安装信号,状态等系列信息

在创建进程时,由父进程来创建子进程

#include

pid_t fork(void);

在父进程中,返回子进程的ID,在子进程中返回,返回0

(2)进程中执行新的代码

在进程执行过程中,要执行新代码,实际上是创建了一个新进程,更多的是期望在这个进程中执行新的代码,而不是原来的程序代码。使用exec相关的函数可以在子进程中,替换原有进程的代码段和数据段(用户空间的信息),转而执行新代码。

#include

extern char **environ;

int execl(const char *path, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

int execle(const char *path, const char *arg,

..., char * const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

参数说明:

path:可执行程序的路径

arg:参数列表(以NULL结束)

file:文件名(要求系统在$PATH环境变量所列路径下搜索)

argv[]:执行这个程序的参数列表字符串指针数组

用法参考:

execl("/bin/ls", "ls", "-l", NULL);

execlp("ls", "ls", "-l", NULL);

char *const argv[] = {"ls", "-l", NULL};

execv("/bin/ls", argv);

(3)进程退出

进程在以下几个情况会退出:

使用kill函数 kill -9 pid:强制退出

显示的执行exit系列函数

NAME

_exit, _Exit - terminate the calling process

SYNOPSIS

#include

void _exit(int status);

#include

void _Exit(int status);

NAME

exit - cause normal process termination

SYNOPSIS

#include

void exit(int status);

exit()与_exit()的区别:

exit()在退出时会对进程资源做清理,例如刷新流流的缓冲区;

_exit()不做任何处理,直接退出。

进程遇到main函数的return或者遇到}没有代码可执行时退出

exit与return的区别:

exit是一个系统函数,退出这个进程;

return是C/C++关键字,退出这个函数。

在退出时,我们可以在退出前注册退出时执行的代码

NAME

atexit - register a function to be called at normal process termination

SYNOPSIS

#include

int atexit(void (*function)(void));

NAME

on_exit - register a function to be called at normal process termination

SYNOPSIS

#include

int on_exit(void (*function)(int , void *), void *arg);

注册回调函数,即执行exit()或者正常退出时去执行相应的回调函数代码,实际上是提供一个功能,在退出进程时完成相应的进程资源清理工作,相当于C++中的析构。

(4)进程资源回收

进程退出有退出的状态,且进程资源的回收在exit的时候仅仅释放了它的用户空间资源,而内核空间资源PCB没有回收,转而由它的父进程通过wait相关的函数来回收。

子进程在退出时会给父亲进程发出一个信号(SIGCHLD),父进程可以显示的调用wait/waitpid等待子进程结束并回收资源,回收资源时,也可以得到子进程退出的状态。

NAME

wait, waitpid, waitid - wait for process to change state

SYNOPSIS

#include

#include

pid_t wait(int *status);

参数status用来存储子进程退出状态,返回值为退出的子进程PID,

这个函数以阻塞的方式等待某个进程退出,当进程退出后,此函数返回。

pid_t waitpid(pid_t pid, int *status, int options);

此函数为指定等待某个进程或者某些进程,其中第一个参数可以为以下值:

< -1 meaning wait for any child process whose process group ID is equal to the absolute value of pid.

-1 meaning wait for any child process.

0 meaning wait for any child process whose process group ID is equal to that of the calling process.

> 0 meaning wait for the child whose process ID is equal to the value of pid.

第二个参数status用来存储子进程退出状态

第三个参数options一般为0

使用示例:

#include

#include

#include

#include

#include

int main()

{

pid_t pid = fork();

if( pid < 0 )

{

perror("fork");

}

else if( pid == 0 )

{

sleep(1);

printf("child ID = %d\n", getpid());

exit(10);

}

else

{

int stat;

pid_t w_pid;

// w_pid = wait(&stat);

w_pid = waitpid(-1, &stat, 0);

printf("status = %d, wait_pid = %d\n", stat>>8, w_pid);

}

return 0;

}

总结资源申请与释放的问题:

创建时,申请进程的所有资源。内核空间中的PCB,用户空间中的加载了代码段,数据段,BSS段,申请了堆栈空间,打开的文件,安装的信号,关联的终端等;

执行时,替代代码段,数据段,BSS段,堆栈空间等用户空间信息,但内核PCB的信号没有修改;

退出时,释放了自己用户空间的资源

回收时,回收内核空间资源。

两个重要的概念:

僵死进程:进程已经退出,但是内核空间资源没有回收的进程

孤儿进程:父进程先于子进程退出,这样的子进程就是孤儿进程,其父进程会被转移到init(pid=1)进程

3. 进程创建详解与父子进程资源

(1)父子执行顺序问题

父子进程在创建完子进程后互不相关联,以独立身份抢占CPU资源,具体谁先执行由调度算法决定,用户空间没办法干预。子进程执行代码的位置是fork/vfork函数返回的位置。

(2)子进程资源申请问题

子进程重新申请新的物理内存空间,复制父进程地址空间所有的信息(现在的操作系统实际采用写时复制等策略,真正的物理内存空间发生在需要写入时)

子进程在用户空间中复制父进程的代码段,数据段,BSS段,堆,栈所有的信息,在内核空间中操作系统为其重新申请一个PCB,并且使用父进程的PCB来初始化,除了pid特殊信息外,几乎所有的信息都是一样的。

父子进程中资源申请问题

#include

#include

#include

#include

#include

int glob = 100;

int main()

{

pid_t pid = fork();

int num = 20;

if( pid < 0 )

{

perror("fork");

exit(EXIT_FAILURE);

}

else if( pid == 0 )

{

num = 1000;

glob = 2000;

printf("child process: num = %d, glob =%d\n", num, glob); // child process: num = 1000, glob =2000

printf("child process: &num = %p, &glob =%p\n", &num, &glob); // child process: &num = 0xbfe94ff8, &glob =0x804a028

}

else

{

sleep(1);

printf("parent process: num = %d, glob =%d\n", num, glob); // parent process: num = 20, glob =100

printf("parent process: &num = %p, &glob =%p\n", &num, &glob); // parent process: &num = 0xbfe94ff8, &glob =0x804a028

}

return 0;

}

观察输出结果:

在子进程中先修改变量的值,并不影响父进程,明数据段,栈(当然也包括其它用户空间内存),子进程是申请新的物理空间;

但从打印的地址来看,父子进程中的变量地址为同一个地址,这是为什么?

这里打印的是虚拟地址,而不是物理地址编号;两个进程的虚拟地址空间是没有任何联系的。

父子进程中文件流的缓冲区状态

#include

#include

#include

#include

#include

int main()

{

printf("hello\nworld");

fork();

printf("bye\n");

return 0;

}

===============

输出结果:

hello

worldbye

worldbye

流的缓冲区会缓存没有刷新的信息,且缓冲区在用户空间中,虽然子进程创建后从fork返回处执行,但缓冲区被子进程复制了一份,这里存储在缓冲区中的world也被复制了一份,因此,输出了两份world。

#include

#include

#include

#include

#include

int main()

{

for(int i=0; i<2; i++)

{

fork();

printf("*");

}

return 0;

}

输出结果:

********

dafdc6105479

输出过程详解

4.子进程创建或执行execX后,对打开的文件的操作方式

根据前面所学的内容,在同一个进程中,两次打开(使用open)同一个文件(只要没有对文件上锁),分别写入文件会存在覆盖的情况。

而使用fcntl/dup复制文件描述,分别使用这两个文件描述符写文件并不会出现覆盖,而是交叉写入。

原因:两次open打开实际上在内核中创建了两个互不相关的文件表项(struct file),也就记录了两个读写位置。而复制文件描述符则在内核中使用同一个文件表项,因此,共用以一个读写位置。

在子进程中是如何操作父进程中打开的文件?

#include

#include

#include

#include

#include

#include

int main()

{

int fd;

int fd2;

fd = open("test.txt", O_CREAT|O_RDWR);

if( fd < 0 )

{

perror("open");

}

pid_t pid = fork();

if( pid == -1 )

{

perror("fork");

exit(EXIT_FAILURE);

}

else if(pid == 0)

{

write(fd, "helloworld", 10);

}

else

{

sleep(1);

write(fd, "abc", 3);

}

close(fd);

return 0;

}

输出结果:

helloworldabc

通过以上代码测试,父子进程共享一个文件表项(file struct),也就是共用一个读写位置。

dafdc6105479

操作文件时,父子进程共用读写位置。

在execX系列函数替换代码后,对打开的文件能够再处理吗?

默认情况下,execX执行的代码可以访问在原来代码中打开的文件,操作是同一个文件描述符,即用一个文件对象。

fcntl(fd,F_SETFD,FD_CLOEXEC);

语句用来使在execX之前打开的文件描述符在新的代码中不可用。

5. 进程属性获取与修改

进程的属性包括进程组属性和用户属性

(1)进程组属性决定了进程中运行过程中控制权限以及相关控制信息

PID:进程号。当前进程在当前系统下唯一的编号,针对这个进程执行的操作多以进程号为标识:例如,等待某个子进程结束waitpid;向某个进程发送信号。用户可以获取getpid(),但不能通过函数修改进程号的值。

PPID:父进程号。一般为创建这个进程的那个进程的ID,一般也不会修改,当这些进程的父进程退出后,当前进程变成孤儿进程,它的PPID会被修改成init进程,即PID=1的这个进程。

PGID:进程组号。将完成协同工作的多个进程默认为一个进程组。例如,在终端运行一个新的程序,新的程序创建的子进程以及自身在一个进程组下,而第一个进程默认为进程组长,进程组号也是进程组长的ID。PGID可以被获取和修改,getpgid(pid_t pid):参数为某个进程的ID,返回该进程的进程组长编号;setpgid(pid_t pid, pid_t pgid):修改某个进程的进程组长。

修改一个进程的进程组号的意义:kill可以向一个进程组发起信号,要影响整个这个进程组的所有进程。

SID:会话ID(session ID)。会话:进行交互。一般,在某个终端下执行的程序所创建的进程/进程组,它们的SID就是这个终端的编号。在一个会话下的所有进程都受到这个会话终端的影响。

getsid(pid):获取某个进程的会话ID。

setsid():设置某个进程为会话组长,要求这个进程不能说进程组长。一般在创建守候进程时会修改SID,避免原来关联进程的终端信号影响子进程。

终端

一个进程可以与某个终端关联,建立与控制终端关联的这个会话首进程为控制进程。

一个会话中的多个进程组可以分为一个前台和多个后台。

在终端下执行键盘命令ctrl+c等,会将信号发送给前台进程组所有进程。

NAME

tcgetpgrp, tcsetpgrp - get and set terminal foreground process group

SYNOPSIS

#include

pid_t tcgetpgrp(int fd);

int tcsetpgrp(int fd, pid_t pgrp);

(2) 用户属性决定了进程在运行时对其他资源的访问权限,如对文件的读写权限

uid/ruid:创建这个进程的用户的ID。例如用户ID为500,uid就是为500

gid/rgid:创建这个进程的用户所在组的id。

EUID:有效用户ID,一般同uid。

EGID:有效用户组ID,一般同gid。

一般对文件真正的访问权限由EUID和EGID决定,当EUID和EGID仅仅是在这个可执行程序的setuid位和setgid位被设置时,相应的EUID和EGID将与执行这个进程的UID/GID不同,上升到了这个可执行程序的setuid用户。

如:

delphi@delphi-vm:~/code/linux_coding$ ll /usr/bin/passwd

-rwsr-xr-x 1 root root 37100 2011-02-15 06:12 /usr/bin/passwd*

passwd的可执行程序setuid被置位,普通用户执行这个程序时,UID是普通用户ID,

但EUID上升到了这个文件的拥有者root,即这个进程对文件的访问权限为root用户的权限。

因此可以修改/etc/passwd这个文件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值