Linux系统编程——进程

1 进程基础

1.1 进程概念

进程是一个或多个线程的集合,集合会占用地址空间和系统资源。
在这里插入图片描述
一个程序文件,只是一堆执行的代码和部分待处理的数据,他们只有被加载到内存中,然后让CPU追条执行其代码,根据代码做出相应的动作,才形成一个真正“活的”、动态的进程,因此进程是一个动态变化的过程,是一出有始有终的戏,而程序文件只是一系列动作的原始蓝本,是一个静态的剧本。

1.2 命令

1.gec@ubuntu:/mnt/hgfs/…/code0517$ pstree
systemd─┬─ManagementAgent───6*[{ManagementAgent}]
├─ModemManager─┬─{gdbus}
│ └─{gmain}
├─NetworkManager─┬─{gdbus}
│ └─{gmain}
├─VGAuthService
├─accounts-daemon─┬─{gdbus}
│ └─{gmain}
├─acpid
2.gec@ubuntu:/mnt/hgfs/…/code0517$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 5月14 ? 00:01:00 /sbin/init auto noprompt
root 2 0 0 5月14 ? 00:00:00 [kthreadd]
root 4 2 0 5月14 ? 00:00:00 [kworker/0:0H]
root 6 2 0 5月14 ? 00:02:10 [ksoftirqd/0]
root 7 2 0 5月14 ? 00:00:34 [rcu_sched]
root 8 2 0 5月14 ? 00:00:00 [rcu_bh]

1.3 进程的状态

在这里插入图片描述
孤儿态:子进程还没有结束,父母进程先退出没有对其进行资源回收,则子进程会被送回“孤儿院”(系统进程),系统进程会对其进行资源回收。
僵尸态:子进程退出,父母进程依然“健在”,但是也没有对其进行资源回收,(由于父母进程还在,所以系统不会把其看作是孤儿)这样子进程就会“僵死”在那,变成僵尸进程。我们平时编程要避免产生僵尸进程,因为过多的僵尸进程会占用浪费大量的系统资源。

1.4 重要API

[1]创建进程
	pid_t fork(void);
	
返回值:
	成功:非负数  0:child  正数:parent
 	失败:-1 
备注:
	1.子进程会从fork()返回值的下一条逻辑语句开始运行。这样就避免了不断调用fork()而产生无线子孙。
	2.父子进程之间是互相平等的:他们的执行次序是随机的(并发执行),除非要使用特殊机制来同步他们,否则无法判断他们的运行顺序。
	3.父子进程之间是相互独立的:由于子进程完整的复制了父进程的内存空间,因此从内存的角度来看他们是相互独立的、互不影响的。
[2]exec簇函数
	#include <unistd.h>
	extern char **environ;

	int execl(const char *path, const char *arg, ...);
	int execlp(const char *file, const char *arg, ...); ---->从PATH环境变量中查找文件并执行
	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[]);
	int execvpe(const char *file, char *const argv[],char *const envp[]);
参数:
	path:即将被加载执行的ELF文件或脚本的路径
	file:即将被加载执行的ELF文件或脚本的名字
	arg:以列表方式罗列的ELF文件或脚本的参数
	argv:以数组方式组织及的ELF文件或脚本的参数
	envp:用户自定义的环境变量数组
返回值:
	成功--->不返回
	失败--->-1
备注:
	l--->意味着参数以列表(list)的方式提供
	v--->意味着参数以矢量(vector)数组的方式提供
	p--->意味着会利用环境变量PATH来寻找指定的执行文件
	c--->意味着用户提供自定义的环境变量
[3]退出本进程
	1.void exit(int status);
	2.void _exit(int status);
参数:
	status:子进程的退出值
备注:
	1.如果子进程正常退出,则status一般为0,异常退出,则status一般为非0
	2.exit()退出时,会自动冲洗(flash)标准IO总残留的数据到内核,如果进程注册了“退出处理函数”,还会自动执行这些函数。
	  _exit()直接退出,不会执行注册函数atexit(3),所以一般使用exit。
			 int atexit(void (*function)(void));
			 参数:函数指针 (返回值void 参数void)
			 作用:进程退出时会调用 function指向的函数
[4]等待子进程
	pid_t wait(int *status);
	pid_t waitpid(pid_t pid, int *status, int options);
参数:
	pid:回收的子进程的PID , 如果为-1 表示回收所用子进程
	status:保存回收的状态值(exit的退出值)
	options:一般为0(阻塞等待子进程退出)
返回值:
	成功:子进程PID  
	失败:-1
备注:
	pid_t wait(int *status); --->等价于waitpid(-1, &status, 0);

