栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

Linux C 中的进程

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Linux C 中的进程

文章目录
  • 1. 进程
      • 进程与程序的区别
  • 2. 进程空间问题
    • 2.1 虚拟内存的划分
    • 2.2 虚拟内存和物理内存之间由MMU(内存管理单元)映射管理
    • 2.3 虚拟内存划分
    • 2.4 进程进行IO操作示意图
  • 3. 进程相关概念
    • 3.1 进程号 PID
    • 3.2 Linux 下进程分类
    • 3.3 进程控制块(PCB)
    • 3.4 进程的调度机制
      • 多个进程如何运行
    • 3.5 进程的运行状态
  • 4. 进程相关命令
      • 查看进程,杀掉进程
      • 前后台切换
  • 5. 进程相关函数
    • 5.1 fork() 创建子进程
      • 父子进程调度机制
      • 父子进程用户空间问题
      • 父子进程共享资源问题 / 父子进程同时访问同一文件
      • dup() 复制文件描述符
    • 5.2 getpid() / getppid() 获取当前进程号 / 父进程号
    • 5.3 exit() / _exit() 退出当前进程
        • `return`与`exit()`
        • `exit()`与`_exit()`区别
    • 5.4 atexit() 进程结束后,可以执行的代码。
    • 5.5 wait() / waitpid() 等待子进程退出,回收资源
  • 6. Linux下的特殊进程
    • 6.1 处理僵尸进程
    • 6.2 创建孤儿进程
    • 6.3 创建守护进程
      • 守护进程创建步骤

1. 进程

进程就是一段程序的执行过程。

进程与程序的区别
  1. 程序是静态的,它是保存在磁盘上的指令的有序集合,没有任何执行的概念
  2. 进程是一个动态的概念,它是程序执行的过程,包括进程的创建、调度和消亡
  3. 程序是保存在磁盘上的,而进程是在内存中运行的

2. 进程空间问题

当一个进程运行或者创建时,操作系统会自动为其分配4G的虚拟内存空间(32位操作系统),虚拟内存的使用主要解决了进程间通信问题,保证每一个进程的空间一样,这样在通信的时候,数据交换更加方便。

既然说是分配4G的虚拟内存,那么实际分配时一定不会直接给它4个G的物理内存,而是实际使用多少,就分配多少的物理内存。


2.1 虚拟内存的划分
  • 4G 的虚拟内存分为 1G 内核空间 和 3G 用户空间
  • 1G 的内核空间是 操作系统中所有进程所公有的
  • 3G 的用户空间是进程私有的

进程间通信就是学习如何在内核空间开辟区域。


2.2 虚拟内存和物理内存之间由MMU(内存管理单元)映射管理

MMU(Memory Management Unit)主要用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权、多任务多进程操作系统。


2.3 虚拟内存划分


2.4 进程进行IO操作示意图


3. 进程相关概念
3.1 进程号 PID

在操作系统中,每一个进程都有一个编号,该编号就是进程号,进程号是进程的唯一标识
进程号是操作系统给当前进程随机分配的,非负整数。

特殊的进程号:

  • 0 → 内核进程号,系统运行起来就是内核进程
  • 1 → init进程,它是所有进程的祖先

3.2 Linux 下进程分类
类型解释
交互进程由shell控制和运行,可以在前台运行,也可以在后台运行
批处理进程不属于某一个终端,它被提交到一个队列中,以便顺序执行
守护执行在后台运行,一般在Linux启动时就开始执行,系统关闭时才结束

3.3 进程控制块(PCB)

进程在创建和运行时,会有专门的一个结构体来保存它的信息,这个结构体为task_struct,又将其称之为进程表项或进程控制块,简称PCB

在/usr/src/linux-headers-3.2.0-29-generic-pae/include/linux下的sched.h头文件里面定义了task_struct结构体


3.4 进程的调度机制 多个进程如何运行

时间片轮转,上下文切换


3.5 进程的运行状态
符号状态解释
D等待不可中断的静止
R运行态正在执行中
S睡眠态阻塞状态
T停止态暂停执行,程序运行时,按下ctrl+Z,进程可进入该状态
Z僵尸态不存在但无法消除
W没有足够页可分配
+前台进程前台运行
<高优先级进程优先运行
N低优先级进程
L有内存分页分配并锁在内存中,多线程中出现
s会话组组长

