报错:SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'admin' for key 'username'解决

报错:SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry ‘admin’ for key ‘username’解决方案

在提交注册信息的时候报错:SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry ‘admin’ for key ‘username’

原因1:主键冲突

Duplicate

从上图可以看出,用户名和邮箱必须唯一,如果填入重复的用户名或者邮箱就会出错

解决方法:在控制器里面进行判断,对唯一的数据字段进行判断。有时会是索引导致的,或者是主键没有加自增导致的。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$data = input('post.');
$uniquename = model('User')->get(['username'=>$data['username']]);

if(sizeof($uniquename)){
$this->error('该用户名已经注册,请重新填写~~~');
}

$uniqueemail = model('User')->get(['email'=>$data['email']]);
if(sizeof($uniqueemail)){
$this->error('该邮箱已经注册,请重新填写~~~');
}

添加上面代码之后,如果填写了重复的用户名或者密码就会给用户相关提示~

对了,上面的代码是在TP5上写的。

记一次处理MySql锁等待[Lock wait timeout exceeded]

记一次处理MySql锁等待(Lock wait timeout exceeded)

环境:

MySQL 5.5


问题:

A.数据更新或新增后数据经常自动回滚。

B.表操作总报 Lock wait timeout exceeded 并长时间无反应


解决方法:

A.应急方法:show processlist; kill掉出现问题的进程

B.根治方法:select * from innodb_trx 查看有是哪些事务占据了表资源。

C.我的方法:设置MySQL锁等待超时 innodb_lock_wait_timeout=50 ,autocommit=on


该类问题导致原因:

据我分析,MySQL的 InnoDB 存储引擎是支持事务的,事务开启后没有被主动Commit。导致该资源被长期占用,其他事务在抢占该资源时,因上一个事务的锁而导致抢占失败!因此出现 Lock wait timeout exceeded.

本文转载于:https://my.oschina.net/quanzhong/blog/222091

lock wait timeout exceeded;try restarting transactio解决方案

SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction 解决方案

问题原因:

今天线上环境,突然出现一个问题,追踪原因是数据库中的一条语句报错,错误内容:lock wait timeout exceeded; try restarting transactio

执行update table set status = 1 where id = 10002;是可以的。

而执行update table set status = 1 where id = 10001;这条语句执行失败。

错误提示的意思,很明显,是因为这条语句被锁住了。所以需要释放这个锁。


解决方案:

新创建的数据库,有information、mysql、performance_schema、test四个数据库。

现在我们要查test库中使用情况,我们可以到information_schema中查询。

解释:information_schema这张数据表保存了MySQL服务器所有数据库的信息。如数据库名、数据库的表、表栏的数据类型与访问权限等。再简单点,这台MySQL服务器上,到底有哪些数据库、各个数据库的有哪些表,每张表的字段类型是什么,各个数据库要什么权限才能访问,等等信息都保存在information_schema表里面。

我们可以用下面三张表来查原因:

1
2
3
innodb_trx        ## 当前运行的所有事务
innodb_locks ## 当前出现的锁
innodb_lock_waits ## 锁等待的对应关系

如果数据库中有锁的话,那么再使用如下命令:

1
2
3
4
5
6
7
8
9
10
show full processlist;    ## 查看当前数据库的线程情况:
select * from information_schema.innodb_trx;
找到占用系统资源的锁,然后杀死即可。
kill 9372221;

另外几个常用命令:
select @@tx_isoloation;
REPEATABLE-READ // MySQL默认的事务隔离级别就是REPEATABLE-READ

注意:MySQL是自动提交事务的(即:autocommit=1),可以使用 show variables like 'autocommit' 或者 select @@autocommit 查看当前数据库是否为自动提交事务;若autocommit的值不是1还可以使用set global autocommit = 1 将自动提交设置为开启。

一般查看当前数据库的线程情况没有看到正在执行的很慢SQL记录线程,再去查看innodb的事务表INNODB_TRX,看下里面是否有正在锁定的事务线程,看看ID是否在show full processlist里面的sleep线程中,如果是,就证明这个sleep的线程事务一直没有commit或者rollback而是卡住了,我们需要手动kill掉。

如下图:
Duplicate

图中红色语句为占用系统资源的语句,我们需要杀掉这个锁,执行kill 线程id号。上面这条记录的id为 9372221,所以我们执行:kill 9372221即可。执行之后:
Duplicate

然后我们再查看事务表:

1
2
查看事务表: select * from information_schema.innodb_trx;
发现为空,则表示已经正常了。

其他的记录不需要关注,因为其他的记录状态为”RUNNING” 即正在执行的事务,并没有锁。

我们可以进一步了解一下那三张表的表结构:
information_schema.innodb_locks表

1
desc information_schema.innodb_locks;
Field Type Null Key Default Remark
lock_id varchar(81) No 锁ID
lock_trx_id varchar(18) No 拥有锁的事务ID
lock_mode varchar(32) No 锁模式
lock_type varchar(32) No 锁类型
lock_table varchar(1024) No 被锁的表
lock_index varchar(1024) Yes 被锁的索引
lock_space bigint(21) unsigned Yes NULL 被锁的表空间号
lock_page bigint(21) unsigned Yes NULL 被锁的页号
lock_rec bigint(21) unsigned Yes NULL 被锁的记录号
lock_data varchar(8192) Yes NULL 被锁的数据

information_schema.innodb_lock_waits表

1
desc information_schema.innodb_lock_waits;
Field Type Null Key Default Remark
requesting_trx_id varchar(18) No 请求锁的事务ID
requesting_lock_id varchar(81) No 请求锁的锁ID
blocking_trx_id varchar(18) No 当前拥有锁的事务ID
blocking_lock_id varchar(81) No 当前拥有锁的锁ID

information_schema.innodb_trx表

1
desc information_schema.innodb_trx;
Field Type Null Key Default Extra Remark
trx_id varchar(18) No 事务ID
trx_state varchar(13) No 事务状态
trx_started datetime No 0000-00-00 00:00:00 事务开始时间
trx_requested_lock_id varchar(81) Yes NULL innodb_locks.lock_id
trx_wait_started datetime Yes NULL 事务开始等待的时间
trx_weight bingint(21) unsigned No 0 #
trx_mysql_thread_id bingint(21) unsigned No 0 事务线程ID
trx_query varchar(1024) Yes NULL 具体SQL语句
trx_operation_state varchar(64) Yes NULL 事务当前操作状态
trx_tables_in_use bingint(21) unsigned No 0 事务中有多少个表被使用
trx_tables_locked bingint(21) unsigned No 0 事务拥有多少个锁
trx_lock_structs bingint(21) unsigned No 0 #
trx_lock_memory_bytes bingint(21) unsigned No 0 事务锁住的内存大小(B)
trx_rows_locked bingint(21) unsigned No 0 事务锁住的行数
trx_rows_modified bingint(21) unsigned No 0 事务更改的行数
trx_concurrency_tickets bingint(21) unsigned No 0 事务并发票数
trx_isolation_level varchar(16) No 事务隔离级别
trx_unique_checks int(1) No 0 是否唯一性检查
trx_foreign_key_checks int(1) No 0 是否外键检查
trx_last_foreign_key_error varchar(256) Yes NULL 最后的外键错误
trx_adaptive_hash_latched int(1) No 0 #
trx_adaptive_hash_timeout bingint(21) unsigned No 0 #

