进程

进程详细讲解(代码示例)

  • 进程
    • 示例代码
    • 创建进程的具体过程?
    • 执行 fork()的时候系统做了什么?
    • 进程间通信
      • 管道
      • 消息队列
      • 共享内存
      • 信号量
      • 套接字
    • 进程间同步
      • 信号量
      • 文件锁
      • 无锁 CAS
      • 校验方式(CRC32校验)

示例代码

// C
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <string>
using namespace std; int main()
{ pid_t pid; char *msg; int k; pid=fork(); switch(pid){ //子进程执行部分 case 0: msg="Child process is running.\n"; k=3; break; case -1: perror("Process creation failed.\n"); break; //父进程执行部分 default: msg="Parent process is running.\n"; k=5; break; } //父子进程共同执行部分 while(k>0){ puts(msg); sleep(1); k--; }
} /*
Parent process is running.
Child process is running.
Parent process is running.
Child process is running.
Parent process is running.
Child process is running.
Parent process is running.
Parent process is running. */

创建进程的具体过程?

通过系统调用 fork(),可创建新进程。新进程的地址空间复制了原来进程的地址空间。这种机制允许父进程与子进程轻松通信。这两个进程(父和子)都继续执行处于系统调用 fork() 之后的指令,但有一点不同:对于新(子)进程,系统调用 fork() 的返回值为 0;而对于父进程,返回值为子进程的进程标识符(非零)。

通常,在系统调用 fork() 之后,有个进程使用系统调用 exec(),以用新程序来取代进程的内存空间。此时,子进程的地址空间覆盖,就不会继续执行从父进程继承的相关指令。

执行 fork()的时候系统做了什么?

vfork最初是因为fork没有实现COW机制,而很多情况下fork之后会紧接着exec,而exec的执行相当于之前fork复制的空间全部变成了无用功,所以设计了vfork。

fork和vfork的区别

(1)fork:子进程拷贝父进程的代码段和数据段

vfork:子进程和父进程共享代码段和数据段

(2)fork中父子进程的先后运行次序不定

vfork:保证子进程先运行,子进程exit后父进程才开始被调度运行

(3) vfork ()保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在 调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

(4)就算fork实现了写时拷贝,但其效率仍然没有vfork高,但是vfork在一般平台上都存在问题,所以一般不推荐使用

进程间通信

进程间通信方法:管道、消息队列、共享内存、信号量、套接口。

管道

它的发明人是道格拉斯.麦克罗伊,这位也是UNIX上早期shell的发明人。

无名管道和有名管道两种,前者用于父进程和子进程间的通信,后者用于运行于同一台机器上的任意两个进程间的通信。

无名管道由pipe()函数创建:

#include <unistd.h>
int pipe(int filedis[2]);

参数filedis返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。

// C++
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>#define STRING "hello world!"int main()
{int pipefd[2];pid_t pid;char buf[BUFSIZ];if (pipe(pipefd) == -1) {perror("pipe()");exit(1);}pid = fork();if (pid == -1) {perror("fork()");exit(1);}if (pid == 0) {/* this is child. */printf("Child pid is: %d\n", getpid());if (read(pipefd[0], buf, BUFSIZ) < 0) {perror("write()");exit(1);}printf("%s\n", buf);bzero(buf, BUFSIZ);snprintf(buf, BUFSIZ, "Message from child: My pid is: %d", getpid());if (write(pipefd[1], buf, strlen(buf)) < 0) {perror("write()");exit(1);}} else {/* this is parent */printf("Parent pid is: %d\n", getpid());snprintf(buf, BUFSIZ, "Message from parent: My pid is: %d", getpid());if (write(pipefd[1], buf, strlen(buf)) < 0) {perror("write()");exit(1);}sleep(1);bzero(buf, BUFSIZ);if (read(pipefd[0], buf, BUFSIZ) < 0) {perror("write()");exit(1);}printf("%s\n", buf);wait(NULL);}exit(0);
}

https://zhuanlan.zhihu.com/p/58489873

Linux系统下,有名管道可由两种方式创建:命令行方式mknod系统调用和函数mkfifo。下面的两种途径都在当前目录下生成了一个名为myfifo的有名管道:

方式一:mkfifo(“myfifo”,“rw”);

方式二:mknod myfifo p

生成了有名管道后,就可以使用一般的文件I/O函数如open、close、read、write等来对它进行操作。

消息队列

TODO

共享内存

共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取错做读出,从而实现了进程间的通信。

采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,对于像管道和消息队里等通信方式,则需要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。

特点:

  • 共享内存是进程间共享数据的一种最快的方法。
    一个进程向共享的内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。

  • 使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥。
    若一个进程正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据。

信号量

含义

信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和释放(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互。内核也可以利用信号来通知用户空间的进程来通知用户空间发生了哪些系统事件。

信号事件有两个来源:

  • 硬件来源,例如按下了cltr+C,通常产生中断信号sigint
  • 软件来源,例如使用系统调用或者命令发出信号。最常用的发送信号的系统函数是kill,raise,setitimer,sigation,sigqueue函数。软件来源还包括一些非法运算等操作。

一旦有信号产生,用户进程对信号产生的相应有三种方式:

  • 执行默认操作,linux对每种信号都规定了默认操作。
  • 捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数。
  • 忽略信号,当不希望接收到的信号对进程的执行产生影响,而让进程继续执行时,可以忽略该信号,即不对信号进程作任何处理。

有两个信号是应用进程无法捕捉和忽略的,即SIGKILL和SEGSTOP,这是为了使系统管理员能在任何时候中断或结束某一特定的进程。

C++
// 创建一个新信号量或取得一个已有信号量
// semget函数成功返回一个相应信号标识符(非零),失败返回-1.
int semget(key_t key, int num_sems, int sem_flags);   // 改变信号量的值
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops); struct sembuf{   short sem_num;  //除非使用一组信号量,否则它为0   short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数, //一个是-1,即P(等待)操作,   //一个是+1,即V(释放信号)操作。   short sem_flg;  //通常为SEM_UNDO,使操作系统跟踪信号,   //并在进程没有释放该信号量而终止时,操作系统释放信号量
};   // 直接控制信号量信息
// command命令主要是两种:
// SETVAL:用来把信号量初始化为一个已知的值。
// IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
int semctl(int sem_id, int sem_num, int command, ...);

工作原理

由于信号量只能进行两种操作等待和释放信号,即P(sv)和V(sv),他们的行为是这样的:

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

套接字

  • server
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h> int main()
{ int server_sockfd = -1; int client_sockfd = -1; int client_len = 0; struct sockaddr_in server_addr; struct sockaddr_in client_addr; //创建流套接字 server_sockfd = socket(AF_INET, SOCK_STREAM, 0); //设置服务器接收的连接地址和监听的端口 server_addr.sin_family = AF_INET;//指定网络套接字 server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//接受所有IP地址的连接 server_addr.sin_port = htons(9736);//绑定到9736端口 //绑定(命名)套接字 bind(server_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); //创建套接字队列,监听套接字 listen(server_sockfd, 5); //忽略子进程停止或退出信号 signal(SIGCHLD, SIG_IGN); while(1) { char ch = '\0'; client_len = sizeof(client_addr); printf("Server waiting\n"); //接受连接,创建新的套接字 client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_len);if(fork() == 0) { //子进程中,读取客户端发过来的信息,处理信息,再发送给客户端 read(client_sockfd, &ch, 1); sleep(5); ch++; write(client_sockfd, &ch, 1); close(client_sockfd); exit(0); } else { //父进程中,关闭套接字 close(client_sockfd); } }
}
  • client
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h> int main()
{ int sockfd = -1; int len = 0; struct sockaddr_in address; int result; char ch = 'A'; //创建流套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); //设置要连接的服务器的信息 address.sin_family = AF_INET;//使用网络套接字 address.sin_addr.s_addr = inet_addr("127.0.0.1");//服务器地址 address.sin_port = htons(9736);//服务器所监听的端口 len = sizeof(address); //连接到服务器 result = connect(sockfd, (struct sockaddr*)&address, len); if(result == -1) { perror("ops:client\n"); exit(1); } //发送请求给服务器 write(sockfd, &ch, 1); //从服务器获取数据 read(sockfd, &ch, 1); printf("char form server = %c\n", ch); close(sockfd); exit(0);
}


https://blog.csdn.net/giantpoplar/article/details/47657303

进程间同步

在Linux下,进程同步的解决方式主要有四种:

  1. 信号量
  2. 文件锁
  3. 无锁CAS
  4. 校验方式(CRC32校验)

信号量

使用方法见【进程间通信方式】一节中的信号量

文件锁

linux下可以使用flock()函数对文件进行加锁解锁等操作。简单介绍下flock()函数:

  1. 定义函数 int flock(int fd,int operation);
  2. 函数说明 flock()会依参数operation所指定的方式对参数fd所指的文件做各种锁定或解除锁定的动作。此函数只能锁定整个文件,无法锁定文件的某一区域。
  3. 参数 operation有下列四种情况:
  • LOCK_SH 建立共享锁定。多个进程可同时对同一个文件做共享锁定。
  • LOCK_EX 建立互斥锁定。一个文件同时只有一个互斥锁定。
  • LOCK_UN 解除文件锁定状态。
  • LOCK_NB 无法建立锁定时,此操作可不被阻断,马上返回进程。通常与LOCK_SH或LOCK_EX 做OR(|)组合。
  1. 返回值 返回0表示成功,若有错误则返回-1,错误代码存于errno。

注意:

  • 单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定。
  • flock锁的释放非常具有特色,即可调用LOCK_UN参数来释放文件锁,也可以通过关闭fd的方式来释放文件锁(flock的第一个参数是fd),意味着flock会随着进程的关闭而被自动释放掉。

简而言之:

一个进程加LOCK_SH,其他进程也可以加LOCK_SH,但不能加LOCK_EX锁。

一个进程加LOCK_EX,其他进程不能对该文件加任何锁。

这种机制类似于读写锁,LOCK_SH是读锁,LOCK_EX是写锁。

C++
// 进程 1#include <stdio.h>
#include <sys/file.h>
#include <unistd.h>
#include <errno.h> int main(void)
{   FILE *fp = NULL;    if ((fp = fopen("./file_lock", "wb+")) == NULL) //打开文件   { printf("file open error,errno=%d!\n",errno); return -1; }        if (flock(fp->_fileno, LOCK_EX) != 0) //给该文件加互斥锁 printf("file lock by others\n");//加锁失败,阻塞 while(1) {      printf("process1-ex\n"); sleep(1);   }     flock(fp->_fileno, LOCK_UN); //文件解锁  fclose(fp); //关闭文件        return 0;
} C++
// 进程 2#include <stdio.h>
#include <sys/file.h>
#include <unistd.h>
#include <errno.h> int main(void)
{   FILE *fp = NULL;   int i = 0;   if ((fp = fopen("./file_lock", "wb+")) == NULL) //打开文件 { printf("file open error,errno=%d!\n",errno); return -1; }    if(flock(fp->_fileno, LOCK_SH) != 0)//文件加共享锁 { printf("file lock by others\n");//加锁失败,阻塞 }  while(1) //进入循环   {      printf("process2-sh\n"); sleep(1);   }      flock(fp->_fileno, LOCK_UN); //释放文件锁   fclose(fp); //关闭文件 return 0;   }进程1运行时,其他进程无法获得任何锁运行;进程2运行时,其他进程可以获得共享锁运行;

无锁 CAS

上述文件锁,可以保证数据访问的一致性,但是加锁会引起性能下降,多个进程竞争一个锁,抢占失败导致上下文切换。

解决办法就是,使用原子操作指令,有一个重要方法就是CAS(compare and swap)。

CAS是一组原语指令,用来实现多进程和多线程下的变量同步。

CAS是一种有名的无锁算法。无锁编程,即不适用锁的情况下实现多线程之间的变量同步,也就是在没有现成被阻塞的情况下实现变量的同步。

总结如下:

  • CAS(Compare And Swap)比较并替换,是线程并发运行时用到的一种技术

  • CAS是原子操作,保证并发安全,而不能保证并发同步

  • CAS是CPU的一个指令

  • CAS是非阻塞的、轻量级的乐观锁

使用意见:

  • 乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的吞吐量。

校验方式(CRC32校验)

循环冗余校验(Cyclic Redundancy Check)

数据通信领域中最常用的一种差错校验码,其特征是信息字段和校验字段的长度可以任意选定。

CRC校验实用程序库:在数据存储和数据通讯领域,为了保证数据的正确性,就不得不采用检错的手段。

CRC码校验原理:

发送端:发送端根据信息字段与生成多项式生产多个CRC码,CRC码作为数据发送给接收端,同时也会把计算出的校验字段的数据一同发送(注:目的是如果接收端检测到发送的数据是正确的,接收端能够从CRC码中提取出信息字段的数据)。

接收端:接收到CRC码数据后,检测接收到的数据是否正确,方法:将CRC码数据与生成多项式进行模2除,如果余数为0,则说明接收到的数据是正确的。然后,从CRC码中提取出信息字段的数据。

实现方法:

1、发送端生成CRC码方法:

CRC码是由两部分组成的,前部分是信息字段,就是需要校验的信息,后部分是校验字段,如果CRC码共长n个bit,信息字段长k个bit,就称为(n,k)码。它的编码规则是:

  • 首先将信息字段值(k bit)左移r位(k+r=n)

  • 运用一个生成多项式g(x) (也可看成二进制数) 模2除上面的式子,得到的余数就是校验字段值。

  • 生成的CRC码值为:信息字段值+校验字段值(单位:位bit,次序:高位到低位),例如字段值为1001,校验字段值为110,则CRC码为1001110

声明
本文是个人学习和总结的笔记和感想,内容涉及网络资料、相关书籍摘录、个人总结和感悟。在这之中也必有疏漏未加标注者,如有侵权请联系删除。

【OS系列-2】- 进程详细讲解(代码示例)相关推荐

  1. SpringBoot教程(6) @Conditional 详细讲解和示例

    @Conditional 详细讲解和示例 一.@Conditional简介 二.示例:加载2个Bean 1. 定义Person类 2. 利用@Configuration + @Bean加载2个Bean ...

  2. python os path isfile_Python path.isfile方法代码示例

    本文整理汇总了Python中os.path.isfile方法的典型用法代码示例.如果您正苦于以下问题:Python path.isfile方法的具体用法?Python path.isfile怎么用?P ...

  3. Linux中的进程控制:进程退出、孤儿进程、僵尸进程 概念及代码示例 [Linux高并发服务器开发]

    目录 一.进程退出 二.孤儿进程 三.僵尸进程 一.进程退出 #include <stdlib.h> void  exit ( int status ); #include <uni ...

  4. ❤️整理2万字带你走进C语言(详细讲解+代码演示+图解)❤️(强烈建议收藏!!!)

    目录 一.什么是C语言? 二.第一个C语言程序 起源 代码 程序分析 程序运行 一个工程中出现两个及以上的main函数 代码 运行结果 分析 三.数据类型 数据各种类型 为什么会有这么多的数据类型? ...

  5. 算法--背包九讲(详细讲解+代码)

    背包九讲 目录  第一讲 01背包问题  第二讲 完全背包问题  第三讲 多重背包问题  第四讲 混合三种背包问题  第五讲 二维费用的背包问题  第六讲 分组的背包问题  第七讲 有依赖的背包问题  ...

  6. HMM超详细讲解+代码

    文章目录 本文目标 评估描述 预测描述 #写在前面 老习惯,正文之前瞎扯一通.HMM学了很久,最初是在<统计学自然语言处理>里面就学到了相关内容,并且知道HMM CRF一直都是NLP比较底 ...

  7. Spring @Conditional注解 详细讲解及示例

    前言: @Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册bean. @Conditional的定义: //此注解可以标注在类和方法上 @Tar ...

  8. Educational Codeforces Round 145 (E. Two Tanks 详细讲解 + 代码注释)

    E. Two Tanks 链接:E. Two Tanks 推荐大佬的视频讲解:B站 yingluosanqian 大晚上琢磨jiang老师的代码看不明白,还好找到这个视频讲解,以下题解基于该视频讲解, ...

  9. 【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)

    目录 概述 内存 内存含义 内存作用: 物理存储器和存储地址空间 物理存储器:实际存在的具体存储器芯片. 存储地址空间:对存储器编码的范围. 内存地址 指针和指针变量 指针基础知识 指针变量的定义和使 ...

最新文章

  1. 告别2019,展望2020:让我们看一看这十年中深度学习的经典瞬间
  2. python常用的日期时间模块
  3. 数据预处理:原始数据集快速分类的方法,numpy的使用技巧,数据的row=mask的column
  4. JavaScript动态加载js文件
  5. SAP SD数据库表一览
  6. Wtm携手LayUI -- .netcore 开源生态我们是认真的!
  7. python extended,python list中的append 与 extended 的区别
  8. poi 设置word表格颜色_办公软件小课堂 Word表格的设置
  9. c#通过反射移除所有事件
  10. Struts2_HelloWorld_3
  11. Louvain 算法原理 及设计实现
  12. 风云第三部 第533回 乌云蔽日 力掌乾坤
  13. 网页设计大作业-五子棋游戏,可以进行双人对弈
  14. 最近在关注浏览器,先转一篇游戏浏览器的评测。
  15. CSS透明opacity和IE各版本透明度滤镜filter的最准确用法
  16. R 语言消除pdf图片的空白
  17. 2023第八届少儿模特明星盛典 小超模李迦曈 担任全球赛小主持人
  18. Gmail邮箱怎么获取授权码?熟悉一下
  19. JavaScript 编程精解 中文第三版 九、正则表达式
  20. 施一公院士:如何做一名优秀的博士生

热门文章

  1. Windows dss代理摄像头rtsp流 rtsp摄像头+ffmpeg+vlc
  2. 计算机软件侵害,如何认定侵害计算机软件著作权?
  3. 为什么谷歌浏览器修改主题背景,只有标签栏变了,但新标签页背景不变?怎么办?
  4. etal斜体吗 参考文献_期刊论文参考文献着录注意问题
  5. usage.txt-1
  6. Houdini 导出.ass文件
  7. Macworld2007发布iPhone!
  8. 【OpenCV 4】图像像素的归一化
  9. 盒马鲜生真的是新零售吗?
  10. 用jq做一个点击图片放大消失