4. 进程相关命令
查看进程,杀掉进程
ps	   #查看当前用户运行的进程
ps aux #查看当前系统中所有的进程,并且可以查看所占内存百分百等信息
ps ajx #查看当前系统中所有的进程,并且可以查看当前进程的父进程的id
top    #动态显示系统中所有的进程
htop   #更加好看的动态显示系统中所有的进程
sudo apt-get install htop #下载htop
nice   #按用户指定的优先级运行进程
renice	#改变正在运行进程的优先级
kill    #向一个进程发送一个信号
kill -9 pid    #将进程号为pid的进程杀死
pstree  #以树形结构显示系统中所有的进程的关系

前后台切换
#将一个进程在后台运行,查看运行状态后面没有+,如果是前台运行,会有+
./a.out  & 

#将挂起的进程(停止态T)在后台执行
bg  

#把后台运行的进程放到前台运行(+),也可以直接把停止态(T)的进程直接唤醒
fg    

#将进程号为pid的进程变为停止态
kill -19 pid  

#将进程号为pid的进程从停止态变为后台进程
kill -18 pid 


5. 进程相关函数
5.1 fork() 创建子进程
#include 
#include 
pid_t fork(void);

功能:
	父进程创建一个子进程
	
参数:
    无
    
返回值:
    成功:
        >0   子进程的进程号,标识父进程的代码区
        0    标识子进程的代码区
        
   失败:-1

fork函数主要用于在一个进程中创建子进程,目的是为了能够在一个程序里面分别独立执行多个任务,相互之间还没有影响

只要执行一次fork(),就会在原有进程基础上再创建一个进程
如果不区分父子进程代码,fork()后所有代码,父子进程都会执行
一般使用fork()都是为了执行不同任务,所以一般都会区分父子进程代码区。

区分父子进程的方法就是判断fork()函数的返回值,在父进程中,fork()返回子进程PID,在子进程中fork()返回0。

父子进程调度机制

父子进程调度机制也是时间片轮转,上下文切换
所以,他们之间执行时,根本没有先后之分。

父子进程用户空间问题

使用fork()函数创建的子进程,会将父进程 (虚拟内存中) 的用户空间完整复制一份,作为子进程 (虚拟内存中) 的用户空间。

父子进程之间的用户空间是完全独立的,他们对各自用户空间的操作,都不会对对方产生任何影响。

这里注意,如果在父进程中已经定义了一个变量a,a的地址为0x123,当子进程创建完后,子进程中也会有变量a,且地址也是0x123,但是他们并不是同一个a,因为他们分别属于不同的用户空间,互不影响。


父子进程共享资源问题 / 父子进程同时访问同一文件

先看一段代码

#include 
#include 
#include 
#include 
#include 

int main(int argc, char const *argv[])
{
    int fd;
    if((fd = open(argv[1], O_RDWR)) == -1)
    {
        perror("open error");
        return -1;
    }
    
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork error");
        return -1;
    }
    else if(pid > 0) //父进程的代码区
    {
        //父进程向文件写入数据
        write(fd, "hello world", 12);
        printf("父: [%d]的offset = %ldn", fd,lseek(fd, 0, SEEK_CUR));
    }
    else //子进程的代码区
    {
        sleep(1);
        //子进程从文件中读取数据
        printf("子: [%d]的offset = %ldn", fd,lseek(fd, 0, SEEK_CUR));
        lseek(fd, 0, SEEK_SET);
        printf("子: 设置[%d]的offset = %ldn", fd,lseek(fd, 0, SEEK_CUR));
        char buf[32] = {0};
        read(fd, buf, 32);
        printf("子:buf = [%s]n", buf);
    }

    while(1){}
    return 0;
}


执行结果

为什么子进程拿到文件描述符的时候,他的偏移量已经大于0了?
简单来看,子进程只拷贝了父进程的用户空间的所有东西,而关于文件的信息的部分,都在内核空间,内核空间的东西都是公用的,所以,子进程拿着复制来的文件描述符进行操作,必然受父进程的影响。

上面的解释也容易让人产生疑虑——既然内核空间的东西都是公用的,那么我对一个文件操作后,文件偏移量增加,如果此时,另一个进程也在操作这个文件,那么他们之间会不会产生影响?答案是不会。因为这两个进程在open()时,虽然都获得了一个文件描述符来表示同一个文件,但是内核为这两个进程创建了两个不同文件结构体struct file(就是上图的文件表项),这个两个文件结构体都指向同一个struct inode,inode节点用来唯一标识一个文件。由此也可断定父子进程间,如果在各自代码块内产生文件描述符,是不会对对方产生影响的(实际也是这样)。