mysql开启数据库记录日志

MySQL开启数据库记录日志

程序出错时,几乎一半的出错都是写错了sql语句,而定位出错位置和找错是非常麻烦的,这个时候就可以借助mysql的日志记录。

打来mysql文件夹的my.ini。

在最底部或者最前面换行重新添加一句:
log=”G:/mysqllog/mysql.sql”

G:是盘符,然后后面是你想要放数据库日志的目录和文件名。

保存后重启mysql,如果在目录没发现你的文件,请自行添加目录和文件名,然后重启。
mysql

这样,你就可以在程序出错时去看mysql的日志,但是这个日志记录的是全部执行过的sql语句,时间久了数据量是非常巨大的,如果查完请记得删除以免影响性能。
mysql

本文转载于 http://www.php20.cn/article/39

Redis集群环境配置

Redis集群环境配置

为什么需要集群?

Redis是一个开源的key->value高速存储系统,但是由于Redis单线程运行,在系统中,只能利用单核的性能。

当Redis的调用越来越频繁时,可能会出现Redis过去繁忙,无法处理数据的情况。

这时候,我们就需要使用Redis集群去减轻Redis的压力,利用Redis集群,去充分利用cpu核数。

Redis集群简介

1、Redis集群采用p2p模式,也就是集群没有中心服务器,每个节点都是同样的权限,通过访问一个节点,就能获取其他节点的信息。

2、Redis集群由于是p2p模式,所以需要连接所有的节点,进行不同的操作,也就是说你有多少台节点,就得有多少个Redis连接。

3、为了实现集群的高可用状态,Redis的全部节点需要定时的去查询其他节点的状态,如果发现ping不成功,说明这个节点可能已经出现问题了。但这里有个问题,假设集群ABC,A的网络出现了问题,那A节点会觉得BC都有问题,BC也会觉得A有问题,那这个时候该怎么办呢? 所有Redis采用了投票机制,如果超过了一半的节点觉得某个节点有问题,那就说明它真的挂了。

4、节点挂了会影响集群挂了吗?当集群的某个节点挂了,并且没有额外的从节点提供服务,那整个集群就会直接挂掉。但是如果有从节点,那么从节点可以继续提供部分服务,集群不会直接终止。

Redis哈希槽

现在我们了解到了Redis是p2p去中心化的实现方式,那么,我如果需要set('a','小灰灰'),Redis会怎么处理呢?

这里就涉及到了集群哈希槽的知识了。

Redis集群规定了16384个槽位,这些槽位将会平均分配给不同的节点,通过对key的取模16384,可获得一个槽位数,这个key将会保存到这个槽位上,然后再根据槽位对应的节点,将数据保存到指定节点中。

例如:

  • 1、set(‘a’,’小灰灰’)
  • 2、对a字符做 crc16 算法,再对其取模16384,获得槽位数:15495
  • 3、再根据槽位数,获得指定节点(假设是C节点)
  • 4、连接C节点,进行set a '小灰灰'命令发送
  • 5、数据保存到C节点
  • 6、get('a')
  • 7、对a字符串做crc16算法,再对其取模16384,获得槽位数:15495
  • 8、再根据槽位数,获得指定节点(假设是C节点)
  • 9、连接C节点,进行get a命令发送
  • 10、获得具体数据

假设某个节点断线挂了,又没有从节点的话,槽位分配就会出错,所有集群就会挂掉。

集群节点

Redis搭建集群,最少需要3个主节点,如果需要实现更加稳定,还需要3个从节点(避免主节点断线后集群终止),所以Redis需要启动6个实例。

这边需要注意的一点是,Redis存储在内存,利用tcp通信,只能利用单核性能,所有如果需要搭建Redis集群,需要注意一下事项:

1、需要多台服务器,或者一台6核以上的服务器搭建集群(充分利用cpu)

2、最好是内网通信,多台服务器也必须采用内网通信(内网带宽更加高)

搭建Redis集群实例

好了,现在开始搭建Redis集群,首先,我们需要安装Redis,这里不多做说明。

创建Redis集群目录

新增redis-cluster文件夹以及存放6个节点的文件夹,用来存储redis集群节点:

1
2
3
4
5
6
mkdir -p /usr/local/redis-cluster/redis01
mkdir -p /usr/local/redis-cluster/redis02
mkdir -p /usr/local/redis-cluster/redis03
mkdir -p /usr/local/redis-cluster/redis04
mkdir -p /usr/local/redis-cluster/redis05
mkdir -p /usr/local/redis-cluster/redis06

Redis复制到集群目录中

把redis-server,redis-cli 和 redis.conf 复制一份到cluster/redis01 中(我这里是直接编译安装的redis)

1
cp redis-5.0.7/{redis.conf,src/redis-server,src/redis-cli} /usr/local/redis-cluster/redis01/

修改Redis集群配置

修改目录中的redis.conf的端口,改为你自己喜欢的端口号,比如9001
redis-cluster

修改redis.conf 的 cluster-enabled,改为yes,去掉#号注释
redis-cluster

修改为守护进程启动:
redis-cluster

这个也最好修改下:修改为对应的9001-9006

1
pidfile /var/run/redis_9001.pid

这步不做命令示例。

每个集群都复制一份Redis,并且修改端口

把redis01的文件全部复制到其他文件夹中

