一、程序、进程和线程的概念
在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算运行,进程则是运行着的程序,是操作系统执行的基本单位。线程是为了节省资源而可以在同一个进程中共享的一个执行单位。
程序和进程的差别
-
进程的出现最初是在UNlX下,用于表示多用户、多任务的操作系统环境下,应用程序在内存环境中基本执行单元的概念。 -
进程是UNlX操作系统环境中的基本概念,是系统资源分配的最小单位。 -
UNlX操作系统下的用户管理和资源分配等工作几乎都是操作系统通过对应用程序进程的控制实现的。 -
C 、C + +、Java 等语言编写的源程序经相应的编译器编译成可执行文件后,提交给计算机处理器运行。应用程序的运行状态称为进程。 -
进程从用户角度来看是应用程序的一个执行过程。从操作系统核心角度来看,进程代表的是操作系统分配的内存、CPU 时间片等资源的基本单位,是为正在运行的程序提供的运行环境。 -
进程与应用程序的区别在于应用程序作为一个静态文件存储在计算机系统的硬盘等存储空间中,而进程则是处于动态条件下由操作系统维护的系统资源管理实体。
进程概念和程序概念最大的不同之处在于:
-
进程是动态的,而程序是静态的。 -
进程有一定的生命期,而程序是指令的集合,本身无”运动” 的含义。没有建立进程的程序不能作为1个独立单位得到操作系统的认可; -
一个进程只能对应一 个程序,一个程序可以对应多个进程。进程和程序的关系就像戏剧和剧本之间的关系。
Linux环境下的进程
Linux 的进程操作方式主要有产生进程、终止进程,并且进程之间存在数据和控制的交互,即进程间通信和同步。
1.进程的产生过程
进程的产生有多种方式,其基本过程是一致的。
- 首先复制其父进程的环境配置。
- 在内核中建立进程结构。
- 将结构插入到进程列表,便于维护。
- 分配资源给此进程。
- 复制父进程的内存映射信息。
- 管理文件描述符和链接点。
- 通知父进程。
2.进程的终止方式
- 从 main返回。
- 调用exit。
- 调用_exit。
- 调用abort。
- 由一个信号终止。
进程在终止的时候,系统会释放进程所拥有的资源,例如内存、文件符和内核结构等。
3.进程之间的通信
进程之间的通信有多种方式,其中管道、共享内存和消息队列是最常用的方式。
-
管道是UNIX族中进程通信的最古老的方式,它利用内核在两个进程之间建立通道,它的特点是与文件的操作类似,仅仅在管道的一 端只读,另一 端只写。利用读写的方式在进程之间传递数据。 -
共享内存是将内存中的一 段地址,在多个进程之间共享。多个进程利用获得的共享内存的地址来直接对内存进行操作。 -
消息队列则是在内核中建立一个链表,发送方按照一 定的标识将数据发送到内核中,内核将其放入量表后,等待接收方的请求。接收方发送请求后,内核按照消息的标识,从内核中将消息从链表中摘下,传递给接收方。消息队列是一 种完全的异步操作方式。
4.进程之间的同步
进程和线程
线程和进程是另一 对有意义的概念,主要区别和联系如下:
-
进程是操作系统进行资源分配的基本单位,进程拥有完整的虚拟空间。进行系统资源分配的时候,除了 CPU 资源之外,不会给线程分配独立的资源,线程所需要的资源需要共享。 -
线程是进程的一 部分,如果没有进行显式地线程分配,可以认为进程是单线程的;如果进程中建立了线程,则可以认为系统是多线程的。 -
多线程和多进程是两种不同的概念,虽然二者都是并行完成功能。但是,多个线程之间像内存、变量等资源可以通过简单的办法共享,多进程则不同,进程间的共享方式有限。 -
进程有进程控制表 PCB , 系统通过 PCB 对进程进行调度;线程有线程控制表TCB 。是,TCB 所表示的状态比PCB 要少得多。
二、进程产生的方式
进程是计算机中运行的基本单位,要产生一个进程,有很多种方式,例如使用fork()函数、system()函数、exec()函数等,这些函数的不同在于其运行环境的构造之间存在差别,其本质都是对程序运行的各种条件进行设置,在系统之间建立一个可以运行的程序。
进程号
每个进程初始化时,系统都分配一个ID号,用于标识此进程。在Linux中进程是唯一的,系统可以用这个值来表示一个进程,描述进程的ID号通常叫做PID,即进程ID(process id)。PID的变量类型为pid_t。
1.getpid()、getppid()函数介绍
getpid()、getppid()函数返回当前进程的ID号,getppid()返回当前进程的父进程ID号。类型pid_t是个typedef类型,定义为unsigned int 。getpid()函数和getppid()函数的原型如下:
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);
pid_t getppid(void);
2.getpid()函数的例子
下面例子使用getpid()函数和getppid()函数例子。程序获取当前程序的PID和父程序的PID:
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
pid_t pid,ppid;
pid = getpid();
ppid = getppid();
printf("当前进程的ID为:%d\n",pid);
printf("当前进程的父进程号ID为:%d\n",ppid);
return 0;
}
进程复制fork()
fork()函数是产生进程的一 种方式 。fork()函数 以父进程为蓝本复制一个进程,其ID号和父进程ID号不同。在Linux环境下,fork()是以写复制实现的,只有内存等与父进程不同时,其他与父进程共享,只有在父进程或者子进程进行了修改后,才重新生成一 份。
1.fork()函数介绍
fork()函数的原型如下,当成功时,fork()函数的返回值是进程的ID;失败则返回-1。
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
特点:执行一次,返回两次。在父进程和子进程中返回的是不同的值,父进程中返回的子进程的ID号,而子进程中则返回0。
2.fork()函数的例子
fork()函数的例子·,在调用fork()函数之后,判断fork()函数的返回值:如果为-1,打印失败信息;如果为0,打印子进程信息;大于0,打印父进程信息。
include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
pid_t pid;
pid=fork();
if(-1 == pid)
{
printf("失败!\n");
return -1;
}else if(pid == 0){
printf("子进程,fork:%d, ID:%d , 父进程:%d\n",pid,getpid(),getppid());
}else{
printf("父进程,fork:%d,ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
}
return 0;
}
Fork出来的子进程的父进程ID号是执行fork()函数的进程的ID号。
system()函数方式
system()函数调用shell的外部命令在当前进程中开始另一个进程。
1.system()函数调用"/bin/sh-c command"执行特定的命令,阻塞当前进程直到command命令执行完毕。system()函数的原型如下:
#include<stdlib.h>
int system(const char *command);
执行system()函数,会调用fork()、execve()、waitpid()等函数,其中任意一个调用失败,将导致system()函数调用失败。system()函数的返回值如下:
-
失败,返回-1。 -
当sh不能执行时,返回127。 -
成功,返回进程状态值。
2.system()函数的例子
例如下面的代码获得当前进程的ID,并使用system()函数进行系统调用ping网络上的某个主机,程序将当前系统分配的PID值和进行system()函数调用的返回值都进行了打印:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int ret;
printf("系统分配的进程号:%d\n",getpid());
ret = system("ping www.baidu.com -c 2");
printf("返回值为:%d\n",ret);
return 0;
}
系统分配给当前进程的ID号为58957;然后系统ping了网络上的某个主机,发送和接收两个ping的请求包,再退出ping程序;此时系统的返回值在原来的程序中才返回。在测试的时候返回512。
进程执行exec()函数系列
使用 fork()函数和 system ()函数的时候,系统中都会建立一 个新的进程,执行调用者的操作,而原来的进程还会存在,直到用户显式地退出; 而 exec()族的函数与之前的fork()和system ()函数不同,exec()族函数会用新进程代替原有的进程,系统会从新的进程运行,新进程的PID值会与原来进程的PID值相同。
1.exec()函数介绍
exec()函数共有6个,其原型如下:
#include<unistd.h>
extern char **environ;
int execl(const char *path,const char *arg,...);
int exevlp(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[]);
-
只有 **execve()**函数是真正意义上的系统调用,其他 5 个函数都是在此基础上经过包装的库函数。 -
exec()函数族的作用是,在当前系统的可执行路径中根据指定的文件名来找到合适的可执行文件名,并用它来取代调用进程的内容,即在原来的进程内部运行一 个可执行文件。上述的可执行文件既可以是二进制的文件,也可以是可执行的脚本文件。 -
与fork()函数不同,exec()函数族的函数执行成功后不会返回,这是因为执行的新程序已经占用了当前进程的空间和资源,这些资源包括代码段、数据段和堆栈等,它们都已经被新的内容取代,而进程的ID等标识性的信息仍然是原来的东西,即 exec()函数族在原来进程的壳上运行了自己的程序,只有程序调用失败了,系统才会返回-1。 -
使用exec()函数比较普遍的一 种方法是先使用fork()函数分叉进程,然后在新的进程中调用exec()函数,这样exec()函数会占用与原来一 样的系统资源来运行。 -
Linux 系统针对上述过程专门进行了优化。由于fork()的过程是对原有系统进行复制,然后建立子进程,这些过程都比较耗费时间。如果在 fork()系统调用之后进行exec()系统调用,系统就不会进行系统复制,而是直接使用 exec()指定的参数来覆盖原有的进程。上述的方法在 Linux 系统上叫做“写时复制”,即只有在造成系统的内容发生更改的时候才进行进程的真正更新。
2.ececve()函数得例子
先打印调用进程的进程号,然后调用execve()函数,这个函数调用可执行文件"/bin/ls"列出当前目录下的文件:
#include<stdio.h>
#include<unistd.h>
int main()
{
char *args[]={"/bin/ls",NULL};
printf("系统分配进程号:%d\n",getpid());
if(execve("/bin/ls",args,NULL)<0)
printf("失败!\n");
return 0;
}
所有用户态进程的产生进程init
-
在Linux 系统中,所有的进程都是有父子或者堂兄关系的,除了初始进程init, 没有哪个进程与其他进程完全独立。系统中每个进程都有一 个父进程,新的进程不是被全新地创建,通常是从一 个原有的进程进行复制或者克隆的。 -
Linux 操作系统下的每一 个进程都有一个父进程或者兄弟进程,并且有自己的子进程。可以在 Linux 下使用命令pstree 来查看系统中运行的进程之间的关系,如下所示。可以看出,init进程是所有进程的祖先,其他的进程都是由init进程直接或者间接fork()出来的。
新的系统使用:systemd
三、进程间通信和同步
半双工管道
管道是一种把两个进程之间的标准输入和标准输出连接起来的机制。管道是一种历史悠久的进程间通信的方法。
1.基本概念
由于管道仅仅是将某个进程的输出和另一个进程的输入****相邻的单向通信的方法,因此称其为"半双工"。在shell中管道用"|"表示,如下所示,管道的一种使用方式。
ls -l | grep *.c
-
把ls -l的输出当做"grep * .c "的输入,管道在前 一 个进程 中建立输入通道 ,在后一 个进程建立输出通道,将数据从管道的左边传输到管道的右边,将Is -I的输出通过管道传给 “grep *.c ”。 -
进程创建管道,每次创建两个文件描述符来操作管道。其中一个对管道进行写操作。另一 个描述符对管道进行读操作。 -
如下图所示,显示了管道如何将两个进程通过内核连接起来,从图中可以看出这两个文件描述符是如何连接在一 起的。如果进程通过管道fda[0] 发送数据,它可以从**fdb[0]**获得信息。
-
由于进程A和进程B都能够访问管道的两个描述符,因此管道创建完毕后要设置在各个进程中的方向,希望数据向那个方向传输。 -
这需要做好规划,两个进程都要做统一 的设置,在进程A中设置为读的管道描述符,在进程B中要设置为写; -
反之亦然,并且要把不关心的管道端关掉。对管道的读写与一 般的IO 系统函数一 致,使用write()函数写入数据, read() 函数读出数据,某些特定的IO 操作管道是不支持的,例如偏移函数lseek()。
2.pipe()函数介绍
创建管道的函数原型:
#include<unistd.h>
int pipe(int filedes[2]);
数组中的filedes是一个文件描述符的数组,用于保存管道返回的两个文件描述符。
数组中的第1个元素(下标为0)是为了读操作而创建和打开的,而第2个元素(下标为1)是为了写操作而创建和打开的。 直观地说,fd1的输入是fd0的输入。当函数执行成功时,返回0,失败时返回值为-1,建立管道的代码如下2:
#include<stdio.h>
#include<unistd.h>
#include<sys/types>
int main(void)
{
int result = -1;
result = pipe(fd);
if(-1 ==result)
{
printf("建立管道失败\n");
return -1;
}
...
...
}
要使用管道有切实的用处,需要于进程的创建结合起来,利用两个管道在父进程和子进程之间通信。如下图所示,在父进程和子进程之间建立一个管道,子进程向管道中写入数据,父进程从管道中读取数据。要实现这样的模型,在父进程中需要关闭写端,在子进程中需要关闭读端。
3.pipe()函数的例子
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
int main(void)
{
int result = -1;
int fd[2],nbytes;
pid_t pid;
char string[] = "你好,管道";
char readbuffer[80];
int *write_fd = &fd[1];
int *read_fd = &fd[0];
result = pipe(fd);
if(-1 == result)
{
printf("建立管道失败\n");
return -1;
}
pid=fork();
if(-1 == pid)
{
printf("fork进程失败\n");
return -1;
}
if(0 == pid)
{
close(*read_fd);
result = write(*write_fd,string,strlen(string));
return 0;
}
else
{
close(*write_fd);
nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
printf("接受到%d个数据,内容为:%s\n",nbytes,readbuffer);
return 0;
}
}
编译运行:
4.管道阻塞和管道操作的原子性
-
当管道的写端没有关闭时,如果写请求的字节数目大于阈值PIPE_BUF, 写操作的返回值是管道中目前的数据字节数。 -
如果请求的字节数目不大于PIPE_BUF, 则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量;或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。 -
注意:PIPE_BUF在include/Linux/limits.h中定义,不同的内核版本可能会有所不同。Posix.l要求PIPE_BUF至少为 5 12 宇节。 -
管道进行写入操作的时候,当写入数据的数目小于 128K 时写入是非原子 的,如果把父进程中的两次写入字节数都改为 128K , 可以发现:写入管道的数据量大于128K 字节时,缓冲区的数据将被连续地写入管道,直到数据全部写完为止,如果没有进程读数据,则一直阻塞。
5.管道操作原子性的代码
例如,下面的代码为一个管道读写的例子。在成功建立管道后 ,子进程向管道 中写入数据,父进程从管道中读出数据。子进程一次写入 128K 个字节的数据 ,父进程每次读取10K字节的数据。当父进程没有数据可读的时候退出。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#define K 1024
#define WRITELEN (128*K)
int main(void)
{
int result = -1;
int fd[2],nbytes;
pid_t pid;
char string[WRITELEN] = "你好,管道";
char readbuffer[10*K];
int *write_fd = &fd[1];
int *read_fd = &fd[0];
result = pipe(fd);
if(-1== result)
{
printf("建立管道失败\n");
return -1;
}
pid = fork();
if(-1 == pid)
{
printf("fork进程失败\n");
return -1;
}
if(0==pid)
{
int write_size = WRITELEN;
result = 0;
close(*read_fd);
while(write_size >= 0)
{
result = write(*write_fd,string,write_size);
if(result >0)
{
write_size -=result;
printf("写入%d个数据,剩余%d个数据\n",result,write_size);
}
else
{
sleep(10);
}
}
return 0;
}
else
{
close(*write_fd);
while(1)
{
nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
if(nbytes <= 0)
{
printf("没有数据写入了\n");
break;
}
printf("接受到%d个数据,内容为:%s\n",nbytes,readbuffer);
}
}
return 0;
}
编译运行:
- 父进程每次读取10K字节的数据,读了13次全部读出,最后一次读数据,由于缓冲区只有8K字节的数据,所以仅读取了8K字节。
- 字进程一次性地写入128K字节的数据,当父进程将全部数据读取完毕时,字进程的write()函数才返回将写入信息(“写入131072个数据,剩余0个数据”)打印出来。
命名管道
命名管道的工作方式与普通的管道非常相似,但也有一些明显的区别。
-
在文件系统中命名管道是以设备特殊的形式存在的。 -
不同的进程可以通过命名管道共享数据。
1.创建FIFO
有许多中方法可以创建命名管道。其中,可以直接用shell来完成。例:在目录**/tmp下建立一个名字为namedfifo的命名管道: 其中的属性中有一个p,表示这是一个管道。为了C语言创建FIFO。用户可以使用mkfifo()**函数。
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *pathname,mode_t,mode);
2.FIFO操作
-
对命名管道FIFO来说,IO操作与普通的管道IO操作基本上是一样的,二者之间存在着一个主要的区别。 -
在FIFO中,必须使用一个 **open()**函数来显式地建立连接到管道的通道。 -
一般来说FIFO总是处于阻塞状态。也就是说,如果命名管道FIFO打开时设置了读权限,则读进程将一 直 “阻塞”,一直到其他进程打开该FIFO 并且向管道中写入数据。 -
这个阻塞动作反过来也是成立的,如果一 个进程打开一 个管道写入数据,当没有进程冲管道中读取数据的时候,写管道的操作也是阻塞的,直到已经写入的数据被读出后,才能进行写入操作。 -
如果不希望在进行命名管道操作的时候发生阻塞,可以在 open()调用中使O_NONBLOCK标志,以关闭默认的阻塞动作。
消息队列
-
消息队列是内核地址空间中的内部链表,通过 Linux 内核在各个进程之间传递内容。 -
消息顺序地发送到消息队列中,并以几种不同的方式从队列中获取,每个消息队列可以用lPC标识符唯一 地进行标识。 -
内核中的消息队列是通过IPC 的标识符来区别的,不同的消息队列之间是相对独立的。每个消息队列中的消息,又构成一 个独立的链表。
1.信息缓冲区结构
常用的结构msgbuf结构。可以使用下面的结构为模板定义自己的信息结构。在头文件<linux/msg.h> 中,定义如下:
struct msgbuf{
long mtype;
char mtext[1];
};
在结构msgbuf中有以下两个成员:
-
mtype: 消息类型,以正数来表示。用户可以给某个消息设定一 个类型,可以在消息队列中正确地发送和接收自己的消息。例如,在 socket编程过程中,一 个服务器可以接受多个客户端的连接,可以为每个客户端设定一个消息类型,服务器和客户端之间的通信可以通过此消息类型来发送和接收消息,并且多个客户端之间通过消息类型来区分。 -
mtext: 消息数据。
消息数据的类型为char,长度为1。在构建自己的消息结构时,这个域并不一定要设为char或者长度为1。可以根据实际的情况进行设定,这个域能存放任意形式的任意数据,应用程序编程人员可以重新定义msgbuf结构:
struct msgmbuf{
long mtype;
char mtext[10];
long length;
};
消息总的大小不能超过8192个字节,这其中包括mtype成员,长度是4个字节(long类型)。
2.结构msgid_ds
内核msgid_ds结构IPC对象分为3类,每一类都有一个内部数据结构,该数据结构是由内核维护的。对于消息队列而言,它的内部数据结构是 msgid_ds结构。对于系统上创建的每个消息队列,内核均为其创建、存储和维护该结构的一 个实例。该结构在 Linux/msg.h 中定义,如下所示。
struct msgid_ds{
struct ipc_perm msg_perm;
time_t msg_stime;
time_t msg_rtime;
time_t msg_ctime;
unsigned long __msg_cbytes;
msggnum_t msg_qnum;
msglen_t msg_qbytes;
pid_t msg_lspid;
pid_t msg_lrpid;
};
下面对每个成员介绍一下:
- msg_perm: 它是 ipc_perm 结构的一 个实例,ipc_perm 结构是在
Linux/ipc.h 中定义的。用于存放消息队列的许可权限信息,其中包括访问许可信息,以及队列创建者的有关信息(如 uid 等)。 - msg_stime: 发送到队列的最后一个消息的时间戳 (time_t)
- msg_rtime: 从队列中获取最后一个消息的时间戳
- msg_ctime: 对队列进行最后一 次变动的时间戳
- msg_ cbytes: 在队列上所驻留的字节总数(即所有消息的大小的总和)
- msg_qnum: 当前处千队列中的消息数目
- msg_qbytes: 队列中能容纳的字节的最大数目
- msg_lspid: 发送最后一个消息进程的PID
- msg_lrpid: 接收最后一个消息进程的PID
3.结构ipc_perm
内核把IPC对象的许可权限信息存放在ipc_perm类型的结构中。例如在前面描述的某个消息队列的内部结构中,msg_perm 成员就是ipc_perm 类型的,它的定义是在文件<linux/ipc.h> 中,如下所示。
struct ipc_perm{
key_t key;
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
unsigned short mode;
unsigned short seq;
};
这个结构主要是一些底层的东西,:
- key:key用于区分消息队列。
- uid:uid消息队列用户的ID号。
- gid:消息队列用户组的ID。
- cuid:消息队列创建的ID号。
- cgid:消息队列创建的组ID号。
- mode:权限,用户控制读写,例如0666,可以对消息进行读写操作。
- seq:序列号。
4.内核中的消息队列关系
作为IPC的消息队列,其消息的传递是通过Linux内核来进行的。如下图所示的结构成员与用户的表述基本一致。在消息的发送和接收的时候,内核通过一个比较巧妙的设置来实现消息插入队列的动作和消息中查消息的算法。 结构list_head形成一个链表,而结构msg_msg之中的m_list成员是一个struct list_head 类型的变量,通过此变量消息形成了一个链表,在查找和插入时,对m_list域进行偏移操作就可以找到对应的信息体位置。内核中的代码在头文件<linux/msg,h> 和<linux/ms.c> 中,主要的实现是插入信息和取出信息的操作。
5.键值构建ftok()函数
ftok()函数将路径和项目的表示法转变为一个系统V地IPC键值。其原型如下:
#include<sys/types.h>
#include<sys/ipc.h>
key_t ftok(const char *pathname,int proj_id);
其中pathname必须是已经存在的目录,而proj_id则是一个8位的值。通常用a,b等表示。例如建立如下目录:
mkdir -p /ipc/msg/
使用如下生成一个键值:
...
key_t key;
char *msgpath = "/ipc/msg/";
key = ftok(msgpath,'a');
if(key != -1)
{
printf("成功建立KEY\n");
}
else
{
printf("建立KEY失败\n");
}
...
6.获得消息msgget()函数
创建一个新的消息队列,或者访问一个现有的队列,可以使用函数msgget(),原型如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgget(key_t key,int msgflg);
msgget()函数的第一个参数是键值,可以用ftok()函数生成,这个关键字将和内核中其他消息队列的现有关键字比较。比较后,打开或者访问操作依赖于msgflg参数内容:
- IPC_CREAT:如果在内核中不存在该队列,则创建它。
- IPC_EXCL:当于IPC_CREAT一起使用,如果队列早已存在则将出错。
只使用IPC_CREAT,msgget()函数或者返回新创建消息队列的消息队列标识符,或者返回现有的具有同一个关键字值的队列的标识符。 同时使用了IPC_EXCL和IPC_CREAT,会出现两个结果:创建一个新的队列,如果该队列存在,则调用将出错,并返回-1。IPC_EXCL本身是没有声明用处的,但在IPC_CREAT组合使用时,可以保证没有一个现存的队列为了访问而被打开。例如:下面创建一个消息队列:
...
key key;
int msg_flags,msg_id;
msg_flags = IPC_CREAT | IPC_EXCL;
msg_id = msgget(key,msg_flags(0x0666);
if( -1 == msg_id)
{
printf("消息队列建立失败\n");
}
...
7.发送消息msgsnd()函数
一旦获得队列标识符,用户就可以开始在该消息队列上执行相关操作,为了向队列传递消息,可以使用msgsnd()函数:
#incldue<sys/types.h>
#include<sys/ipc.h>
#incldue<sys/msg.h>
int msgsnf(int msqid,const void *msgp,size_t msgsz,int msgflg);
第一个参数是队列标识符,是前面调用msgget()获得的返回值。第二个参数msgp,是一个void类型指针,指向一个消息缓冲区。msgsz参数则包含着消息的大小。以字节为单位,其中不包括消息类型的长度(4个字节长)。
msgflg参数可以设置为0(表示忽略),也可以设置为IPC_NOWAIT 。如果消息队列已满,则消息将不会被写入到队列中。如果没有指定IPC_NOWAIT ,则调用进程将被中断(阻塞),直到可以写消息为止,例:下面的代码已经向打开的消息队列发送信息:
...
struct msgmbuf{
int mtype;
char mtext[10];
};
int msg_sflags;
int msg_id;
struct msgmbuf msg_mbuf;
msg_sflags = IPC_NOWAIT;
msg_mbuf.mtype = 10;
memcpy(msg_mbuf.mtext,“测试消息”,sizeof("测试消息"));
ret = msgsnd(msg_id,&msg_mbuf,sizeof(“测试消息”,msg_sflags);
if(-1 == ret)
{
printf("发送消息失败\n");
}
...
将发送消息打包到msg_mbuf.ntext 域中,然后调用msgsnd发送消息给内核。这里mtype设置了类型为10,当接受时必须设置此域为10,才能接收到这时发送的消息。msgsnd()函数的msg_id是之前msgget创建的。
8.接收消息msggrcv(0函数
当获得队列标识符后,可以在该消息队列上执行消息队列的接受操作。msgrcv()函数用于接受队列标识符中的信息,函数原型如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
ssize_t msgrcv(int msqid,void *msgp,size_t msgsz,long msgtyp,int msgflg);
-
第1个参数msqid是用来指定,在消息获取过程中所使用的队列(该值是由前面调用msgget()得到的返回值)。 -
第2个参数msgp代表消息缓冲区变量的地址,获取的消息将存放在这里。 -
第3个参数msgsz代表消息缓冲区结构的大小,不包括mtype成员的长度。 -
第4个参数mtype指定要从队列中获取的消息类型。内核将查找队列中具有匹配类型的第一 个到达的消息,并把它复制返回到由msgp 参数所指定的地址中。如果mtype参数传送 一 个为0 的值,则将返回队列中最老的消息,不管该消息的类型是什么。 -
如果把IPC_NOWAIT作为 一 个标志传送给该函数,而队列中没有任何消息,则该次调用将会向调用进程返回ENOMSG。否则,调用进程将阻塞,直到满足msgrcv()参数的消息到达队列为止。 -
如果在客户等待消息的时候队列被删除了,则返回EIDRM。如果在进程阻塞并等待消息的到来时捕获到 一 个信号,则返回EINTR 。
函数 msgrcv 的使用代码如下:
msg_rflags = IPC_NOWAIT MSG_NOERROR;
ret = msgrcv(msg_id,&msg_mbuf,10,msg_rflags);
if( -1 == ret)
{
printf("接受消息失败\n");
}
else
{
printf("接收信息成功,长度:%d\n",ret);
}
上面的代码将mtype设置为10,可以获得之前发送的内核的消息获得(因为之前发送的mtypes值也设置为10),msgrcv返回值为接收到消息长度。
9.消息控制msgctl()函数
已经了解如何简单地创建和利用消息队列,下面介绍一种如何直接地对那些与特定的消息队列相联系的内部结构进行操作,可以使用**msgctl()**函数在消息队列上执行控制操作。
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
msgclt()向内核发送cmd命令,内核根据此来判断进行何种操作,buf为应用层和内核空间进行数据交换的指针。其中的cmd可以为下值:
-
IPC_STAT: 获取队列的msqid_ds结构,并把它存放在buf变量所指定的地址中,通过这种方式,应用层可以获得当前消息队列的设置情况,例如是否有消息到来、消息队列的缓冲区设置等。 -
TPC_SET: 设置队列的msqid_ds结构的ipc_perm成员值,它是从buf中取得该值的。通过 IPC_SET 命令,应用层可以设置消息队列的状态,例如修改消息队列的权限,使其他用户可以访问或者不能访问当前的队列;甚至可以设置消息队列的某些当前值来伪装。 -
IPC_RMID: 内核删除队列。使用此命令执行后,内核会把此消息队列从系统中删除。
消息队列的一个例子
本例建立消息队列后,打印其属性,并在每次发送和接收后均查看其属性,最后对消息队列进行修改。
1.显示信息属性的函数msg_show_attr()
**msg_show_ attr()**函数根据用户输入的消息 ID , 将消息队列中的字节数、消息数、最大字节数、最后发送消息的进程、最后接收消息的进程、最后发送消息的时间、最后接收消息的时间、最后消息变化的时间,以及消息的 UID 和 GID 等信息进行打印。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/msg.h>
#include<unistd.h>
#include<time.h>
#include<sys/ipc.h>
void msg_show(int msg_id,struct msqid_ds msg_info)
{
int ret = -1;
sleep(1);
ret = msgctl(msg_id,IPC_STAT,&msg_info);
if(-1 == ret)
{
printf("获得消息信息失败\n");
return;
}
printf("\n");
printf("现在队列中的字节数:%ld\n",msg_info.msg_cbytes);
printf("队列消息数:%d\n",(int)msg_info.msg_qnum);
printf("队列中最大字节数:%d\n",(int)msg_info.msg_qbytes);
printf("最后发送消息的进程pid:%d\n",msg_info.msg_lspid);
printf("最后接收消息的进程pid:%d\n",msg_info.msg_lrpid);
printf("最后发送消息的时间:%s",ctime(&(msg_info.msg_stime)));
printf("最后接收消息的时间:%s",ctime(&(msg_info.msg_rtime)));
printf("最后变化时间:%s",ctime(&(msg_info.msg_ctime)));
printf("消息UID是:%d\n",msg_info.msg_perm.uid);
printf("消息GID是:%d\n",msg_info.msg_perm.gid);
}
2.主函数
主函数先用函数ftok()使用路径"/tmp/msg/b"获得一个键值,之后进行相关的操作并打印消息的属性:
- 调用函数**msgget()**获得一个消息后,打印消息的属性。
- 调用函数**msgsnd()**发送一个消息后,打印消息的属性。
- 调用函数**msgrcv()**接收一个消息后,打印消息的属性。
int main(void)
{
int ret = -1;
int msg_flags,msg_id;
key_t key;
struct msgmbuf{
int mtype;
char mtext[10];
};
struct msqid_ds msg_info;
struct msgmbuf msg_mbuf;
int msg_sflags,msg_rflags;
char *msgpath = "/ipc/msg/";
key = ftok(msgpath,'b');
if(key !=-1)
{
printf("成功建立KEY\n");
}
else
{
printf("建立KEY失败\n");
}
msg_flags = IPC_CREAT|IPC_EXCL;
msg_id = msgget(key,msg_flags|0x0666);
if(-1 ==msg_id)
{
printf("消息建立失败\n");
return 0;
}
msg_show_atr(msg_id,msg_info);
msg_sflags = IPC_NOWAIT;
msg_mbuf.mtype = 10;
memcpy(msg_mbuf.mtext,"测试信息",sizeof("测试信息"));
ret = msgsnd(msg_id,&msg_mbuf,sizeof("测试信息"),msg_sflags);
if(-1 == ret)
{
printf("发送消息失败\n");
}
msg_show_attr(msg_id,msg_info);
msg_info.msg_perm.uid = 8;
msg_info.msg_perm.gid = 8;
msg_info.msg_qbytes = 12345;
ret = msgctl(msg_id,IPC_RMID,NULL);
if(-1==ret)
{
printf("设置消息属性失败\n");
return 0;
}
}
编译运行:
信号量
信号量是一种计数器,用来控制对多个进程共享的资源所进行的访问。它们常常被用做一个锁机制,在某个进程正在对特定资源进行操作时,信号可以防止另一个进程去访问它,生产者和消费者的模型是信号量的典型使用。
1.信号量数据结构
信号量数据结构是信号量程序设计中经常使用的数据结构,由于在之后的函数经常用到,这里将结构的原型列出来,便于查找:
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
2.新建信号量semget()
semget()函数用于创建一个新的信号量集合,或者访问现有的集合。其原型如下,其中第1个参数key是ftok生成键值,第2个参数nsems参数可以指定新的集合中应该创建的信号量的数目,第3个参数semflsg是打开信号的方式。
#include<sys/types.h>
#incllude<sys/ipc.h>
#include<sys/sem.h>
int semget(key_t key,int nsems,int semflg);
semflsg是打开信号量的方式:
-
IPC_CREAT: 如果内核中不存在这样的信号量集合,则把它创建出来。 -
IPC_EXCL: 当与IPC_CREAT一起使用时,如果信号量集合早已存在,则操作将失败。如果单独使用IPC _ CREA T, semget()或者返回新创建的信号量集合的信号量集合标识符;或者返回早已存在的具有同 个关键字值的集合的标识符。如果同时使用IPC_EXCL和IPC_CREAT, 那么将有两种可能的结果:如果集合不存在,则创建一个新的集合;如果集合早已存在,则调用失败,并返回-1。IPC_EXCL本身是没有什么用处的,但当与IPC_CREAT组合使用时,它可以用于防止为了访问而打开现有的信号量集合。
typedef int sem_t;
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
}arg;
sem_t CreateSem(key_t key,int value)
{
union semun sem;
sem_t semid;
sem.val = val;
semid = semget(key,0,IPC_CREAT|0666);
if(-1 == semid)
{
printf("create semaphore error\n");
return -1;
}
semctl(semid,0,SETVAL,sem);
return semid;
}
CreateSem()函数按照用户的键值生成一个信号量,把信号量的初始值设为用户输入的value。
3.信号量操作函数semop()
信号量的P、V操作是通过向语已经建立好的信号量(使用semget()函数),发送命令来完成的。向信号量发送命令的函数是semop(),原型如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semop(int semid,struct sembuf *sops,unsigned nsops);
函数中第2个参数(sops)是一个指针,指向将要在信号量集合上执行操作的一个数组,第3个参数(nsops)则是该数组中操作的个数。sops参数指向的是类型为sembuf结构的一个数组。sembuf结构是在linux/sem.h中定义:
struct sembuf{
ushort sem_num;
short sem_op;
short sem_flag;
};
-
sem_num:用户要处理的信号量的编号。 -
sem_opp:将要执行的操作(正、负或者零)。 -
sem_flg:信号量操作的标准。如果sem_op为负,则从信号量中减掉一个值。为正,则从信号量中加上值。如果为0,则将进程设置为睡眠状态,直到信号量的值为0为止。
例如"struct sembuf sem= {O, +1, NOWAJT}; "表示对信号量 0,进行加1的操作。
用函数semop()可以构建基本的P、V操作,代码如下所示。Sem_P构建{O, +I, NOWAJT} 的sembuf结构来进行增加1个信号量值的操作; Sem_V构建{O, -1, NOWAJT} 的sembuf结构来进行减少1个信号量的操作,所对应的信号量由函数传入(semid)。
int Sem_P(sem_t semid)
{
struct sembuf sops={0,+1,IPC_NOWAIT};
return (semop(semid,&sops,1));
}
int Sem_V(sem_t semid)
{
struct sembuf sops = {0,-1,IPC_NOWAIT};
return (semop(semid,&sops,1));
}
4.控制信号量参数semctl()函数
与文件操作的ioctl()函数类似,信号量的其他操作是通过函数semctl()来完成。函数semctl()的原型如下:
#include<sys/types.h>
#include<sys/ipc.h>
#inlcude<sys/sem.h>
int semctl(int semid,int semnum,int cmd,...);
-
函数semctl()用于在信号量集合上执行控制操作。这个调用类似于函数msgctl(), msgctl() 函数是用于消息队列上的操作。 -
第1个参数是关键字的值(在我们的例子中它是调用semget()函数所返回的值)。 -
第2个参数(semun)是将要执行操作的信号量的编号,它是信号量集合的 一 个索引值,对于集合中的第 1个信号量 (有可能只有这一 个信号量)来说,它的索引值将是 一 个为0 的值。
cmd 参数代表将要在集合上执行的命令。其取值如下所述:
-
IPC_STAT: 获取某个集合的semid_ds结构,并把它存储在semun联合体的buf参数所指定的地址中。 -
IPC_SET:设置某个集合的semid_ds结构的ipc_perm成员的值。该命令所取的值是从semun联合体的buf参数中取到的。 -
IPC_RMID : 从内核删除该集合。 -
GETALL: 用于获取集合中所有信号量的值。整数值存放在无符号短整数的一个数组中,该数组由联合体的array成员所指定。 -
GETNCNT: 返回当前正在等待资源的进程的数目。 -
GETPID: 返回最后一 次执行 semop 调用的进程的PID 。 -
GETVAL: 返回集合中某个信号量的值。 -
GETZCNT: 返回正在等待资源利用率达到百分之百的进程的数目。 -
SETALL: 把集合中所有信号量的值,设置为联合体的array成员所包含的对应值。 -
SETVAL: 把集合中单个信号量的值设置为联合体的val成员的值。 -
参数arg代表类型semun的 一 个实例。这个特殊的联合体是在 Linux/sem .h 中定义的。 -
val: 当执行SETVAL命令时将用到这个成员,它用于指定要把信号量设置成什么值。 -
buf: 在命令IPC_STAT/IPC_SET中使用。它代表内核中所使用的内部信号量数据结构的一 个复制。 -
array: 用在GETALL/SET ALL命令中的 一 个指针。它应当指向整数值的一 个数组。在设置或获取集合中所有信号量的值的过程中,将会用到该数组。 -
剩下的参数**_buf和_pad**将在内核中的信号量代码的内部使用,对于应用程序开发人员来说,它们用处很少,或者说没有用处。这两个参数是Linux操作系统所特有的,在其他的UNIX实现中没有。
利用semctl()函数设置和获得信号量的值构建同用的函数:
void SetvalueSem(sem_t semid,int value)
{
union semun sem;
sem.val = value;
semctl(semid,0,SETVAL,sem);
}
int GetvalueSem(sem_t semid)
{
union semun sem;
return semctl(semid,0,GETVAL,sem);
}
void DestroySem(sem_t semid)
{
union semun sem;
sem.val = 0;
semctl(semid,0,IPC_RMID,sem);
}
5.信号量操作的例子
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
typedef int sem_t;
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
}arg;
sem_t CreateSem(key_t key,int value)
{
union semun sem;
sem_t semid;
sem.val = value;
semid = semget(key,0,IPC_CREAT|0666);
if(-1 == semid)
{
printf("create semaphore error\n");
return -1;
}
semctl(semid,0,SETVAL,sem);
return semid;
}
int Sem_P(sem_t semid)
{
struct sembuf sops = {0,+1,IPC_NOWAIT};
return (semop(semid,&sops,1));
}
int Sem_V(sem_t semid)
{
struct sembuf sops = {0,-1,IPC_NOWAIT};
return (semop(semid,&sops,1));
}
void SetvalueSem(sem_t semid,int value)
{ union semun sem;
sem.val = value;
semctl(semid,0,SETVAL,sem);
}
int GetvalueSem(sem_t semid)
{
union semun sem;
return semctl(semid,0,GETVAL,sem);
}
void DestroySem(sem_t semid)
{
union semun sem;
sem.val = 0;
semctl(semid,0,IPC_RMID,sem);
}
int main(void)
{
key_t key;
int semid;
char i;
int value = 0;
key = ftok("/ipc/sem",'a');
semid = CreateSem(key,100);
for(i=0;i<=3;i++){
Sem_P(semid);
Sem_V(semid);
}
value = GetvalueSem(semid);
printf("信号量为:%d\n",value);
DestroySem(semid);
return 0;
}
共享内存
1.创建共享内存函数
shmget()用于创建一个新的共享内存,或者访问一个现有的共享内存段,与消息队列和信号量集合对应的函数相似,原型如下:
#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(key_t key,size_t size,int shmfilg);
第一个参数是关键字的值,将与内核中现有的其他共享内存段的关键字比较。比较后,打开和访问操作都依赖于shmflg参数内容:
-
IPC_CREAT:内核不存在内存段,则创建。 -
IPC_EXCL:当与IPC_CREAT 一起使用时,如果该内存段早已存在,则此次调用将失败。 -
只使用IPC_CREAT,shmget()或者将返回新创建的内存段标识符,或者早已存在于内核中的具有相同关键字值的内存段的标识符。 -
同时使用IPC_CREAT和IPC_EXCL,会有两种结果:如果内存段不存在,将新创建一个新的内存段;如果早已存在,则此次调用失败,返回-1。 -
IPC_EXCL本身没有什么用处,但与IPC_CREAT组合使用,可以防止一个现有的内存段为了访问而打开着。一旦进程获得了给定内存段的合法IPC标识符,下一步操作就是连接该内存段,或者把该内存映射到自己的选址空间中。
2.获得共享内存地址函数shmat()
获取共享内存成功后,可以像使用通用内存一样对其进行读写操作,函数原型:
#include<sys/types.h>
#include<sys/shm.h>
void *shmat(int shmid,const,void *shmaddr,int shmflg);
int shmdt(const void *shmaddr);
-
如果shmaddr参数值等于0,则内核将试着查找一 个未映射的区域。用户可以指定一个地址,但通常该地址只用于访问所拥有的硬件,或者解决与其他应用程序的冲突。 -
SHM_RND标志可以与标志参数进行OR操作,结果再置为标志参数,这样可以让传送的地址页对齐(舍入到最相近的页面大小)。 -
如果把SHM_RDONLY标志与标志参数进行OR操作,结果再置为标志参数,这样映射的共享内存段只能标记为只读方式。 -
当申请成功时,对共享内存的操作与一 般内存一 样,可以直接进行写入和读出,以及偏移的操作。
3.删除共享内存函数shmdt()
函数shmdt()用于删除一段共享内存。函数原型如下:
#include<sys/types.h>
#include<sys/shm.h>
int shmdt(const,void *shmaddr);
-
当某进程不再需要一个共享内存段时,它必须调用这个函数来断开与该内存段的连接。 -
正如前面所介绍的那样,这与从内核删除内存段是两回事。在成功完成了断开连接操作以后,相关的shmid_ds结构的shm_nattch成员的值将减去1。如果这个值减到0, 则内核将真正删除该内存段。
4.共享内存控制函数shmctl()
共享内存的控制函数**smctl()的使用类似ioctl()**的方式对共享内存进行操作:
向共享内存的句柄发送命令来完成某种功能。
函数shmctl()的原型如下,其中shmid是共享内存的句柄,cmd是向共享内存发送的命令,最后一 个参数buf则是向共享内存发送命令的参数。
#include<sys/ipc.h>
#include<sys/shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
结构shmid_ds结构如下:
struct shmid_ds{
struct ipc_perm shm_perm;
size_t shm_segsz;
time_t shm_atime;
time_t shm_dtime;
time_t shm_ctime;
pid_t shm_cpid;
pid_t shm_lpid;
shmatt_t shm_mattch;
...
}
此函数与消息队列的msgctl()函数调用完全类似,合法命令值如下:
-
IPC_SET: 获取内存段的shmid_ds结构,并把它存储在buf参数所指定的地址中。 IPC_SET 设置内存段 shm id_ds 结构的 ipc_perm 成员的值,此命令是从 buf参数中获得该值的。 -
IPC_RMID: 标记某内存段,以备删除。该命令并不真正地把内存段从内存中删除。相反,它只是标记上该内存段,以备将来删除。只有当前连接到该内存段的最后一个进程正确地断开了与它的连接,实际的删除操作才会发生。当然,如果当前没有进程与该内存段相连接,则删除将立刻发生。为了正确地断开与其共享内存段的连接,进程需要调用 shmdt()函数。
5.一个共享内存的例子
下面在父进程和子进程之间利用共享内存进行通信,父进程向共享内存中写入数据,子进程读出数据,两个进程之间的控制采用了信号量的方法,父进程写入数据成功后,信号量加1,子进程在访问信号量之前先等待信号。
#include<stdio.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/sem.h>
static char msg[] = "你好,共享内存\n";
typedef int sem_t;
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
};
sem_t CreateSem(key_t key,int value)
{
union semun sem;
sem_t semid;
sem.val = value;
semid = semget(key,0,IPC_CREAT|0666);
if(-1 == semid)
{
printf("create semaphore error\n");
return -1;
}
semctl(semid,0,SETVAL,sem);
return semid;
}
int Sem_P(sem_t semid)
{
struct sembuf sops = {0,+1,IPC_NOWAIT};
return (semop(semid,&sops,1));
}
int Sem_V(sem_t semid)
{
struct sembuf sops = {0,-1,IPC_NOWAIT};
return (semop(semid,&sops,1));
}
void DestroySem(sem_t semid)
{
union semun sem;
sem.val = 0;
semctl(semid,0,IPC_RMID,sem);
}
int main(void)
{
key_t key;
int semid,shmid;
char i,*shms,*shmc;
struct semid_ds buf;
int value = 0;
char buffer[80];
pid_t p;
key = ftok("ipc/sem",'a');
shmid = shmget(key,1024,IPC_CREAT|0604);
semid = CreateSem(key,0);
p = fork();
if(p>0)
{
shms = (char *)shmat(shmid,0,0);
memcpy(shms,msg,strlen(msg)+1);
sleep(10);
Sem_P(semid);
shmdt(shms);
DestroySem(semid);
}
else if(p == 0)
{
shmc = (char *)shmat(shmid,0,0);
Sem_V(semid);
printf("共享内存的值为:%s\n",shmc);
shmdt(shmc);
}
return 0;
}
信号
信号用于在一个或多个进程之间传递异步信号,信号可以由各种异步事件产生,例如键中断等。Shell也可以使用信号将作业控制命令传递给它的子进程。
Linux使用kill命令**(kill -l)**查看信号:
- SIGABRT: 调用abort()函数时产生此信号,进程异常终止。
- SIGALRM: 超过用 alarm()函数设置的时间时产生此信号。
- SIGBUS: 指示一 个实现定义的硬件故障。
- SIGCHLD: 在一个进程终止或停止时,SIGCHLD 信号被送给其父进程。如果希望从父进程中了解其子进程的状态改变,则应捕捉此信号。信号捕捉函数中通常要调用 wait()函数以取得子进程ID 和其终止状态。
- SIGCONT: 此作业控制信号送给需要继续运行的处于停止状态的进程。如果接收到此信号的进程处于停止状态,则操作系统的默认动作是使该停止的进程继续运行,否则默认动作是忽略此信号。
- SIGEMT: 指示一个实现定义的硬件故障。
- SIGFPE: 此信号表示一 个算术运算异常,例如除以0, 浮点溢出等。
- SIGHUP:如果终端界面检测到一个连接断开,则将此信号送给与该终端相关的进程。
- SIGILL: 此信号指示进程已执行一条非法硬件指令。
- SIGINT: 当用户按中断键(一 般采用Delete 或 Ctrl+C ) 时,终端驱动程序产生这个信号并将信号送给前台进程组中的每 一 个进程。当一 个进程在运行时失控,特别是它正在屏幕上产生大量不需要的输出时,常用此信号终止它。
- SIGIO: 此信号指示 一 个异步 IO 事件。
- SIGIOT: 这指示一个实现定义的硬件故障。
- SIGPIPE: 如果在读进程时已终止写管道,则产生此信号。
- SIGQUIT: 当用户在终端上按退出键( 一 般采用 Ctrl+C ) 时,产生此信号,并送至前台进程组中的所有进程。
- SIGSEGV: 指示进程进行了 一 次无效的存储访问。
- SIGSTOP: 这是一个作业控制信号,它停止一个进程。
- SIGSYS: 指示一 个无效的系统调用。由于某种未知原因,某个进程执行了一 条系统调用命令,但是调用命令所用的参数无效。
- SIGTERM: 这是由kill命令发送的系统默认终止信号。
- SIGTRAP: 指示一 个实现定义的硬件故障。
- SIGTSTP:交互停止信号,当用户在终端上按挂起键( 一 般采用 Ctrl+Z ) 时,终端驱动程序产生此信号。
- SIGTTIN:当一 个后台进程组进程试图读其控制终端时,终端驱动程序产生此信号。
- SIGTTOU:当一 个后台进程组进程试图写其控制终端时产生此信号。
- SIGURG:此信号通知进程已经发生 一 个紧急情况。在网络连接上,接到非规定波特率的数据时,此信号可选择地产生。
- SIGUSR1:这是一 个用户定义的信号,可用于应用程序。
- SIGUSR2: 这这是一个用户定义的信号,可用于应用程序。
1.信号截取函数signal()
用于截取系统的信号,对此信号挂接用户自己的处理函数。原型如下:
#include<signal.h>
typedef void(*sighandler_t) (int);
sighandler_t signal(int signum,sighandler_t handler);
-
signal()函数返回一个函数指针,而指针指向的函数无返回值(void)。 -
第一个参数是个整型数,第二个参数是函数指针,指向的函数需要一个整型参数,无返回值。(向信号处理程序传送一个整型参数,却无返回值)。 -
调用signal设置处理程序时,第2个参数指向该函数(信号处理程序)的指针。signal的返回值指向以前信号处理程序的指针。
例:下面代码截取了系统的信号SIGSTOP和SIGKILL,用命令kill杀死其不可能的。
#include<signal.h>
#include<stdio.h>
typedef void(*sighandler_t)(int);
static void sig_handle(int signo)
{
if(SIGSTOP==signo)
{
printf("接受到信号SIGSTOP\n");
}
else if(SIGKILL==signo)
{
printf("接受到信号SIGKIL\n");
}
else
{
printf("接受到信号:%d\n",signo);
}
return;
}
int main(void)
{
sighandler_t ret;
ret = signal(SIGSTOP,sig_handle);
if(SIG_ERR == ret)
{
printf("为SIGSTOP挂接处理函数失败\n");
return -1;
}
ret = signal(SIGKILL,sig_handle);
if(SIG_ERR == ret)
{
printf("为SIGSTOP挂接信号处理函数失败\n");
return -1;
}
for(;;);
}
2.向进程发送信号函数kill()和raise()
在挂接信号处理函数后,可用等待系统信号的到来。同时,用户可以自己构建信号发送到目标进程中,此类函数有kill()和raise()函数,函数原型如下:
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
int raise(int sig);
-
kill()函数向进程号为pid的进程发送信号,信号为sig。 -
pid为0时,向当前系统所有进程发送信号sig,即“群发”的意思 -
raise()函数在当前进程中自举一个信号sig,即向当前进程发送信号。
注:kill()函数的名称虽然为“杀死”意思,但是并不是杀死某个进程,而是向某个进程发送信号,这个信号除了SIGSTOP和SIGKILL,一般不会使进程显示地退出。
四、 Linux下的线程
使用线程的优点:
-
系统资源消耗低。 -
速度快。 -
线程间的数据共享比进程容易得多。
多线程编程实例
Linux系统下的多线程遵循POSIX标准,叫做pthread,可以使用man pthread在Linux系统下查看系统对线程的解释。编写线程需要包含头文件pthread.h ,在生成可执行文件的时候需要链接库libpthread.a或者libpthread.so。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
static int run = 1;
static int retvalue;
void *start_routine(void *arg)
{
int *running = arg;
printf("子线程初始化完毕,传入参数:%d\n",*running);
while(*running)
{
printf("子线程正在运行\n");
usleep(1);
}
printf("子线程退出\n");
retvalue=8;
pthread_exit( (void*)&retvalue);
}
int main(void)
{
pthread_t pt;
int ret = -1;
int times = 3;
int i = 0;
int *ret_join = NULL;
ret = pthread_create(&pt,NULL, (void*)start_routine,&run);
if(ret !=0)
{
printf("建立线程失败\n");
return 1;
}
usleep(1);
for(;i<times;i++)
{
printf("主线程打印\n");
usleep(1);
}
run = 0;
pthread_join(pt,(void*)&ret_join);
printf("线程返回值:%d\n",*ret_join);
return 0;
}
编译执行,生成链接线程库libpthread:
gcc -o pthread pthread.c -lpthread
再次运行时,结果可能不同,主要是两个线程争夺CPU资源造成的。
Linux下线程创建函数pthread_create()
此函数调用时,传入的参数有线程属性、线程函数、线程函数变量,用于生成一个某种特性的线程,线程中执行线程函数。创建线程使用函数pthread_create():
int pthread_create(pthread_t * thread,
pthread_attr_t * attr,
void * (*start_routine)(void *),
void * arg);
-
thread:用于标识一 个线程,它是一 个 pthread_t类型的变量,在头文件 pthreadtypes.h中定义,typedef unsigned long int pthread_t ; -
attr:用于设置线程的属性,本例设置空,采用默认属性。 -
start_routine:线程的资源分配成功后,线程中所运行的单元,上例设置为自己编写的一个函数start_routine()。 -
arg:线程函数运行时传入的参数,上例将一个run的参数传入用于控制线程的结束。
- 当创建线程成功时,函数返回0; 若不为0, 则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。
- EAGAIN表示:系统中的线程数量达到了上限,EINVAL:表示线程的属性非法。
- 线程创建成功后,新创建的线程按参数3和参数4确定一个运行函数,原来的线程在线程创建函数返回后继续运行下一行代码。
线程的结束函数pthread_join()和函数pthread_exit()
pthread_jion()用来表示一个线程运行结束,为阻塞函数,一直等待线程结束为止,函数才返回并且收回被等待线程的资源,:
extern int pthread_join _P (pthread_t _th,void **_thread_return));
-
_th :线程的标识符,即pthread_create()函数创建成功的值。 -
_thread_return :线程返回值,它是一个指针,可以用来存储被等待线程的返回值。 -
如ret_join, 当线程返回时可以返回一 个指针,pthread_join()在等待的线程返回时,获得 此值。 -
这个参数是一 个指向指针的指针类型参数,在调用此函数来获得线程参数传出的时候需要注意,通常用一个指针变量的地址来表示。 -
上面的代码中先建立一个 int 类型的指针,int *ret_join = NULL , 然后调用函数pthread_join()来获得线程退出时的传出值pthread_join(pt,(void*)&retjoin) 。
线程函数(上例start_routine())的结束方式有两种,一种是线程函数运行结束,不用返回结果;另一种方式是通过函数pthread_exit()实现。将结果传出。原型如下:
extern void pthread_exit _P ((void *_retval)) _attribute_ ((_noretuen_));
线程的属性
pthread_create()函数创建线程时,使用了默认参数,即将该函数的第二个参数设为NULL。通常时,建立一个线程时,使用默认属性就够了,但很多时候需要调整线程属性,特别是线程的优先级。
1.线程的属性结构
线程的属性结构为pthread_attr_t,在头文件<phtreadtypes.h>中定义:
typedef struct _pthread_attr_s
{
int _detachstate;
int _schedpolicy;
struct _sched_param _schedparam;
int _inheritsched;
int _scope;
size_t _guardsize;
int _stackaddr_set;
void *_stackaddr;
size_t _stacksize;
}pthread_attr_t;
但是线程的属性值不能直接设置,须使用相关函数进行操作。线程属性的初始化函数为pthread_attr_init(), 这个函数必须在**pthread_ create()**函数之前调用。 属性对象主要包括线程的摘取状态、调度优先级、运行栈地址、运行栈大小、优先级。
2.线程的优先级
线程的优先级是经常设置的属性,由两个函数进行控制:**pthread_attr_getschedparam()函数获得线程的优先级设置; 函数pthread_attr_setschedparam()**设置线程的优先级。
int pthread_attr_sedschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t *attr,struct sched_param *param);
线程的优先级存放在结构 sched_param 中。其操作方式是先将优先级取出来,然后对需要设置的参数修改后再写回去,这是对复杂结构进行设置的通用办法,防止因为设置不当造成不可预料的麻烦。例如设置优先级的代码如下,因为结构 sched_param 在头文件sched.h 中,所以要加入头文件sched.h。
#include<stdio.h>
#include<pthread.h>
#include<sched.h>
pthread_attr_t attr;
struct sched_param sch;
pthread_t pt;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, &sch);
sch.sched_priority = 256;
pthread_attr_setschedparam(&attr,&sch);
pthread_create(&pt,&attr,(void*)start_routine,&run);
3.线程的绑定状态
设置线程绑定状态的函数pthread_attr_setscope(),有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,包含两个值:PTHREAD_SCOPE_SYSTEM (绑定的)和PTHREAD_SCOPE_PROCESS (非绑定的),下面创建一个绑定的线程:
#include<pthread.h>
pthread_attr_t attr;
pthread_t tid;
pthread_attr_setscope(&attr,PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid,&attr,(void *) my_function,NULL);
4.线程的分离状态
-
线程分离状态决定线程的终止方法,线程的分离状态有分离线程和非分离线程两种。 -
上面的例子中,线程建立的时没有设置属性,默认终止方法为非分离状态。这种情况下。需要等待创建线程结束。只有当pthread_join()函数返回时,线程才算终止,并且释放线程创建的时候系统分配资源。 -
分离线程不用其他线程等待,当前线程运行结束后线程就结束了,并且马上释放资源。
线程的分离方式可以根据需要,选择适当的分离状态。设置线程状态函数:
int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate);
- 参数detachstate可以为分离线程或者非分离线程,
PTHREAD_CREATE_DETACHED 用于设置分离线程。PTHREAD_CREATE_JOINABLE 用于设置非分离线程。
- 当将一个线程设置为分离线程时,如果线程的运行非常快,可能在 pthread_ create() 函数返回之就终止了。
- 由于一个线程在终止以后可以将线程号和系统资源移交给其他的线程使用,此时再使用函数pthread_ create()获得的线程号进行操作会发生错误。
线程间的互斥
互斥锁是用来保护一段临界区的,可以保证某时间段内只有一 个线程在执行一 段代码或者访问某个资源。下面一 段代码是一 个生产者/消费者的实例程序,生产者生产数据,消费者消耗数据,它们共用一 个变量,每次只有一 个线程访问此公共变量。
1.线程互斥的函数介绍
与线程互斥有关的函数原型和初始化的常量如下,主要包含互斥的初始化方式宏定义、互斥的初始化函数pthread_mutex_init() 、互斥的锁定函数pthread_mutex _lock() 、互斥的预锁定函数pthread_mutex_trylock() 、互斥的解锁函数pthread_ mutex_unlock() 、互斥的销毁函数pthread_ mutex _ destroy() 。
#include <pthread.h>
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthrea _mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP; pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mμtex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
-
函数pthread_mutex _init() , 初始化一个mutex变量,结构 pthread_mutex_t 为系统内部 私有的数据类型,在使用时直接用pthread_mutex _t 就可以了,因为系统可能对其实现进行修改。在上例中属性为NULL, 表明使用默认属性。 -
pthread_mutex _lock() 函数声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex _ unlock() 函数为止,均不能执行被保护区域的代码,也就是说,在同一时间内只能有一个线程执行。 -
当一个线程执行到pthread_mutex_lock() 函数处时,如果该锁此时被另一个线程使用,此线程被阻塞,即程序将等待另一 个线程释放此互斥锁。 -
互斥锁使用完毕后记得要释放资源,调用pthread_mutex_destroy() 函数进行释放。
2.线程互斥函数的例子
用线程互斥的方法构建一个生产者和消费者:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<sched.h>
void *producter_f (void *arg);
void *consumer_f(void *arg);
int buffer_has_item=0;
pthread_mutex_t mutex;
int running = 1;
int main(void)
{
pthread_t consumer_t;
pthread_t producter_t;
pthread_mutex_init(&mutex,NULL);
pthread_create(&producter_t,NULL,(void*)producter_f,NULL);
pthread_create(&consumer_t,NULL,(void *)consumer_f,NULL);
usleep(1);
running = 0;
pthread_join(consumer_t,NULL);
pthread_join(producter_t,NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
void *producter_f(void *arg)
{
while(running)
{
pthread_mutex_lock(&mutex);
buffer_has_item++;
printf("生产,总数:%d\n",buffer_has_item);
pthread_mutex_unlock(&mutex);
}
}
void *consumer_f(void *arg)
{
while(running)
{
pthread_mutex_lock(&mutex);
buffer_has_item--;
printf("消费,总数量:%d\n",buffer_has_item);
pthread_mutex_unlock(&mutex);
}
}
用线程互斥锁函数pthread_mutex_lock() 和函数pthread_mutex_unlock() 来保护对公共变量buffer_has_item 的访问。
线程中使用信号量
-
线程的信号量与进程的信号量类似,但是使用线程的信号量可以高效地完成基于线程的资源计数。 -
信号量实际上是一个非负的整数计数器 ,用来实现对公共资源 的控制 。 -
在公共资源增加的时候,信号量的值增加;公共资源消耗的时候,信号量的值减少;只有当信号量的值大于0的时候,才能访问信号量所代表的公共资源。 -
信号量的主要函数:初始化函数sem_init()、信号量的销毁函数sem_destroy()、信号量的增加函数sem_post()、信号量的减少函数sem_wait()等。 -
函数sem_trywait()与互斥的函数pthread_mutex_trylock()是一致的,先对资源是否可用进行判断。函数的原型在头文件semaphore.h 中定义。
1.线程信号初始化函数sem_init()
extern int sem_init _P ((sem_t *_sem,int _pshared,unsigned int _value));
- 参数sem指向信号量结构的一个指针,信号量初始化成功可以使用这个指针进行信号量的增加减少操作。
- 参数pshared用于表示信号量的共享类型,不为0时信号量可以在进程间共享,否则只能当前进程的多个线程之间共享。
- 参数value用于设置信号量初始化的时候信号量的值。
2.线程信号量增加函数sem_post()
此函数每次增加的值为1,当有线程等待这个信号量时,等待的线程将返回。函数原型如下:
#include<semaphore.h>
int sem_post(sem_t *sem);
3.线程信号量等待函数sem_wait()
此函数用于减少信号量的值,如果信号量为0,则线程将一直阻塞到信号量大于0为止。每次使信号量的值减少1.当信号量的值为0时不再减少。:
#include<semaphore.h>
int sem_wait(sem_t *sem);
4.线程信号量销毁函数sem_destroy()
此函数用于释放信号量sem:
#include<semaphore.h>
int sem_destroy(sem_t *sem);
5.线程信号量的例子
在线程互斥例子中,使用全局变量来计算,本例使用信号量来做相同工作,其中一个线程增加信号来模仿生产者,另一个线程获得信号量来模仿消费者。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<semaphore.h>
void *producter_f (void *arg);
void *consumer_f(void *arg);
sem_t sem;
int running = 1;
int main(void)
{
pthread_t consumer_t;
pthread_t producter_t;
sem_init(&sem,0,16);
pthread_create(&producter_t,NULL,(void*)producter_f,NULL);
pthread_create(&consumer_t,NULL,(void *)consumer_f,NULL);
sleep(1);
running = 0;
pthread_join(consumer_t,NULL);
pthread_join(producter_t,NULL);
sem_destroy(&sem);
return 0;
}
void *producter_f(void *arg)
{
int semval = 0;
while(running)
{
usleep(1);
sem_post(&sem);
sem_getvalue(&sem,&semval);
printf("生产,总数量:%d\n",semval);
}
}
void *consumer_f(void *arg)
{
int semval =0;
while(running)
{
usleep(1);
sem_wait(&sem);
sem_getvalue(&sem,&semval);
printf("消费。总数量:%d\n",semval);
}
}
编译运行: 执行结果看出,各个线程间存在竞争关系。而数值并未按产生一个消耗一个的顺序显示出来,而是以交叉的方式进行,有的时候产生多个后再消耗多个,造成这种现象的原因是信号量的产生和消耗线程对CPU竞争的结果。
|