既然如此,那fork()前创建的文件描述符,在父子进程间为什么会互相影响呢?
进程在创建的时候,内核会为其创建一个进程结构体struct task_struct,也就是进程控制块(PCB),保存了进程的各种信息(内核空间是公用的,内核中的进程结构体(进程控制块PCB)只负责各自进程的信息)。这个结构体中有一个struct files_struct *来指向保存该进程所有打开的文件信息的结构体,在files_struct中又有struct file * [32]来保存所有文件描述符对应的文件信息,每创建一个文件描述符,就会创建一个struct file(文件表项)并把地址存在struct file * [32]数组中。在父进程创建子进程时候,内核会为子进程拷贝父进程的所有信息,并作出必要的修改,因为这两个进程都是独立存在的,所以在拷贝结构体时应当都是深拷贝。

至少从现象来看,内核在拷贝struct files_struct *(指针)和struct file *(指针)时,为子进程开辟空间复制了一份struct files_struct(结构体变量)和struct file(结构体变量)并更新struct files_struct *(指针)和struct file *(指针)。在拷贝struct file * [32](结构体指针数组变量)时,只拷贝了数组中的指针值,而没有重新为每个指针指向内容重新开辟空间拷贝。文件描述符就是这个结构体指针数组的下标。当一个新的文件描述符指向一个已存在struct file文件表项时,文件表项中的打开引用计数 f_count就会加一,然后每关一个文件描述符,该f_count 打开引用计数就会减一,如果想要关掉这个文件表项,就需要满足打开引用计数 f_count为0,所以,子进程与父进程使用fork()前创建的文件描述符时,会互相影响偏移量、文件状态标志等,因为他们共用同一文件表项中的内容。如果其中一个进程把文件描述符关掉的话,这个进程的文件描述符表会把对应文件描述符去除,文件表项中的打开引用计数 f_count减一,这个进程将不能用该文件描述符访问原文件。而另外一个进程仍然可以正常使用该文件描述符,因为其对应的文件表项还存在。

在同一进程内,open()同一个文件多次,产生多个文件描述符符,这些文件描述符都对应独立的文件表项,不会互相影响偏移量、文件状态标志等。

总结:

先open()再fork(),子进程的会复制父进程的文件描述符表,但并不会创建新的文件表项,文件描述符对应的文件表项中打开引用计数会加一,此时,父子进程共享同一文件表项,意味着共享文件偏移量、文件状态标志等。关闭文件描述符时,文件表项中打开引用计数会减一,当打开引用计数==0时,文件表项才会关闭。所以父子进程间其中一个关闭文件描述符对另外一个并不影响。

先fork()再open(),父子进程有独立的文件描述符表,各自创建文件描述符时,内核会为他们各自再创建一个文件表项,意味着他们此时,就算打开的是同一个文件,也不会互相影响到对方的偏移量、文件状态等。但是向文件中写内容可会出现被覆盖等异常。

借用大佬的"高清图"

如果对文件描述符创建这个过程不太了解可以看这个文件描述符。

这里参考了Linux文件共享(四)——父进程与子进程之间的文件共享 和 关于文件描述符(file_struct)

扩展:

dup() 复制文件描述符

子进程复制父进程文件描述符的过程,与函数dup()比较类似

#include 
int dup(int oldfd);
int dup2(int oldfd, int newfd);

#define _GNU_SOURCE             
#include               
#include 
int dup3(int oldfd, int newfd, int flags);

#include 
int dup(int oldfd);

功能:
	用一个新的文件描述符,复制已存在的文件描述符
	执行成功后,新的文件描述符和旧的文件描述符可以同时使用,
	他们都引用同一个文件表项,共享文件偏移量和文件状态标志。
	(they refer to the same open file description (see open(2))
	  and thus share file offset and file status flags)
	这两个文件描述符不共享文件描述符标志位中的(close-on-exec flag) 
	该标志是指 在执行 exec 函数时,会关闭 所有有close_on_exec flag的文件描述符。
   (The  two  file  descriptors  do not share file descriptor flags 
	(the close-on-exec flag))

参数:
	oldfd 传要复制的文件描述符