1
2
3
4
5
cp -r redis-cluster/redis01/* redis-cluster/redis02
cp -r redis-cluster/redis01/* redis-cluster/redis03
cp -r redis-cluster/redis01/* redis-cluster/redis04
cp -r redis-cluster/redis01/* redis-cluster/redis05
cp -r redis-cluster/redis01/* redis-cluster/redis06

修改其他5个目录中的conf端口号,改为9002-9006

启动Redis集群实例

现在需要编写一个shell,用来启动所有的redis实例,新增startAll.sh文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash
#PATH=/bin:/sbin:/usr/bin/:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
#export PATH;
cd /usr/local/redis-cluster/redis01;
./redis-server ./redis.conf
cd /usr/local/redis-cluster/redis02;
./redis-server ./redis.conf
cd /usr/local/redis-cluster/redis03;
./redis-server ./redis.conf
cd /usr/local/redis-cluster/redis04;
./redis-server ./redis.conf
cd /usr/local/redis-cluster/redis05;
./redis-server ./redis.conf
cd /usr/local/redis-cluster/redis06;
./redis-server ./redis.conf

给予执行权限并运行:

1
chmod +x startAll.sh

注意,如果是windows编辑的这行代码,那需要注意格式问题,可能会执行报错,需要用`dos2unix`命令转换一下。

这个时候,6个服务器已经全部启动完成了,当然,你不一定要在一台服务器上运行6个,你可以在6台不同的服务器运行6个redis服务器。

查看运行情况:
redis-cluster;

创建Redis集群环境

目前,我们已经成功运行了redis的6个实例,现在需要创建集群环境。

首先,需要安装ruby环境(redis5.0,该步骤废除,留着是为了记录gem下载地址)

1
2
yum install ruby
yum install rubygems

离线下载redis.gem文件(redis5.0,该步骤废除,留着是为了记录gem下载地址)

下载地址:https://rubygems.org/gems/redis/versions/3.0.0

需要注意的是,版本需要跟ruby版本对应,我这里下载的是3.3.0,刚好能够安装。

安装命令见下面:(redis5.0,该步骤废除,留着是为了记录gem下载地址)

1
gem install ./redis-3.3.0.gem

执行搭建集群

还记得之前的redis-cli吗?执行以下命令:

1
./redis-cli --cluster create 127.0.0.1:9001 127.0.0.1:9002 127.0.0.1:9003 127.0.0.1:9004 127.0.0.1:9005 127.0.0.1:9006 --cluster-replicas 1

过程中会让你输入yes,输入之后,然后集群搭建完成。
redis-cluster;

到现在为止,整个集群就已经搭建成功了。

测试

执行set

1
2
3
4
[root@localhost redis01]# ./redis-cli -p 9001
127.0.0.1:9001>set a 1
(error) MOVED 15495 127.0.0.1:9003
127.0.0.1:9001>

这个的意思是,槽位15945需要在9003上面的实例执行。
我们再连接9003试试:

1
2
3
4
[root@localhost redis03]# ./redis-cli -p 9003
127.0.0.1:9003>set a 1
OK
127.0.0.1:9003>

到目前为止,整个集群已经搭建成功了。
查看集群:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
127.0.0.1:9003>cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:3
cluster_stats_messages_ping_sent:916
cluster_stats_messages_pong_sent:878
cluster_stats_messages_meet_sent:4
cluster_stats_messages_sent:1798
cluster_stats_messages_ping_received:876
cluster_stats_messages_pong_received:920
cluster_stats_messages_meet_received:2
cluster_stats_messages_received:1798
127.0.0.1:9003>

其他补充

前面教大家如何快速的创建一个集群,现在有一些点需要完善补充的,省得后面有坑!

集群配置项

在上面,只讲到了端口配置以及开启集群,但是有一些配置项需要注意配置的,虽然不配置也能运行集群。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 端口号
port 9001

# 日志的记录级别,notice是适合生产环境的
loglevel notice

# 指定log的保存路径,默认是创建在Redis安装目录下,如果有子目录需要手动创建,如此处的目录
logfile "Logs/redis9001_log.log"

# 是否使用系统日志
syslog-enabled yes

# 在系统日志的标识名
syslog-ident redis9001

# 数据的保存为aof格式
appendonly yes

# 数据保存文件
appendfilename "appendonly.9001.aof"

# 是否开启集群
cluster-enabled yes
cluster-config-file nodes.9001.conf
cluster-node-timeout 15000
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage yes

集群ip

需要注意的是,我们配置的ip都是默认,127.0.0.1,当需要多台服务器开启集群,或者内网集群时,需要修改ip为内网/外网ip号,确保redis节点能互相创建,否则集群无法连接,无法创建成功。

本文章转载于http://www.php20.cn

使用file_get_contents函数发送post请求

使用file_get_contents函数发送post请求

作为一名phper,我们一般是用使用file_get_contents()函数发送get请求,殊不知它还可以用来发送post请求,接下来我将简单用demo来说明一下:

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
// 请求的URL地址
$url = 'http://www.example.com';
// 请求参数
$requestParams = [
'name' => 'xhh',
'age' => 18
];

// 设置请求头
$requestHeaders = [
'Content-Type: application/x-www-form-urlencoded'
];

// 设置参数
$options = [
'http' => [
'method' => 'POST',
'content' => http_build_query($requestParams),
'header' => implode("\r\n", $headers),
],
];

// 请求得到的响应内容
$response = file_get_contents($url, false, stream_context_create($options));

到这里,请求就发送完成了,至于$response是什么数据需要根据请求地址返回的内容而定。

PHP使用phpmailer发送邮件

PHP使用phpmailer发送邮件

前言

现今对于用户登录除了密码验证外,都有额外的验证机制;验证码防止机器登录,短信验证、邮箱验证确保本人登录等。

使用邮箱发送验证信息,需要一个发送邮件的账号,例如QQ邮箱验证,需要在QQ邮箱中设置开启IMAP/SMTP服务。在QQ邮箱中找到如下设置开启:
smtp

demo示例:

1、PHP发送邮件,需下载一个phpmailer插件包,通过composer下载依赖包。

1
composer require phpmailer/phpmailer

2、发送邮件代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
// 使用上述插件
require_once './vendor/autoload.php';
use PHPMailer\PHPMailer\PHPMailer;

/**
* @param $receiveMail
*/
function sendMails($receiveMail)
{
$mail = new PHPMailer();
// var_dump($mail);exit;
// 服务器设置
$mail->SMTPDebug = 0; // 是否开启Debug调试模式 0不开启 1开启
$mail->isSMTP(); // 使用SMTP
$mail->CharSet = "UTF-8"; // 设置字符集
$mail->Host = 'smtp.qq.com'; // 设置主机
$mail->SMTPAuth = true; // 开启SMTP验证
$mail->Username = 'email@qq.com'; // 发件人邮箱用户名
$mail->Password = 'shouquanma'; // 授权码,开启IMAP/SMTP服务时授权的
$mail->SMTPSecure = "ssl"; // 使用ssl协议方式
$mail->FromName = '明辉科技考试云平台';
$mail->Port = 465; // 端口
// 发件人
$mail->setFrom($mail->Username); // 来自
$mail->addAddress($receiveMail); // 设置收件人地址
// 设置邮件内容
$mail->isHTML(true); // 设置邮件格式为HTML
$mail->Subject = '验证码-来自明辉科技考试云平台'; // 邮件主题
$code = rand(1000,9999);
$_SESSION['code'] = $code;
$mail->Body = "<b>您的验证码是:{$code}</b>,如果非本人操作无需理会!";
try {
$mail->send();
echo '邮件发送成功!';
} catch(Exception $e) {
// 输出异常信息
echo $e->getMessage();
}
}