处理子进程退出状态值的宏 在这里插入图片描述

2 管道

2.1 管道的概念

管道分为无名管道和有名管道。
两者的区别:

  1. 有名管道有文件名,可以使用ls去查看。无名管道没有文件名。
  2. 无名管道的生存周期是跟随进程的,所以相关的所有进程退出或者无名管道的相关文件描述符被关闭,无名管道也跟随消失。
  3. 无名管道的读写具有方向性,具有两个文件描述符,一个读一个写。有名管道可以像普通文件一样去操作,但是多了一个阻塞性。

为什么pipe和fifo不能使用lseek()定位?
因为他们的数据不像普通文件那样按照块的方式存放在硬盘、flash等块设备上,而更像一个看不见源头的水龙头,无法定位。

2.2 无名管道PIPE

[1]无名管道的创建
	#include <unistd.h>
	int pipefd[2];
	int pipe(int pipefd[2]);
	
	参数:
    	pipefd:具有2int型数据的数组,分别是pipefd[0]读、pipefd[1]写端描述符
    返回值:成功:0	失败:-1

无名管道的特征:

  1. 没有名字,因此无法使用open()
  2. 只能用于亲缘进程间(父子、兄弟、祖孙…)通信
  3. 半双工工作方式:读写端分离
  4. 写入操作不具有原子性,因此只能用于一对一的简单通信情形
  5. 不能使用lseek()来定位

pipe.c

	int fd[2];
	//创建无名管道
	if(-1 == pipe(fd))
	{
		perror("pipe");
		exit(-1);
	}

	//创建线程
	pid_t pid = fork();
	if(0 == pid)
	{
		puts("child");
		char *s = "hello,zix";
		write(fd[1],s,strlen(s));//通过写端fd[1]将数据写入pipe
	}
	if(pid > 0)
	{
		puts("parent");
		char buf[100];
		memset(buf,0,sizeof(buf));
		read(fd[0],buf,sizeof(buf));//通过读端fd[0]将数据读出来
		printf("from child:%s\n",buf);
	}

	//关闭文件描述符
	close(fd[0]);
	close(fd[1]);

2.3 有名管道FIFO

[1]使用命令创建
	gec@ubuntu:~$ mkfifo fifo_file
		注:不能在与windows的共享路径下创建
[2]使用函数创建
	int mkfifo(const char *pathname, mode_t mode);
	参数:
	  	参数1:文件路径+名字
	  	参数2:模式---> O_CREAT | 0666
	返回值:成功:0	失败:-1   

有名管道的特征:

  1. 有名字,存储于普通文件系统中
  2. 任何具有相应权限的进程都可以使用open()来获取FIFO的文件描述符
  3. 跟普通文件一样,使用read()/write()来读写
  4. 跟普通文件不同,不能使用lseek()来定位
  5. 具有写入原子性,支持多写者同时进行写擦欧总而数据不会互相践踏
  6. 最先被写入FIFO的数据,最先被读出来

fifo.c

#define FIFO_PATH "/home/gec/fifo_file"

	//1.创建有名管道
	if(access(FIFO_PATH,F_OK))//判断是否存在
	{
		int ret = mkfifo(FIFO_PATH,O_CREAT | 0666);
		if(-1 == ret)
		{
			perror("create fifo");
			exit(-1);
		}
	}

	//2.打开
	int fd = open(FIFO_PATH,O_RDWR);
	if(-1 == fd)
	{
		perror("open fifo");
		exit(-1);
	}

	//3.读写
	char buf[100];
	while(1)
	{
		memset(buf,0,sizeof(buf));
#ifdef RD
		sleep(6);
		read(fd,buf,sizeof(buf));
		printf("%s\n",buf);
#elif defined(WR)
		fgets(buf,sizeof(buf),stdin);
		write(fd,buf,strlen(buf));
#endif
	}

	//4.关闭
	close(fd);

在这里插入图片描述

3 信号

2.1 信号的概念

信号是一种特殊的IPC,大部分的信号时异步的。一般情况下,进程什么时候收到信号,收到怎么的信号都是无法事先预料的。
在这里插入图片描述

分析:
	[1] 前面的31个信号都有特殊的名字,这些信号都是从UNIX系统继承下来的,也被称为“不可靠信号”。
		特征:1.非实时信号不排队,信号的响应可以互相嵌套
			 2.如果目标进程没有及时响应非实时信号,那么随后到达的信号将会被丢弃
			 3.每一个非实时信号都对应着一个系统事件,当这个事件发生时,将产生这个信号
			 4.如果进程的挂起信号中含有实时和非实时信号,那么进程会优先响应实时信号,
			 并且会按照从大到小依次响应,而非实时信号没有固定的顺序。
	[2]后面的3个信号(34-64)是Liunx系统新增的实时信号,也被称为“可靠信号”。
		特征:1.实时信号的响应次序按照接收顺序排队,不嵌套
			 2.即使相同的实时信号被同时发送多次,也不会被丢弃,而会依次挨个响应
			 3.实时信号没有特殊的系统事件与之对应