返回值
	成功 新的文件描述符
	失败 -1
		errno 会保存错误码

	dup2 类似dup,会使用指定的文件描述符newfd,来复制旧的文件描述符oldfd
	如果newfd已经被使用,那么执行该函数后,在重新使用newfd之前,
	newfd会静默关闭。关闭文件描述符,和重新使用都是自动执行。
	注意:1. 如果oldfd是无效的,那么会调用失败,newfd也会关闭
		 2. 如果oldfd是有效的,且newfd == oldfd,那么什么也不做返回newfd
	
	dup3 类似dup2,但是可以通过flags传参 O_CLOEXEC 来为文件描述符设置 close_on_exec flag,
	该标志是指 在执行 exec 函数时,会关闭 所有有close_on_exec flag的文件描述符。
	与dup2不同,dup3中,如果oldfd == newfd ,会报错,errno == EINVAL


我们也可以使用fcntl()来实现对已打开文件描述符,设置close-on-exec flag

int fd=open("foo.txt",O_RDONLY);  
int flags = fcntl(fd, F_GETFD);  
flags |= FD_CLOEXEC;  
fcntl(fd, F_SETFD, flags);  

在open()函数中也可以传参O_CLOEXEC[linux 2.6.23以后支持]

open("a", O_RDWR | O_CLOEXEC);

在创建socket也可以

socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);

参考关于fd的close on exec


5.2 getpid() / getppid() 获取当前进程号 / 父进程号
#include 
#include 

获取当前进程的进程号
pid_t getpid(void);

获取当前进程的父进程的进程号
pid_t getppid(void);

5.3 exit() / _exit() 退出当前进程
#include 
void exit(int status);

	功能:
		退出当前进程
	参数: 
		status 当前进程退出的状态值
	返回值:
		无
	