sendMails('123456@qq.com'); // 发送邮件给123456@qq.com

3、注意:邮件发送不成功的话,请开启调试模式,检查相关扩展是否安装或者打开。

php-yield关键字以及协程的实现

php yield关键字以及协程的实现

php的yield是在php5.5版本就出来了,而在初级php界却很少人提起,接下来简单说说个人对php yield的理解

Iterator接口(迭代器接口)

在php中,除了数组、对象可以被foreach遍历之外,还有另外一种特殊对象,也就是继承了Iterator接口的对象,也可以被对象遍历,但和普通对象的遍历又有所不同,下面是3种类型的遍历情况:

yield
yield
yield

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$arr = array(
'a' => 1,
'b' => 2,
'c' => 3
);

foreach ($arr as $key => $value) {
echo "数组遍历\n";
var_dump($key, $value);
}
return;
1
2
3
4
5
6
7
8
9
10
11
<?php
class Obj
{
public $a = 1, $b = 2, $c = 3;
public function a() {}
}
foreach (new Obj() as $key => $value) {
echo "对象遍历\n";
var_dump($key, $value);
}
return;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
class Iteratorobj implements Iterator {
protected $data;
public function __construct($arr) {
$this->data = $arr;
}
public function rewind() {
// TODO: Implement rewind() method.
echo "指针重置\n";
reset($this->data);
}
public function current() {
// TODO: Implement current() method.
echo "当前指针数据\n";
return current($this->data) . "自定义遍历值";
}
public function next() {
// TODO: Implement next() method.
echo "指针下移\n";
next($this->data);
}
public function key() {
// TODO: Implement key() method.
echo "返回当前数组键值\n";
return key($this->data);
}
public function valid() {
// TODO: Implement valid() method.
echo "检查迭代指针是否正常\n";
return current($this->data);
}
}
foreach (new Iteratorobj(array(
'a' => 1,
'b' => 2,
'c' => 3,
)) as $key => $value) {
echo "迭代器自定义遍历";
var_dump($key, $value);
}

可以看出,迭代器的遍历,会依次调用重置,检查当前数据,返回当前数据,返回当前指针数据,指针下移方法,结束遍历的条件在于检查数据返回true或者false。

生成器

生成器和迭代器类似,但也不完全相同。
生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组,那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样,和普通函数只返回一次不同的是,生成器可以根据需要 yield 多次,以便生成需要迭代的值。生成器使用yield关键字进行生成迭代的值。

例如:
yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
/**
* 函数内部有yield关键字,将返回一个生成器对象来进行迭代
* @param $start
* @param $limit
* @param int $step
* @return Generator
*/
function xrange($start, $limit, $step = 1) {
if ($start < $limit) {
if ($step <= 0) {
throw new LogicException("步进值必须大于0");
}
for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
} else {
if ($step >= 0) {
throw new LogicException('步进值必须小于0');
}
for ($i = $start; $i >= $limit; $i += $step) {
yield $i;
}
}
}
var_dump(xrange(1, 9, 2));
foreach (xrange(1, 9, 2) as $number) {
echo "{$number} ";
}

一、生成器方法

生成器它的内部实现了以下方法:

1
2
3
4
5
6
7
8
9
10
11
Generator implements Iterator { // 返回当前产生的值
public mixed current(void) // 返回当前产生的键
public mixed key(void) // 生成器继续执行
public void next(void) // 重置迭代器,如果迭代已经开始了,这里会抛出一个异常。
public void rewind(void) // 向生成器中传入一个值,当前yield接收值,然后继续执行下一个yield
public mixed send(mixed $value) // 向生成器中抛出一个异常
public void throw(Exception $exception) // 检查迭代器是否被关闭,已经关闭返回 FALSE,否则返回 TRUE
public bool valid(void) // 序列化回调
public void __wakeup(void) // 返回generator函数的返回值,PHP version 7+
public mixed getReturn(void)
}

二、语法

生成器的语法有很多种用法,需要一一说明,首先,

yield必须有函数包裹,包裹yield的函数称为“生成器函数”,该函数将返回一个可遍历对象。

1、颠覆常识的yield

yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
function Generator() {
for ($i = 0; $i < 3; $i++) {
echo "输出存在感1\n";
yield $i;
echo "输出存在感2\n";
}
}
echo "#######返回对象#######\n";
var_dump((Generator())); // 返回对象

echo "#######返回对象#######\n\n\n";
echo "#######遍历一次情况#######\n";
foreach (Generator() as $value) {
var_dump($value);
break; // 只便利一次情况
}
echo "#######遍历一次情况#######\n\n\n";

echo "#######一直遍历情况#######\n";
foreach (Generator() as $value) {
var_dump($value); // 遍历多次
}
echo "#######一直遍历情况#######\n";

可能在这里你发现了几个东西,和之前php完全不同的认知,如果你没有发现,那我简单说下:

  • 1、在调用函数返回的时候,可以发现for里面的语句并没有执行
  • 2、在遍历一次的时候,可以发现调用函数,却没有正常的for循环3次,只循环了一次
  • 3、在遍历一次的时候,“存在感2”竟然没有调用,在一直遍历的情况下才调用

再看看另一个例子:

yield
yield
yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
function Generator() {
while (true) {
echo yield . "\n";
echo yield . "\n";
echo "哈哈\n";
return '啦啦啦';
}
echo '嗯嗯嗯';
return '返回值';
}
$obj = Generator();
var_dump($obj);
$obj->send(1); // 返回对象
$obj->send(2);
$obj->send(3);
$obj->send(4);
$obj->send(5);
$obj->send(6);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
function Generator() {
while (true) {
echo yield . "\n";
echo yield . "\n";
echo "哈哈\n";
// return '啦啦啦';
}
echo '嗯嗯嗯';
return '返回值';
}
$obj = Generator();
var_dump($obj);
$obj->send(1); // 返回对象
$obj->send(2);
$obj->send(3);
$obj->send(4);
$obj->send(5);
$obj->send(6);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
function Generator() {
while (true) {
echo yield . "\n";
echo yield . "\n";
echo "哈哈\n";
break;
}
echo '嗯嗯嗯';
return '返回值';
}
$obj = Generator();
var_dump($obj);
$obj->send(1); // 返回对象
$obj->send(2);
$obj->send(3);
$obj->send(4);
$obj->send(5);
$obj->send(6);