在这里插入图片描述

3.2 相关API

[1] 向指定进程或者进程组,发送一个指定的信号
	[命令]:
		kill -<信号值>  <PID>
		killall  -<信号值> ./main
	[函数]int kill(pid_t pid, int sig);
			参数1:小于-1---->信号将被发送给组ID等于-pid的进程组里面的所有进程
				      -1---->信号将被发送给所有进程
				       0---->信号将被发送给与当前进程同一个进程组内的所有进程
				   大于0----->信号将被发送给PID等于pid的指定进程
			参数2:要发送的信号
			返回值:成功0 失败-1
[2] 捕捉一个指定的信号
	sighandler_t signal(int signum, void (*sighandler_t)(int));
		参数1:要捕捉的信号
		参数2:函数指针--->接收到信号signum
			  SIG_DEF--->默认行为 
			  SIG_IGN--->忽略
		返回值:成功--->最近一次调用该函数时参数2的值
			   失败--->SIG_ERR
[3] 自己给自己发送一个指定的信号
	int raise(int sig);
		参数:要唤醒的信号
		返回值:成功0 失败非0
[4] 将本进程挂起,知道接收到一个信号
	int pause(void);
		返回值:   -1--->收到非致命信号或者已经被捕捉到的信号
		   不返回--->收到致命信号导致进程异常退出
		备注:pause()响应函数返回值偶,随后返回。
		   若想在一个进程中持续接收信号,可以在死循环中写。
信号集操作函数的接口规范:
	1.将信号集清空
		int sigemptyset(sigset_t *set);
	2.将所有的信号添加到信号集中
		int sigfillset(sigset_t *set);
	3.将指定的一个信号添加到信号集中
		int sigaddset(sigset_t *set,int signum);
	4.将指定的一个信号从信号集中删除
		int sigdelset(sigset_t *set,int signum);
	5.判断一个指定的信号是否被信号集包含
		int sigismember(const sigset_t *set,int signum);
	参数set:信号集
[5] 定时发送alarm信号给进程自己
	unsigned int alarm(unsigned int seconds);
		参数:定时的秒数
		备注:若定时到了,系统会自动发送14号信号给调用alarm函数的进程

kill.c

void wait_fun(void)
{
	int status,ret;
	ret = wait(&status);

	if(ret == -1)
	{
		perror("wait error");
	}
	else if(WIFEXITED(status))
	{
		printf("child PID:%d , exit:%d\n",ret,WEXITSTATUS(status));
	}
	else if(WIFSIGNALED(status))
	{
		printf("child PID:%d , sig:%d\n",ret,WTERMSIG(status));
	}
}

int main(int argc,char *argv[])
{
	pid_t pid = fork();

	if(pid == 0)
	{
		puts("child");
		sleep(3);
		kill(getppid(),9);
		puts("send 9#sig");
		while(1) sleep(1);
	}
	else
	{
		puts("parent");
		wait_fun();
		puts("child is exit");
	}
	return 0;
}

4 IPC对象

system-V IPC是消息队列、共享内存的总称,也被叫做IPC对象。
IPC对象一旦被创建,就存在于内核中,属于内核管理。

[1] 获取一个当前未用的IPC的键值
	key_t ftok(const char *pathname, int proj_id);
		参数1:路径名
       	参数2:标识符 (范围为1-255)
       	返回值:成功键值 失败-1
IPC对象命令操作
	[查看]ipcs
			-m     共享内存
			-q     消息队列
			-s     信号量
			-a     全部IPC对象
	[删除]ipcrm 
		ipcrm [ -M key | -m id | -Q key | -q id | -S key | -s id ]

4.1 消息队列msg

带有标识的消息的发送与接受的功能的对象。

4.1.1 消息队列的使用方法

[1] 发送者

  1. 获取消息队列的ID
  2. 将数据放入一个附带有标识的特殊的结构体,发送给消息队列

[2]接收者

  1. 获取消息队列的ID
  2. 将指定标识的消息读出

当发送者和接收者都不再使用消息队列时,及时删除并释放资源。