#include 
void _exit(int status);
	功能:
		退出当前进程
	参数:
	    status 当前进程退出的状态值,
	    
	    可以返回给父进程(父进程可以通
	    过wait函数获取这个值
	    
	    一般不需要将这个值返回给父进程,
	    所以一般设置为0表示成功
	    
	    退出,非0表示错误退出
	    
	返回值:无
return与exit()

return在主函数中执行,会退出主函数,并结束进程,但在子函数中执行,会退出子函数,但不会结束整个进程。
exit()不管在哪里执行都会退出整个进程。

exit()与_exit()区别

exit()是库函数,会刷新缓冲区
_exit()是系统调用,不会刷新缓冲区,相当于直接kill进程


5.4 atexit() 进程结束后,可以执行的代码。
#include 
int atexit(void (*function)(void));

功能:
	他在普通进程结束或者主函数return后
	被执行。这个函数可能会被注册很多次,
	他的执行顺序和注册顺序相反,每次注
	册都会被调用一次。
	
	fork()的子进程也会继承该函数的注册,
	如果用exec()函数,这些注册信息就会
	被清除。

参数:
	void (*function)(void) 函数指针
	如果不传参的话,会被忽略

返回值:
	成功 0
	失败 非0

当使用_exit()(系统调用)退出进程时,atexit()也不会执行。

示例代码

#include 
#include 
#include 
int b = 100;

void myexit()
{
    printf("这是进程结束后执行的最后一个代码n");
}
void myexit2(){
	puts("程序结束后,我又调用了一次");
}
void myexit3(){
	printf("程序结束了,但是还想打印一个变量[%d]n",b);
}

void myfun()
{
    printf("nihao beijing");
    exit(0);
	//不刷新缓冲区,也不会执行atexit()
    //_exit(0);

    printf("hahahahahahahan");
}

int main(int argc, char const *argv[])
{
    //当进程结束(非_exit())之后,还是可以执行代码的,
    //所执行的代码是atexit的回调函数
	//先注册的后执行,后注册的先执行
    atexit(myexit);
	atexit(myexit2);
    atexit(myexit3);
    printf("hello worldn");
	myfun();

    return 0;
}


5.5 wait() / waitpid() 等待子进程退出,回收资源
#include 
#include 
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:
	阻塞等待子进程的状态(子进程退出状态)改变

参数:
    pid:指定要等待的子进程
        <-1   只阻塞等待进程组id等于这个值的绝对值的组中任意一个子进程
        -1    阻塞等待所有子进程的退出状态
        0    阻塞等待进程组id等于当前进程的进程号的组中任意一个子进程
        >0   阻塞等待子进程的进程号等于这个值的子进程
    wstatus:保存子进程状态改变值
    options:选项
        0        阻塞
        WNOHANG  非阻塞   
          
返回值:
    成功:退出的子进程的进程号
    失败:-1
    
wait(NULL) <==> waitpid(-1, NULL, 0);

6. Linux下的特殊进程
特殊进程分类含义
僵尸进程子进程结束,父进程没有结束,并且此时父进程没有将子进程的资源释放,此时子进程就是僵尸进程
孤儿进程父进程结束,而子进程没有结束,此时子进程的父进程变为init进程,这个子进程称为孤儿进程
守护进程Daemon进程,Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端,并且周期性的执行某种任务,或等待处理某些发生的事件
6.1 处理僵尸进程
  1. 父进程结束,此时僵尸进程会变成孤儿进程,其父进程变为init进程,那么他的资源就会被init回收。
  2. 使用wait()函数,阻塞等待子进程退出,释放僵尸进程的资源。缺点,wait()是阻塞函数,父进程只有等待wait()处理完僵尸进程,才能执行下一步操作。
  3. 使用waitpid()设置非阻塞来处理僵尸进程,但是必须循环判断,如果没有循环,一样没用。
  4. 比较好的方式,使用信号来处理僵尸进程。

示例代码

#include 
#include 
#include 
#include 
#include 

int main(int argc, char const *argv[])
{
    pid_t pid;
    if((pid = fork()) == -1)
    {
        perror("fork error");
        exit(1);
    }
    else if(pid > 0) //父进程
    {
        printf("父进程正在执行...n");
        //wait(NULL);
        for(;;)
        {
            waitpid(-1, NULL, WNOHANG);
            printf("hello worldn");
            sleep(1);
        }
    }
    else //子进程
    {
        printf("子进程正在执行...n");

        sleep(10);

        printf("子进程退出了n");
        exit(0);
    }

    return 0;
}

6.2 创建孤儿进程
#include 
#include 
#include 
#include 
#include 

int main(int argc, char const *argv[])
{
    pid_t pid;
    if((pid = fork()) == -1)
    {
        perror("fork error");
        exit(1);
    }
    else if(pid > 0) //父进程
    {
        int ret;
        printf("父进程正在执行...n");

        sleep(10);

        printf("父进程退出了n");
        exit(0);
    }
    else //子进程
    {
        printf("子进程正在执行...n");
        for(;;)
        {   
            printf("pid = %d, ppid = %dn", getpid(), getppid());
            printf("hello worldn");
            sleep(1);
        }
    }

    return 0;
}

6.3 创建守护进程

守护进程常常在系统启动时开始运行,在系统关闭时终止

Linux系统有很多守护进程,大多数服务都是用守护进程实现的

在Linux中,每一个系统与用户进行交流的界面称为终端。从该终端开始运行的进程都会依附于这个终端,这个终端称为这些进程的控制终端。当控制终端被关闭时,相应的进程都会被自动关闭。

守护进程能够突破这种限制,它从开始运行,直到整个系统关闭才会退出。如果想让某个进程不会因为用户或终端的变化而受到影响,就必须把这个进程变成一个守护进程。

守护进程创建步骤


第一步:将子进程变成孤儿进程,目的是为了不受终端影响

第二步:将子进程设置为会话组组长,目的是为了不受其他进程影响

第三步:将子进程的工作目录改为根目录,目的是为了不受工作目录影响

第四步:将子进程对文件操作的权限修改为最高,目的是为了不受文件权限的影响

第五步:将子进程中所有打开的文件描述符关闭

示例代码

#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char const *argv[])
{
    //第一步:将子进程变成孤儿进程,目的是为了不受终端影响
    pid_t pid;
    if((pid = fork()) == -1)
    {
        perror("fork error");
        exit(1);
    }
    else if(pid > 0)
    {
        exit(0);
    }

    printf("pid = %dn", getpid());
    //第二步:将子进程设置为会话组组长,目的是为了不受其他进程影响
    setsid();

    //第三步:将子进程的工作目录改为根目录,目的是为了不受工作目录影响
    chdir("/");

    //第四步:将子进程对文件操作的权限修改为最高,目的是为了不受文件权限的影响
    umask(0);
    
    //第五步:将子进程中所有打开的文件描述符关闭
    int maxfd = getdtablesize();
    int i;
    for(i = 0; i <= maxfd; i++)
    {
        close(i);
    }

    //守护进程执行服务
    int fd = open("file.txt", O_WRonLY | O_CREAT | O_TRUNC, 0664);
    if(fd == -1)
    {
        perror("fail to open");
        exit(1);
    }
    while(1)
    {
        write(fd, "hello worldn", 12);
        
        sleep(1);
    }

    return 0;
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/297463.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号