什么???while(true)竟然还能正常的执行下去???没错,生成器函数就是这样的,根据这个例子,我们发现了这些东西:

  • 1、while(true)没有阻塞调用函数下面的代码执行,却导致了下面的echo “额额额” 和 return 无法执行
  • 2、return 返回值竟然是没有作用的
  • 3、send(1)时,没有echo “哈哈”,send(2)时,才开始出现 “哈哈”。

2、yield的其他语法

yield表达式中,可以赋值,但赋值需要使用括号包裹:
yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
function Generator() {
while (true) {
$data = (yield);
echo $data . "\n";
echo $data . "\n";
echo "哈哈\n";
// break;
}
echo '嗯嗯嗯';
return '返回值';
}
$obj = Generator();
var_dump($obj);
$obj->send(1); // 返回对象
$obj->send(2);
$obj->send(3);
$obj->send(4);
$obj->send(5);
$obj->send(6);

只需要在表达式后面加上$key => $value,即可生成键值的数据:
yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
function Generator() {
for ($i = 0; $i < 3; $i++) {
echo "输出存在感1\n";
yield "key_{$i}" => $i;
echo "输出存在感2\n";
}
}
echo "#######返回对象#######\n";
var_dump((Generator())); // 返回对象

echo "#######返回对象#######\n\n\n";
echo "#######遍历一次情况#######\n";
foreach (Generator() as $value) {
var_dump($value);
break; // 只便利一次情况
}
echo "#######遍历一次情况#######\n\n\n";
echo "#######一直遍历情况#######\n";
foreach (Generator() as $key => $value) {
var_dump($key, $value); // 遍历多次
}
echo "#######一直遍历情况#######\n";

在函数前增加引用定义,就可以像returning reference from functions(从函数返回一个引用)一样引用生成值
yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function &gen_reference() {
$value = 3;
while ($value > 0) {
yield $value;
}
}

/**
* 我们可以在循环中修改$number的值,而生成器是使用的引用值来生成,所以gen_reference()内部的$value值也会跟着变化。
*/
foreach (gen_reference() as &$number) {
echo (--$number) . "\n";
}

三、特性总结

1、yield是生成器所需要的关键字,必须在函数内部,有yield的函数叫做“生成器函数”

2、调用生成器函数时,函数将返回一个继承了Iterator的生成器

3、yield作为表达式使用时,可将一个值加入到生成器中进行遍历,遍历完会中断下面的语句运行,并且保存状态,当下次遍历时会继续执行(这就是while(true))没有造成阻塞的原因)

4、当send传入参数时,yield可作为一个变量使用,这个变量等于传入的参数

协程

一、实现一个简单的协程

协程,是一个编程逻辑的转变,使多个任务能交替运行,而不是之前的一直根据流程往下走,举个例子,当有一个逻辑,每次调用这个文件时,该文件要做3件事:

1、写入300个文件
2、发送邮件给500个会员
3、插入100条数据

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
function task1() {
for ($i = 0; $i <= 300; $i++) {
// 写入文件,大概要3000微秒
usleep(3000);
echo "写入文件{$i}\n";
}
}
function task2() {
for ($i = 0; $i <= 500; $i++) {
// 发送邮件给500名会员,大概3000微秒
usleep(3000);
echo "发送邮件{$i}\n";
}
}
function task3() {
for ($i = 0; $i <= 100; $i++) {
// 模拟插入100条数据,大概3000微秒
usleep(3000);
echo "插入数据{$i}\n";
}
}
task1();
task2();
task3();

这样,就实现了这3个功能了,然而,技术组长又说:能不能改成交替运行呢?就是说,写入文件一次之后,马上去发送一次邮件,然后再去插入一条数据。

然后我该改一改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
function task1($i) {
// 使用$i标识,写入文件,大概要3000微秒
if ($i > 300) {
return false; // 超过300不用写了
}
echo "写入文件{$i}\n";
usleep(3000);
return true;
}
function task2($i) {
// 使用$i标识,发送邮件,大概要3000微秒
if ($i > 500) {
return false; // 超过500不用发送了
}
echo "发送邮件{$i}\n";
usleep(3000);
return true;
}
function task3($i) {
// 使用$i标识,插入数据,大概要3000微秒
if ($i > 100) {
return false; // 超过100不用插入
}
echo "插入数据{$i}\n";
usleep(3000);
return true;
}
$i = 0;
$task1Result = true;
$task2Result = true;
$task3Result = true;
while (true) {
$task1Result && $task1Result = task1($i);
$task2Result && $task2Result = task2($i);
$task3Result && $task3Result = task3($i);
if ($task1Result === false && $task2Result === false && $task3Result === false) {
break; // 全部任务完成,退出循环
}
$i++;
}

运行一下:
代码1:
yield

代码2:
yield

确实是实现了任务交替执行,但是代码2明显让代码变得非常的难读,扩展性也很差,那么,有没有更好的方式实现这个功能呢?这时候我们就必须借助yield了。

首先,我们得封装一个任务类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
/**
* 任务对象
* Class Task
*/
class Task {
protected $taskId; // 任务id
protected $coroutine; // 生成器
protected $sendValue = null; // 生成器send值
protected $beforeFirstYield = true; // 迭代指针是否是第一个

public function __construct($taskId, Generator $coroutine) {
$this->taskId = $taskId;
$this->coroutine = $coroutine;
}

public function getTaskId() {
return $this->taskId;
}

/**
* 设置插入数据
* @param $sendValue
*/
public function setSendValue($sendValue) {
$this->sendValue = $sendValue;
}

/**
* send数据进行迭代
* @return mixed
*/
public function run() {
// 如果是
if ($this->beforeFirstYield) {
$this->beforeFirstYield = false;
var_dump($this->coroutine->current());
return $this->coroutine->current();
} else {
$retval = $this->coroutine->send($this->sendValue);
$this->sendValue = null;
return $retval;
}
}

/**
* 是否完成
* @return bool
*/
public function isFinished() {
return !$this->coroutine->valid();
}
}