4.1.2 相关API
[1] 获取消息队列的ID
	int msgget(key_t key, int msgflg);
		参数1:消息队列的键值
		参数2:IPC_CREAT---->如果key对应的msg不存在,则创建该对象
			  IPC_EXCL----->如果key对应的msg已存在,则报错
			  mode--------->msg的访问权限
		返回值:成功-->该消息队列的ID  失败-1
		备注:如果key指定为IPC_PRIVATE,则自动产生一个随即未用的新键值
			 只有读写权限,没有执行权限。0777只能赋rw-(0666)
[2] 消息的发送
	int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
		参数1:发送消息的消息队列ID
		参数2:发送的数据的存储区域指针
		参数3:发送的数据的大小
		参数4:不设置特殊方式为0
			  IPC_NOWAIT、MSG_EXCEPT、MSG_NOERROR
		返回值:成功0 失败-1
		备注:struct msgbuf
			 {
				 long mtype;//消息的biaoshi
				 char mtext[20];//消息的正文
			 };
[3] 消息的接收
	ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, 
					long msgtyp, int msgflg);
		参数4:要接收的消息的标识
[4] 设置或者获取消息队列的属性
	int msgctl(int msqid, int cmd, struct msqid_ds *buf);
		参数2:函数执行命令
			  IPC_STAT---->查看属性
		 	  IPC_SET----->修改属性
			  IPC_RMID---->删除对象
		 	  IPC_INFO---->查看IPC信息
		参数3:相关信息结构体缓冲区
		返回值:成功0 失败-1

msg.c

struct msgbuf
{
	long mtype;
	char mtext[20];
};

#define TYPE 10

int main(int argc,char *argv[])
{
	int msgid = msgget(ftok(".",1),IPC_CREAT | 0666);
	if(-1 == msgid)
	{
		perror("msgget");
		exit(-1);
	}

	struct msgbuf Mbuf;
	int ret;
#ifdef SND
	Mbuf.mtype = TYPE;
	while(1)
	{
		fgets(Mbuf.mtext,sizeof(Mbuf.mtext),stdin);
		ret = msgsnd(msgid,&Mbuf,strlen(Mbuf.mtext),0);
		if(-1 == ret)
			perror("msgsnd");
		if(strncmp(Mbuf.mtext,"over",4) == 0)	break;
	}
#elif defined(RCV)
	while(1)
	{
		ret = msgrcv(msgid,&Mbuf,sizeof(Mbuf),TYPE,0);	
		if(-1 == ret)
			perror("msgrcv");
		printf("msgrcv recive %dbytes:%s\n",ret,Mbuf.mtext);
		if(strncmp(Mbuf.mtext,"over",4) == 0)	break;
	}
#endif

#ifdef RMID
	msgctl(msgid,IPC_RMID,NULL);
#endif

	return 0;
}

4.2 共享内存msg

内存共享是效率最高的IPC,因为他抛弃了内核这个代理人,直接将一块裸露的内存放在数据传输的进程面前。
在这里插入图片描述
当进程p1向虚拟内存中区域1写入数据时,进程p2同时在其虚拟内存的区域2中可以看到这些数据,中间过程无转发,效率极高。

4.2.1 共享内存的使用方法

步骤:

  1. 获取共享内存对象的ID
  2. 将共享内存映射至本进程虚拟内存空间的某个区域
  3. 当不再使用时,解除映射关系
  4. 当没有进程再需要这块共享内存时,删除它。
4.2.2 相关API
[1] 获取共享内存的ID
	int shmget(key_t key, size_t size, int shmflg);
		参数1:消息队列的键值
		参数2:共享内存的尺寸
		参数3:IPC_CREAT---->如果key对应的msg不存在,则创建该对象
			  IPC_EXCL----->如果key对应的msg已存在,则报错
			  mode--------->msg的访问权限
		返回值:成功-->该共享内存的ID   失败-1
		备注:如果key指定为IPC_PRIVATE,则自动产生一个随即未用的新键值
[2] 对共享内存进行映射
	void *shmat(int shmid, const void *shmaddr, int shmflg);
		参数1:消息队列ID
		参数2:需要映射的首地址,设置为NULL系统自动选择合适的
		参数3:一般设置为0,SHM_RDONLY为只读
		返回值:成功-->映射的首地址  失败-->(void *)-1
		备注:共享内存只能以只读、可读写方式映射,不能以只写的方式映射。
[3]对共享内存解除映射
	int shmdt(const void *shmaddr);
		参数1:需要解除映射的首地址
		返回值:成功0	 失败-1
[4] 获取或者设置共享内存的相关属性
	int shmctl(int shmid, int cmd, struct msqid_ds *buf);

