Linux中的进程基础知识
进程:执行中的程序实例叫做进程。Linux是多任务系统,可以运行多个进程并发执行。进程和程序的关系:进程由程序创建,一个程序可以产生多个进程。进程由内核管理调度。每个进程有一个进程号PID。进程的层次结构,除了第一个进程外每个进程都有一个父进程。(shell进程的父进程是init进程,子进程的属性继承自父进程)
ps命令:显示进程属性。
ps [options]
-e或-A 显示包含用户和系统进程的所有进程
-a 显示所有用户进程
-ax 显示系统进程
-f 显示PPID等详细进程信息
-l 显示进程状态等相关信息
-u 显示CPU和内存占用情况
wh1516@PinkRAY:~$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 2629 2620 0 80 0 - 6087 wait pts/0 00:00:00 bash
0 R 1000 2637 2629 0 80 0 - 7667 - pts/0 00:00:00 ps
//PPID 父进程的进程号 UID 创建者
进程状态:进程在任意时刻都处于一个特定状态。进程状态可用ps -l命令查看(列表中的第二列,僵尸进程在最后一列显示<defunct>)
可运行状态R | 进程创建之后,实际运行之前 |
运行状态O | 进程实际运行时 |
睡眠状态S | 在等待输入信号时 |
深度睡眠状态D | 进程不响应异步信号 |
挂起状态T | 用户按下Ctrl+z挂起进程时 |
僵尸状态Z | 父进程不再等待该进程的退出状态时 |
进程的虚拟地址空间:一个进程在运行一个C程序时,会在内存中开辟一块虚拟地址空间供程序访问。
命令行参数和环境变量 |
栈(stack) |
动态共享库 |
堆(heap) |
数据 |
程序文本 |
虚拟地址空间的构成:
程序文本:包含要执行的指令
数据:程序使用的全局、静态变量
栈:函数的参数和局部变量以及要返回的地址
堆:动态内存分配(malloc,calloc)
命令行参数和环境变量存储在栈的底部
动态共享库的使用位于栈和堆之间
init进程:Linux系统启动时,系统中只有一个可见进程,叫init,其PID为1。在Linux创建进程的唯一方法是复制现有进程,所以init进程是所有随后进程的祖先进程。复制自身、修改内容,达到创建新进程的效果。init进程绝不会终止。
守护进程:没有和终端相关联的进程,不能读和写终端,守护进程处于睡眠状态,在接受输入信号时被唤醒,在TTY列中用?表示。(打印机守护进程 lpsched,邮件收发进程sendmail)
进程控制
任务(job)控制:任务是一组进程,如管道组合 ls | head 作为一个整体被控制。
任务后台运行:
①在命令行末尾加上&:command &
父进程不会等待子进程的退出,shell会显示该命令的PGID(jobID)和PID。仍处于running状态。任务的标准输出和错误输出到终端。最后一个后台进程ID被存储在 $! 变量中。
②nohup命令:shell终端退出后可以保证进程不终止。格式:nohup command &
任务的标准输出和错误输出到nohup.out,在用户注销之后后台程序依然运行。
每个进程都属于一个进程组(任务),组中的每个进程有相同的PGID。发送给进程组的信号会发送给组中每一个进程。后台任务读取终端输入时会被挂起。使用stty tostop命令使任务在每次向终端输出时被挂起。
任务控制命令 | |
%PGID | 任务ID |
%str | 以str开头的任务名 |
%?str | 包含str的任务名 |
作业控制命令 | 功能 |
fg | 将任务移到前台运行 |
bg | 将任务移到后台运行 |
[Ctrl+z] | 挂起当前的前台任务 |
jobs | 列出活动任务 |
kill | 杀死任务 |
在C语言中实现进程控制:C语言提供了一系列的系统调用和库函数来实现对进程的控制。头文件:#include <unistd.h>
查看进程属性的系统调用:
int getpid();
int getppid();
int getuid();
int getgid();
char* getenv(const char *name);
int setenv(const char *name, const char *val, int overwrite);
进程创建:一个进程只能由另一个进程来创建。
进程创建机制包含3个部分:
fork:创建已有进程的副本来创建新进程,新进程是原进程的子进程。
exec:运行一个程序,用新程序的代码和数据来覆盖自己的映像。
wait:父进程等待子进程执行程序,并在子进程退出后记录其退出状态。
例子:在shell中执行cat file命令。
shell进程fork自己的副本,生成新的shell进程。
新的shell进程用cat的可执行映像覆盖自己,运行cat命令。
原shell进程等待cat终止,然后获取子进程的退出状态。
子进程继承的属性:fork创建进程时,子进程会继承父进程的大部分属性(创建进程的用户UID和组GID、运行该进程的当前目录、父进程中打开的所有文件描述符、环境变量值)。继承的这些属性是由子进程维护的副本,不影响父进程。用户定义的变量不会由子进程继承(可使用export命令将变量导出至所有该进程创建的子进程中,子进程对变量的修改在父进程中不可见)。
fork:复制当前进程信息以创建新进程。在fork返回后,两个进程都在fork语句之后继续执行。
pid_t fork(void);
返回值:创建的子进程的PID
fork在父进程和子进程中都有返回值:
父进程中:子进程的PID
子进程中:0
exec函数族:运行程序,用新程序的地址空间覆盖进程地址空间。一个成功的exec函数族中的库函数的执行不会返回。
库函数execl, execv, execlp, execvp
int execl(const char *path, const char *arg0, ..., NULL);
//用哪一个命令替换当前的命令,每一个参数作为一个字符串放在后面,最后一个一定为NULL
int execv(const char *path, char *const argv[]);
//path参数给出程序的绝对或相对路径 argv为字符串数组
int execlp(const char *file, const char *arg0, ..., NULL);
int execvp(const char *file, char *const argv[]);
//file参数给出程序文件名称,在环境变量PATH指定的路径中寻找
僵尸进程:子进程终止,但父进程还没有接受其退出状态时,该子进程处于僵尸状态。
僵尸形成的机制:每一个进程退出时,内核释放其占用的资源,但是仍然保留其进程号和退出状态。父进程回收子进程的退出状态,在此之前子进程处于僵尸状态。若父进程退出,init进程会接管其所有子进程,回收他们的退出状态。若父进程未退出但没有回收子进程状态,则僵尸进程一直存在。
僵尸进程的危害:会占用有限的进程号资源。
避免僵尸进程:通过wait或waitpid函数。
wait:等待子进程死亡,并收集子进程退出状态。调用进程在执行wait时一直等待,直到其子进程终止。调用进程在wait返回后继续执行。
pid_t wait(int *stat)
参数:保存子进程退出状态的变量地址
返回值:死亡或挂起子进程的PID
进程通信
信号:一种进程间通信的简单形式。信号可以是同步的,也可以是异步的。
信号来源:
键盘输入:影响前台作业,[Ctrl+c]生成SIGINT信号终止进程,[Ctrl+z]生成SIGSTP信号暂停进程。
硬件异常:算术异常SIGFPE,非法指令SIGILL,内存访问违规SIGSEGV。
C程序:生成信号的函数,如alarm生成SIGALRM。
其他:SIGCHILD通知父进程子进程死亡,SIGTIN阻止后台作业读取终端。
信号的生命周期:
生成→[延迟]→送达→处理
同步信号要等待,有延迟
异步信号立刻送达
常用信号:信号符号名称以SIG开头。
信号编号 | 信号符号名称 | 功能(有的信号可以被捕获) |
2 | SIGINT | 中断当前终端,Ctrl+c |
3 | SIGQUIT | 退出当前终端,并生成一个核心文件,Ctrl+\ |
9 | SIGKILL | 杀死进程,不能被忽略或捕获 |
15 | SIGTERM | 终止进程,可能被忽略(kill命令默认参数) |
17 | SIGCHILD | 内核通知父进程子进程终止 |
18 | SIGCONT | 恢复进程运行 |
20 | SIGTSTP | 挂起当前前台进程,Ctrl+z暂停,后期可恢复 |
使用kill命令向进程发送控制信号:kill -s 信号 进程ID 或 kill -信号编号 PID
默认信号为SIGTERM
信号选项可不带SIG前缀
可使用kill -l查看所有信号
信号处理:信号发送给进程时,内核在进程表中设置该信号的掩码字段。进程检查进程表中的该字段,然后检查相应的信号处置表。根据信号处置表对信号进行处理。如果处理方式是捕获信号,则挂起自身,调用信号处理器(相当于触发一个函数调用)。当处理器返回时,进程恢复执行。
信号处理相关系统调用:
sigaction 指定信号处理器
alarm 设定一个计时器,经过一定时间后生成SIGALRM信号
pause 阻塞程序执行,直到接收到信号为止
kill 向进程发送一个信号
sigaction系统调用:指定信号处理器。前提是信号可以被捕获。头文件:#include <signal.h>。用法:指定该系统调用,由act结构来处理接收到的sig信号,oact不为NULL时存储之前的处理方式。
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact)
sig 要设置的是哪一个信号的处理方式
act 指定处理方式 调用之前要设置好结构体属性(①函数指针:给函数名称 ②mask掩码 ③flag)
oact 处理方式 原先的处理方式
可以为NULL——用第二个参数覆盖原来的,也可以给一个结构体变量——保存一下老的处理方式
struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
}
sigaction结构
sa_handler设置信号处置方式
函数名,自定义信号处理函数
SIG_IGN,忽略信号,不处理 //覆盖原来处理方式
SIG_DFL,默认信号处理
kill系统调用:向指定进程发送指定信号。
int kill(pid_t pid, int sig);
pid 指定进程PID
sig 指定信号
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
pid_t pid;
int main (int argc, char **argv)
{
int i, status;
void death_handler(int sig);
struct sigaction act;
act.sa_handler=death_handler; //set handler
sigaction(SIGCHLD, &act, NULL); //handler for SIGCHLD 子进程退出时
sigaction(SIGALRM, &act, NULL); //handler for SIGALRM 计时器到点
switch (pid=fork()) //fork a process
{
case -1:
fprintf(stderr, "Fork fail\n"); break;
case 0: //child process
execvp(argv[1], &argv[1]); perror("exec"); break;
default: //parent process
alarm(5); pause(); fprintf(stderr, "Parent dies.\n"); //等待子进程退出
}
return 0;
}
void death_handler(int sig)
{
int status;
switch (sig)
{
case SIGCHLD:
waitpid(-1, &status, 0);
fprintf(stderr,"Child dies. Exit status: %d.\n",WEXITSTATUS(status));break;
case SIGALRM:
if (kill(pid, SIGTERM)==0) //send SIGTERM to child process
fprintf(stderr, "Time out. Child killed.\n");
}
}
无名管道通信(单向通信):pipe系统调用。
int pipe(int fd[2]) //fd[0]只管输入、读 fd[1]只管输出、写
参数:
2个整数组成的数组,分别表示管道的输入端fd[0]和输出端fd[1]
可以使用write向fd[1]写入数据
可以使用read从fd[0]读取数据
写入fd[1]中的内容都可以由fd[0]读取
#include<stdio.h>
#include<unistd.h>
#include"quit.h"
//用管道连接父进程和子进程
int main()
{
int n,fd[2];
char buf[100];
if (pipe(fd)<0) //管道创建失败
quit("pipe",1);
//创建成功后产生两个文件描述符 通常为3和4 分别存在于fd[0]和fd[1]中
switch (fork())
{
case -1:
quit("fork fail",2); break;
case 0: //子进程 读
close(fd[1]);
n=read(fd[0],buf,100);
write(STDOUT_FILENO,buf,n);
break;
default: //父进程 写
close(fd[0]);
write(fd[1],"Parent writing to pipe\n",23);
}
return 0;
}
#include<stdio.h>
#include<unistd.h>
#include"quit.h"
//模拟shell中的管道通信
int main()
{
int fd[2];
if (pipe(fd)<0)
quit("pipe",1);
switch (fork())
{
case -1:
quit("Fork fail",2); break;
case 0: //子 要写
close(fd[0]);
dup2(fd[1],STDOUT_FILENO); //把文件描述符复制一份给STDOUT 令标准输出的文件描述符与管道出口指向相同
close(fd[1]); //管道仍然存在 因为有标准输出
execlp("cat", "cat","/etc/passwd",NULL); //后面只要是在标准输出上的都进入管道
quit("cat",3);
break;
default: //父 要读
close(fd[1]);
dup2(fd[0],STDIN_FILENO); //管道输入端与标准输入连在一起 读数据从标准输入中读
close(fd[0]);
execlp("tr", "tr","'[a-z]'","'[A-Z]'",NULL);
quit("tr",4);
}
}
//子进程输出作为父进程tr输入
//父进程输出还是标准输出
//输出结果是把passwd文件的小写字母全部替换为大写
//与shell命令效果相同
命名管道通信:无名管道只能用于同源进程间通信(父子关系)(天然存在于父子进程之间)。命名管道FIFO是一种特殊的管道,存在文件系统中(生成一个文件)。可以用open/close打开/关闭FIFO管道,用read/write进行读/写。需要在两个通信的进程中分别打开相同的命名管道。
mkfifo库函数:#include <sys/types.h, sys/stat.h>
int mkfifo(const char *fifoname, mode_t mode)
fifoname 管道文件名
mode 文件读写权限,三位八进制数,前加0
删除命名管道:unlink(fifoname)命令
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <string.h>
//FIFO_read.c
int main()
{
const char *fifo_name = "./my_fifo";
int pipe_fd = -1;
int data_fd = -1;
int res = 0;
int open_mode = O_RDONLY;
char buffer[PIPE_BUF + 1];
int bytes_read = 0;
int bytes_write = 0;
//清空缓冲数组
memset(buffer, '\0', sizeof(buffer));
printf("Process %d opening FIFO O_RDONLY\n", getpid());
//以只读阻塞方式打开管道文件,注意与fifowrite.c文件中的FIFO同名
pipe_fd = open(fifo_name, open_mode);
//以只写方式创建保存数据的文件
data_fd = open("DataFormFIFO.txt", O_WRONLY|O_CREAT, 0644);
printf("Process %d result %d\n",getpid(), pipe_fd);
if(pipe_fd != -1)
{
do
{
//读取FIFO中的数据,并把它保存在文件DataFormFIFO.txt文件中
res = read(pipe_fd, buffer, PIPE_BUF);
bytes_write = write(data_fd, buffer, res);
bytes_read += res;
}while(res > 0);
close(pipe_fd);
close(data_fd);
}
else
exit(EXIT_FAILURE);
printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
unlink(fifo_name);
exit(EXIT_SUCCESS);
}
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
//FIFO_write.c
int main()
{
const char *fifo_name = "./my_fifo";
int pipe_fd = -1;
int data_fd = -1;
int res = 0;
const int open_mode = O_WRONLY;
int bytes_sent = 0;
char buffer[PIPE_BUF + 1];
if(access(fifo_name, F_OK) == -1)
{
//管道文件不存在
//创建命名管道
res = mkfifo(fifo_name, 0777);
if(res != 0)
{
fprintf(stderr, "Could not create fifo %s\n", fifo_name);
exit(EXIT_FAILURE);
}
}
printf("Process %d opening FIFO O_WRONLY\n", getpid());
//以只写阻塞方式打开FIFO文件,以只读方式打开数据文件
pipe_fd = open(fifo_name, open_mode);
data_fd = open("/etc/passwd", O_RDONLY);
printf("Process %d result %d\n", getpid(), pipe_fd);
if(pipe_fd != -1)
{
int bytes_read = 0;
//向数据文件读取数据
bytes_read = read(data_fd, buffer, PIPE_BUF);
buffer[bytes_read] = '\0';
while(bytes_read > 0)
{
//向FIFO文件写数据
res = write(pipe_fd, buffer, bytes_read);
if(res == -1)
{
fprintf(stderr, "Write error on pipe\n");
exit(EXIT_FAILURE);
}
//累加写的字节数,并继续读取数据
bytes_sent += res;
bytes_read = read(data_fd, buffer, PIPE_BUF);
buffer[bytes_read] = '\0';
}
close(pipe_fd);
close(data_fd);
}
else
exit(EXIT_FAILURE);
printf("Process %d finished\n", getpid());
unlink(fifo_name);
exit(EXIT_SUCCESS);
}
命名管道的阻塞问题:在同一个进程中命名管道是单向的,不能以O_RDWR方式打开命名管道(只能读或者写)。以阻塞方式打开的管道,只有在管道另一端有操作时才会继续执行读/写操作。以非阻塞方式打开的管道,会立即执行读/写操作(有可能失败)。
open的非阻塞选项:
open(fifoname, O_RDONLY | O_NONBLOCK)
open(fifoname, O_WRONLY | O_NONBLOCK)
消息队列:消息队列时消息的链表,存放在内核中并由消息队列标识符表示。从一个进程向另一个进程发送一个数据块(结构体 消息过滤)。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。通过如下四个系统调用使用。
msgget 创建和访问一个消息队列
int msgget(key_t, key, int msgflg);
key 消息队列名称 //如果相同 则访问的是同一个队列
msgflg 消息队列权限
成功时返回消息队列标识符,失败时返回-1
msgsnd 向消息队列中发送消息
int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
msgid 消息队列标识符
msg_ptr 指向待发消息的指针
msg_sz 消息的长度
msgflg 队列是否满的标志
成功时返回0,失败时返回-1
消息类型:结构体类型
struct my_msg {
long int msg_type;
char text[size] //消息里的内容
}
msgrcv 从消息队列中接收消息
int msgrcv(int msgid, void *msg_ptr, size_t msg_sz; long int msgtype, int msgflg);
msgid 消息队列标识符
msg_ptr 指向收到消息的指针
msg_sz 消息长度
msgtype 消息类型(消息过滤 不一定要和msg里的type对应 是一种接收模式)
//=0接收第一个可用消息;>0接收相同类型消息;<0接收类型小于等于其绝对值的消息
msgflg 队列是否为空的标志
成功时返回接收的字符数,失败时返回-1
msgctl 控制消息队列
int msgctl(int msgid; int command, struct msgid_ds *buf);
msgid 消息队列标识符
command 控制命令
buf 指向消息队列模式和访问权限的结构
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/msg.h>
#include <errno.h>
//msg_send.c
#define MAX_TEXT 512
struct msg_st
{
long int msg_type;
char text[MAX_TEXT];
};
int main()
{
int running = 1;
struct msg_st data;
char buffer[BUFSIZ];
int msgid = -1;
//建立消息队列
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if(msgid == -1)
{
fprintf(stderr, "msgget failed with error: %d\n", errno);
exit(EXIT_FAILURE);
}
//向消息队列中写消息,直到写入end
while(running)
{
//输入数据
printf("Enter some text: ");
fgets(buffer, BUFSIZ, stdin);
data.msg_type = 1; //注意2
strcpy(data.text, buffer);
//向队列发送数据
if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1)
{
fprintf(stderr, "msgsnd failed\n");
exit(EXIT_FAILURE);
}
//输入end结束输入
if(strncmp(buffer, "end", 3) == 0)
running = 0;
sleep(1);
}
exit(EXIT_SUCCESS);
}
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>
struct msg_st
{
long int msg_type;
char text[BUFSIZ];
};
//msg_recv.c
int main()
{
int running = 1;
int msgid = -1;
struct msg_st data;
long int msgtype = 0; //注意1
//建立消息队列
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if(msgid == -1)
{
fprintf(stderr, "msgget failed with error: %d\n", errno);
exit(EXIT_FAILURE);
}
//从队列中获取消息,直到遇到end消息为止
while(running)
{
if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)
{
fprintf(stderr, "msgrcv failed with errno: %d\n", errno);
exit(EXIT_FAILURE);
}
printf("You wrote: %s\n",data.text);
//遇到end结束
if(strncmp(data.text, "end", 3) == 0)
running = 0;
}
//删除消息队列
if(msgctl(msgid, IPC_RMID, 0) == -1)
{
fprintf(stderr, "msgctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
共享内存通信:共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。【注意:共享内存并未提供同步保护机制,还要自己实现读写】通过如下四个系统调用使用。
shmget 创建共享内存
int shmget(key_t key, size_t size, int shmflg);
key 命名共享内存
size 共享内存容量
shmflg 共享内存权限
成功时返回共享内存标识,失败时返回-1
shmat 连接共享内存以开启访问
void *shmat(int shm_id, const void *shm_addr, int shmflg);
shm_id 共享内存标识符
shm_addr 连接位置地址,通常留空
shmflg 标志位,通常为0
成功时返回指向共享内存的指针,失败时返回-1
shmdt 分离共享内存
int shmdt(const void *shmaddr);
shmaddr 是shmat返回的共享内存地址
成功时返回0,失败时返回-1
shmctl 共享内存控制
int shmctl(int shm_id, int command, struct shmid_ds *buf);
shm_id 共享内存标识符
command 控制命令
buf 指向共享内存模式和访问权限的结构
成功时返回0,失败时返回-1
线程
线程基本概念:线程是进程内的一个执行单元或一个可调度实体。一个进程中可以有多个不同的线程,同一个进程下的线程共享进程拥有的资源(地址空间、文件系统资源、文件描述符和信号处理程序)。线程按期调度者分为用户线程(对用户可见,其调度算法和调度过程全部由用户决定)和内核线程(由OS管理,对用户不可见)。用户线程可与内核线程实现一对一、多对一、多对多的关联。
线程和进程:进程是系统中程序执行和资源分配的基本单位。每个进程有自己的数据段、代码段等。线程是在进程的内存空间中并发执行的多个控制流,他们共享一个进程的资源。因为线程和进程比起来很小,花费更少的CPU资源,也叫轻进程。
C语言中实现线程创建和控制:Linux提供了与线程相关的API。与线程相关的头文件 #include <pthread.h> 。实现线程函数的共享库文件libpthread.so,在编译时需要链接该动态库文件 -lpthread。可以编写makefile规则来简化编译命令。
线程创建:pthread_create函数。执行pthread_create的线程继续执行,同时新线程并发执行线程函数。新线程不会block原来线程,两者PID相同。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*routine)(void *),void *arg);
thread 存储新线程id
attr 线程属性,通常为NULL
routine 指向新线程执行的函数代码
arg 新线程的执行参数
执行成功返回0,失败返回-1,错误号计入errno中
线程退出:pthread_exit函数,退出当前线程。使用pthread_exit会使自身线程退出,不影响其他线程;使用exit会使整个进程退出。pthread_join函数:等待线程结束,接收线程退出值。
void pthread_exit(void *value_ptr)
value_ptr 存储线程的退出值
执行成功返回0,失败返回-1,错误号计入errno
int pthread_join(pthread_t thread, void **value_ptr)
thread 等待线程的id
value_ptr 存储线程的退出值
执行成功返回0,失败返回错误号
线程属性:线程属性包括绑定属性、分离属性、堆栈地址、堆栈大小、优先级。系统默认属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。可在调用pthread_create函数前设置线程属性。
属性名称 | 是 | 否 |
绑定属性 | 一个用户级线程固定的分配给一个内核级线程 | 用户及线程与内黑级线程的关系不是始终固定的,而是由系统来控制分配 |
分离属性 | 一个线程结束时会立即释放它所占用的资源 | 只有当pthread_join()函数返回时,该线程才释放自己占用的资源 |
线程属性设置:
线程初始化:
int pthread_attr_init(pthread_attr_t *attr)
attr 存储线程属性
线程绑定属性设置:
pthread_attr_setscope(pthread_attr_t *attr, init scope)
scope PTHREAD_SCOPE_SYSTEM (绑定)
PTHREAD_SCOPE_PRCESS (非绑定)
线程分离属性设置:
pthread_attr_setdetachstate(pthread_attr_t *attr, init detachstate)
detachstate PTHREAD_CREAT_DETACHED (分离)
PTHREAD_CREAT_JOINABLE (非分离)
线程同步和互斥:并发执行的线程之间共享数据、文件等资源。由于CPU调度顺序的不确定,对共享数据的修改也变得不确定。因此,在操作共享数据的线程之间需要一个互斥机制,确保在任何时刻最多只有一个线程访问共享资源。多个线程在执行次序上的协调机制称为线程同步机制。
同步互斥机制:互斥锁、条件变量、信号量。
互斥锁机制:一个互斥锁变量,只有两个状态,锁定和非锁定。线程在访问临界区时必须获得互斥锁。被互斥锁锁住的操作不会被其他线程打断。线程在离开临界区时释放互斥锁。若线程在请求互斥锁时,该锁已被其他线程获得,则该线程阻塞。
互斥锁实现:
锁初始化:pthread_mutex_init
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr)
mutex 互斥锁指针
mutexattr 互斥锁属性,设为NULL时使用默认属性
线程加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex)
线程尝试加锁:
int pthread_mutex_trylock(pthread_mutex_t *mutex)
//申请 如被别人占用则阻塞返回错误 成功获得锁则返回0
线程释放锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex)
销毁锁:
int pthread_mutex_destroy(pthread_mutex_t *mutex)
条件变量机制:一个线程需要频繁读取某个共享变量状态时,需要频繁调用互斥锁,资源消耗量较大(效率低)。条件变量可以用来阻塞一个线程,将其放入一个等待队列,直到收到其他线程的信号唤醒。可以避免对共享变量的频繁访问,而是在满足条件时唤醒线程进行操作。由于等待队列也是一个需要竞争的共享资源,因此条件变量需要互斥锁保护。在调用条件变量阻塞线程前,需要锁定互斥锁,并在阻塞线程后释放互斥锁,以避免死锁。常与互斥锁共同使用。在条件不成立时,阻塞当前线程,释放互斥锁。当条件变量被其他线程改变时,唤醒被条件变量阻塞的线程,重新判断条件是否成立,是否获得互斥锁。当收到信号被唤醒时,重新锁定互斥锁。
条件变量实现:
初始化:
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *cond_attr)
cond 条件变量
cond_attr 条件变量属性,填NULL为默认属性
阻塞线程:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
mutex 互斥锁名称
//该函数调用前,需本线程加锁互斥量,加锁状态的时间内函数完成线程加入等待队列操作,线程进入等待前函数解锁互斥量,在满足条件离开pthread_cond_wait函数之前重新获得互斥量并加锁,因此,本线程之后需要再次解锁互斥量
//1.阻断当前线程 2.告诉线程要unlock哪个锁 3.等待signal 4.恢复线程 5.加锁
唤醒线程:
int pthread_cond_signal(pthread_cond_t *cond)
//该函数向队列第一个等待线程发送信号,解除这个线程的阻塞状态
释放线程:
int pthread_cond_destroy(pthread_cond_t *cond)
//条件不成立时主动阻塞自己,一起运行的线程看到条件满足,发送信号将自己唤醒
信号量机制:信号量是解决线程同步与互斥的最重要机制之一,也叫P/V操作。PV操作是对整数计数器信号量sem的操作。一次P操作使sem减一,一次V操作使sem加一。用于互斥时,几个进程(或线程)往往只设置一个信号量sem。用于同步时,往往设置多个信号量,并安排不同的值来实现它们之间的顺序执行。
信号量实现:头文件:semaphore.h
初始化:
int sem_init(sem_t *sem, int pshared, unsigned int value)
sem 指向信号量结构的指针
pshared 不为0时为进程间共享,为0时只在进程内的线程间共享
value 信号量初始值
P操作:
sem_wait(sem_t *sem) //信号量减一,小于0时阻塞线程
V操作:
sem_post(sem_t *sem) //信号量加一,大于0时唤醒线程
销毁:
sem_destory(sem_t *sem) //销毁信号量,释放资源