这个封装类,可以更好地去调用运行生成器函数,但只有这个也是不够的,我们还需要一个调度任务类,来代替前面的while:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php
/**
* 任务调度
* Class Scheduler
*/
class Schedular {
protected $maxTaskId = 0; // 任务id
protected $taskMap = []; // taskId => task
protected $taskQueue; // 任务队列

public function __construct()
{
$this->taskQueue = new SplQueue();
}

public function newTask(Generator $coroutine) {
$tid = ++$this->maxTaskId;
// 新增任务
$task = new Task($tid, $coroutine);
$this->taskMap[$tid] = $task;
$this->schedule($task);
return $tid;
}

/**
* 任务入列
* @param Task $task
*/
public function schedule(Task $task) {
$this->taskQueue->enqueue($task);
}

public function run() {
while (!$this->taskQueue->isEmpty()) {
// 任务出列进行遍历生成器数据
$task = $this->taskQueue->dequeue();
$task->run();

if ($task->isFinished()) {
// 完成则删除该任务
unset($this->taskMap[$task->getTaskId()]);
} else {
// 继续入列
$this->schedule($task);
}
}
}
}

很好,我们已经有了一个调取类,还有了一个任务类,可以继续实现上面的功能了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
function task1() {
for ($i = 0; $i <= 300; $i++) {
// 写入文件,大概要3000微妙
usleep(3000);
echo "写入文件{$i}\n";
yield $i;
}
}
function task2() {
for ($i = 0; $i <= 500; $i++) {
// 发送邮件给500名会员,大概要3000微妙
usleep(3000);
echo "发送邮件{$i}\n";
yield $i;
}
}
function task3() {
for ($i = 0; $i <= 100; $i++) {
// 模拟插入100条数据,大概3000微妙
usleep(3000);
echo "插入数据{$i}\n";
yield $i;
}
}
$scheduler = new Scheduler();

$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->newTask(task3());

$scheduler->run();

除了上面的2个类,task函数和代码1不同的地方,就是多了个yield,那我们试着运行一下:
yield

很好,我们已经实现了可以调用调度任务,进行任务交叉运行的功能了,这就是”协程“,协程可以将多个不同的任务交叉运行。

二、协程与调度器的通信

我们在上面已经实现了一个协程封装了,但是任务和调度器缺少了通信,我们可以重新封装下,使协程当中能够获取当前的任务id,新增任务,以及杀死任务。

先封装一下调用的封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class YieldCall
{
protected $callback;

public function __construct(callable $callback)
{
$this->callback = $callback;
}

/**
* 调用时将返回结果
* @param Task $task
* @param Scheduler $scheduler
* @return mixed
*/
public function __invoke(Task $task, Scheduler $scheduler)
{
// TODO: Implement __invoke() method.
$callback = $this->callback;
return $callback($task, $scheduler);
}
}

同时我们需要小小的改动下调取器的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
public function run() {
while (!$this->taskQueue->isEmpty()) {
// 任务出列进行遍历生成器数据
$task = $this->taskQueue->dequeue();
$retval = $task->run();

// 如果返回的是YieldCall实例,则先执行
if ($retval instanceof YieldCall) {
$retval($task, $this);
continue;
}
if ($task->isFinished()) {
// 完成则删除该任务
unset($this->taskMap[$task->getTaskId()]);
} else {
// 继续入列
$this->schedule($task);
}
}
}

新增 getTaskId 函数去返回task_id:

1
2
3
4
5
6
7
8
9
10
11
<?php
function getTaskId() {
// 返回一个YieldCall的实例
return new YieldCall(
// 该匿名函数会先获取任务id,然后send给生成器,并且由YieldCall将task_id返回给生成器函数
function (Task $task, Scheduler $scheduler) {
$task->setSendValue($task->getTaskId());
$scheduler->schedule($task);
}
);
}

然后,我们修改下task1,task2,task3函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
function task1() {
$task_id = (yield getTaskId());
for ($i = 0; $i <= 300; $i++) {
// 写入文件,大概要3000微妙
usleep(3000);
echo "任务{$task_id}写入文件{$i}\n";
yield $i;
}
}
function task2() {
$task_id = (yield getTaskId());
for ($i = 0; $i <= 500; $i++) {
// 发送邮件给500名会员,大概要3000微妙
usleep(3000);
echo "任务{$task_id}发送邮件{$i}\n";
yield $i;
}
}
function task3() {
$task_id = (yield getTaskId());
for ($i = 0; $i <= 100; $i++) {
// 模拟插入100条数据,大概3000微妙
usleep(3000);
echo "任务{$task_id}插入数据{$i}\n";
yield $i;
}
}
$scheduler = new Scheduler();

$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->newTask(task3());

$scheduler->run();

执行结果:
yield

这样的话,当第一个执行的时候,会先调用getTaskId将task_id返回,然后将任务继续执行,这样,我们就获取到了调度器分配给任务的task_id,是不是很神奇?

三、生成新任务以及杀死任务

现在新增了一个需求:当发送邮件给会员时,需要新增一个发送短信的子任务,当会员id大于200时,则停止(别问我为什么要这样做,我自己都不知道)。

同时,我们可以利用YieldCall,去新增任务和杀死任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
/**
* 传入一个生成器函数用于新增任务给调取器调用
* @param Generator $coroutine
* @return YieldCall
*/
function newTask(Generator $coroutine) {
return new YieldCall(
// 该匿名函数,会在调度器中新增一个任务
function (Task $task, Scheduler $scheduler) use ($coroutine) {
$task->setSendValue($scheduler->newTask($coroutine));
$scheduler->schedule($task);
}
);
}
function killTask($taskId) {
return new YieldCall(
function (Task $task, Scheduler $scheduler) use ($taskId) {
$task->setSendValue($scheduler->killTask($taskId));
$scheduler->schedule($task);
}
);
}

同时,调度器也得有killTask的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/**
* 杀死一个任务
* @param $taskId
* @return bool
*/
public function killTask($taskId)
{
if (!isset($this->taskMap[$taskId])) {
return false;
}
unset($this->taskMap[$taskId]);

/**
* 遍历队列,找出id相同的则删除
*/
foreach ($this->taskQueue as $i => $task) {
if ($task->getTaskId() === $taskId) {
unset($this->taskQueue[$i]);
break;
}
}
return true;
}

有了新增和删除,我们就可以重新写一下task2以及新增task4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
function task2() {
$task_id = (yield getTaskId());
$child_task_id = (yield newTask(task4()));
for ($i = 0; $i <= 500; $i++) {
// 发送邮件给500名会员,大概要3000微妙
usleep(3000);
echo "任务{$task_id}发送邮件{$i}\n";
yield $i;
if ($i == 200) {
yield killTask($child_task_id);
}
}
}
function task4() {
$task_id = (yield getTaskId());
while (true) {
echo "任务{$task_id}发送短信\n";
yield;
}
}

运行结果:
yield

这样,我们就完美的实现了新增任务,以及杀死任务了。

总结