shm.c

	unsigned long const SIZE = 100;
	int shmid = shmget(ftok(".",1),SIZE,IPC_CREAT | 0666);
	if(-1 == shmid)
	{
		perror("shmget");
		exit(-1);
	}

	char *shmaddr = shmat(shmid,NULL,0);
	if((void *)-1 == shmaddr)
	{
		perror("shmat");
		exit(-1);
	}

	char buf[100];
#ifdef SND
	while(1)
	{
		memset(buf,0,sizeof(buf));
		
		fgets(buf,sizeof(buf),stdin);
		memcpy(shmaddr,buf,strlen(buf));
	}
#elif defined(RCV)
	while(1)
	{
		memset(buf,0,sizeof(buf));

		memcpy(buf,shmaddr,sizeof(buf));
		puts(buf);
		sleep(3);
	}
#endif
	return 0;

4.3 信号量sem

信号量SEM全称Sempphore,信号灯。

4.3.1 信号量概念

Linux常见的信号量有三种:ststem-V信号量、POSIX有名信号量、POSIX无名信号量。

  • P:申请资源(-1) V:释放资源(+1)
  • P、V操作的特征:它们是原子性的
4.3.2 相关API
[1] 获取信号量ID
	int semget(key_t key, int nsems, int semflg);
		参数1:信号量的键值
		参数2:信号量元素的个数
		参数3:IPC_CREAT---->如果key对应的msg不存在,则创建该对象
			   IPC_EXCL----->如果key对应的msg已存在,则报错
			   mode--------->msg的访问权限
		返回值:成功-->该信号量ID 失败-1
[2] 对信号量进行P、V操作
	int semop(int semid, struct sembuf *sops, unsigned nsops);
		参数1:信号量ID
		参数2:信号量操作结构体数组
			struct sembuf{
				unsigned short sem_num;  /*信号量序号*/
				short          sem_op;   /*PV操作*/
				short          sem_flg;  /*操作选项 一般为0*/	 
			};
		参数3:结构体数组元素个数
		返回值:成功0 失败-1
		备注:当sem_op > 0时:进行V操作,即信号量元素的值将会加上sem_op
			  当sem_op = 0时:进行等零操作
			  当sem_op < 0时:进行P操作,即信号量元素的值将会减去sem_op			 
[3] 获取或者设置信号量的相关属性
	int semctl(int semid, int semnum, int cmd, ...);
		参数1:信号量ID
		参数2:信号量元素序号(数组下标)
		参数4:联合体的值val
			union semun {
							int              val;  /*当cmd为SETVAL时使用*/  
							struct semid_ds *buf;  /*当cmd为IPC_STAT或IPC_SET时使用*/   
							unsigned short  *array; /*当cmd为GETALL或SETALL时使用*/  
							struct seminfo  *__buf; /*当cmd为IPC_INFO时使用*/  
			       		}

sem.c

union semun {
	int              val;    /* SETVAL*/
	struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
	unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  
};

int main(int argc,char *argv[])
{
/****************共享内存***********************/
	size_t size = 100;
	int shmid = shmget(ftok(".",1),size,IPC_CREAT|0666);
	if(shmid == -1)
	{
		perror("shmget");
		return -1;
	}
	char *shmaddr = shmat(shmid,NULL,0);
	if(shmaddr == (void*)-1)
	{
		perror("shmat");
		return -1;
	}
/***********************************************/

/****************信号量初始化*******************/
	int semid = semget(ftok(".",2),1,IPC_CREAT|IPC_EXCL|0666);
	if(semid == -1)
	{
		if(errno != EEXIST)
		{
			perror("semget");
			return -1;
		}

		else
		{
			semid = semget(ftok(".",2),1,0666);
		}
	}
	else
	{
		union semun SemSet;
		SemSet.val = 1;
		semctl(semid,0,SETVAL,SemSet);
	}
/***********************************************/

/***************信号量读写**********************/
	char buf[100];

	struct sembuf SemSet;
	SemSet.sem_num = 0;
	SemSet.sem_flg = 0;
#ifdef SND
	while(1)
	{
		memset(buf,0,sizeof(buf));
		fgets(buf,sizeof(buf),stdin);
		memcpy(shmaddr,buf,strlen(buf));

		SemSet.sem_op = 1;
		semop(semid,&SemSet,1);
	}

#elif defined(RCV)
	while(1)
	{	
		SemSet.sem_op = -1;
		semop(semid,&SemSet,1);
		
		memset(buf,0,sizeof(buf));
		memcpy(buf,shmaddr,sizeof(buf));
		puts(buf);
	}

#endif
/***********************************************/
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值