前面所说的,协程只是一种编程逻辑,一种写代码的技巧,协程能够帮助我们更好的切换代码中任务。从上面的例子不难发现,其实协程实现封装较为麻烦,并且不用协程也能实现这些功能,那为什么要用协程呢?
因为协程可以让代码更加的简洁,任务相互之间独立区分开,可以使代码更加的清爽,协程让我们可以更好的控制切换任务流。
前面介绍了那么多,或许有很多人感觉不对,会说“协程不能提升效率吗?”,“协程到底用来干什么的?”
或许由上面的例子很难看出协程的用户,那我们继续举例子吧:
js ajax是phper都了解的一个技术,当点击一个按钮时,先将点击事件ajax传输给后端进行增加一条点击数据,然后出现一个动画,这是一个很正常的事,那么请问,如果ajax是同步,并且在网络不好的情况,会发生什么呢?没错,点击之后,页面会卡几秒(网络不好),请求完毕之后,才会出现一个动画。

协程的用处就在这了,我们可以利用协程,把一些同步io等待的代码逻辑,改为异步,在等待的时间内,可以让cpu去处理其他任务。就如同小学时候做的一道题:
小明烧开水需要10分钟,刷牙需要3分钟,吃早餐需要5分钟,请问做完这些事情总共需要多少分钟?答案是10分钟,因为在烧开水这个步骤时,不需要坐在那里看水壶(异步,io耗时)可以先去刷牙,然后去吃早餐。

以上就是php yield关于协程的全部内容了。

swoole

由总结可以看出,协程用在最多的应用场景,在于需要io耗时,cpu可以节省出来的场景,并且必须是异步操作,这里推荐swoole扩展:https://www.swoole.com

Swoole-4.1.0 正式版发布,主要改动及新特性:

  • PHP 原生Redis、PDO、MySQLi轻松协程化,使用Swoole\Runtime::enableCoroutine()即可将普通的同步阻塞Redis、PDO、MySQL操作变为协程调度的异步非阻塞IO

  • 协程跟踪功能:新增两个方法 Coroutine::listCoroutine() 可遍历当前所有协程,Coroutine::getBackTrace($cid)可获取某个协程的函数调用栈

  • 支持在协程和Server中exit,此时将抛出可捕获的\Swoole\ExitException异常

  • 移除所有迭代器(table/connection/coroutine_list)的PCRE依赖机制
  • 新增http_compression配置项,底层会自动判断客户端传入的Accept-Encoding选择合适的压缩方法,支持GoogleBrotli压缩
  • 重构了底层Channel和协程Http客户端的C代码为C++协程模式,解决历史遗留的异步时序问题,稳定性大大提升
  • 更完整稳定的HTTP2支持和SSL处理
  • 增加open_websocket_close_frame配置,可以在onMessage时间中接收close帧

具体更新内容文档:https://wiki.swoole.com/wiki/page/966.html

easyswoole

EasySwoole 是一款基于Swoole Server 开发的常驻内存型的分布式框架,专为API而生,摆脱传统PHP运行模式在进程唤起和文件加载上带来的性能损失。EasySwoole 高度封装了 Swoole Server而依旧维持 Swoole Server 原有特性,支持同时混合监听HTTP、自定义TCP、UDP协议,让开发者以最低的学习成本和精力编写多进程,可异步,高可用的应用服务。

官网:http://www.easyswoole.com

本文为转来自www.php20.cn。

php进程通信-进程信号

php进程通信-进程信号

注意:本文所有内容均在linux环境下

一、进程信号对照

在php进程信号常量中,有定义以下常量(该表格参考网上的,不完整)

信号名 信号值 信号类型 信号说明
SIGHUP 1 终止进程(终端线路挂断) 本信号在用户终端连接(正常或非正常、结束时发出,通常是在终端的控制进程结束时,通知同一session内的各个作业,这时它们与控制终端不再关联。
SIGINT 2 终止进程(中断进程) 程序终止(interrupt、信号,在用户键入INTR字符(通常是Ctrl-C、时发出
SIGQUIT 3 建立CORE文件终止进程,并且生成CORE文件 SIGQUIT 和 SIGINT 类似,但由QUIT字符(通常是Ctrl-、来控制。进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
SIGILL 4 建立CORE文件(非法指令) SIGILL 执行了非法指令。通常是因为可执行文件本身出现错误,或者试图执行数据段。堆栈溢出时也有可能产生这个信号。
SIGTRAP 5 建立CORE文件(跟踪自陷) SIGTRAP 由断点指令或其它trap指令产生。由debugger使用。
SIGABRT 6 SIGABRT 程序自己发现错误并调用abort时产生.
SIGIOT 6 建立CORE文件(执行I/O自陷) SIGIOT 在PDP-11上由iot指令产生,在其它机器上和SIGABRT一样。
SIGBUS 7 建立CORE文件(总线错误) SIGBUS 非法地址,包括内存地址对齐(alignment、出错。eg: 访问一个四个字长的整数,但其地址不是4的倍数。
SIGFPE 8 建立CORE文件(浮点异常) SIGFPE 在发生致命的算术运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。
SIGKILL 9 终止进程(杀死进程) SIGKILL 用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。
SIGUSR1 10 终止进程(用户自定义信号1) SIGUSR1 留给用户使用
SIGSEGV 11 SIGSEGV 试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据
SIGUSR2 12 终止进程(用户自定义信号2) SIGUSR2 留给用户使用
SIGPIPE 13 终止进程(向一个没有读进程的管道写数据) Broken pipe
SIGALRM 14 终止进程(计时器到时) SIGALRM 时钟定时信号,计算的是实际的时间或时钟时间。 alarm函数使用该信号。
SIGTERM 15 终止进程(软件终止信号) SIGTERM 程序结束(terminate、信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。
SIGCHLD 17 忽略信号(当子进程停止或退出时通知父进程) SIGCHLD 子进程结束时,父进程会收到这个信号.
SIGCONT 18 忽略信号(继续执行一个停止的进程) SIGCONT 让一个停止(stopped、的进程继续执行。本信号不能被阻塞。可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作。 例如,重新显示提示符
SIGSTOP 19 停止进程(非终端信号) SIGSTOP 停止(stopped、进程的执行。 注意它和terminate以及interrupt的区别: 该进程还未结束,只是暂停执行。本信号不能被阻塞、处理或忽略。
SIGTSTP 20 停止进程(终端信号) SIGTSTP 停止进程的运行,但该信号可以被处理和忽略。用户键入SUSP字符时 (通常是Ctrl-Z、发出这个信号
SIGTTIN 21 停止进程(后端进程读终端) SIGTTIN 当后台作业要从用户终端读数据时,该作业中的所有进程会收到SIGTTIN 信号。缺省时这些进程会停止执行。
SIGTTOU 22 停止进程(后端进程写终端) SIGTTOU 类似于SIGTTIN,但在写终端(或修改终端模式、时收到。
SIGURG 23 忽略信号(I/O紧急信号) SIGURG 有“紧急”数据或out-of-band数据到达socket时产生。
SIGXCPU 24 终止进程(CPU实现超时) SIGXCPU 超过CPU时间资源限制。 这个限制可以由getrlimit/setrlimit来读取/ 改变
SIGXFSZ 25 终止进程(文件长度过长) SIGXFSZ 超过文件大小资源限制。
SIGVTALRM 26 终止进程(虚拟计时器到时) SIGVTALRM 虚拟时钟信号。 类似于SIGALRM,但是计算的是该进程占用的CPU时间。
SIGPROF 27 终止进程(统计分布图用计时器到时) SIGPROF 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间。
SIGWINCH 28 忽略信号(窗口大小发生变化) SIGWINCH 窗口大小改变时发出。
SIGIO 29 忽略信号(描述符上可以进行I/O) SIGIO 文件描述符准备就绪,可以开始进行输入/输出操作。
SIGPWR 30 SIGPWR Power failure

二、php基础进程相关函数

注意:(需要安装pcntl扩展支持)

具体相关函数可查看php手册:http://php.net/manual/zh/book.pcntl.php

1、declare(ticks=1);

每执行一条php低级语句,则触发一次register_tick_function()函数,并且每执行1次低级语句会检查一次该进程是否有未处理过的信号,该函数是在php版本低于5.3,用于进行php信号处理的函数,例如:

1
2
3
4
5
6
7
8
9
10
11
<?php
declare(ticks = 1); // 每执行一条时,触发register_tick_function()注册的函数
$a = 1; // 在注册之前,不记录
$a = 1; // 在注册之前,不记录
function test() { // 定义一个函数
echo "执行\n";
}
register_tick_function('test'); // 该条注册函数会被当成低级语句被执行
for ($i = 0; $i <= 3; $i++) { // for算一条低级语句
$i = $i; // 赋值算一条
}

在php7.2中,运行结果如下:
pcntl

2、pcntl_signal;

注册一个信号处理函数,和declare(ticks=1)组合使用:

1
2
3
4
5
6
7
8
<?php
declare(ticks = 1);
pcntl_signal(SIGINT, function() {
echo "触发信号Ctrl+C\n";
});
while (1) {
sleep(1); // 死循环运行低级语句
}

当执行该脚本,再ctrl+c的时候,将会捕捉到该信号,并输出,如图:
pcntl

3、getmypid

获取当前进程id,posix_kill发送信号
为什么会拿这2个一起说呢?因为posix_kill函数如果需要发送信号,是需要进程id的,而getmypid(),则是获取当前进程id的函数。

以下是将上面的函数组合使用的例子:

1
2
3
4
5
6
7
8
9
10
<?php
// 文件1
declare(ticks = 1);
echo getmypid(); // 获取当前进程id
pcntl_signal(SIGUSR1, function () {
echo "触发信号用户自定义信号1";
});
while (1) {
sleep(1); // 死循环运行低级语句
}
1
2
3
<?php
// 文件2
posix_kill(文件1进程id, SIGUSR1);

运行文件1结果:
pcntl

运行文件2之后文件1结果:
pcntl

4、pcntl_signal_dispatch

到这之后,你可能会想到,declare每次运行一次低级语句,都会尝试执行2种结果,效率会不会很差呢?答案是会的,所以在php5.3之后,有了新的函数,那就是pcntl_signal_dispatch。

pcntl_signal_dispatch:调用等待信号的处理器,有了它,将不再需要declare,只需要在循环中增加该函数,就可以调用信号通过了。

1
2
3
4
5
6
7
8
9
<?php
echo getmypid(); // 获取当前进程id
pcntl_signal(SIGUSR1, function () {
echo "触发信号用户自定义信号1";
});
while (1) {
pcntl_signal_dispatch(); // 也就是说当收到SIGUSR1信号时就会调用上面的处理器
sleep(1); // 死循环运行低级语句
}

结果和3同样。

5、pcntl_async_signals

看到4,你可能会觉得,信号还是没有那么的智能,能不能不做死循环,就完成异步的信号接收并处理呢?在php7.1之后,有了新的信号处理函数:pcntl_async_signals(),返回或设置是否异步信号处理:

1
2
3
4
5
6
7
8
9
10
<?php
// 文件1
echo getmypid();
pcntl_async_signals(true); // 设置异步信号处理 这里开启
pcntl_signal(SIGUSR1, function () { // 安装个user1信号处理函数
echo "触发信号";
posix_kill(getmypid(), SIGSTOP);
});

posix_kill(getmypid(), SIGSTOP); // 给进程发送暂停信号
1
2
3
4
<?php
// 文件2
posix_kill(文件1进程, SIGCONT); // 给进程发送继续信号
posix_kill(文件1进程, SIGUSR1); // 给进程发送user1信号

首先文件1运行,再另外运行文件2运行之后,文件1的结果:
pcntl

文件1进程strace命令结截图:
pcntl

可看到,进程休眠之后,被21834进程文件(文件2)唤醒之后,并发送sigusr1信号,再然后输出了一段文字,再然后自己给自己发送了进程休眠信号,继续休眠

linux 查看进程命令,可看:
https://blog.csdn.net/liangzhao_jay/article/details/50457197

6、pcntl_alarm

pcntl_alarm和创建一个计时器,在指定的秒数后向进程发送一个SIGALRM信号。

每次对pcntl_alarm()的调用都会取消之前设置的alarm信号。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
pcntl_signal(SIGALRM, function () {
echo '定时到时' . PHP_EOL;
});

pcntl_alarm(5);
$i = 0;
while (1) {
echo $i . PHP_EOL . $i++;
pcntl_signal_dispatch(true);
sleep(1);
}

pcntl

该函数使用场景之一:php熔断

1
2
3
4
5
6
7
8
9
10
11
<?php
pcntl_async_signals(true);
pcntl_signal(SIGALRM, function () {
echo 'php处理超时' . PHP_EOL;
});

pcntl_alarm(30);
/**
* 这里是一大段php处理函数
*/
pcntl_alarm(-1);

先进行30秒的定时,当处理函数超过30秒时,将触发php处理超时函数,从而进行超时逻辑,当在30秒处理完毕时,php将关闭该定时信号,正常往下执行。

三、其他

1、php进程信号中,无法捕获SIGKILL信号,该信号将会强制关闭进程。

本站总访问量 | 本页面被访问 | 您是第位小伙伴

© 炫彩信息科技有限公司 版权所有 备案号 : 赣ICP备19008485号